From 50563880e62cd8353e1ca3ab6b5910c2bbcd0b94 Mon Sep 17 00:00:00 2001 From: Tim Molter Date: Sat, 20 Jun 2026 10:28:38 +0200 Subject: [PATCH 1/2] test: add regression test for issue #707 (last bar label overwritten) Adds a headless render test that exercises the exact scenario from issue #707: a Bar-style CategoryChart with setLabelsVisible(true). The bug (using shared 'path' so closePath() repainted the last bar over its label) was fixed in ff2536f4 by switching to a local 'barPath'. This test guards against any future regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../org/knowm/xchart/CategoryChartTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java b/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java index 008b404d..1843a098 100644 --- a/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java +++ b/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java @@ -13,6 +13,7 @@ import org.knowm.xchart.custom.CustomTheme; import org.knowm.xchart.internal.series.Series; import org.knowm.xchart.style.Styler; +import org.knowm.xchart.style.Styler.ChartTheme; public class CategoryChartTest { @@ -245,4 +246,26 @@ void paint() { .contains(chart.getStyler().getDefaultSeriesRenderStyle()); } } + + /** + * Regression test for issue 707. + * + *

The last bar in a Bar-style CategoryChart was drawn twice (overwriting its label) because + * the bar loop reused the shared {@code path} variable that {@code closePath()} consumes after + * the loop. The fix uses a local {@code barPath} so {@code path} stays {@code null} for bar + * series and {@code closePath()} becomes a no-op. + */ + @Test + void barChartWithLabelsVisibleRendersWithoutError() throws Exception { + CategoryChart barChart = + new CategoryChart(800, 600, ChartTheme.Matlab); + barChart.getStyler().setLabelsVisible(true); + barChart.addSeries( + "y(x)", + Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0), + Arrays.asList(2.0, 1.5, 4.0, 3.77, 2.5)); + + assertDoesNotThrow( + () -> BitmapEncoder.getBitmapBytes(barChart, BitmapEncoder.BitmapFormat.PNG)); + } } From a4661ce2552ee15bf69110fe4974ebaf0acd5bd4 Mon Sep 17 00:00:00 2001 From: Tim Molter Date: Sat, 20 Jun 2026 10:48:35 +0200 Subject: [PATCH 2/2] test: strengthen issue #707 regression test with pixel-diff comparison Replace the no-throw assertion with a pixel-level regression check that actually detects the visual bug: - Render the 5-bar chart twice (labels ON and OFF) and count differing pixels -> diff5 (covers all 5 label contributions). - Render the same chart with the last value as NaN (bar 5 skipped) twice (labels ON and OFF) -> diffNaN (covers only 4 label contributions). - Assert diff5 > diffNaN. When the original bug is reintroduced (bar loop reuses shared 'path' and closePath() repaints the last bar over its label), the 5th label's pixels vanish and diff5 equals diffNaN, making the assertion fail. The 5th label currently contributes 64 distinct pixels, providing a clear margin. Addresses Copilot PR review comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../org/knowm/xchart/CategoryChartTest.java | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java b/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java index 1843a098..3c042ea6 100644 --- a/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java +++ b/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java @@ -6,6 +6,8 @@ import static org.knowm.xchart.style.Styler.ChartTheme.GGPlot2; import static org.knowm.xchart.style.Styler.ChartTheme.XChart; +import java.awt.Color; +import java.awt.image.BufferedImage; import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -254,18 +256,67 @@ void paint() { * the bar loop reused the shared {@code path} variable that {@code closePath()} consumes after * the loop. The fix uses a local {@code barPath} so {@code path} stays {@code null} for bar * series and {@code closePath()} becomes a no-op. + * + *

To catch a visual regression we render to a {@link BufferedImage} and compare + * pixel-diff counts (labels-on minus labels-off) between: + * + *

    + *
  1. A 5-bar chart with all values present (all 5 labels rendered). + *
  2. The same 5-bar chart where the last value is {@link Double#NaN} (4 labels rendered, + * same x/y axis extents and bar positions). + *
+ * + *

When the bug is reintroduced the 5th label is overwritten by the bar fill, making its + * diff indistinguishable from the NaN chart's diff. The assertion {@code diff5 > diffNaN} + * therefore fails precisely when and only when the 5th label is missing. */ @Test - void barChartWithLabelsVisibleRendersWithoutError() throws Exception { - CategoryChart barChart = - new CategoryChart(800, 600, ChartTheme.Matlab); - barChart.getStyler().setLabelsVisible(true); - barChart.addSeries( - "y(x)", - Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0), - Arrays.asList(2.0, 1.5, 4.0, 3.77, 2.5)); - - assertDoesNotThrow( - () -> BitmapEncoder.getBitmapBytes(barChart, BitmapEncoder.BitmapFormat.PNG)); + void issue707LastBarLabelIsVisibleAndNotOverwritten() throws Exception { + List xData = Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0); + + // Chart A — all 5 values present; should render 5 labels. + CategoryChart chart5On = buildIssue707Chart(xData, Arrays.asList(2.0, 1.5, 4.0, 3.77, 2.5), true); + CategoryChart chart5Off = buildIssue707Chart(xData, Arrays.asList(2.0, 1.5, 4.0, 3.77, 2.5), false); + + // Chart B — last value is NaN so bar 5 is skipped; only 4 labels are rendered. + CategoryChart chartNaNOn = + buildIssue707Chart( + xData, Arrays.asList(2.0, 1.5, 4.0, 3.77, Double.NaN), true); + CategoryChart chartNaNOff = + buildIssue707Chart( + xData, Arrays.asList(2.0, 1.5, 4.0, 3.77, Double.NaN), false); + + int diff5 = countDiffPixels(BitmapEncoder.getBufferedImage(chart5On), BitmapEncoder.getBufferedImage(chart5Off)); + int diffNaN = countDiffPixels(BitmapEncoder.getBufferedImage(chartNaNOn), BitmapEncoder.getBufferedImage(chartNaNOff)); + + // When the fix is in place, diff5 > diffNaN because the 5th label contributes pixels. + // When the bug is reintroduced, the 5th label is overwritten and diff5 == diffNaN. + assertThat(diff5) + .as( + "labels-on/off pixel diff for 5-bar chart (%d) must exceed that of the same chart " + + "with NaN last bar (%d); equality means the 5th label was overwritten", + diff5, + diffNaN) + .isGreaterThan(diffNaN); + } + + private CategoryChart buildIssue707Chart( + List xData, List yData, boolean labelsVisible) { + CategoryChart chart = new CategoryChart(800, 600, ChartTheme.Matlab); + chart.getStyler().setLabelsVisible(labelsVisible); + chart.getStyler().setLabelsFontColorAutomaticEnabled(false); + chart.getStyler().setLabelsFontColor(new Color(255, 0, 255)); + chart.addSeries("y(x)", xData, yData); + return chart; + } + + private int countDiffPixels(BufferedImage a, BufferedImage b) { + int count = 0; + for (int x = 0; x < a.getWidth(); x++) { + for (int y = 0; y < a.getHeight(); y++) { + if (a.getRGB(x, y) != b.getRGB(x, y)) count++; + } + } + return count; } }