Wednesday, April 17, 2013

Vaadin Charts add-on showing custom properties in tooltip hover

Recently I had to investigate if it is possible to show other data than the X and Y value when hovering over a stacked bar graph in the Vaadin Charts add-on. And yes it is possible!
Nowhere I could find an example on how to do this, so that's why I wrote this blogpost. Hope it helps some people :)

Setup:
- Vaadin 7.0.1
- Charts add-on 1.0.1. In version 1.0.0 you can't set the color of the stacked bars using PlotOptions and ContainerDataSeries. This version has a fix for that.
- Note: I had to manually modify some labels of the hover in the screenshot since I can't use the real data used in the project. Same goes for the code, did not check if it still compiles. Let me know if something essential is missing :) Oh yeah and this is all demo-code so no quality complaints please....

Goal:
- Be able to add a custom field to the hover. And even better: be able to add an array to the hover tooltip.

Here's a screenshot of what the goal was. Click on it for a larger image:



The code:
Notice the Grapes ok: 7 Total: 16 in the hover text. These are the standard x and y values you can show easily via:




        Tooltip tooltip = new Tooltip();
        tooltip.setFormatter("function() { return ''+ this.x +'"+ " '+ this.series.name +': '+ this.y 
+' '+'Total: '+ this.point.stackTotal;}");

        conf2.setTooltip(tooltip);



But also note the extra this.point.custom = 708. That's an example of adding a custom value. The tooltip code then looks like:




