Skip to content
Snippets Groups Projects
Commit 8df896a7 authored by Tim Molter's avatar Tim Molter
Browse files

complete refactor of date format and axis logic

parent 1aa2104c
No related branches found
Tags
No related merge requests found
......@@ -94,7 +94,7 @@ public class AxisTickBarChartCalculator extends AxisTickCalculator {
else if (chartPainter.getAxisPair().getXAxis().getAxisType() == AxisType.Date) {
long span = (long) Math.abs(maxValue - minValue); // in data space
long gridStepHint = (long) (span / (double) tickSpace * styleManager.getXAxisTickMarkSpacingHint());
long timeUnit = dateFormatter.getTimeUnit(gridStepHint);
long timeUnit = dateFormatter.getTimeSpan(gridStepHint);
tickLabels.add(dateFormatter.formatDate(((Number) ((Date) category).getTime()).doubleValue(), timeUnit));
}
double tickLabelPosition = (int) (margin + firstPosition + gridStep * counter++);
......
......@@ -152,7 +152,11 @@ public abstract class AxisTickCalculator {
// System.out.println("largestLabelWidth: " + largestLabelWidth);
// System.out.println("tickSpacingHint: " + tickSpacingHint);
return (largestLabelWidth * 1.6 < tickSpacingHint);
// if (largestLabelWidth * 1.1 >= tickSpacingHint) {
// System.out.println("WILL NOT FIT!!!");
// }
return (largestLabelWidth * 1.1 < tickSpacingHint);
}
}
......@@ -15,6 +15,11 @@
*/
package com.xeiam.xchart.internal.chartpart;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.xeiam.xchart.StyleManager;
import com.xeiam.xchart.internal.Utils;
import com.xeiam.xchart.internal.chartpart.Axis.Direction;
......@@ -26,7 +31,72 @@ import com.xeiam.xchart.internal.chartpart.Axis.Direction;
*/
public class AxisTickDateCalculator extends AxisTickCalculator {
DateFormatter dateFormatter;
private static final long MILLIS_SCALE = TimeUnit.MILLISECONDS.toMillis(1L);
private static final long SEC_SCALE = TimeUnit.SECONDS.toMillis(1L);
private static final long MIN_SCALE = TimeUnit.MINUTES.toMillis(1L);
private static final long HOUR_SCALE = TimeUnit.HOURS.toMillis(1L);
private static final long DAY_SCALE = TimeUnit.DAYS.toMillis(1L);
private static final long MONTH_SCALE = TimeUnit.DAYS.toMillis(1L) * 30;
// private static final long QUARTER_SCALE = TimeUnit.DAYS.toMillis(1L) * 120;
private static final long YEAR_SCALE = TimeUnit.DAYS.toMillis(1L) * 365;
private static List<TimeSpan> timeSpans = new ArrayList<TimeSpan>();
static {
timeSpans.add(new TimeSpan(MILLIS_SCALE, 1, "ss.SSS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 2, "ss.SSS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 5, "ss.SSS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 10, "ss.SSS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 50, "ss.SS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 100, "ss.SS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 200, "ss.SS"));
timeSpans.add(new TimeSpan(MILLIS_SCALE, 500, "ss.SS"));
timeSpans.add(new TimeSpan(SEC_SCALE, 1, "ss.SS"));
timeSpans.add(new TimeSpan(SEC_SCALE, 2, "ss.S"));
timeSpans.add(new TimeSpan(SEC_SCALE, 5, "ss.S"));
timeSpans.add(new TimeSpan(SEC_SCALE, 10, "HH:mm:ss"));
timeSpans.add(new TimeSpan(SEC_SCALE, 15, "HH:mm:ss"));
timeSpans.add(new TimeSpan(SEC_SCALE, 20, "HH:mm:ss"));
timeSpans.add(new TimeSpan(SEC_SCALE, 30, "HH:mm:ss"));
timeSpans.add(new TimeSpan(MIN_SCALE, 1, "HH:mm:ss"));
timeSpans.add(new TimeSpan(MIN_SCALE, 2, "HH:mm:ss"));
timeSpans.add(new TimeSpan(MIN_SCALE, 5, "HH:mm:ss"));
timeSpans.add(new TimeSpan(MIN_SCALE, 10, "HH:mm"));
timeSpans.add(new TimeSpan(MIN_SCALE, 15, "HH:mm"));
timeSpans.add(new TimeSpan(MIN_SCALE, 20, "HH:mm"));
timeSpans.add(new TimeSpan(MIN_SCALE, 30, "HH:mm"));
timeSpans.add(new TimeSpan(HOUR_SCALE, 1, "HH:mm"));
timeSpans.add(new TimeSpan(HOUR_SCALE, 2, "HH:mm"));
timeSpans.add(new TimeSpan(HOUR_SCALE, 4, "HH:mm"));
timeSpans.add(new TimeSpan(HOUR_SCALE, 8, "HH:mm"));
timeSpans.add(new TimeSpan(HOUR_SCALE, 12, "HH:mm"));
timeSpans.add(new TimeSpan(DAY_SCALE, 1, "EEE HH:mm"));
timeSpans.add(new TimeSpan(DAY_SCALE, 2, "EEE HH:mm"));
timeSpans.add(new TimeSpan(DAY_SCALE, 3, "EEE HH:mm"));
timeSpans.add(new TimeSpan(DAY_SCALE, 5, "MM-dd"));
timeSpans.add(new TimeSpan(DAY_SCALE, 10, "MM-dd"));
timeSpans.add(new TimeSpan(DAY_SCALE, 15, "MM-dd"));
timeSpans.add(new TimeSpan(MONTH_SCALE, 1, "MM-dd"));
timeSpans.add(new TimeSpan(MONTH_SCALE, 2, "MM-dd"));
timeSpans.add(new TimeSpan(MONTH_SCALE, 3, "MM-dd"));
timeSpans.add(new TimeSpan(MONTH_SCALE, 4, "MM-dd"));
timeSpans.add(new TimeSpan(MONTH_SCALE, 6, "yyyy-MM"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 1, "yyyy-MM"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 2, "yyyy-MM"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 5, "yyyy"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 10, "yyyy"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 20, "yyyy"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 100, "yyyy"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 500, "yyyy"));
timeSpans.add(new TimeSpan(YEAR_SCALE, 1000, "yyyy"));
}
/**
* Constructor
......@@ -40,7 +110,6 @@ public class AxisTickDateCalculator extends AxisTickCalculator {
public AxisTickDateCalculator(Direction axisDirection, double workingSpace, double minValue, double maxValue, StyleManager styleManager) {
super(axisDirection, workingSpace, minValue, maxValue, styleManager);
dateFormatter = new DateFormatter(styleManager);
calculate();
}
......@@ -50,7 +119,8 @@ public class AxisTickDateCalculator extends AxisTickCalculator {
double tickSpace = styleManager.getAxisTickSpacePercentage() * workingSpace; // in plot space
// this prevents an infinite loop when the plot gets sized really small.
if (tickSpace < 10) {
if (tickSpace < styleManager.getXAxisTickMarkSpacingHint()) {
// System.out.println("Returning!");
return;
}
......@@ -59,43 +129,130 @@ public class AxisTickDateCalculator extends AxisTickCalculator {
// the span of the data
long span = (long) Math.abs(maxValue - minValue); // in data space
// System.out.println("span: " + span);
// Generate the labels first, see if they "look" OK and reiterate with an increased tickSpacingHint
int tickSpacingHint = styleManager.getXAxisTickMarkSpacingHint() - 5;
int tickSpacingHint = styleManager.getXAxisTickMarkSpacingHint();
int gridStepInChartSpace = 0;
// System.out.println("calculating ticks...");
long gridStepHint = (long) (span / tickSpace * tickSpacingHint); // in time units (ms)
// System.out.println("gridStepHint: " + gridStepHint);
//////////////////////////////////////////////
// iterate forward until the matching timespan is found
int index = 0;
for (int i = 0; i < timeSpans.size() - 1; i++) {
if (span < ((timeSpans.get(i).getUnitAmount() * timeSpans.get(i).getMagnitude() + timeSpans.get(i + 1).getUnitAmount() * timeSpans.get(i + 1).getMagnitude()) / 2.0)) {
index = i;
break;
}
}
// use the pattern from the first timeSpan
String datePattern = timeSpans.get(index).getDatePattern();
// System.out.println("index: " + index);
// iterate BACWARDS from previous point until the appropriate timespan is found for the gridStepHint
for (int i = index - 1; i > 0; i--) {
if (gridStepHint > timeSpans.get(i).getUnitAmount() * timeSpans.get(i).getMagnitude()) {
index = i;
break;
}
}
//////////////////////////////////////////////
// now increase the timespan until one is found where all the labels fit nicely. It will often be the first one.
index--;
do {
// System.out.println("calculating ticks...");
tickLabels.clear();
tickLocations.clear();
tickSpacingHint += 5;
long gridStepHint = (long) (span / tickSpace * tickSpacingHint);
long timeUnit = dateFormatter.getTimeUnit(gridStepHint);
double gridStep = 0.0;
int[] steps = dateFormatter.getValidTickStepsMap().get(timeUnit);
for (int i = 0; i < steps.length - 1; i++) {
if (gridStepHint < (timeUnit * steps[i] + timeUnit * steps[i + 1]) / 2.0) {
gridStep = timeUnit * steps[i];
break;
}
}
double gridStep = timeSpans.get(++index).getUnitAmount() * timeSpans.get(index).getMagnitude(); // in time units (ms)
// System.out.println("gridStep: " + gridStep);
gridStepInChartSpace = (int) (gridStep / span * tickSpace);
// System.out.println("gridStepInChartSpace: " + gridStepInChartSpace);
double firstPosition = getFirstPosition(gridStep);
// Define Date Pattern
// override pattern if one was explicitly given
if (styleManager.getDatePattern() != null) {
datePattern = styleManager.getDatePattern();
}
// System.out.println("datePattern: " + datePattern);
SimpleDateFormat simpleDateformat = new SimpleDateFormat(datePattern, styleManager.getLocale());
simpleDateformat.setTimeZone(styleManager.getTimezone());
simpleDateformat.applyPattern(datePattern);
// return simpleDateformat.format(value);
//////////////////////////////
// generate all tickLabels and tickLocations from the first to last position
for (double value = firstPosition; value <= maxValue + 2 * gridStep; value = value + gridStep) {
// if (value <= maxValue && value >= minValue) {
tickLabels.add(dateFormatter.formatDate(value, timeUnit));
///////////////////////////////
tickLabels.add(simpleDateformat.format(value));
// here we convert tickPosition finally to plot space, i.e. pixels
double tickLabelPosition = margin + ((value - minValue) / (maxValue - minValue) * tickSpace);
// System.out.println("tickLabelPosition: " + tickLabelPosition);
tickLocations.add(tickLabelPosition);
// }
}
} while (!willLabelsFitInTickSpaceHint(tickLabels, tickSpacingHint));
} while (!willLabelsFitInTickSpaceHint(tickLabels, gridStepInChartSpace));
}
static class TimeSpan {
private final long unitAmount;
private final int magnitude;
private final String datePattern;
/**
* Constructor
*
* @param unitAmount
* @param magnitude
* @param datePattern
*/
public TimeSpan(long unitAmount, int magnitude, String datePattern) {
this.unitAmount = unitAmount;
this.magnitude = magnitude;
this.datePattern = datePattern;
}
public long getUnitAmount() {
return unitAmount;
}
public int getMagnitude() {
return magnitude;
}
public String getDatePattern() {
return datePattern;
}
@Override
public String toString() {
return "TimeSpan [unitAmount=" + unitAmount + ", magnitude=" + magnitude + ", datePattern=" + datePattern + "]";
}
}
}
......@@ -60,7 +60,7 @@ public class AxisTickNumericalCalculator extends AxisTickCalculator {
double tickSpace = styleManager.getAxisTickSpacePercentage() * workingSpace; // in plot space
// this prevents an infinite loop when the plot gets sized really small.
if (tickSpace < 10) {
if (tickSpace < styleManager.getXAxisTickMarkSpacingHint()) {
return;
}
......@@ -77,12 +77,17 @@ public class AxisTickNumericalCalculator extends AxisTickCalculator {
if (axisDirection == Direction.Y && tickSpace < 160) {
tickSpacingHint = 25 - 5;
}
int gridStepInChartSpace = 0;
do {
// System.out.println("calculating ticks...");
tickLabels.clear();
tickLocations.clear();
tickSpacingHint += 5;
// System.out.println("tickSpacingHint: " + tickSpacingHint);
double gridStepHint = span / tickSpace * tickSpacingHint;
// gridStepHint --> significand * 10 ** exponent
......@@ -105,7 +110,7 @@ public class AxisTickNumericalCalculator extends AxisTickCalculator {
}
}
// calculate the grid step with hint.
// calculate the grid step width hint.
double gridStep;
if (significand > 7.5) {
// gridStep = 10.0 * 10 ** exponent
......@@ -124,11 +129,23 @@ public class AxisTickNumericalCalculator extends AxisTickCalculator {
gridStep = Utils.pow(10, exponent);
}
//////////////////////////
// System.out.println("gridStep: " + gridStep);
// System.out.println("***gridStepInChartSpace: " + gridStep / span * tickSpace);
gridStepInChartSpace = (int) (gridStep / span * tickSpace);
// System.out.println("gridStepInChartSpace: " + gridStepInChartSpace);
BigDecimal gridStepBigDecimal = BigDecimal.valueOf(gridStep);
// System.out.println("***gridStep: " + gridStep);
BigDecimal cleanedGridStep = gridStepBigDecimal.setScale(10, RoundingMode.HALF_UP).stripTrailingZeros(); // chop off any double imprecision
// System.out.println("cleanedGridStep: " + cleanedGridStep);
BigDecimal firstPosition = BigDecimal.valueOf(getFirstPosition(cleanedGridStep.doubleValue()));
// TODO figure this out. It happens once in a blue moon.
BigDecimal firstPosition = null;
try {
firstPosition = BigDecimal.valueOf(getFirstPosition(cleanedGridStep.doubleValue()));
} catch (java.lang.NumberFormatException e) {
System.out.println("cleanedGridStep: " + cleanedGridStep);
System.out.println("cleanedGridStep.doubleValue(): " + cleanedGridStep.doubleValue());
System.out.println("NumberFormatException caused by this number: " + getFirstPosition(cleanedGridStep.doubleValue()));
}
// System.out.println("firstPosition: " + firstPosition); // chop off any double imprecision
BigDecimal cleanedFirstPosition = firstPosition.setScale(10, RoundingMode.HALF_UP).stripTrailingZeros(); // chop off any double imprecision
// System.out.println("cleanedFirstPosition: " + cleanedFirstPosition);
......@@ -147,7 +164,7 @@ public class AxisTickNumericalCalculator extends AxisTickCalculator {
tickLocations.add(tickLabelPosition);
// }
}
} while (!willLabelsFitInTickSpaceHint(tickLabels, tickSpacingHint));
} while (!willLabelsFitInTickSpaceHint(tickLabels, gridStepInChartSpace));
}
......
/**
* Copyright 2011 - 2015 Xeiam LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.xeiam.xchart.internal.chartpart;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import com.xeiam.xchart.StyleManager;
/**
* @author timmolter
*/
public class DateFormatter {
public static final long MILLIS_SCALE = TimeUnit.MILLISECONDS.toMillis(1L);
public static final long SEC_SCALE = TimeUnit.SECONDS.toMillis(1L);
public static final long MIN_SCALE = TimeUnit.MINUTES.toMillis(1L);
public static final long HOUR_SCALE = TimeUnit.HOURS.toMillis(1L);
public static final long DAY_SCALE = TimeUnit.DAYS.toMillis(1L);
public static final long MONTH_SCALE = TimeUnit.DAYS.toMillis(1L) * 31;
public static final long YEAR_SCALE = TimeUnit.DAYS.toMillis(1L) * 365;
private Map<Long, int[]> validTickStepsMap;
private final StyleManager styleManager;
/**
* Constructor
*/
public DateFormatter(StyleManager styleManager) {
this.styleManager = styleManager;
validTickStepsMap = new TreeMap<Long, int[]>();
validTickStepsMap.put(MILLIS_SCALE, new int[] { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 });
validTickStepsMap.put(SEC_SCALE, new int[] { 1, 2, 5, 10, 15, 20, 30, 60 });
validTickStepsMap.put(MIN_SCALE, new int[] { 1, 2, 3, 5, 10, 15, 20, 30, 60 });
validTickStepsMap.put(HOUR_SCALE, new int[] { 1, 2, 4, 6, 12, 24 });
validTickStepsMap.put(DAY_SCALE, new int[] { 1, 2, 3, 5, 10, 15, 31 });
validTickStepsMap.put(MONTH_SCALE, new int[] { 1, 2, 3, 4, 6, 12 });
validTickStepsMap.put(YEAR_SCALE, new int[] { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 });
}
/**
* @param gridStepHint
* @return
*/
public long getTimeUnit(long gridStepHint) {
for (Entry<Long, int[]> entry : validTickStepsMap.entrySet()) {
long groupMagnitude = entry.getKey();
int[] steps = entry.getValue();
long validTickStepMagnitude = (long) ((groupMagnitude * steps[steps.length - 2] + groupMagnitude * steps[steps.length - 1]) / 2.0);
if (gridStepHint < validTickStepMagnitude) {
return groupMagnitude;
}
}
return YEAR_SCALE;
}
/**
* Format a date value
*
* @param value
* @param timeUnit
* @return
*/
public String formatDate(double value, long timeUnit) {
String datePattern;
if (styleManager.getDatePattern() == null) {
// intelligently set date pattern if none is given
if (timeUnit == MILLIS_SCALE) {
datePattern = "ss.SSS";
}
else if (timeUnit == SEC_SCALE) {
datePattern = "mm:ss";
}
else if (timeUnit == MIN_SCALE) {
datePattern = "HH:mm";
}
else if (timeUnit == HOUR_SCALE) {
datePattern = "HH:mm";
}
else if (timeUnit == DAY_SCALE) {
datePattern = "MM-dd";
}
else if (timeUnit == MONTH_SCALE) {
datePattern = "yyyy-MM";
}
else {
datePattern = "yyyy";
}
}
else {
datePattern = styleManager.getDatePattern();
}
SimpleDateFormat simpleDateformat = new SimpleDateFormat(datePattern, styleManager.getLocale());
simpleDateformat.setTimeZone(styleManager.getTimezone());
simpleDateformat.applyPattern(datePattern);
return simpleDateformat.format(value);
}
Map<Long, int[]> getValidTickStepsMap() {
return validTickStepsMap;
}
}
/**
* Copyright 2011 - 2015 Xeiam LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.xeiam.xchart;
import static org.fest.assertions.api.Assertions.assertThat;
import java.util.Locale;
import java.util.TimeZone;
import org.junit.Test;
import com.xeiam.xchart.internal.chartpart.DateFormatter;
/**
* @author timmolter
*/
public class DateFormatterTest {
private final Locale locale = Locale.US;
@Test
public void testDateFormatting() {
StyleManager styleManager = new StyleManager();
DateFormatter dateFormatter = new DateFormatter(styleManager);
TimeZone timeZone = TimeZone.getTimeZone("UTC");
styleManager.setLocale(locale);
styleManager.setTimezone(timeZone);
// ms
double value = 1358108105531L;
double min = 1358108105100L;
double max = 1358108105900L;
double span = Math.abs(max - min); // in data space
long gridStepHint = (long) (span / 1000 * 74);
long timeUnit = dateFormatter.getTimeUnit(gridStepHint);
String stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("05.531");
// sec
value = 1358108105000L;
min = 1358108101000L;
max = 1358108109000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("05.000");
// min
value = 1358111750000L;
min = 1358111690000L;
max = 1358111870000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("15:50");
// hour
value = 1358111870000L;
min = 1358101070000L;
max = 1358115470000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("21:17");
// day
value = 1358112317000L;
min = 1357939517000L;
max = 1358285117000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("21:25");
// week
value = 1358112317000L;
min = 1357075517000L;
max = 1359149117000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("01-13");
// month
value = 1358112838000L;
min = 1354397638000L;
max = 1361223238000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("01-13");
// year
value = 1358113402000L;
min = 1263419002000L;
max = 1421185402000L;
span = Math.abs(max - min); // in data space
gridStepHint = (long) (span / 1000 * 74);
timeUnit = dateFormatter.getTimeUnit(gridStepHint);
stringValue = dateFormatter.formatDate(value, timeUnit);
assertThat(stringValue).isEqualTo("2013-01");
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment