#561 Tick units don't use date format to avoid overlapping

closed
General (896)
5
2014-08-28
2005-10-24
m.hilpert
No

I wrote my own standard tick units with the method
below. I noticed that the format of the tick label can
produce overlapped labels.

-------------------------------------------
/**
* Create tick units for 1 explicit time period..
*
* @param timePeriod A JFreeChart
RegularTimePeriod class (e.g. org.jfree.data.time.Month).
* @param dateFormat Optional format string to
display tick labels.
* @param timeZone Optional time zone (default:
TimeZone.getDefault()).
* @return Tick unit source for given
<i>timePeriod</i>.
*/
public TickUnitSource createStandardTickUnits(Class
timePeriod, String dateFormat, TimeZone timeZone) {
TickUnits result = new TickUnits();

if (timePeriod != null) {
String f = null; //default date format
if (timePeriod == Hour.class) {
f = "HH d-MM";
} else if (timePeriod == Day.class) {
f = "d-MM-yy";
} else if (timePeriod == Week.class) {
f = "w.yy";
} else if (timePeriod == Month.class) {
f = "MMM-yy";
} else if (timePeriod == Quarter.class) {
f = "MMM-yy";
} else if (timePeriod == Year.class) {
f = "yyyy";
}

if (dateFormat == null) {
dateFormat = f;
}

if (timeZone == null) {
timeZone = TimeZone.getDefault(); //default
}

DateFormat df = new SimpleDateFormat
(dateFormat);
df.setTimeZone(timeZone);

if (timePeriod == Hour.class) {
for (int i = 1; i <= 2400; i++) { //most labels
for 100days without overlapping
result.add(new DateTickUnit
(DateTickUnit.HOUR, i, DateTickUnit.HOUR, 1, df));
}
} else if (timePeriod == Day.class) {
for (int i = 1; i <= 3650; i++) { //most labels
for 10 years without overlapping
result.add(new DateTickUnit
(DateTickUnit.DAY, i, DateTickUnit.DAY, 1, df));
}
} else if (timePeriod == Week.class) {
for (int i = 1; i <= 120; i++) { //most labels for
10 years without overlapping
result.add(new DateTickUnit
(DateTickUnit.MONTH, i, DateTickUnit.MONTH, 1, df));
}
} else if (timePeriod == Month.class) {
for (int i = 1; i <= 120; i++) { //most labels for
10 years without overlapping
result.add(new DateTickUnit
(DateTickUnit.MONTH, i, DateTickUnit.MONTH, 1, df));
}
} else if (timePeriod == Quarter.class) {
result.add(new DateTickUnit
(DateTickUnit.MONTH, 3, DateTickUnit.MONTH, 1,
df)); //show first month of each quarter
} else if (timePeriod == Year.class) {
for (int i = 1; i <= 100; i++) { //most labels for
100 years without overlapping
result.add(new DateTickUnit
(DateTickUnit.YEAR, i, DateTickUnit.YEAR, 1, df));
}
}
}//else: input unavailable

return result;
}//createStandardTickUnits()
------------------------------

I use this method for an example chart:

-----------------------
/**
* Chart with time series for x axis.
*
* Tested with JFreeChart 1.0.0-rc1
*
* @return JFreeChart.
*/
private JFreeChart testDateAxis2() {
JFreeChart result = null;

TimeSeriesCollection ds = new
TimeSeriesCollection();
TimeSeries ts = new TimeSeries
("series1", "dates", "amounts", Month.class);
SimpleDateFormat sdf = new SimpleDateFormat
("yyyy-MM-dd");
sdf.setLenient(false); //strict parsing
ts.add(new Month(sdf.parse("2003-12-31", new
ParsePosition(0))), new BigDecimal(100.0));
ts.add(new Month(sdf.parse("2004-01-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-02-29", new
ParsePosition(0))), new BigDecimal(104.0));
ts.add(new Month(sdf.parse("2004-03-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-04-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-05-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-06-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-07-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-08-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-09-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-10-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2004-11-30", new
ParsePosition(0))), new BigDecimal(100.0));
ts.add(new Month(sdf.parse("2004-12-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-01-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-02-28", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-03-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-04-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-05-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-06-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-07-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-08-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-09-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-10-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2005-11-30", new
ParsePosition(0))), new BigDecimal(100.0));
ts.add(new Month(sdf.parse("2005-12-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-01-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-02-28", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-03-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-04-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-05-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-06-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-07-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-08-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-09-30", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-10-31", new
ParsePosition(0))), new BigDecimal(106.0));
ts.add(new Month(sdf.parse("2006-11-30", new
ParsePosition(0))), new BigDecimal(100.0));
ts.add(new Month(sdf.parse("2006-12-31", new
ParsePosition(0))), new BigDecimal(106.0));
ds.addSeries(ts);

result = ChartFactory.createTimeSeriesChart
("Time Series Chart", "X", "Y", ds, true, false, false);

//set date format that causes the trouble:
XYPlot xyPlot = result.getXYPlot();
DateAxis da = (DateAxis) xyPlot.getDomainAxis();
da.setStandardTickUnits(createStandardTickUnits
(Hour.class, null, null));

return result;
}//testDateAxis2()
----------------------------------------------------

