ASE Home Page Products Download Purchase Support About ASE
ChartDirector Support
Forum HomeForum Home   SearchSearch

Message ListMessage List     Post MessagePost Message

  Bubble Chart with leader lines
Posted by Daniel on Sep-14-2010 16:40
Attachments:
Hi Peter/Everyone,

I have run into a bit of a sticky issue involving overlapping bubbles. I would like to add leader lines to a bubble if the bubble significantly overlaps, or failing that just add leader lines to all the bubbles. (please see visual) is there a way to produce such an effect?

My preferred language is PHP, but will try to translate anything you throw at this issue.

Best regards

Daniel
charting.jpg

  Re: Bubble Chart with leader lines
Posted by Peter Kwan on Sep-14-2010 18:08
Hi Daniel.

Unluckily, there is no automatic layout method that may achieve what you want.

Whereas it is not too hard to add lead lines to all the labels, this alone will not solve the problem. If the lead lines are of the same length and direction, all the labels are just shifted by the same amount, and the labels still overlaps. If the lead lines are of different lengths, the labels that originally overlap will no longer overlap. However, the labels may overlap with other labels. It is also possible the label will overlap with another bubble.

For example, in your attached chart, there are some labels with lead lines (the labels 11, 3, 4, 10). Suppose there is a big bubble at the text position 11, and a big bubble at the text position 3 (thus blocking both sides for the lead lines), how should the lead lines be drawn?

Anyway, currently ChartDirector can provide you with the positions of the bubbles. Based on this, you can draw the lead lines and labels (ChartDirector allows you to draw lines and texts at any position you like). So you may try to develop a way to compute the lengths and direction of the lead lines to draw this. The code should be like:

.... draw a bubble layer, with no labels ....

#layout the chart
$c->layout();

for ($i = 0; $i < count($myDataX); ++$i) {
    $centerX = $c->getXCoor($myDataX[$i]);
    $centerY = $c->getYCoor($myDataY[$i]);
    ...
    ... write some code to determine where to put the label and if a lead line is needed
    ...
    #add a lead line if needed
    $c->addLine(..........);  #add lead line if needed
    $c->addText(..........); #add label
}

Hope this can help.

Regards
Peter Kwan

  Re: Bubble Chart with leader lines
Posted by Daniel on Sep-17-2010 17:18
Attachments:
Hi Peter,

My bubbles are looking quite good now,
I have run into an issue
1) When all values are above 0 and therefore setAutoScale sets the yaxis minimum to 0, but the z size pushes the bubble over the y-axis 0 point. (see image)

Is there an automatic way of dealing with this, or should I create a script to ascertain minimum (y-(z/2)) and create y axis scale manually?

Best regards

Daniel
chart.jpg

  Re: Bubble Chart with leader lines
Posted by Peter Kwan on Sep-18-2010 00:48
Hi Daniel,

The "minimum of (y-(z/2))" should work well if your z unit is the same as the y unit. (Just from the chart, we cannot know if the z unit is specified in pixel unit, or in y unit, or in x unit.)

One simply method is just to add a dummy point at "minimum of (y-(z/2))" (a scatter layer with one point, using a transparent fill and edge color will do). In this way, when ChartDirector auto-scales the y-axis, it will make sure the y-axis scale includes "minimum of (y-(z/2))".

If the z is specify in pixel units (not in percentages), another approach is to use Axis.setMargin to include some fixed margins in pixel units. You may refer to the sample code "Y-Axis Scaling" (you may look for "Y-Axis Scaling" in the ChartDirector documentation index) on what is the effect of an axis margin.

Hope this can help.

Regards
Peter Kwan

  Re: Bubble Chart with leader lines
Posted by Daniel on Sep-14-2010 19:24
Thanks Peter,

I thought that may be the case, I will update this topic when I come up with a solution, I imagine it will require a lot of foreach constructs holding position data.

I will keep you posted.

Daniel

  Re: Bubble Chart with leader lines - PHP Code
