diff --git a/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue171.java b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue171.java
new file mode 100644
index 000000000..97a99d88f
--- /dev/null
+++ b/xchart-demo/src/main/java/org/knowm/xchart/standalone/issues/TestForIssue171.java
@@ -0,0 +1,64 @@
+package org.knowm.xchart.standalone.issues;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import org.knowm.xchart.CategoryChart;
+import org.knowm.xchart.CategoryChartBuilder;
+import org.knowm.xchart.CategorySeries;
+import org.knowm.xchart.SwingWrapper;
+import org.knowm.xchart.style.Styler;
+
+/**
+ * Demonstrates that x-axis labels no longer overlap when there are many categories.
+ *
+ *
Reproduces the 161-category histogram from issue #171. The default xAxisTickMarkSpacingHint
+ * now automatically skips labels on category charts so they don't overlap.
+ */
+public class TestForIssue171 {
+
+ public static void main(String[] args) {
+ new SwingWrapper<>(getChart()).displayChart();
+ }
+
+ /** Constructs and returns the chart without launching a window (headless-safe). */
+ public static CategoryChart getChart() {
+ int n = 80;
+ int nbRandomWalks = 1_000_000;
+ Random generator = new Random(42);
+
+ int[] histogramY = new int[2 * n + 1];
+ for (int j = 0; j < nbRandomWalks; j++) {
+ int delta = 0;
+ for (int i = 0; i < n; i++) {
+ delta += generator.nextInt(2) < 1 ? -1 : 1;
+ }
+ histogramY[delta + n] += 1;
+ }
+
+ List xData = new ArrayList<>();
+ List yData = new ArrayList<>();
+ for (int i = 0; i < 2 * n + 1; i++) {
+ xData.add(i - n);
+ yData.add(histogramY[i]);
+ }
+
+ CategoryChart chart =
+ new CategoryChartBuilder()
+ .width(800)
+ .height(600)
+ .title("Random Walk Distribution (Issue #171)")
+ .xAxisTitle("delta")
+ .yAxisTitle("occurrences")
+ .build();
+
+ chart.getStyler().setChartTitleVisible(true);
+ chart.getStyler().setLegendPosition(Styler.LegendPosition.InsideNW);
+ chart.getStyler().setLegendVisible(false);
+ chart.getStyler().setDefaultSeriesRenderStyle(CategorySeries.CategorySeriesRenderStyle.Stick);
+
+ chart.addSeries("data", xData, yData);
+
+ return chart;
+ }
+}
diff --git a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/AxisTickCalculator_Category.java b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/AxisTickCalculator_Category.java
index a5c07e9b6..2e3cd5315 100644
--- a/xchart/src/main/java/org/knowm/xchart/internal/chartpart/AxisTickCalculator_Category.java
+++ b/xchart/src/main/java/org/knowm/xchart/internal/chartpart/AxisTickCalculator_Category.java
@@ -78,6 +78,13 @@ private void calculate(List> categories, Series.DataType axisType) {
firstPosition = 0;
}
+ // When no explicit label count cap is set, respect the spacing hint to avoid overlap.
+ // Compute how many categories to skip so adjacent labels are at least spacingHint pixels apart.
+ int skipFactor = 1;
+ if (xAxisMaxLabelCount == 0 && gridStep < styler.getXAxisTickMarkSpacingHint()) {
+ skipFactor = (int) Math.ceil(styler.getXAxisTickMarkSpacingHint() / gridStep);
+ }
+
// set up String formatters that may be encountered
if (axisType == Series.DataType.String) {
axisFormat = new Formatter_String();
@@ -96,16 +103,17 @@ private void calculate(List> categories, Series.DataType axisType) {
int counter = 0;
for (Object category : categories) {
- if (axisType == Series.DataType.String) {
- tickLabels.add(category.toString());
- } else if (axisType == Series.DataType.Number) {
- tickLabels.add(axisFormat.format(new BigDecimal(category.toString()).doubleValue()));
- } else if (axisType == Series.DataType.Date) {
- tickLabels.add(axisFormat.format((((Date) category).getTime())));
+ if (counter % skipFactor == 0) {
+ if (axisType == Series.DataType.String) {
+ tickLabels.add(category.toString());
+ } else if (axisType == Series.DataType.Number) {
+ tickLabels.add(axisFormat.format(new BigDecimal(category.toString()).doubleValue()));
+ } else if (axisType == Series.DataType.Date) {
+ tickLabels.add(axisFormat.format((((Date) category).getTime())));
+ }
+ tickLocations.add(margin + firstPosition + gridStep * counter);
}
-
- double tickLabelPosition = margin + firstPosition + gridStep * counter++;
- tickLocations.add(tickLabelPosition);
+ counter++;
}
}
}
diff --git a/xchart/src/test/java/org/knowm/xchart/internal/chartpart/AxisTickCalculatorCategoryTest.java b/xchart/src/test/java/org/knowm/xchart/internal/chartpart/AxisTickCalculatorCategoryTest.java
index 90c93ce8a..b2443e1a1 100644
--- a/xchart/src/test/java/org/knowm/xchart/internal/chartpart/AxisTickCalculatorCategoryTest.java
+++ b/xchart/src/test/java/org/knowm/xchart/internal/chartpart/AxisTickCalculatorCategoryTest.java
@@ -59,4 +59,36 @@ public void shouldAllowAllLabelsIfThereisEnoughSpace() {
assertThat(calculator.tickLocations)
.isEqualTo(Arrays.asList(105.0, 243.0, 381.0, 519.0, 657.0, 795.0));
}
+
+ @Test
+ public void shouldAutoSkipLabelsWhenCrowded() {
+ // 20 categories in 200px: gridStep ≈ 8.5 px < default spacingHint (74 px)
+ // skipFactor = ceil(74 / 8.5) = 9 → labels at indices 0, 9, 18 → 3 labels
+ List categories =
+ Arrays.asList(
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q",
+ "r", "s", "t");
+ CategoryStyler styler = new CategoryStyler();
+
+ AxisTickCalculator_Category calculator =
+ new AxisTickCalculator_Category(
+ Axis_.Direction.X, 200, categories, Series.DataType.String, styler);
+
+ assertThat(calculator.tickLabels.size()).isLessThan(categories.size());
+ assertThat(calculator.tickLabels.get(0)).isEqualTo("a");
+ }
+
+ @Test
+ public void shouldNotAutoSkipWhenSpacingHintIsDisabled() {
+ // Setting spacingHint to 0 disables auto-skip; all labels should appear
+ List categories = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
+ CategoryStyler styler = new CategoryStyler();
+ styler.setXAxisTickMarkSpacingHint(0);
+
+ AxisTickCalculator_Category calculator =
+ new AxisTickCalculator_Category(
+ Axis_.Direction.X, 200, categories, Series.DataType.String, styler);
+
+ assertThat(calculator.tickLabels.size()).isEqualTo(categories.size());
+ }
}