Wednesday Feb 26, 2014

Customizing the Axis Labels in ADF Graphs

The various data visualization (DVT) graphs provided as part of the ADF Faces component set provide a very rich and comprehensive set of visualizations for representing your data.  One of the issues with them, that some folks struggle with however, is the fact that not all the features are controlled in an a completely declarative manner. 

In this article I want to concentrate on labeling capabilities for a graph axis, looking first of all that the declarative approaches, but then following that up with the more advanced programmatic option. 

Managing Labels Declaratively 

Control over the labels on the axis tick points is a good example of making the simple things declarative and the advanced things possible.  For basic numeric formatting you can do everything with tags - for example formatting as currency, percentage or with a certain precision.    

This is a default (bar)graph plotting employee salary against name, notice how the Y1 Axis has defaulted to a fairly sensible representation of the salary data using 0-14K:

I can change that default scaling by setting the scaling attribute in the <dvt:y1TickLabel> tag. This allows scaling at the level of none | thousand | million | billion | trillion | quadrillion (enough to show national debt then!):

<dvt:y1TickLabel id="y1TickLabel1" scaling="none"/>

Changes the graph to:

We can then further change the pattern of the numbers themselves by embedding <af:convertNumber> inside of the <dvt:y1TickLabel> tag.


<dvt:y1TickLabel id="y1TickLabel1" scaling="none">
  <af:convertNumber type="currency" currencyCode="USD"/>

Adds currency formatting:

And using the <dvt:graphFont> we can change colors and style:

<dvt:y1TickLabel id="y1TickLabel1" scaling="none">
  <dvt:graphFont name="SansSerif" size="8" color="#FF0000" bold="true" italic="true" />
  <af:convertNumber type="currency" currencyCode="USD"/>


Need More Control?  Using the TickLabelCallback...

So we can achieve quite a lot by simply using the tags.  However, what about a more complex requirement such as replacing a numerical value on an axis with a totally different string e.g. converting to a Roman Numeral (I, IV XII etc.) or maybe converting a millisecond value to a formatted date string?  To do this, ADF provides a simple callback that you can implement to map a value to whatever string you need.  Here's a simple case where I've plotted the salaries in department 100 of the standard HR EMPLOYEES demo table against the HireDate on a scatter plot.  For the sake of illustration I've actually converted the HireDate to it's time value (e.g. a long value representing the number of milliseconds since 01/01/1970) .  In a real graph I'd use the proper support that we have for representing time and date and just map a date object. Then you get to use the timeSelector and can directly set the formatting, however, bear with me because I'm just illustrating the point here.

Here's the default output with the millisecond version of the date, as you can see the millisecond value gets automatically scaled to the billions level.

To override the default representation of the millisecond value we will need to create a java class that implements the oracle.dss.graph.TickLabelCallback interface.  Here's the simple example I'll use in this case:

import java.text.SimpleDateFormat;
import oracle.dss.graph.DataTickLabelInfo;
import oracle.dss.graph.GraphConstants;
import oracle.dss.graph.TickLabelCallback;
import oracle.dss.graph.TickLabelInfo;

public class MSToDateFormatLabelCallback implements TickLabelCallback, Serializable {