Posted by Daniel on Sep-15-2010 17:45
Attachments:
Hi Peter/Everyone

Here is the script I made to find empty areas on the chart for labels. A bit over complicated and Im sure it could be reduced in size however it seems to do the job, If anyone has any suggestions to improve it that would be great.

$c->layout();

for ($i=0; $i<count($dataX1); $i++) {

$xPos1 = $scatterPeersLayerObj->getXCoor($dataX1[$i]);
$yPos1 = $scatterPeersLayerObj->getYCoor($dataY1[$i]);

// inital angle of lead line
$angle = ($yPos1>(380/2))? 90 : 270;

// length of line (hyp)
$h = 30+($dataZ1[$i]/2);

$loopcount = 1;
$loop = true;

// new array for conflicts with bubbles
$bubbleConflictArray = array();
$bubbleConflictArray['name'] = $dataName1[$i];

// new array for conflicts with other text labels
$textConflictArray = array();
$textConflictArray['name'] = $dataName1[$i];

while($loop){

$xPos2 = $xPos1 + cos(deg2rad($angle))*$h;
$yPos2 = $yPos1 + sin(deg2rad($angle))*$h;

for ($t=0; $t<count($dataX1); $t++) {

if($t!= $i){

// bubble center points
$xBubblePos = $scatterPeersLayerObj->getXCoor($dataX1[$t]);
$yBubblePos = $scatterPeersLayerObj->getYCoor($dataY1[$t]);

// get bubble_margin based on circle size
$bubble_margin = ($dataZ1[$t]/2)+30;

$text_margin = 10;

// text label center points, if coordinates exist use them instead of default
if(
(isset($dataX2[$t]))||
(isset($dataY2[$t]))
){
$xTextPos = $dataX2[$t];
$yTextPos = $dataY2[$t];
} else {
$xTextPos = $xBubblePos;
$yTextPos = $yBubblePos;
}

// check current text x position against bubble x center +- $bubble_margin
if(
($xPos2 < ($xBubblePos + $bubble_margin))&&
($xPos2 > ($xBubblePos - $bubble_margin))
){
$bubbleConflictArray[$t]['x'] = true;
} else {
$bubbleConflictArray[$t]['x'] = false;
}

// check current text x position against text x center +- $text_margin
if(
($xPos2 < ($xTextPos + $text_margin))&&
($xPos2 > ($xTextPos - $text_margin))
){
$textConflictArray[$t]['x'] = true;
} else {
$textConflictArray[$t]['x'] = false;
}

// check current text y position against bubble y center +- $bubble_margin
if(
($yPos2 < ($yBubblePos + $bubble_margin))&&
($yPos2 > ($yBubblePos - $bubble_margin))
){
$bubbleConflictArray[$t]['y'] = true;
} else {
$bubbleConflictArray[$t]['y'] = false;
}


// check current text y position against text y center +- $text_margin
if(
($yPos2 < ($yTextPos + $text_margin))&&
($yPos2 > ($yTextPos - $text_margin))
){
$textConflictArray[$t]['y'] = true;
} else {
$textConflictArray[$t]['y'] = false;
}
}
}

if($_REQUEST['debug']=='true'){
$c->addLine($xPos1, $yPos1, $xPos2, $yPos2, 0xCC000000, 0.5);
}

// loop through conflicts checking for an X and Y conflict on the same point
$bubbleConflictBool = false;

for ($p=0; $p<count($bubbleConflictArray)+1; $p++) {
if(
($bubbleConflictArray[$p]['x'])&&($bubbleConflictArray[$p]['y'])
){
$bubbleConflictBool = true;
}
}

if(
($bubbleConflictBool)
){
if($_REQUEST['debug']=='true'){
$this->addRect($c, ($xPos2-$bubble_margin),($yPos2-$bubble_margin),($bubble_margin*2),($bubble_margin*2), 0xCCFF0000, 0.5);
}
$h++;
}


// loop through conflicts checking for an X and Y conflict on the same point
$textConflictBool = false;

for ($p=0; $p<count($textConflictArray); $p++) {
if(($textConflictArray[$p]['x'])&&($textConflictArray[$p]['y'])){
$textConflictBool = true;
}
}

if(
($textConflictBool)
){
if ($_REQUEST['debug']=='true'){
$this->addRect($c,($xPos2-$text_margin),($yPos2-$text_margin),($text_margin*2),($text_margin*2), 0xCC00FF00, 0.5);
}
$angle = ($angle>360)? $angle = 0 : $angle+45;
$h++;
}

if(
($textConflictBool)||($bubbleConflictBool)

){
// no conflict for this label
} else {

$loop = !loop;
$dataX2[$i] = $xPos2;
$dataY2[$i] = $yPos2;
}

// break infinite loop
if($loopcount > 1000){
$loop = !loop;
}
$loopcount++;
}

#add a lead line if needed
$c->addLine($xPos1, $yPos1, $xPos2, $yPos2, 0x000000, 0.5);  #add lead line if needed

if(($angle>=0)&&($angle<90)){ $alignment = 7;

} elseif (($angle>90)&&($angle<180)){ $alignment = 9;

} elseif ($angle==90){ $alignment = 8;

} elseif (($angle>=180)&&($angle<180)){ $alignment = 3;

} elseif ($angle == 270){ $alignment = 2;

} elseif (($angle>270)&&($angle<360)){ $alignment = 1;

}

$c->addText($xPos2, $yPos2, $dataName1[$i], "arialbd.ttf", 7, 0x000000, $alignment); #add label
}
lines1.jpg
lines2.jpg
lines3.jpg

  Re: Bubble Chart with leader lines - PHP Code
