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