Problem with the DateAxis labels

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
Locked
airon
Posts: 4
Joined: Mon Apr 23, 2007 3:51 pm
Location: Spain

Problem with the DateAxis labels

Post by airon » Mon Apr 23, 2007 4:01 pm

In our application we have been using jfreechart-1.0.3 for some time, and recently we tried 1.0.4 and now 1.0.5. In both we had the next problem: the labels in the DateAxis were different. In 1.0.4 and 1.0.5 the starting label was just wrong. Our idea was to display a range of time, for example from 9 to 17 with labels every 15 minutes. That works correctly and automatically in versions <= 1.0.3, obtaining the following result:

9:15, 9:30, 9:45, and so on

With 1.0.4 and 1.0.5 the result is now different:

9:14, 9:29, 9:44, ...

So, as you see there's a delay of minute in the position where the label should be get.

I've been investigating the source code and I found the place that causes that:

In DataAxis.java:

Code: Select all

protected Date previousStandardDate(Date date, DateTickUnit unit) {

                int milliseconds;
                int seconds;
                int minutes;
                int hours;
                int days;
                int months;
                int years;

                Calendar calendar = Calendar.getInstance(getTimeZone());
                calendar.setTime(date);
                int count = unit.getCount();
                int current = calendar.get(unit.getCalendarField());
                int value = count * (current / count);

                switch (unit.getUnit()) {

                    case (DateTickUnit.MILLISECOND) :
                        years = calendar.get(Calendar.YEAR);
                        months = calendar.get(Calendar.MONTH);
                        days = calendar.get(Calendar.DATE);
                        hours = calendar.get(Calendar.HOUR_OF_DAY);
                        minutes = calendar.get(Calendar.MINUTE);
                        seconds = calendar.get(Calendar.SECOND);
                        calendar.set(years, months, days, hours, minutes, seconds);
                        calendar.set(Calendar.MILLISECOND, value);
                        return calendar.getTime();

                    case (DateTickUnit.SECOND) :
                        years = calendar.get(Calendar.YEAR);
                        months = calendar.get(Calendar.MONTH);
                        days = calendar.get(Calendar.DATE);
                        hours = calendar.get(Calendar.HOUR_OF_DAY);
                        minutes = calendar.get(Calendar.MINUTE);
                        if (getTickMarkPosition() == DateTickMarkPosition.START) {
                            milliseconds = 0;
                        }
                        else if (getTickMarkPosition() == DateTickMarkPosition.MIDDLE) {
                            milliseconds = 500;
                        }
                        else {
                            milliseconds = 999;
                        }
                        calendar.set(Calendar.MILLISECOND, milliseconds);
                        calendar.set(years, months, days, hours, minutes, value);
                        return calendar.getTime();

                    case (DateTickUnit.MINUTE) :
                        years = calendar.get(Calendar.YEAR);
                        months = calendar.get(Calendar.MONTH);
                        days = calendar.get(Calendar.DATE);
                        hours = calendar.get(Calendar.HOUR_OF_DAY);
                        if (getTickMarkPosition() == DateTickMarkPosition.START) {
                            seconds = 0;
                        }
                        else if (getTickMarkPosition() == DateTickMarkPosition.MIDDLE) {
                            seconds = 30;
                        }
                        else {
                            seconds = 59;
                        }
                        calendar.clear(Calendar.MILLISECOND);
                        calendar.set(years, months, days, hours, value, seconds);
                        Date d0 = calendar.getTime();
                        if (d0.getTime() >= date.getTime()) {
                            calendar.set(Calendar.MINUTE, value - 1);
                            d0 = calendar.getTime();
                        }
                        return d0;

                    case (DateTickUnit.HOUR) :
                        years = calendar.get(Calendar.YEAR);
                        months = calendar.get(Calendar.MONTH);
                        days = calendar.get(Calendar.DATE);
                        if (getTickMarkPosition() == DateTickMarkPosition.START) {
                            minutes = 0;
                            seconds = 0;
                        }
                        else if (getTickMarkPosition() == DateTickMarkPosition.MIDDLE) {
                            minutes = 30;
                            seconds = 0;
                        }
                        else {
                            minutes = 59;
                            seconds = 59;
                        }
                        calendar.clear(Calendar.MILLISECOND);
                        calendar.set(years, months, days, value, minutes, seconds);
                        Date d1 = calendar.getTime();
                        if (d1.getTime() >= date.getTime()) {
                            calendar.set(Calendar.HOUR_OF_DAY, value - 1);
                            d1 = calendar.getTime();
                        }
                        return d1;

                    case (DateTickUnit.DAY) :
                        years = calendar.get(Calendar.YEAR);
                        months = calendar.get(Calendar.MONTH);
                        if (getTickMarkPosition() == DateTickMarkPosition.START) {
                            hours = 0;
                            minutes = 0;
                            seconds = 0;
                        }
                        else if (getTickMarkPosition() == DateTickMarkPosition.MIDDLE) {
                            hours = 12;
                            minutes = 0;
                            seconds = 0;
                        }
                        else {
                            hours = 23;
                            minutes = 59;
                            seconds = 59;
                        }
                        calendar.clear(Calendar.MILLISECOND);
                        calendar.set(years, months, value, hours, 0, 0);
                        // long result = calendar.getTimeInMillis();  
                            // won't work with JDK 1.3
                        Date d2 = calendar.getTime();
                        if (d2.getTime() >= date.getTime()) {
                            calendar.set(Calendar.DATE, value - 1);
                            d2 = calendar.getTime();
                        }
                        return d2;

                    case (DateTickUnit.MONTH) :
                        years = calendar.get(Calendar.YEAR);
                        calendar.clear(Calendar.MILLISECOND);
                        calendar.set(years, value, 1, 0, 0, 0);
                        Month month = new Month(calendar.getTime());
                        Date standardDate = calculateDateForPosition(
                                month, getTickMarkPosition());
                        long millis = standardDate.getTime();
                        if (millis > date.getTime()) {
                            month = (Month) month.previous();
                            standardDate = calculateDateForPosition(
                                    month, getTickMarkPosition());
                        }
                        return standardDate;

                    case(DateTickUnit.YEAR) :
                        if (getTickMarkPosition() == DateTickMarkPosition.START) {
                            months = 0;
                            days = 1;
                        }
                        else if (getTickMarkPosition() == DateTickMarkPosition.MIDDLE) {
                            months = 6;
                            days = 1;
                        }
                        else {
                            months = 11;
                            days = 31;
                        }
                        calendar.clear(Calendar.MILLISECOND);
                        calendar.set(value, months, days, 0, 0, 0);
                        Date d3 = calendar.getTime();
                        if (d3.getTime() >= date.getTime()) {
                            calendar.set(Calendar.YEAR, value - 1);
                            d3 = calendar.getTime();
                        }
                        return d3;

                    default: return null;

                }
In particular in the section:

Code: Select all

case (DateTickUnit.MINUTE) :
                        years = calendar.get(Calendar.YEAR);
                        months = calendar.get(Calendar.MONTH);
                        days = calendar.get(Calendar.DATE);
                        hours = calendar.get(Calendar.HOUR_OF_DAY);
                        if (getTickMarkPosition() == DateTickMarkPosition.START) {
                            seconds = 0;
                        }
                        else if (getTickMarkPosition() == DateTickMarkPosition.MIDDLE) {
                            seconds = 30;
                        }
                        else {
                            seconds = 59;
                        }
                        calendar.clear(Calendar.MILLISECOND);
                        calendar.set(years, months, days, hours, value, seconds);
                        Date d0 = calendar.getTime();
                        if (d0.getTime() >= date.getTime()) {
                            calendar.set(Calendar.MINUTE, value - 1);
                            d0 = calendar.getTime();
                        }
                        return d0;
The problem is the >= comparison done, which seems to force to take the minute before instead of the required minute.

So if I replace:

Code: Select all

if (d0.getTime() >= date.getTime()) {
                            calendar.set(Calendar.MINUTE, value - 1);
                            d0 = calendar.getTime();
                        }
with:

Code: Select all

if (d0.getTime() > date.getTime()) {
                            calendar.set(Calendar.MINUTE, value - 1);
                            d0 = calendar.getTime();
                        }
then I obtain the labels in the positions that I expect to have.

airon
Posts: 4
Joined: Mon Apr 23, 2007 3:51 pm
Location: Spain

Post by airon » Mon Jun 18, 2007 10:33 am

This problem persists in jfreechart-1.0.6. I still have to apply the patch when using the new version.

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Mon Jun 18, 2007 10:40 am

Oops! I'll take another look at this...
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

joolz
Posts: 56
Joined: Thu Nov 17, 2005 2:38 am
Location: Australia

Post by joolz » Thu May 29, 2008 10:10 am

David, did you get around to looking at this?

Using 1.0.9, I am experiencing the exact same problem (behaviour!?!) as all the timestamps in my dataset are at 0:00, 0:15, 0:30, 0:45 etc, but sometimes, depending on the width of the graph, it shows 0:14, 0:29, 0:44 etc.

A quick look into the DateAxis.java code shows that it hasn't changed. I made the same modification as airon and the problem was resolved.

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Mon Jun 02, 2008 9:08 pm

I'm trying to reproduce this, but failing so far...although I'm pretty sure I've seen this happen before. Any chance you could post a small(ish) self-contained demo showing the fault?

Changing the '>=' to '>' seems wrong based on what the method says it does, but it might be that the adjustment can be made further up the call chain. I just need to reproduce it to see...
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

joolz
Posts: 56
Joined: Thu Nov 17, 2005 2:38 am
Location: Australia

Post by joolz » Tue Jun 03, 2008 12:35 am

I used production code to generate the dates, hence all the longs (they are on the 15 minute mark - 0:00, 0:15: 0:30 etc), but it reproduces the 0:14, 0:29, 0:44 axis label behaviour.

Code: Select all

package demo;

import java.awt.BasicStroke;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.swing.JPanel;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.time.Minute;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;

public class AxisLabelTest extends ApplicationFrame
{
	public AxisLabelTest(String title)
	{
		super(title);
		TimeSeriesCollection dataset = createDataset();
		JFreeChart chart = createChart(dataset);
		ChartPanel chartPanel = new ChartPanel(chart, false);
		chartPanel.setPreferredSize(new java.awt.Dimension(800, 500));
		chartPanel.setMouseZoomable(true, false);
		setContentPane(chartPanel);
	}

	private static JFreeChart createChart(TimeSeriesCollection dataset)
	{
		JFreeChart chart = ChartFactory.createTimeSeriesChart(null, "Time of day", "Value", dataset, true, true, false);
		XYPlot plot = (XYPlot) chart.getPlot();
		ValueAxis yaxis = plot.getRangeAxis();
		yaxis.setLowerBound(0);
		DateAxis xaxis = (DateAxis)plot.getDomainAxis();
		xaxis.setLowerMargin(0);
		xaxis.setUpperMargin(0);
		xaxis.setDateFormatOverride(new SimpleDateFormat("h:mm a"));
		xaxis.setVerticalTickLabels(true);
		TickUnits standardUnits = new TickUnits();
		standardUnits.add(new DateTickUnit(DateTickUnit.MINUTE, 0));
		standardUnits.add(new DateTickUnit(DateTickUnit.MINUTE, 15));
		standardUnits.add(new DateTickUnit(DateTickUnit.MINUTE, 30));
		standardUnits.add(new DateTickUnit(DateTickUnit.MINUTE, 45));
		xaxis.setStandardTickUnits(standardUnits);
		plot.setDomainAxis(xaxis);
		XYItemRenderer renderer = plot.getRenderer();
		renderer.setBaseStroke(new BasicStroke(1.2f));
		return chart;
	}

	private static TimeSeriesCollection createDataset()
	{
		TimeSeries monday = new TimeSeries("Data", Minute.class);
		monday.add(new Minute(new Date(946648800000L)), 5.691, false);
		monday.add(new Minute(new Date(946649700000L)), 5.991, false);
		monday.add(new Minute(new Date(946650600000L)), 4.837, false);
		monday.add(new Minute(new Date(946651500000L)), 7.274, false);
		monday.add(new Minute(new Date(946652400000L)), 4.600, false);
		monday.add(new Minute(new Date(946653300000L)), 5.734, false);
		monday.add(new Minute(new Date(946654200000L)), 4.685, false);
		monday.add(new Minute(new Date(946655100000L)), 5.349, false);
		monday.add(new Minute(new Date(946656000000L)), 4.424, false);
		monday.add(new Minute(new Date(946657800000L)), 17.018, false);
		monday.add(new Minute(new Date(946658700000L)), 7.578, false);
		monday.add(new Minute(new Date(946659600000L)), 6.941, false);
		monday.add(new Minute(new Date(946660500000L)), 7.323, false);
		monday.add(new Minute(new Date(946661400000L)), 6.019, false);
		monday.add(new Minute(new Date(946662300000L)), 6.11, false);
		monday.add(new Minute(new Date(946663200000L)), 5.407, false);
		monday.add(new Minute(new Date(946664100000L)), 6.403, false);
		monday.add(new Minute(new Date(946665000000L)), 5.96, false);
		monday.add(new Minute(new Date(946665900000L)), 10.61, false);
		monday.add(new Minute(new Date(946666800000L)), 7.234, false);
		monday.add(new Minute(new Date(946667700000L)), 7.801, false);
		monday.add(new Minute(new Date(946668600000L)), 6.41, false);
		monday.add(new Minute(new Date(946669500000L)), 7.747, false);
		monday.add(new Minute(new Date(946670400000L)), 7.149, false);
		monday.add(new Minute(new Date(946671300000L)), 11.332, false);
		monday.add(new Minute(new Date(946672200000L)), 12.406, false);
		monday.add(new Minute(new Date(946673100000L)), 11.629, false);
		monday.add(new Minute(new Date(946674000000L)), 15.563, false);
		monday.add(new Minute(new Date(946674900000L)), 12.256, false);
		monday.add(new Minute(new Date(946675800000L)), 12.887, false);
		monday.add(new Minute(new Date(946676700000L)), 14.657, false);
		monday.add(new Minute(new Date(946677600000L)), 17.397, false);
		monday.add(new Minute(new Date(946678500000L)), 17.177, false);
		monday.add(new Minute(new Date(946679400000L)), 18.426, false);
		monday.add(new Minute(new Date(946680300000L)), 27.764, false);
		monday.add(new Minute(new Date(946681200000L)), 19.733, false);
		monday.add(new Minute(new Date(946682100000L)), 19.892, false);
		monday.add(new Minute(new Date(946683000000L)), 19.327, false);
		monday.add(new Minute(new Date(946683900000L)), 20.958, false);
		monday.add(new Minute(new Date(946684800000L)), 21.947, false);
		monday.add(new Minute(new Date(946685700000L)), 20.704, false);
		monday.add(new Minute(new Date(946686600000L)), 20.727, false);
		monday.add(new Minute(new Date(946687500000L)), 28.04, false);
		monday.add(new Minute(new Date(946688400000L)), 25.956, false);
		monday.add(new Minute(new Date(946689300000L)), 14.345, false);
		monday.add(new Minute(new Date(946690200000L)), 28.63, false);
		monday.add(new Minute(new Date(946691100000L)), 21.573, false);
		monday.add(new Minute(new Date(946692000000L)), 19.206, false);
		monday.add(new Minute(new Date(946692900000L)), 21.152, false);
		monday.add(new Minute(new Date(946693800000L)), 28.972, false);
		monday.add(new Minute(new Date(946694700000L)), 22.906, false);
		monday.add(new Minute(new Date(946695600000L)), 22.965, false);
		monday.add(new Minute(new Date(946696500000L)), 23.134, false);
		monday.add(new Minute(new Date(946698300000L)), 46.642, false);
		monday.add(new Minute(new Date(946699200000L)), 24.871, false);
		monday.add(new Minute(new Date(946700100000L)), 24.413, false);
		monday.add(new Minute(new Date(946701000000L)), 30.53, false);
		monday.add(new Minute(new Date(946701900000L)), 23.538, false);
		monday.add(new Minute(new Date(946702800000L)), 24.973, false);
		monday.add(new Minute(new Date(946703700000L)), 22.246, false);
		monday.add(new Minute(new Date(946704600000L)), 20.501, false);
		monday.add(new Minute(new Date(946705500000L)), 21.449, false);
		monday.add(new Minute(new Date(946706400000L)), 20.86, false);
		monday.add(new Minute(new Date(946707300000L)), 22.015, false);
		monday.add(new Minute(new Date(946708200000L)), 28.226, false);
		monday.add(new Minute(new Date(946709100000L)), 18.267, false);
		monday.add(new Minute(new Date(946710000000L)), 16.572, false);
		monday.add(new Minute(new Date(946710900000L)), 9.48, false);
		monday.add(new Minute(new Date(946711800000L)), 18.105, false);
		monday.add(new Minute(new Date(946712700000L)), 13.118, false);
		monday.add(new Minute(new Date(946713600000L)), 18.582, false);
		monday.add(new Minute(new Date(946714500000L)), 15.609, false);
		monday.add(new Minute(new Date(946715400000L)), 14.193, false);
		monday.add(new Minute(new Date(946716300000L)), 10.027, false);
		monday.add(new Minute(new Date(946717200000L)), 10.712, false);
		monday.add(new Minute(new Date(946718100000L)), 9.458, false);
		monday.add(new Minute(new Date(946719000000L)), 13.534, false);
		monday.add(new Minute(new Date(946719900000L)), 6.958, false);
		monday.add(new Minute(new Date(946720800000L)), 7.465, false);
		monday.add(new Minute(new Date(946721700000L)), 7.431, false);
		monday.add(new Minute(new Date(946722600000L)), 6.982, false);
		monday.add(new Minute(new Date(946723500000L)), 7.020, false);
		monday.add(new Minute(new Date(946724400000L)), 9.643, false);
		monday.add(new Minute(new Date(946725300000L)), 6.745, false);
		monday.add(new Minute(new Date(946726200000L)), 6.783, false);
		monday.add(new Minute(new Date(946727100000L)), 6.871, false);
		monday.add(new Minute(new Date(946728000000L)), 6.998, false);
		monday.add(new Minute(new Date(946728900000L)), 6.728, false);
		monday.add(new Minute(new Date(946729800000L)), 9.135, false);
		monday.add(new Minute(new Date(946730700000L)), 5.322, false);
		monday.add(new Minute(new Date(946731600000L)), 6.149, false);
		monday.add(new Minute(new Date(946732500000L)), 6.267, false);
		monday.add(new Minute(new Date(946733400000L)), 6.452, false);
		monday.add(new Minute(new Date(946734300000L)), 6.24, false);
		TimeSeriesCollection dataset = new TimeSeriesCollection();
		dataset.addSeries(monday);
		return dataset;
	}

	public static JPanel createDemoPanel()
	{
		JFreeChart chart = createChart(createDataset());
		return new ChartPanel(chart);
	}

	public static void main(String[] args)
	{
		AxisLabelTest demo = new AxisLabelTest("AxisLabelTest");
		demo.pack();
		RefineryUtilities.centerFrameOnScreen(demo);
		demo.setVisible(true);
	}
}

david.gilbert
JFreeChart Project Leader
Posts: 11734
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

Post by david.gilbert » Wed Jun 04, 2008 9:15 pm

Thanks for the demo code. I think the problem lies in the calculateLowestVisibleTickValue() method which assumes that the lower bound of the axis is NOT a standard date - when it is, like in your demo, you get this odd shift.

I'm still trying to figure out the best way to fix this.
David Gilbert
JFreeChart Project Leader

:idea: Read my blog
:idea: Support JFree via the Github sponsorship program

luizihjei
Posts: 9
Joined: Thu Sep 03, 2009 9:41 am
antibot: No, of course not.

Re: Problem with the DateAxis labels

Post by luizihjei » Mon Sep 07, 2009 4:53 am

Does Jfreechart 1.0.13 has solved this problem?

olawson
Posts: 1
Joined: Mon Nov 02, 2009 4:38 am
antibot: No, of course not.

Re: Problem with the DateAxis labels

Post by olawson » Mon Nov 02, 2009 4:59 am

luizihjei wrote:Does Jfreechart 1.0.13 has solved this problem?
Jfreechart 1.0.13 does not fix this issue, but I worked around it by creating a DateAxis subclass and overriding the calculateLowestVisibleTickValue method with the following:

Code: Select all

    public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
    	// To avoid date axis bug: http://www.jfree.org/phpBB2/viewtopic.php?f=3&t=20993
    	long unitsInMilliSec = (long)unit.getSize();
    	Date minimumDate = getMinimumDate();
    	Date newStart = nextStandardDate(minimumDate,unit);

    	if (unitsInMilliSec < 86400000 /* 24*3600*1000) */) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(newStart);
    		long tzOff = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); 
	    	long mod = (newStart.getTime() + tzOff) % unitsInMilliSec;
	    	if (mod != 0) {
	    		newStart = new Date(newStart.getTime() - mod);
	    	}
	    	if (newStart.before(minimumDate)) {
	    		newStart = new Date(newStart.getTime() + unitsInMilliSec);
	    	}
    	}
    	
    	return newStart;
    }
The code ensures that the tick mark times displayed are always an integral number of units from midnight.

Locked