Posted by Peter Kwan on Sep-15-2010 23:43
Hi Daniel,

Thanks a lot for your great code. From your attached chart, I see that it works very well for even for difficult cases. I am sure it will help a lot of people.

Regards
Peter Kwan

  Re: Bubble Chart with leader lines - PHP Code
Posted by Daniel on Sep-16-2010 00:29
Hi Peter,

Thank you very much, I will tidy it all up, convert it into a class and add more comments and resubmit a new version if thats ok.

Best regards

Daniel

  Bubble Chart with leader lines- php class
Posted by Daniel on Sep-16-2010 19:52
Attachments:
Hi Peter/Everyone,

I have completed a PHP class for automatically generating leader lines on a scatter layer.

Code is commented and should be fairly easy to use.

Feel free to modify in your own projects!

Best regards

Daniel
chartLeaderLines.php
<?php

/**
 * created by Daniel Conaghan
 *
 * To be used to label individual chart director XY points
 *
 */
 
 /* usage
 	
     $c->layout();
	
     $leadLines = new addLeadLineForGraph();
     $leadLines->graphInstance = $c;
     $leadLines->layerInstance = $scatterLayerObj;
     $leadLines->angleIncrement = 45;
     $leadLines->graphHeight = 380;
     $leadLines->graphWidth = 590;
     
     $dataArray['x'] = $dataX1;
     $dataArray['y'] = $dataY1;
     $dataArray['z'] = $dataZ1;
     $dataArray['labels'] = $dataLabelArray;
     
     $leadLines->data = $dataArray;
     $leadLines->addLeadLines();
 
 
 */
 
class addLeadLineForGraph{
     
     // graph information
     var $graphInstance;
     var $graphHeight;
     var $graphWidth;
     
     // layer information
     var $layerInstance;
     
     // line information
     // default is 30 pixels
     var $lineLength = 30;
     
     // if labels overlap the increment of which to change the angle
     var $angleIncrement = 45;
     
     // find new position using a change of line angle
     var $angleBool = true;
     
     // find new line using change of line length
     var $lineBool = true;
     
     // turn off/on visual debugging of final positions
     var $labelDebug = true;
     var $pointDebug = false;
     
