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:

@Override
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!

Kommentare:

  1. Great job, thanks !!
    I am using it for a candle stick chart, and it works perfectly !
    But I have a question, is it possible to hide some periods ?
    I have no data on for week end, I would like to hide week end in my chart, is it possible ?

    AntwortenLöschen
  2. Hi, no that's not possible. And I am not sure, if you want to exclude weekends completely from the chart, which would mean the axis would no longer be linear, but would be warped. Or if you want to just avoid that weekends are displayed as tick label.

    The first one is probably hard to implement, as it affects the axis in a strange way. You would have to do it in getDisplayPosition() and try to calculate the position without taking weekends into account.
    The second one is easier. You would have to make sure in calculateTickValues that the result does not contain a weekend.

    AntwortenLöschen
  3. Superb job.. been very useful for me. I would also love to add more than one y-axis to the same plot. Is that possible for you to add that feature? like a common date axis on x and multiple y axes.

    AntwortenLöschen
    Antworten
    1. Sorry, I don't quite understand that requirement. If you want multiple axes, that isn't a feature, which can't be added to a single Axis implementation.

      Do you just need multiple series (like my image above, the yellow and green)? That can be achieved by the existing JavaFX chart implementation. See my example above.

      Löschen
  4. I am sorry for not being clear with my requirements. Please see the image in this link

    http://www.mathworks.com/matlabcentral/fx_files/30405/1/multiple_timeseries.png

    This is what i mean by multiple y-axis. The values on different datasets are not to be displayed on the same scale. So i need more than one axis. Hope you got my point.. Thanks a lot for sparing some time of yours.

    AntwortenLöschen
    Antworten
    1. This is a feature request you should file at JavaFX Jira. (https://javafx-jira.kenai.com/browse/RT). It is a Charts issue, not a DateAxis issue.

      Löschen
  5. I had a look at his solution looks quite complex. I am doing similar thing with except i just want to go to period level 2012 01 2012 02 so far all i have done is override the Formatter on ticks. Only problem is display ticks don't want. Be nice easy way to just change ticks to base 12 or something

    AntwortenLöschen
  6. Real great job! I was worrying whether I could use it several thousands of data in my series without having a Date object printed on my x axis for each item - all my worries have gone!

    AntwortenLöschen
  7. Is it possible to include the DateAxis to Scene Builder 2.0?

    AntwortenLöschen
    Antworten
    1. I'm not sure about it, I never worked with Scene Builder.

      Löschen
  8. I like the DateAxis, A very useful tool for real life applications. Thank you

    On small thing though. If I do a priceseries.getData().clear() and add priceseries.getData().add(new XYChart.Data(tradeDate, price)) and if the new data is in different date range, the date range of the Axis increases and the graph looks weird. I can overcome by lineChart.getXAxis().setAutoRanging(false)

    AntwortenLöschen
  9. Thanks!
    This helped me a lot to write a Date Axis for java.time.ZonedDateTime.

    AntwortenLöschen
  10. Looks good but is there any way to get it working with jfxutils zooomable pannable chart system. That seems to require Axis that are based on ValueAxis (which this is not) Would be awesome if it could work with that in a future release.

    AntwortenLöschen
    Antworten
    1. Unfortunately I don't see a way to to make DateAxis a ValueAxis.
      ValueAxis is "A axis who's data is defined as Numbers" (see JavaDoc), but Dates are no numbers.

      Löschen
  11. Hi,

    do you have sample code for using Date Axis in line chart?

    Thanks

    AntwortenLöschen
    Antworten
    1. Have a look at the samples:

      https://bitbucket.org/sco0ter/extfx/src/729c935ec306d3d5a6c5ef28f9c43b4352f11deb/src/test/java/extfx/samples/DateAxisSample.java?at=master&fileviewer=file-view-default

      Löschen
    2. This one
      "LineChart lineChart = new LineChart<>(dateAxis, numberAxis, series);"
      does not function.
      ==> "incompatible types: DateAxis cannot be convertes to Axis"

      Löschen
    3. It should work, because DateAxis extends Axis

      The samples in the repository compile and run.

      Löschen
    4. Möchte Dich zwar nicht nerven, aber der Zugang wird mir leider verwehrt... :-(
      ==> Access denied

      Löschen
    5. Oh, danke für den Hinweis. Sollte jetzt gehen.

      Löschen
  12. how can i make a scrollable line chart?

    AntwortenLöschen