Flex: 散点折线图的制作

在图表数据的可视化过程中,有时会碰到包含散点的折线图这样比较极端的状况:
三月份(Mar)的利润为1000,由于二月份和四月份的数据缺失,因此体现在图表上为散点

包含散点的折线图其实质还是折线图(LineChart),只是由于数据点的缺失(这在现实生活中很常见)导致某些折线线段不可见,继而产生了散点。那么,在Flex中这样的散点折线图该如何制作呢?

在Flex中,绘制折线图是一件很容易的事情 — 使用LineChart组件即可(Adobe: Using Line Charts)。如果全年各个月份的利润数据均存在,那么用LineChart组件绘制成折线图就是以下这个样子:

相应代码:

  <mx:Script>
    <![CDATA[
      [Bindable]
      public var expenses:ArrayCollection = new ArrayCollection([
        {Month:"Jan", Profit:3000},
        {Month:"Feb", Profit:3000},
        {Month:"Mar", Profit:1000},
        {Month:"Apr", Profit:3000},
        {Month:"May", Profit:1000},
        {Month:"Jun", Profit:4500},
        {Month:"Jul", Profit:1000},
        {Month:"Aug", Profit:4500},
        {Month:"Sep", Profit:1000},
        {Month:"Oct", Profit:1500},
        {Month:"Nov", Profit:1000},
        {Month:"Dec", Profit:4500}
      ]);
    ]]>
  </mx:Script>
...
  <mx:LineChart id="myChart1" dataProvider="{expenses}" height="300" width="400">
    <mx:horizontalAxis>
      <mx:CategoryAxis dataProvider="{expenses}" categoryField="Month" />
    </mx:horizontalAxis>
    <mx:series>
      <mx:LineSeries id="ls1" yField="Profit" displayName="Profit"></mx:LineSeries>
    </mx:series>
  </mx:LineChart>
  <mx:Legend dataProvider="{myChart1}"/>
...

当然,我们也可以调整折线图的样式以符合实际需求:

相应的代码改动:

  <mx:LineChart id="myChart1"
    ...
    showDataTips="true" seriesFilters="{[]}">
    <mx:series>
      <mx:LineSeries ...>
        <mx:lineStroke>
          <mx:Stroke color="0x007DC3" weight="3" />
      </mx:LineSeries>
    </mx:series>
  </mx:LineChart>

如何表示数据点的缺失呢?直接将相关数据点从数据源中移除是不行的,那样一来,这个数据点的位置在折线图中也就不存在了。以上面的折线图为例,将四月份(Apr)从expense数组中移除的话,LineChart会用折线连接三月(Mar)和五月(May)的数据点;毕竟,计算机程序并不知道一年有12个月。把四月份的数据值设为0也是不行的,因为那样只会将四月的数据点绘制在LineChart的底部。

正确的做法是:定义数据值的范围,同时将缺失的数据点的值设置在数据范围之外(比如说-1):

  <mx:Script>
    <![CDATA[
      [Bindable]
      public var expenses:ArrayCollection = new ArrayCollection([
        {Month:"Jan", Profit:3000},
        {Month:"Feb", Profit:3000},
        {Month:"Mar", Profit:1000},
        {Month:"Apr", Profit:-1},
        {Month:"May", Profit:1000},
        {Month:"Jun", Profit:4500},
        {Month:"Jul", Profit:1000},
        {Month:"Aug", Profit:4500},
        {Month:"Sep", Profit:1000},
        {Month:"Oct", Profit:1500},
        {Month:"Nov", Profit:1000},
        {Month:"Dec", Profit:4500}
      ]);
    ]]>
  </mx:Script>
  <mx:LineChart id="myChart1" ...>
    ...
    <mx:verticalAxis >
      <mx:LinearAxis minimum="0" />
    </mx:verticalAxis>
    ...
  </mx:LineChart>


四月(Apr)的数据缺失

对于制作包含散点的折线图,仅仅将缺失的数据值设置在LineChart数据范围之外是不够的 — 缺失的数据点及其相邻折线确实不见了,但如果某个存在的数据点其前后相邻数据点均缺失,那么这个存在的数据点也会莫名其妙的消失:

    public var expenses:ArrayCollection = new ArrayCollection([
        {Month:"Jan", Profit:3000},
        {Month:"Feb", Profit:-1},
        {Month:"Mar", Profit:1000},
        {Month:"Apr", Profit:-1},
        {Month:"May", Profit:1000},
        {Month:"Jun", Profit:4500},
        {Month:"Jul", Profit:1000},
        {Month:"Aug", Profit:4500},
        {Month:"Sep", Profit:1000},
        {Month:"Oct", Profit:1500},
        {Month:"Nov", Profit:1000},
        {Month:"Dec", Profit:4500}
      ]);


三月份(Mar)的数据存在却不可见

在上面的例子中,三月份(Mar)的数据点是确实存在的,把鼠标移动到相应位置也会让该数据点高亮;然而一旦移开鼠标,这个数据点就消失了。问题的原因在于:这个数据点前后的两个数据位置都没有点,这样就无法绘制通过该数据点的折线,而Flex的LineChart组件又不会在此单独渲染这个点,于是该散点就“消失”了。

那么,如何绘制这种带散点的折线图呢?一种方案是让LineChart组件在绘制折线之外,渲染每一个数据点:

  <mx:Script>
  ...
  private var _stroke:Stroke = new Stroke(0x007DC3);
  ...
  </mx:Script>
  <mx:LineChart id="myChart1" ...>
    ...
    <mx:series>
      <mx:LineSeries ... stroke="{_stroke}">
        <mx:itemRenderer>
          <mx:Component>
            <mx:CircleItemRenderer/>
          </mx:Component>
        </mx:itemRenderer>
        <mx:fill>
          <mx:SolidColor color="0x007DC3" />
        </mx:fill>
      </mx:LineSeries>
    </mx:series>
    ...
  </mx:LineChart>

这样做,数据点是全部显示出来了,但同时却带来了一个副作用:折线图的样式被改变了 — 所有的数据点都被加强显示,而这并不一定是开发者所希望看到的;尤其是在数据点很多的情况下,加强显示所有的数据点会让整个折线图变得非常难看。

尝试到这一步,接下来的思路也就很自然了 — 只要显示所有的点,而同时将显示的点的直径控制在折线线段宽度之内,就能让折线线段遮蔽显示的数据点。这样一来,散点可以得到显示,而折线图本身的样式也不会被改变。

为了做到这一点,我们需要编写一个自定义的ItemRenderer(与CircleItemRenderer一样,继承自ProgrammaticSkin,实现IDataRenderer接口),在该Renderer的updateDisplayList函数中,设定描画的点的大小:

public class CustomItemRenderer extends ProgrammaticSkin implements IDataRenderer {  
  ...
  override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
    ...
    var g:Graphics = graphics;
    ...
    g.drawEllipse(2.5,2.5,3,3);
    ...
  }
  ...
}

然后在LineChart组件中使用该自定义的ItemRenderer即可:

  <mx:LineChart id="myChart1" ...>
    ...
    <mx:series>
      <mx:LineSeries ... >
        <mx:itemRenderer>
          <mx:Component>
            <test:CustomItemRenderer/>
          </mx:Component>
        </mx:itemRenderer>
        <mx:fill>
          <mx:SolidColor color="0x007DC3" />
        </mx:fill>
      </mx:LineSeries>
    </mx:series>
    ...
  </mx:LineChart>