Freitag, 13. September 2013

JavaFX Charts: Display Date values on a DateAxis

As Pedro Duque Vieira recently mentioned in his blogpost about his DateAxis, Diego Cirujano-Cuesta and me developed a DateAxis, which uses actual Date objects.
I don't want to hesitate to introduce it to you and spend some words on how it works.

You can find the source code here.

How it looks like

How to use it

  ObservableList<XYChart.Series<Date, Number>> series = FXCollections.observableArrayList();
  ObservableList<XYChart.Data<Date, Number>> series1Data = FXCollections.observableArrayList();
  series1Data.add(new XYChart.Data<Date, Number>(new GregorianCalendar(2012, 11, 15).getTime(), 2));
  series1Data.add(new XYChart.Data<Date, Number>(new GregorianCalendar(2014, 5, 3).getTime(), 4));
  ObservableList<XYChart.Data<Date, Number>> series2Data = FXCollections.observableArrayList();
  series2Data.add(new XYChart.Data<Date, Number>(new GregorianCalendar(2014, 0, 13).getTime(), 8));
  series2Data.add(new XYChart.Data<Date, Number>(new GregorianCalendar(2014, 7, 27).getTime(), 4));
  series.add(new XYChart.Series<>("Series1", series1Data));
  series.add(new XYChart.Series<>("Series2", series2Data));
  NumberAxis numberAxis = new NumberAxis();
  DateAxis dateAxis = new DateAxis();
  LineChart<Date, Number> lineChart = new LineChart<>(dateAxis, numberAxis, series);

As you can see, it allows you to define XYChart.Data with Date objects.

Basically that's all you need to do.
As with other axes, you can either define a lower and upper bound or you can chose to let the axis find out its bounds by itself. Just set autorange to true.

Furthermore you can also use your own tick label formatter or enable or disable animation.

Some words on the implementation

The first important part to implement was the getDisplayPosition(Date) method in order to get the display position for a given date. To do this, we get the percentage value, where the date is "located" between the lower and upper bound. Imagine the lower bound as 0 and the upper bound as 1 and we get a value between 0 and 1. Then we have to multiply this value with the length of the axis and we are basically done.

Here's my implementation:

public double getDisplayPosition(Date date) {
    final double length = getSide().isHorizontal() ? getWidth() : getHeight();

    // Get the difference between the max and min date.
    double diff = currentUpperBound.get() - currentLowerBound.get();

    // Get the actual range of the visible area.
    // The minimal date should start at the zero position, that's why we subtract it.
    double range = length - getZeroPosition();

    // Then get the difference from the actual date to the min date and divide it by the total difference.
    // We get a value between 0 and 1, if the date is within the min and max date.
    double d = (date.getTime() - currentLowerBound.get()) / diff;

    // Multiply this percent value with the range and add the zero offset.
    if (getSide().isVertical()) {
        return getHeight() - d * range + getZeroPosition();
    } else {
        return d * range + getZeroPosition();

The next important method to implement was calculateTickValues(double v, Object range).

Uhh, this was a tricky one ;-).

Depending on the range, we want to display different values. E.g. If we display a large range, ranging over several years, we want each tick label to represent one year, where as if the range ranges only over a few days, we want to display the days, and if it ranges over half a day only, we want to display hours.

So, I defined intervals, which define in which intervals tick labels will be shown, e.g. years, months, 3 months, weeks, days, ... and so on. Then we find out, which interval suites best for the current range. We do this by keep adding one interval to the lower bound until we have reached the upper bound or until there are too many tick labels. Then we try it with the next smaller interval until we found good one.

As last step, we want to make sure, that years always start on January 1st and months always start on the first date of the month and so on.

As a end result we have a nice list of Dates, which represent the tick marks. In fact, it is even a little bit trickier, but if you're really interested in the details, have a look at the code ;-).

Last important method is the getTickMarkLabel(Date date), which converts a date to a String representation.

It just guesses what might be the best representation depending on the range / interval, which was used. E.g. if only years are displayed the date values are formatted only to display the year, where as smaller ranges are displayed with days or hours.

If you are in need of a DateAxis, give it a try and I hope you like it!