     // scatter/bubble data
     // data needs to be either 2 dimensional array for scatter or 3 dimensional array for bubble
     // e.g.
     // $this->data['x'] = array(1,2,3,4,5);
     // $this->data['y'] = array(1,2,3,4,5);
     // $this->data['z'] = array(5,4,3,2,1);
     // $this->data['labels'] = array('a','b','c','d','e');
     
     var $data = array();
     
     // blank array to hold additional label coordinates
     var $labelData = array();
     
     public function __construct(){
     
     }
     
     public function addLeadLines(){
          
          for ($i=0; $i<count($this->data['x']); $i++) {
               
               // data point coordinates we need to find a label position for
               $xCurrentPoint = $this->layerInstance->getXCoor($this->data['x'][$i]);
               $yCurrentPoint = $this->layerInstance->getYCoor($this->data['y'][$i]);
               
               // inital angle of lead line
               // if y position of point is more than half graph height line will point down, else point up
               $angle = ($yCurrentPoint>($this->graphHeight/2))? 90 : 270;
               
               // length of line (hyp)
               // line is calculated based on z coordinate of current point
               // if no z eg for scatter use lineLength
               if(isset($this->data['z'][$i])){
                    $h = $this->lineLength+($this->data['z'][$i]/2);
               } else {
                    $h = $this->lineLength;
               }
               // new array for conflicts with bubbles
               $pointConflictArray = array();
               $pointConflictArray['name'] = $this->data['label'][$i];
               
               // new array for conflicts with other text labels
               $labelConflictArray = array();
               $labelConflictArray['name'] = $this->data['label'][$i];
               
               // init loopcount to stop recursive search for a label position
               $loopcount = 1;
               $loop = true;
               
               // count the amount of times the current labels angle has changed
               // used to change angle increment if current increment doesn't produce any satisfactory results
               $angleChangeCount = 0;
               
               // count the amount of times the current labels lead line length has changed
               // used to change angle increment if current angle + lengths don't produce any satisfactory results
               $lengthChangeCount = 0;
               
               while($loop){
                    
                    // position of current label
                    $xLabelPos = $xCurrentPoint + cos(deg2rad($angle))*$h;
                    $yLabelPos = $yCurrentPoint + sin(deg2rad($angle))*$h;
               
                    for ($t=0; $t<count($this->data['x']); $t++) {
                         
                         // if checked data is not the current data
                         if($t!= $i){
                              
                              // point center points
                              $xPointPos = $this->layerInstance->getXCoor($this->data['x'][$t]);
                              $yPointPos = $this->layerInstance->getYCoor($this->data['y'][$t]);
                              
                              // get bubble_margin based on circle size
                              if(isset($this->data['z'][$i])){
                                   $pointMargin = ($this->data['z'][$t]/2)+30;
                              } else {
                                   $pointMargin = 20;
                              }
                              
                              $labelMargin = 20;
                              
                              // text label center points, if coordinates exist use them instead of default
                              if(
                                   (isset($this->labelData['x'][$t]))||
                                   (isset($this->labelData['y'][$t]))
                              ){
                                   $xTextPos = $this->labelData['x'][$t];
                                   $yTextPos = $this->labelData['y'][$t];
                              } else {
                                   $xTextPos = $xPointPos;
                                   $yTextPos = $yPointPos;
                              }
                              
                              // check label collisions with points 
                              $pointConflictArray['x'][$t] = $this->checkCollision($xLabelPos,$xPointPos,$pointMargin);
                              $pointConflictArray['y'][$t] = $this->checkCollision($yLabelPos,$yPointPos,$pointMargin);
                              
                              // check label collisions with labels 
                              $labelConflictArray['x'][$t] = $this->checkCollision($xLabelPos,$xTextPos,$labelMargin);
                              $labelConflictArray['y'][$t] = $this->checkCollision($yLabelPos,$yTextPos,$labelMargin);
                              
                         }
                    }
                    
                    //print_r($labelConflictArray);
                    
                    if($this->labelDebug=='true'){
                         $this->graphInstance->addLine($xCurrentPoint, $yCurrentPoint, $xLabelPos, $yLabelPos, 0xCC000000, 0.5); 
                         $this->addRect(($xLabelPos-$labelMargin),($yLabelPos-$labelMargin),($labelMargin*2),($labelMargin*2), 0xCC00FF00, 0.5);
                    }
                    
                    if($this->pointDebug=='true'){
                         $this->addRect(($xLabelPos-$pointMargin),($yLabelPos-$pointMargin),($pointMargin*2),($pointMargin*2), 0xCCFF0000, 0.5);
                    }     
     
                    // loop through conflicts checking for an X and Y conflict on the same point
                    $pointConflictBool = false;
                    
                    for ($p=0; $p<count($pointConflictArray['x'])+1; $p++) {
                         if(
                              ($pointConflictArray['x'][$p])&&($pointConflictArray['y'][$p])
                         ){
                              $pointConflictBool = true;
                              break;
                         }
                    }
                    
                    if($pointConflictBool){
                         if($this->lineBool){
                              $h++;
                         }
                    } 
                    
                    
                    // loop through conflicts checking for an X and Y conflict on the same point
                    $labelConflictBool = false;
                    
                    for ($p=0; $p<count($labelConflictArray['x']); $p++) {
                         if(
                              ($labelConflictArray['x'][$p])&&($labelConflictArray['y'][$p])
                         ){
                              $labelConflictBool = true;
                              break;
                         }
                    }
                    
                    if($labelConflictBool){
                         
                         // change the current labels angle by an increment set by class
                         if($this->angleBool){
                              if ($angle>360){
                                   $angle = 0;
                              } else { 
                                   $angle += $this->angleIncrement;
                              }
                         }
                         //$h++;
                    }     
                                   
                    if(
                         ($labelConflictBool)||
                         ($pointConflictBool)
                         
                    ){
                         // no conflict for this label
                    } else {
                         
                         $loop = !loop;
                         $this->labelData['x'][$i] = $xLabelPos;
                         $this->labelData['y'][$i] = $yLabelPos;
                    }
                    
                    // break infinite loop
                    if($loopcount > 1000){
                         $loop = !loop;
                    }
                    $loopcount++;
               }
               
               #add a lead line if needed
               $this->graphInstance->addLine($xCurrentPoint, $yCurrentPoint, $xLabelPos, $yLabelPos, 0x999999, 0.5);  #add lead line if needed
               
               $alignment = $this->getLabelAlignment($angle);
               
               $this->graphInstance->addText($xLabelPos, $yLabelPos, $this->data['labels'][$i], "arialbd.ttf", 8, 0x000000, $alignment); #add label
          
          }
     }
     