tooltip.setFormatter("function() {"return ''+ this.x +'"+ " '+this.series.name +': '+ this.y +' '+
'Total: '+ this.point.stackTotal + ' this.point.custom = ' + this.point.custom + "''"



And then below that line you see a set of table-like data: Quality 1: 2 Stored: 3 Interesting: 4. This is the data that comes from three arrays. The tooltip code then looks like this:




tooltip.setFormatter("function() { var qualityValuesArray = JSON.parse(this.point.qualities); " +
        "var storedValuesArray = JSON.parse(this.point.stored); " + 
        "var interestingValuesArray = JSON.parse(this.point.interesting); " + 
        "return ''+ this.x +'"+ " '+this.series.name +': '+ this.y +' '+
        'Total: '+ this.point.stackTotal + ' this.point.custom = ' + this.point.custom + " +
        "'
Quality 1: ' + qualityValuesArray[0]  + " + "'Stored: ' + storedValuesArray[0]  + " + "'Interesting: ' + interestingValuesArray[0]  + " +
        "'
Quality 2: ' + qualityValuesArray[1]  + " + "'Stored: ' + storedValuesArray[1]  + " + "'Interesting: ' + interestingValuesArray[1]  + " +
        "'
Quality 3: ' + qualityValuesArray[2]  + " + "'Stored: ' + storedValuesArray[2]  + " + "'Interesting: ' + interestingValuesArray[2]  + " +
        "'
Quality 4: ' + qualityValuesArray[3]  + " + "'Stored: ' + storedValuesArray[3]  + " + "'Interesting: ' + interestingValuesArray[3]  + " +
        "'
Quality 5: ' + qualityValuesArray[4]  + " + "'Stored: ' + storedValuesArray[4]  + " + "'Interesting: ' + interestingValuesArray[4]  + " +
        "'
Quality 6: ' + qualityValuesArray[5]  + " + "'Stored: ' + storedValuesArray[5]  + " + "'Interesting: ' + interestingValuesArray[5]  + " +
        "'
Quality 7: ' + qualityValuesArray[6]  + " + "'Stored: ' + storedValuesArray[6]  + " + "'Interesting: ' + interestingValuesArray[6]  + " +
        "'
Quality 8: ' + qualityValuesArray[7]  + " + "'Stored: ' + storedValuesArray[7]  + " + "'Interesting: ' + interestingValuesArray[7]  + " +
        "'.';}");



The span can also be replaced by a HTML b tag of course.
Too bad you can't use the setHTML() for better formatting, like putting in a HTML table tag. This is a known issue, see here.

Essential in constructing the data is to use the BeanItemContainer, ContainerDataSeries and the addAttributeToPropertyIdMapping() in which you can put custom class (bean) OrderChartItem:



 /**
  * Class with custom value and 3 arrays to be displayed in tooltip
  */
    public class OrderChartItem {

     private static final int NR_QUALITIES = 8;
     
     private final String name;
  private final int x;
  private final Number y;
  private final int someValue;
  
  private final Number[] quality = new Number[NR_QUALITIES];
  private final Number[] stored = new Number[NR_QUALITIES];
  private final Number[] interesting = new Number[NR_QUALITIES];

  public OrderChartItem(String name, int x, Number y, int someValue) {
   this.name = name;
   this.x = x;
   this.y = y;
   this.someValue = someValue;
   
   for (int i = 0; i < NR_QUALITIES; i++) {
    quality[i] = i + 2;
    stored[i] = i + 3;
    interesting[i] = i + 4;
   }
  }

  public String getName() {
   return name;
  }

  public int getX() {
   return x;
  }

  public Number getY() {
   return y;
  }

  public int getSomeValue() {
   return someValue;
  }

  public List getQualities() {
   return java.util.Arrays.asList(quality);
  }
  public List getStored() {
   return java.util.Arrays.asList(stored);
  }
  public List getInteresting() {
   return java.util.Arrays.asList(interesting);
  }
    }


I made the arrays the same size because of its table data, but that doesn't have to be of course.

The basic mechanism is:
  1. Set up a BeanItemContainer with values of OrderChartItem. Take special care when putting in 0 values. The graph then shows the number 0. To avoid that, put in 'null' as a value.

    
    
    BeanItemContainer containerGood = new BeanItemContainer(OrderChartItem.class);
    containerGood.addBean(new OrderChartItem("some name good", 1, null, 701));
    
    


     So the OrderChartItem has the properties that can be reached from the tooltip Javascript. Note the OrderChartItem just gets some name, later it is overwritten by setting the ContainerDataSeries names:

    
    
    seriesGood.setName("good");
    
    

  2. Then wrap each BeanItemContainer in a ContainerDataSeries:

    
    
    ContainerDataSeries seriesGood = new ContainerDataSeries(containerGood);
    
    

  3. Then add the custom properties for each container. First the one value:

    
    
    seriesGood.addAttributeToPropertyIdMapping("custom", "someValue");
    
    

    Then the arrays:
    
    
    seriesGood.addAttributeToPropertyIdMapping("qualities", "qualities");
    
    

  4. And add the series to the configuration:

    
    
    conf2.addSeries(seriesGood);
    
    


In summary: the OrderChartItem is used as data-provider by adding them to the ContainerDataSeries. The properties-mappings of the custom OrderChartItem are then added to the series, after which they are accessible in the tooltip Javascript. Note the "trick" of using JSON.parse() in the formatter and the List getQualities() to get the arrays in the correct format.

Full source code can be found here.
The widgetsets directory has been cleared out because of distribution issues.





5 comments:

Pierce Krouse said...

Perfect! That's the exact solution I was looking for.

Juan Bta. said...

Thanks a lot! excellent example for custom tooltip.

Datta said...

But here conf2.addSeries(seriesGood) gives type compatibility issue as it is meant to add Series object.

Datta said...

#### Adding extra information vaadin charts tooltip apart from axis values

Step 1 : Create a data structure to store the extra information related with bar on the chart.
For example this one will store the nps categories values related with x axis value on the chart

Hashtable chartXValueNPSScoreHash = new Hashtable();

String npsScore = promoters+" "+passives+" "+detractors;
chartXValueNPSScoreHash.put(xAxisValue,npsScore);

Step 2 : Construct a json string of the format
{'0 TO 25000 [SS:309 NPS:8]':'a',
'GREATER THAN 25000 AND LESS THAN 75000 [SS:160 NPS:-1]':'b',......}

Here 'a' and 'b' is the extra information to be shown for the bar.

String mappingString = "{";
Set keys = chartXValueNPSScoreHash.keySet();
int count = 0;
for(String key : keys)
{
if(count<keys.size()-1)
mappingString = mappingString+" '"+key+"':'"+chartXValueNPSScoreHash.get(key)+"',";
else
mappingString = mappingString+" '"+key+"':'"+chartXValueNPSScoreHash.get(key)+"'";

count++;
}

mappingString = mappingString+"};";

Step 3 : Update the tooltip formatter.
For example,
Tooltip tooltip = new Tooltip();
tooltip.setFormatter("function() { "
+ "mapping = "+mappingString
+ "return ''+ this.x +': '+ this.y +' '+ mapping[this.x]; }");
Configuration conf = chart.getConfiguration();
conf.setTooltip(tooltip);

Anonymous said...

I stumbled upon this blog post while trying to do this myself. Wanting to put extra data, per point, into the tooltip in a fancy way.

While your solution did work, things got tricky when trying to programatically alter the state of items in the container - it refused to reflect the changes in the UI. In my case, I wanted to click on items in the legend and have them auto-select in the chart. setSelected(true) was doing nothing.

The way to get state changes reflected is to be able to fire the relevant data update event for the item, an example of which can be found in "Configuration#fireDataUpdated". Using your solution, you could achieve this by calling it through some gnarly reflection, e.g:

ReflectionUtils.invokeMethod(Objects.requireNonNull(ReflectionUtils.findMethod(Configuration.class, "fireDataUpdated", Series.class, DataSeriesItem.class, Integer.class)), chart.getConfiguration(), series, item, pointIndex);

However, a big improvement to the solution is to make your custom object extend DataSeriesItem, and only put your custom/extra properties in there with no overlap with DataSeriesItem's properties. Thus way it has all the base properties hooked in by default, and it means you can use a "DataSeries" rather than a "ContainerDataSeries" and do away with all the manual property mappings. DataSeries has a very handy 'update' method (that ContainerDataSeries does not have) which will call the previously mentioned fireDataUpdated event, rather than having to reflectively grab it.

If you put extra fields into your custom extension of DataSeriesItem, they'll still be accessible by the tooltip formatter in the same way as they did in the original version in the blog post.

Thanks for this blog post and I hope this comment similarly useful for others that find themselves here.