Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<Integer> xData = new ArrayList<>();
List<Integer> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> 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());
}
}
Loading