From 77166f4474525ec7273690bea83d56266c6d3b6b Mon Sep 17 00:00:00 2001 From: Tim Molter <tim@knowm.org> Date: Thu, 1 Oct 2015 12:46:44 +0200 Subject: [PATCH] "smart" tick spacing hint generation to provent tick label crowding and overlap --- .../xeiam/xchart/internal/chartpart/Axis.java | 4 ++ .../xchart/internal/chartpart/AxisTick.java | 17 ++++--- .../chartpart/AxisTickCalculator.java | 38 ++++++++++++++ .../chartpart/AxisTickDateCalculator.java | 50 ++++++++++++------- 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/Axis.java b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/Axis.java index 1deb8df4..1cdfa436 100644 --- a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/Axis.java +++ b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/Axis.java @@ -270,6 +270,8 @@ public class Axis implements ChartPart { if (getChartPainter().getStyleManager().isXAxisTicksVisible()) { // get some real tick labels + // System.out.println("XAxisHeightHint"); + // System.out.println("workingSpace: " + workingSpace); AxisTickCalculator axisTickCalculator = axisTick.getAxisTickCalculator(workingSpace); String sampleLabel = " "; // find the longest String in all the labels @@ -279,11 +281,13 @@ public class Axis implements ChartPart { } } + // get the height of the label including rotation TextLayout textLayout = new TextLayout(sampleLabel, getChartPainter().getStyleManager().getAxisTickLabelsFont(), new FontRenderContext(null, true, false)); AffineTransform rot = getChartPainter().getStyleManager().getXAxisLabelRotation() == 0 ? null : AffineTransform.getRotateInstance(-1 * Math.toRadians(getChartPainter().getStyleManager() .getXAxisLabelRotation())); Shape shape = textLayout.getOutline(rot); Rectangle2D rectangle = shape.getBounds(); + axisTickLabelsHeight = rectangle.getHeight() + getChartPainter().getStyleManager().getAxisTickPadding() + getChartPainter().getStyleManager().getAxisTickMarkLength(); } return titleHeight + axisTickLabelsHeight; diff --git a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTick.java b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTick.java index ec0f3236..6331a8fd 100644 --- a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTick.java +++ b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTick.java @@ -74,6 +74,8 @@ public class AxisTick implements ChartPart { // System.out.println("workingspace= " + workingSpace); } + System.out.println("AxisTick: " + axis.getDirection()); + // System.out.println("workingSpace: " + workingSpace); axisTickCalculator = getAxisTickCalculator(workingSpace); if (axis.getDirection() == Axis.Direction.Y && getChartPainter().getStyleManager().isYAxisTicksVisible()) { @@ -83,15 +85,15 @@ public class AxisTick implements ChartPart { bounds = new Rectangle2D.Double( - axisTickLabels.getBounds().getX(), + axisTickLabels.getBounds().getX(), - axisTickLabels.getBounds().getY(), + axisTickLabels.getBounds().getY(), - axisTickLabels.getBounds().getWidth() + getChartPainter().getStyleManager().getAxisTickPadding() + axisTickMarks.getBounds().getWidth(), + axisTickLabels.getBounds().getWidth() + getChartPainter().getStyleManager().getAxisTickPadding() + axisTickMarks.getBounds().getWidth(), - axisTickMarks.getBounds().getHeight() + axisTickMarks.getBounds().getHeight() - ); + ); // g.setColor(Color.red); // g.draw(bounds); @@ -102,9 +104,8 @@ public class AxisTick implements ChartPart { axisTickLabels.paint(g); axisTickMarks.paint(g); - bounds = - new Rectangle2D.Double(axisTickMarks.getBounds().getX(), axisTickMarks.getBounds().getY(), axisTickLabels.getBounds().getWidth(), axisTickMarks.getBounds().getHeight() - + getChartPainter().getStyleManager().getAxisTickPadding() + axisTickLabels.getBounds().getHeight()); + bounds = new Rectangle2D.Double(axisTickMarks.getBounds().getX(), axisTickMarks.getBounds().getY(), axisTickLabels.getBounds().getWidth(), axisTickMarks.getBounds().getHeight() + + getChartPainter().getStyleManager().getAxisTickPadding() + axisTickLabels.getBounds().getHeight()); // g.setColor(Color.red); // g.draw(bounds); diff --git a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickCalculator.java b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickCalculator.java index ad5f53bd..8f5d5723 100644 --- a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickCalculator.java +++ b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickCalculator.java @@ -15,6 +15,11 @@ */ package com.xeiam.xchart.internal.chartpart; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; import java.util.LinkedList; import java.util.List; @@ -116,4 +121,37 @@ public abstract class AxisTickCalculator { return tickLabels; } + /** + * Given the generated tickLabels, will they fit side-by-side without overlapping each other and looking bad? Sometimes the given tickSpacingHint is simply too small. + * + * @param tickLabels + * @param tickSpacingHint + * @return + */ + boolean willLabelsFitInTickSpaceHint(List<String> tickLabels, int tickSpacingHint) { + + // Assume that for Y-Axis the ticks will all fit based on their tickSpace hint because the text is usually horizontal and "short". This more applies to the X-Axis. + if (this.axisDirection == Direction.Y) { + return true; + } + + String sampleLabel = " "; + // find the longest String in all the labels + for (int i = 0; i < tickLabels.size(); i++) { + if (tickLabels.get(i) != null && tickLabels.get(i).length() > sampleLabel.length()) { + sampleLabel = tickLabels.get(i); + } + } + + TextLayout textLayout = new TextLayout(sampleLabel, styleManager.getAxisTickLabelsFont(), new FontRenderContext(null, true, false)); + AffineTransform rot = styleManager.getXAxisLabelRotation() == 0 ? null : AffineTransform.getRotateInstance(-1 * Math.toRadians(styleManager.getXAxisLabelRotation())); + Shape shape = textLayout.getOutline(rot); + Rectangle2D rectangle = shape.getBounds(); + double largestLabelWidth = rectangle.getWidth(); + // System.out.println("largestLabelWidth: " + largestLabelWidth); + // System.out.println("tickSpacingHint: " + tickSpacingHint); + + return (largestLabelWidth * 1.8 < tickSpacingHint); + + } } diff --git a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickDateCalculator.java b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickDateCalculator.java index 40b6d956..fbc7f021 100644 --- a/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickDateCalculator.java +++ b/xchart/src/main/java/com/xeiam/xchart/internal/chartpart/AxisTickDateCalculator.java @@ -55,28 +55,42 @@ public class AxisTickDateCalculator extends AxisTickCalculator { // the span of the data long span = (long) Math.abs(maxValue - minValue); // in data space - long gridStepHint = (long) (span / tickSpace * styleManager.getXAxisTickMarkSpacingHint()); - - 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; + // Can tickSpacingHint be intelligently calculated by looking at the label data? + // YES. Generate the labels first, see if they "look" OK and reiterate with an increased tickSpacingHint + // TODO apply this to other Axis types including bar charts + // TODO only do this for the X-Axis + int tickSpacingHint = styleManager.getXAxisTickMarkSpacingHint() - 10; + do { + + System.out.println("calulating ticks..."); + tickLabels.clear(); + tickLocations.clear(); + tickSpacingHint += 10; + 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 firstPosition = getFirstPosition(gridStep); + // System.out.println("gridStep: " + gridStep); - // generate all tickLabels and tickLocations from the first to last position - for (double value = firstPosition; value <= maxValue + 2 * gridStep; value = value + gridStep) { + double firstPosition = getFirstPosition(gridStep); - tickLabels.add(dateFormatter.formatDate(value, timeUnit)); - // here we convert tickPosition finally to plot space, i.e. pixels - double tickLabelPosition = margin + ((value - minValue) / (maxValue - minValue) * tickSpace); - tickLocations.add(tickLabelPosition); - } + // generate all tickLabels and tickLocations from the first to last position + for (double value = firstPosition; value <= maxValue + 2 * gridStep; value = value + gridStep) { + + tickLabels.add(dateFormatter.formatDate(value, timeUnit)); + // here we convert tickPosition finally to plot space, i.e. pixels + double tickLabelPosition = margin + ((value - minValue) / (maxValue - minValue) * tickSpace); + tickLocations.add(tickLabelPosition); + } + } while (!willLabelsFitInTickSpaceHint(tickLabels, tickSpacingHint)); } } -- GitLab