Flex: 多个图表间的联动标记线

在对多个图表数据进行可视化的过程中,有时会需要加入鼠标联动标记线,以方便图表之间进行对比: 红色联动标记线,当鼠标在任意一个图表中时,随鼠标的移动而移动,并显示所经过的数据点的值。

在对联动标记线进行实现的过程中,一开始很自然的想法,就是监听图表组件(以LineChart为例)的mouseOver事件:

...
protected function myChart_mouseOverHandler(event:MouseEvent):void {  
  //draw line
} 
...
<mx:LineChart ... mouseOver="myChart_mouseOverHandler(event)">  
...
</mx:LineChart>  

仅仅监听mouseOver事件是不够的,因为那样画出来的标记线会有非常严重的延迟 — 为了实时获取鼠标移动后的位置,必须监听mouseMove事件。同时,为了使当鼠标从图表中移开后隐藏标记线,还需要监听mouseOut事件:

...
protected function myChart_mouseOverHandler(event:MouseEvent):void {  
  //draw line
} 

protected function myChart_mouseMoveHandler(event:MouseEvent):void {  
  //update line
}

protected function myChart_mouseOutHandler(event:MouseEvent):void {  
  //hide line
}  
...
<mx:LineChart ... mouseOver="myChart_mouseOverHandler(event)" mouseMove="myChart_mouseMoveHandler(event)" mouseOut="myChart_mouseOutHandler(event)">  
...
</mx:LineChart>  

如果每一次调整标记线的位置都需要重新生成标记线组件的话,那么整个UI的性能将受到严重的拖累。因此,正确的做法是每个图表只生成一个标记线组件,通过调整标记线组件的x属性和visible属性来对其进行控制:

...
private var lines:Array;

protected function creationCompleteHandler(event:FlexEvent):void {  
  lines = new Array();

  var line1 = new Shape();
  line1.graphics.lineStyle(2, 0xff0000, .55);
  line1.graphics.moveTo(0,0);
  line1.graphics.lineTo(0,270);
  line1.visible = false;
  myChart1.addChild(line1);

  var line2 = new Shape();
  line2.graphics.lineStyle(2, 0xff0000, .55);
  line2.graphics.moveTo(0,0);
  line2.graphics.lineTo(0,270);
  line2.visible = false;
  myChart2.addChild(line2);

  lines.push(line1);
  lines.push(line2);
}

private function hideLines():void {  
  for (var i:int=0; i<lines.length; i++) {
    (lines[i] as Shape).visible = false;
  }
}

private function displayLines():void {  
  for (var i:int=0; i<lines.length; i++) {
    (lines[i] as Shape).visible = true;
  }
}

private function adjustLines(x:Number):void {  
  for (var i:int=0; i<lines.length; i++) {
    (lines[i] as Shape).x = x;
  }
}
...

在mouseOver/mouseMove/mouseOut事件的监听函数中,也需要进行一定的调整 — 当鼠标从LineChart的空白区域移动到线条上时,将会触发mouseOver事件;而当鼠标在线条上移动时,则会触发mouseMove事件;在这些事件中,event的target对象是线条本身(LineSeries)而不是LineChart;因此,从这些事件中获取的鼠标x值是从Y轴开始计算的,而不是从LineChart组件的最左侧开始的。所以,在三个鼠标事件的监听函数中需要对事件来源进行判断,同时根据不同的事件来源调整x的值:

...
protected function myChart_mouseOverHandler(event:MouseEvent):void {  
  var x:Number = event.localX;
  if (event.target is LineSeries) {
    x = x + 50;//50 is the gutterLeft of LineChart
  }
} 
...
<mx:LineChart ... gutterLeft="50">  
</mx:LineChart>  

这些不同的事件来源包括:LineSeries, AxisRenderer等等。

最后一步,是当标记线经过数据点时,显示该数据点的值。为了做到这一点,首先需要获取所有数据点的x值:

...
private var xValArray:Array;

protected function creationCompleteHandler(event:FlexEvent):void {  
  ...
  xValArray = getDataPointsXArray(340, expenses.length);//340 is the result of 400-50-10: the width of LineChart minus its gutterLeft, then minus its gutterRight.
}

private function getDataPointsXArray(lineWidth:Number, pointNum:uint):Array {  
  var xVals:Array = new Array(pointNum);
  var baseSegWidth:Number = lineWidth/(pointNum-1);
  var assignedWidth:int = 0;
  xVals[0] = 0;
  for (var i:int=1; i<pointNum; i++) {
    xVals[i] = Math.round(baseSegWidth*(i));
  }

  return xVals;
}
...
<mx:LineChart ... gutterLeft="50" gutterRight="10" width="400">  
</mx:LineChart>  

在获取所有数据点的x值之后,就可以判断当前鼠标是否划过了某个点,一旦确定,就可以使用Flex提供的annotationElement来自定义高亮相应的数据点:

private function hideDataPointAnnotation():void {  
  canvas1.removeAllChildren();
  canvas1.clear();
  canvas2.removeAllChildren();
  canvas2.clear();
}

//dpIndex is the index number of data points, starting from 0
private function displayDataPointAnnotation(dpIndex:int):void {  
  canvas1.beginFill(0xff0000);
  canvas2.beginFill(0xff0000);

  canvas1.drawCircle(expenses.getItemAt(dpIndex)["Month"], expenses.getItemAt(dpIndex)["Profit"], 3);
  var l:Label = new Label();
  l.text = expenses.getItemAt(dpIndex)["Profit"];
  canvas1.addChild(l);
  canvas1.updateDataChild(l, expenses.getItemAt(dpIndex)["Month"], expenses.getItemAt(dpIndex)["Profit"]);

  canvas2.drawCircle(expenses2.getItemAt(dpIndex)["Month"], expenses2.getItemAt(dpIndex)["Profit"], 3);
  var l2:Label = new Label();
  l2.text = expenses2.getItemAt(dpIndex)["Profit"];
  canvas2.addChild(l2);
  canvas2.updateDataChild(l2, expenses2.getItemAt(dpIndex)["Month"], expenses2.getItemAt(dpIndex)["Profit"]);
}

protected function myChart_mouseMoveHandler(event:MouseEvent):void {  
  ...
  hideDataPointAnnotation();
  var x:Number = event.localX;
  if (xValArray.indexOf(x-50) != -1) {
    var index:int = xValArray.indexOf(x-50);
    displayDataPointAnnotation(index);
  }
}
...
<mx:LineChart ...>  
  ...
  <mx:annotationElements>
    <mx:CartesianDataCanvas id="canvas1" includeInRanges="true" />
  </mx:annotationElements>
</mx:LineChart>