the result is that all tick labels overlap. If I just change
the format for hour from "HH d-MM" to "HH" the labels
don't overlap anymore. Hence, the label format can
produce overlapped labels.

Discussion

  • David Gilbert

    David Gilbert - 2005-11-02

    Logged In: YES
    user_id=112975

    All that is happening here is that JFreeChart keeps looking
    through your custom collection of tick units for larger and
    larger tick units to avoid the overlapping labels. It gets
    to the largest tick unit you've specified (2400 hours or 100
    days, resulting in 3 or 4 tick marks per year) and is
    forced to use that since there is no larger tick unit.
    Depending on the chart size, this can result in overlapping
    labels. The solution is to add some larger tick units to
    your custom collection, appropriate for the range of data
    values you are displaying.

    Another thing I noticed is that you've specified *every*
    unit between 1 and 2400 hours. That's allowed, but probably
    overkill. Who's going to notice the difference between tick
    marks at every 2400 hours along the axis, versus every 2399
    hours along the axis, versus every 2398 hours, and so on?
    It probably makes more sense to choose various multiples
    (100 hours, 200 hours, 500 hours, 1000 hours, 2000 hours).

    Regards,

    Dave Gilbert
    JFreeChart Project Leader

     
  • David Gilbert

    David Gilbert - 2005-11-02
    • labels: --> General
    • assigned_to: nobody --> mungady
    • status: open --> closed-rejected
     
  • m.hilpert

    m.hilpert - 2005-11-03

    Logged In: YES
    user_id=667728

    I understand the algo now, but this is not enough for generic
    charts. You will always have another case where a fixed set
    of such units is not enough or not optimal. Every once in a
    while I get an error report from our end users, that a chart
    shows "bad" axis lables: too less, too many, overlapping, too
    much space after the right last label, left first label too much
    to the left, etc. And these are reports that just occur for some
    charts (with the same chart definition, but other data to
    display). So, I'm looking for a general solution that is bullet
    proof for any amount of data items - very few, some, many,
    and a large set.

     
  • David Gilbert

    David Gilbert - 2005-11-03

    Logged In: YES
    user_id=112975

    The axis finds tick units through the TickUnitSource
    interface, so you can always write your own class that
    implements this (see StandardTickUnitSource for an example
    that works for a NumberAxis).

    Calculating an appropriate tick size isn't that hard, even
    for dates - the hard part is choosing a formatter for the
    tick labels. That's why JFreeChart uses a look-up table
    approach, so that the formatter can be tailored according to
    the particular tick size. It is completely customisable,
    since you can replace the look up table.

    The downside of the look-up table approach is that there can
    only be a finite number of tick sizes (and associated label
    formats) to choose from - but the default table in the
    DateAxis class handles the ranges from a few milliseconds
    right up to a few centuries, so that usually isn't a
    practical problem for date axes.

    Regards,

    Dave Gilbert
    JFreeChart Project Leader

     
  • m.hilpert

    m.hilpert - 2008-02-25

    Logged In: YES
    user_id=667728
    Originator: YES

    I can't agree with you:

    "The downside of the look-up table approach is that there can
    only be a finite number of tick sizes (and associated label
    formats) to choose from - but the default table in the
    DateAxis class handles the ranges from a few milliseconds
    right up to a few centuries, so that usually isn't a
    practical problem for date axes."

    In my case, I have a chart with a time span of 10 years (=> much less than a few centuries), and it's open to any size of time span. So this "endless" creation of DateTickUnits is pretty useless just to avoid overlapping. Every workaround I write is just doomed to break after some time as another example has an even wider time range and the labels overlapp again. So I keep on adding more and more DateTickUnits, keep on instantiating more objects, which can't be good solution just to avoid label overlapping.

    My current code is:

    --------------------------------------------------------------------
    /**
    * Create tick units for a time period (e.g. Month.class) and date format (e.g. "MMM yyyy").
    *
    * @param timePeriod A JFreeChart RegularTimePeriod class (e.g. org.jfree.data.time.Month).
    * @param dateFormat Optional format string to display tick labels (e.g. "MMM yyyy").
    * @param timeZone Optional time zone (default: TimeZone.getDefault()).
    * @return Tick unit source for given <i>timePeriod</i>.
    */
    @SuppressWarnings("unchecked")
    static public final TickUnitSource createStandardTickUnits(final Class<?> timePeriod, String dateFormat, TimeZone timeZone) {
    final TickUnits result = new TickUnits();

    if (timePeriod != null) {
    String f = null; //default date format
    if (timePeriod == Hour.class) {
    f = "HH d-MM";
    } else if (timePeriod == Day.class) {
    f = "d-MM-yy";
    } else if (timePeriod == Week.class) {
    f = "w-yy";
    } else if (timePeriod == Month.class) {
    f = "MM-yy";
    } else if (timePeriod == Quarter.class) {
    f = "MM-yy";
    } else if (timePeriod == Year.class) {
    f = "yyyy";
    }

    if (dateFormat == null) {
    dateFormat = f;
    }

    if (timeZone == null) {
    timeZone = TimeZone.getDefault(); //default
    }

    final DateFormat df = new SimpleDateFormat(dateFormat);
    df.setTimeZone(timeZone);

    if (timePeriod == Hour.class) {
    for (int i = 1; i <= 24; i++) {
    result.add(new DateTickUnit(DateTickUnit.HOUR, i, DateTickUnit.HOUR, 1, df));
    }
    } else if (timePeriod == Day.class) {
    for (int i = 1; i <= 31; i++) {
    result.add(new DateTickUnit(DateTickUnit.DAY, i, DateTickUnit.DAY, 1, df));
    }
    } else if (timePeriod == Week.class) {
    for (int i = 1; i <= 56; i++) {
    result.add(new DateTickUnit(DateTickUnit.MONTH, i, DateTickUnit.MONTH, 1, df));
    }
    } else if (timePeriod == Month.class) {
    for (int i = 1; i <= 120; i++) { //span 10 years to avoid overlapping
    result.add(new DateTickUnit(DateTickUnit.MONTH, i, DateTickUnit.MONTH, 1, df));
    }
    } else if (timePeriod == Quarter.class) {
    for (int i = 1; i <= 4; i++) {
    result.add(new DateTickUnit(DateTickUnit.YEAR, i, DateTickUnit.MONTH, 4, df));
    }
    } else if (timePeriod == Year.class) {
    result.add(new DateTickUnit(DateTickUnit.YEAR, 1, DateTickUnit.YEAR, 1, df));
    result.add(new DateTickUnit(DateTickUnit.YEAR, 2, DateTickUnit.YEAR, 1, df));
    result.add(new DateTickUnit(DateTickUnit.YEAR, 5, DateTickUnit.YEAR, 1, df));
    result.add(new DateTickUnit(DateTickUnit.YEAR, 10, DateTickUnit.YEAR, 1, df));
    result.add(new DateTickUnit(DateTickUnit.YEAR, 25, DateTickUnit.YEAR, 5, df));
    result.add(new DateTickUnit(DateTickUnit.YEAR, 50, DateTickUnit.YEAR, 10, df));
    result.add(new DateTickUnit(DateTickUnit.YEAR, 100, DateTickUnit.YEAR, 20, df));
    }
    }//else: input unavailable

    return result;
    }//createStandardTickUnits()
    ----------------------------------------------------------------------------------

     
  • m.hilpert

    m.hilpert - 2008-02-25
    • status: closed-rejected --> closed
     

Log in to post a comment.