Flex: Customizing data tip location and behavior in a LineChart

The Flex LineChart control has a feature to display pop-up “data tips” when the user hovers near a data point in the chart. These are basically just tooltips with information about the x and y values at the given point. Unfortunately, Flex offers little in the way of customizing when and how these data tips display. For example, the following is a LineChart using the horizontal-step style:

LineChart with circled data points

In a horizontal step chart, a line is drawn horizontally to the right of each data point, and vertically at each change in value. The circles around each data point indicate the area where hovering the mouse would produce a data tip. No other areas on the chart produce a data tip and this can be slightly confusing. The user might wonder why they can’t hover at other bends in the line to see the current value. Other users might want to simply hover anywhere along the line.

Solution

In order to gain control over how the LineChart decides where to show data tips, we will need to create a subclass of LineChart and override the findDataPoints function. This function is responsible for returning a list of data points “near” a given coordinate. We will change this to get the behavior we want.

public override function findDataPoints(x:Number, y:Number):Array {
   var result:Array = []; //array of HitData

   // Ignore mouse points outside of the data region
   // of the chart. (e.g. on the axis labels)
   if (!dataRegion.contains(x, y)) {
      return result;
   }

   // The x,y coordinates given to findDataPoints are
   // relative to the LineChart. Make them local to
   // the component which holds the data series
   var mouseLoc:Point = new Point(x - _seriesHolder.x,
      y - _seriesHolder.y);

   // Loop through each LineSeries displayed on the chart
   // This can also work with AreaSeries, etc. without too
   // much modification
   for each (var line:LineSeries in this.series) {

      if (!line.visible) {
         continue;
      }

      // Get the value on the chart at the given mouse point
      var valueAtMouse:Array = line.localToData(mouseLoc);

      // Loop through the data points for this line and find
      // the closest data point to the left of the mouse.
      // NOTE: This is specific to a "step" chart. For a regular
      // "segment" chart, you'll need to find the 2 points on either
      // side of the mouse and interpolate the value.
      var last:LineSeriesItem = null;
      for each (var item:LineSeriesItem in line.items) {
         if (item.xValue < valueAtMouse[0]) {
            last = item;
         }
         else {
            break;
         }
      }

      // If we found a data point and the line is within 50
      // pixels of the mouse vertically, we'll show a data tip
      // If you want to show data tips for ALL points at a specific
      // x-value, just remove the vertical proximity check
      if (last && Math.abs(last.y - mouseLoc.y) < 50) {

         // We need to clone the data point that we found and modify
         // its xValue to match that of our mouse location. This way
         // the popup data tip that displays will show the xValue at
         // the mouse instead of the nearest data point
         var hitPoint:LineSeriesItem = LineSeriesItem(last.clone());
         hitPoint.x = last.x;
         hitPoint.y = last.y;
         hitPoint.xValue = Math.floor(valueAtMouse[0]);
         hitPoint.yValue = last.yValue;

         // Compute the distance between the mouse and the data point.
         var dist:Number = Math.sqrt(Math.pow(mouseLoc.x - last.x, 2) +
            Math.pow(mouseLoc.y - last.y, 2));

         // Create a HitData object which the data tip will use. I ignore
         // the ID param for now since I didn't find it useful. If you
         // need to implement it, see LineSeries.createDataID
         var hitData:HitData = new HitData(0, dist, x,
            hitPoint.y + _seriesHolder.y, hitPoint);

         // Make the data tip color scheme match the line. If you do
         // anything fancy with your line coloring you'll probably have
         // to modify this.
         var stroke:SolidColorStroke = SolidColorStroke(
            line.getStyle('lineStroke'));
         hitData.contextColor = stroke.color;

         // Create the function which actually gives the text value
         // for the data tip. Normally the LineSeries constructs its
         // own HitData objects and is responsible for supplying
         // this function.
         hitData.dataTipFunction = makeDataTipFunction(line);

         result.push(hitData);
      }
   }

   return result;
}

protected function makeDataTipFunction(line:LineSeries):Function {

   // Create a data tip function for a given LineSeries. The code
   // below was copied almost exactly from LineSeries.formatDataTip
   return function(hd:HitData):String {
      var txt:String = "";

      var n:String = line.displayName;
      if (n && n != "") {
         txt += "<b>" + n + "</b><BR/>";
      }

      var xName:String = line.dataTransform.getAxis(
         CartesianTransform.HORIZONTAL_AXIS).displayName;

      if (xName != "") {
         txt += "<i>" + xName+ ":</i> ";
      }

      txt += line.dataTransform.getAxis(
         CartesianTransform.HORIZONTAL_AXIS).formatForScreen(
         LineSeriesItem(hd.chartItem).xValue) + "\n";

      var yName:String = line.dataTransform.getAxis(
         CartesianTransform.VERTICAL_AXIS).displayName;

      if (yName != "") {
         txt += "<i>" + yName + ":</i> ";
      }

      txt += line.dataTransform.getAxis(
         CartesianTransform.VERTICAL_AXIS).formatForScreen(
         LineSeriesItem(hd.chartItem).yValue) + "\n";

      return txt;
   }
}

Our new findDataPoints() finds the value of each line on the chart at the current mouse coordinates and returns and returns a HitData object for each value. The above code was written with step style LineCharts in mind, but can work with any style with a few modifications. The key is that you need to interpolate the value of each line at the mouse’s x-coordinate and use this value to display the data tip.

My code also only displays data tips for lines that are within 50 pixels of the mouse vertically. You can easily remove this check if you want to display tips for every line.

Here is the final result.

3 Comments to “Flex: Customizing data tip location and behavior in a LineChart”

  1. Benoit 23 March 2011 at 12:59 am #

    It’s create. I think this way is better than to listener the itemMouseOver event of the LineChart. In the handler function to calucate the global coordinates and put a datatip instance on the top of the application.

  2. Brian Bishop 7 June 2011 at 1:58 pm #

    Great solution and running sample. Would be even better if View Source was enabled:)

  3. buddyp450 24 May 2012 at 8:00 pm #

    +1 for view source


Leave a Reply