     private function checkCollision($pos1,$pos2,$margin) {
          if(
               ($pos1 < ($pos2 + $margin))&&
               ($pos1 > ($pos2 - $margin))
          ){
               return true;
          } else {
               return false;
          }
     }
     
     public function addRect($xPos, $yPos, $width, $height, $color, $thickness){
          $this->graphInstance->addLine($xPos, $yPos, $xPos+$width, $yPos, $color, $thickness);
          $this->graphInstance->addLine($xPos, $yPos+$height, $xPos+$width, $yPos+$height, $color, $thickness);
          $this->graphInstance->addLine($xPos+$width, $yPos, $xPos+$width, $yPos+$height, $color, $thickness);
          $this->graphInstance->addLine($xPos, $yPos, $xPos, $yPos+$height, $color, $thickness);
     }
     
     private function getLabelAlignment($angle){
          if(($angle>=0)&&($angle<90)){ 
               return 7;
          } elseif (($angle>90)&&($angle<180)){ 
               return 9;
          } elseif ($angle==90){ 
               return 8;
          } elseif (($angle>=180)&&($angle<180)){ 
               return 3;
          } elseif ($angle == 270){ 
               return 2;
          } elseif (($angle>270)&&($angle<360)){ 
               return 1; 
          }
     }
     
}

?>