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,57 @@
package org.knowm.xchart.standalone.issues;

import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.style.Styler;
import org.knowm.xchart.style.theme.XChartTheme;

/**
* Issue #503 — Axis Title Padding does not apply to the second (right-side) Y-Axis.
*
* <p>A custom theme (extending {@link org.knowm.xchart.style.theme.AbstractBaseTheme} via {@link
* XChartTheme}) returns a large {@code getAxisTitlePadding()}. On the LEFT axis the padding opens up
* a wide gap between the rotated axis title and the tick numbers. On the RIGHT axis the title stays
* jammed against its tick numbers no matter how big the padding is — the padding is spent as dead
* space on the outer edge of the title instead of between the title and its labels.
*
* <p>Run {@link #main} and compare the two sides.
*/
public class TestForIssue503 {

private static final int BIG_PADDING = 60;

/** Custom theme, as in the bug report: only the axis-title padding is overridden. */
static class BigTitlePaddingTheme extends XChartTheme {
@Override
public int getAxisTitlePadding() {
return BIG_PADDING;
}
}

public static XYChart getChart() {

XYChart chart = new XYChart(900, 500);
chart.setTitle("Issue #503 — axisTitlePadding=" + BIG_PADDING + " (left works, right doesn't)");
chart.getStyler().setTheme(new BigTitlePaddingTheme());

chart.getStyler().setYAxisGroupPosition(0, Styler.YAxisPosition.Left);
chart.getStyler().setYAxisGroupPosition(1, Styler.YAxisPosition.Right);

chart.setXAxisTitle("x");
chart.setYAxisGroupTitle(0, "LEFT axis title");
chart.setYAxisGroupTitle(1, "RIGHT axis title");

chart
.addSeries("left series", new double[] {1, 2, 3}, new double[] {10, 20, 30})
.setYAxisGroup(0);
chart
.addSeries("right series", new double[] {1, 2, 3}, new double[] {1000, 2000, 3000})
.setYAxisGroup(1);

return chart;
}

public static void main(String[] args) {
new SwingWrapper<>(getChart()).displayChart();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ public void paint(Graphics2D g) {

// ///////////////////////////////////////////////

int axisTitlePadding = chart.getStyler().getAxisTitlePadding();
boolean onRight =
chart.getStyler().getYAxisGroupPosistion(yAxis.getYIndex()) == YAxisPosition.Right;
int xOffset;
if (onRight) {
// Push the title outward past the tick labels by the padding, so the padding forms a
// gap between the tick labels and the title — mirroring the left side, where the padding
// is baked into the title bounds width that the tick labels are offset by. See issue #503.
xOffset =
(int)
(yAxis.getAxisTick().getBounds().getX()
+ yAxis.getAxisTick().getBounds().getWidth()
+ axisTitlePadding
+ nonRotatedRectangle.getHeight());
} else {
xOffset = (int) (yAxis.getBounds().getX() + nonRotatedRectangle.getHeight());
Expand Down Expand Up @@ -101,11 +106,18 @@ public void paint(Graphics2D g) {
// System.out.println(nonRotatedRectangle.getHeight());

// bounds
// The bounds start at the inner edge of the title's column. On the right the padding sits
// between the tick labels and the title text, so the origin is padding further inward than
// the text; on the left the padding trails the text and the origin is at the text itself.
double boundsX =
onRight
? xOffset - nonRotatedRectangle.getHeight() - axisTitlePadding
: xOffset - nonRotatedRectangle.getHeight();
bounds =
new Rectangle2D.Double(
xOffset - nonRotatedRectangle.getHeight(),
boundsX,
yOffset - nonRotatedRectangle.getWidth(),
nonRotatedRectangle.getHeight() + chart.getStyler().getAxisTitlePadding(),
nonRotatedRectangle.getHeight() + axisTitlePadding,
nonRotatedRectangle.getWidth());
// g.setColor(Color.blue);
// g.draw(bounds);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.knowm.xchart.internal.chartpart;

import static org.assertj.core.api.Assertions.assertThat;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.util.List;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;
import org.knowm.xchart.BitmapEncoder;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.XYSeries;
import org.knowm.xchart.style.Styler.YAxisPosition;

/**
* Regression test for issue #503: {@code axisTitlePadding} must open up a gap between the axis title
* and its tick labels on BOTH the left and the right Y axis. Before the fix the padding widened the
* left-axis gap but was dumped as dead space on the outer edge of the right-axis title, leaving the
* right title jammed against its tick numbers no matter how big the padding was.
*
* <p>The title text and tick labels are painted in distinct colors so their painted positions can
* be located directly from the rendered pixels — the axis title bounds origin is pinned to the
* column's inner edge and does not reflect where the rotated text actually lands.
*/
public class RegressionIssue503Test {

private static final Color TITLE_COLOR = new Color(255, 0, 0); // red
private static final Color TICKS_COLOR = new Color(0, 0, 255); // blue

private static BufferedImage render(int padding) {
XYChart chart = new XYChartBuilder().width(900).height(500).build();
chart.getStyler().setAxisTitlePadding(padding);
chart.getStyler().setYAxisGroupPosition(0, YAxisPosition.Left);
chart.getStyler().setYAxisGroupPosition(1, YAxisPosition.Right);

XYSeries left = chart.addSeries("left", List.of(1, 2, 3), List.of(10, 20, 30));
left.setYAxisGroup(0);
XYSeries right = chart.addSeries("right", List.of(1, 2, 3), List.of(1000, 2000, 3000));
right.setYAxisGroup(1);

// Neutralize series colors so the data lines/markers don't pollute the red/blue pixel search.
chart.getStyler().setLegendVisible(false);
chart.getStyler().setPlotGridLinesVisible(false);
for (XYSeries s : new XYSeries[] {left, right}) {
s.setLineColor(Color.BLACK);
s.setMarker(org.knowm.xchart.style.markers.SeriesMarkers.NONE);
}

chart.setYAxisGroupTitle(0, "LEFT TITLE");
chart.setYAxisGroupTitle(1, "RIGHT TITLE");

// Color title text and tick labels distinctly so they can be located in the pixels.
chart.getStyler().setYAxisGroupTitleColor(0, TITLE_COLOR);
chart.getStyler().setYAxisGroupTitleColor(1, TITLE_COLOR);
chart.getStyler().setYAxisGroupTickLabelsColorMap(0, TICKS_COLOR);
chart.getStyler().setYAxisGroupTickLabelsColorMap(1, TICKS_COLOR);

return BitmapEncoder.getBufferedImage(chart);
}

/** Smallest x containing a pixel of the given color, or -1 if none. */
private static int minX(BufferedImage img, Color c) {
for (int x = 0; x < img.getWidth(); x++) {
if (columnHasColor(img, x, c)) {
return x;
}
}
return -1;
}

/** Largest x containing a pixel of the given color, or -1 if none. */
private static int maxX(BufferedImage img, Color c) {
for (int x = img.getWidth() - 1; x >= 0; x--) {
if (columnHasColor(img, x, c)) {
return x;
}
}
return -1;
}

private static boolean columnHasColor(BufferedImage img, int x, Color c) {
for (int y = 0; y < img.getHeight(); y++) {
if (isColor(img.getRGB(x, y), c)) {
return true;
}
}
return false;
}

private static boolean isColor(int rgb, Color c) {
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
// Antialiasing blends toward the background, so match on the dominant channel.
if (c.equals(TITLE_COLOR)) {
return r > 120 && g < 100 && b < 100;
}
return b > 120 && r < 100 && g < 100;
}

/**
* On the right side (title is outboard of the tick labels) the gap is the distance from the tick
* labels' right edge to the title text's left edge.
*/
private static int rightGap(BufferedImage img) {
int ticksRight = maxX(img, TICKS_COLOR);
// Title text left edge = smallest title x that is to the right of the tick labels.
for (int x = ticksRight + 1; x < img.getWidth(); x++) {
if (columnHasColor(img, x, TITLE_COLOR)) {
return x - ticksRight;
}
}
throw new AssertionError("right axis title text not found");
}

/**
* On the left side (title is outboard of the tick labels, i.e. to their left) the gap is the
* distance from the title text's right edge to the tick labels' left edge.
*/
private static int leftGap(BufferedImage img) {
int ticksLeft = minX(img, TICKS_COLOR);
for (int x = ticksLeft - 1; x >= 0; x--) {
if (columnHasColor(img, x, TITLE_COLOR)) {
return ticksLeft - x;
}
}
throw new AssertionError("left axis title text not found");
}

@Test
public void rightAxisTitleGapScalesWithPadding() {
int smallGap = rightGap(render(5));
int largeGap = rightGap(render(105));
// The gap must grow by ~100 (the padding delta); before the fix it stayed fixed.
assertThat((double) (largeGap - smallGap)).isCloseTo(100.0, Offset.offset(3.0));
}

@Test
public void leftAxisTitleGapStillScalesWithPadding() {
int smallGap = leftGap(render(5));
int largeGap = leftGap(render(105));
assertThat((double) (largeGap - smallGap)).isCloseTo(100.0, Offset.offset(3.0));
}
}
Loading