  public String getTickLabel(TickLabelInfo tickLabelInfo, int axisID) {
    String label = null;
    if (axisID == GraphConstants.X1TICKLABEL) {
      long timeInMillis = (long) ((DataTickLabelInfo) tickLabelInfo).getValue();
      SimpleDateFormat fmt = new SimpleDateFormat("MM/yy");
      label = fmt.format(timeInMillis);
    } else {
      label = "" + ((DataTickLabelInfo) tickLabelInfo).getValue();
    return label;

As you can see the formatting  is applied only to the specified axis and we have to upcast the tickLabelInfo argument to a DataTickLabelInfo in order to gain access to the value that is being applied. Note that the callback class must also be Serializable. 

Once you have this class, then you need to apply it to the chart instance by calling the relevant set*TickLabelCallback. So for example I might set this in the backing bean for a page in the setter used by the binding attribute on the dvt graph component

public class GraphPageHandler {
    private UIGraph scatterPlot;
    public void setScatterPlot(UIGraph scatterPlot) {
        this.scatterPlot = scatterPlot;
        scatterPlot.setX1TickLabelCallback(new MSToDateFormatLabelCallback());

Now with the callback in place and the addition of:

  1. Using  the axisMinValue attribute along with axisMinAutoScaled="false" on the <dvt:x1Axis>  to reset the start date for the plot to the year 2000 rather than the default 1970
  2. Some of the currency formatting that we've already looked at to format the Y axis

Here's the result:

Sunday Jan 08, 2012

An Enhanced Gauge control using HorizontalBar

I thought this one was worth writing up because it highlights a couple of really nice features within the ADF DVT charts -  Reference Objects and Alerts. The scenario was based on a problem where there was a requirement for a conventional horizontal gauge control, showing the normal thresholds for the control - shown in this image here as the red, yellow and green zones:

Basic Gauge

The twist in this case was that we required an extra reference marker on the data bar that indicated the "optimal" value within a particular threshold. So in the image above imagine that within the green zone, 150 was the optimal value and we need to somehow indicate that.

Simply using the gauge it is possible to do this using an extra threshold value at the required value, or rather two extra threshold definitions, we have to terminate the threshold that currently starts at 90 at 150 (the reference value we want to mark), then a short threshold from 150-151 just to provide the reference marker we need, then an extra green threshold from 152 to 200 to complete the green bar.  You end up with this:

Gauge with reference line

So that works pretty well - but it's a limitation of the use of thresholds that all you can achieve is a block of colour or line behind the indicator bar, plus the fact that I've used an extra level of the thresholdset here means it's not very dynamic (although of course it could be if you want to start writing code). Think about if we wanted to overlay several different dynamic markers such as min/max levels reached in the last 24 hours. That would be do-able but it is getting messy. 

Another approach to take is to use the bar chart (horizontal in this case) to simulate the gauge. We don't have the dvt:threshold /dvt:thresholdSet  in the case of charts but we do have the equivalent with dvt:referenceObject. Reference objects allow you to place a filled area or line in front or behind the data series so we can emulate the same look that we had with the gauge:

Gauge using horizontal bar chart

As you can see it looks pretty similar, although there are some slight differences:

  1. Unlike the gauge, which displays value labels at the threshold boundaries, the axis on the chart has a regular labelling at fixed intervals based on the y1Axis setting.
  2. We're missing the tick marks between the Y axis and the labels - well in fact that's a slight bug, they are there, but the size in proportional to the height of the chart (46px in this case). In 11.1.2 and above you should see them correctly.
  3. The proportions of the series bar / chart area are  slightly different to the gauge. But that's only noticeable if you are mixing and matching.

 Let's break down how to create some of the features here:

Overall Size

The height / width of the cart had to be controlled somewhat to bring it down to gauge dimensions.  This is acheived using inlineStyle on the horizontalBarGraph tag:

<dvt:horizontalBarGraph id="gaugeClone"

We also need to ensure that the y axis is fixed. By default it will be scaled based on the max value of the data which we don't want. To do this we define the min/max values on the nested y1Axis tag and set the axisMaxAutoScaled attribute to false. We also define the tickmark label interval to 30 here.

<dvt:y1Axis axisMinValue="0"

Bar Styling

By default the gauge has that grey-ish colour  whereas the deafult colour for the first series in a bar chart will be a blue (which is nice, but for the sake of consistency I wanted to change). The colours / shapes used for bars, lines and markers in charts are all controlled by dvt:series tags. So here I've set up the values for series 0 which defines the bar data and set both the fill and the border to emulate the look of the gauge. Note that the series tag needs to be wrapped in a seriesSet.

  <dvt:series index="0" 

Threshold banding

Next we want to add the banding to emulate the gauge thresholds. To do this we use the referenceObject tag with the RO_AREA type set to make it fill the defined area rather than draw a line. Again the referenceObject tags need to be enclosed in a parent, referenceObjectSet:

   <dvt:referenceObject index="1" type="RO_AREA"
                        association="SERIES" location="RO_BACK"
                        lowValue="0" highValue="30"/>
   <dvt:referenceObject index="2" type="RO_AREA"
                        association="SERIES" location="RO_BACK"
                        lowValue="30" highValue="90"/>
   <dvt:referenceObject index="3" type="RO_AREA"
                        association="SERIES" location="RO_BACK"
                        lowValue="90" highValue="200"/>
   <dvt:referenceObject index="4" type="RO_AREA"
                        association="SERIES" location="RO_BACK"
                        lowValue="200" highValue="230"/>
   <dvt:referenceObject index="5" type="RO_AREA"
                        association="SERIES" location="RO_BACK"
                        lowValue="230" highValue="280"/>

The Reference Line 

Just like with the gauge we manipulate the reference object set to add the reference line at 150. However, reference object actually has a line subtype so we can simply use that rather than having to use an area with width of 1. We add the following into the referenceObjectSet:

<dvt:referenceObject index="6" type="RO_LINE" lineValue="150.0"
                     association="SERIES" location="RO_BACK"
                     color="#000000" /> 

The nice thing here is that we can flip this line so it overlays the series. This is not something we can do with Gauge:

<dvt:referenceObject index="6" type="RO_LINE" lineValue="150.0"
                     association="SERIES" location="RO_FRONT"
                     color="#000000" /> 

Which gives us this:

Bar based gauge with overlay reference

The lineValue attribute can, of course, be an EL expression rather than a hard-coded value so you can make the reference point dynamic.

Using Alerts to Add Markers

The final twist is to move away from these plain lines that we've been using as marker values so far and just retain the referenceObjectSet for the threshold banding.  Graph supplies a second "set" of things - the alertSet, which will allow us to overlay gif and png images over the series. Using that we can overlay multiple markers that look a little more attractive - like this:

Bar based gauge with overlay marker icons

You could imagine this being used to reflect min / max values or that kind of thing, it's a really neat capability. To do this all we need to do is add a simple alertSet (note that all of the hardcoded values here could be replaced by Expression Language for a dynamic gauge).

   <dvt:alert xValue="Power Consumption"
              yValue="150" yValueAssignment="Y1AXIS"
   <dvt:alert xValue="Power Consumption"
              yValue="220" yValueAssignment="Y1AXIS"

The xValue attribute maps the alert marker onto the required series bar.

Finally, just for fun, see if you can work out how to do this one:

The puzzle

Answers in a comment please...

Final Thoughts

So should you use this technique rather than the out of the box gauge control? Well only of you really need to.  Gauge is simple and lightweight and if all you need is a simple reference line, thresholds do the trick quite well. However, this technique does go to show that if  you think out of the box a little you can do a lot with the DVT tools at your disposal. If nothing else you now know about referenceObjects and alerts!


Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!


« July 2016