diff --git a/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java b/xchart/src/test/java/org/knowm/xchart/CategoryChartTest.java index 008b404d..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; @@ -13,6 +15,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 +248,75 @@ 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. + * + *

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 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; + } }