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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,22 @@ chart.getStyler().setyAxisTickLabelsFormattingFunction(x ->NumberWordConverter.c

### Multiple Axes

Linear numeric Y-axis groups can use different tick spacing hints (in pixels):

```java
chart.getStyler().setYAxisTickMarkSpacingHint(40); // default for all groups
chart.getStyler().setYAxisGroupTickMarkSpacingHint(1, 120); // wider spacing for group 1
chart.getStyler().setYAxisGroupTickMarkSpacingHint(1, null); // restore the default
```

A group override also applies to equally spaced data and custom tick label formatters.
It takes precedence over the short-plot squeeze. If the hint exceeds the available tick
space, the minimum and maximum ticks are retained when there is positive space and
their formatted labels differ.
It is a pixel hint, not a fixed interval in data units. Logarithmic and categorical axes
keep their existing tick rules; merged axes follow the master group's ticks.


XChart has multiple y axes feature. Y offset is calculated according to the Y-Axis the series configured. Max `y` value in this axis is calculated
according to the series on this axis only.
To set the y group:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.knowm.xchart.standalone.issues;

import org.knowm.xchart.SwingWrapper;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.style.Styler;

/** Demonstrates independent tick spacing for two Y-axis groups. */
public class TestForIssue797 {

public static void main(String[] args) {
new SwingWrapper<>(getChart()).displayChart();
}

/** Constructs the chart without opening a window. */
public static XYChart getChart() {
XYChart chart =
new XYChartBuilder()
.width(900)
.height(600)
.title("Y-axis group tick spacing")
.xAxisTitle("Sample")
.yAxisTitle("Measurement")
.build();
double[] x = {0, 1, 2, 3, 4, 5};
chart.addSeries("Measurement", x, new double[] {0, 20, 40, 60, 80, 100});
chart.addSeries("Enabled", x, new double[] {0, 0, 1, 1, 0, 1}).setYAxisGroup(1);
chart.setYAxisGroupTitle(1, "Enabled");
chart.getStyler().setYAxisGroupPosition(1, Styler.YAxisPosition.Right);
chart.getStyler().setYAxisMin(1, 0.0);
chart.getStyler().setYAxisMax(1, 1.0);
chart.getStyler().setYAxisTickMarkSpacingHint(40);
chart.getStyler().setYAxisGroupTickMarkSpacingHint(1, 200);
return chart;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package org.knowm.xchart.internal.chartpart;

import java.awt.*;
import java.awt.Font;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.*;
import java.util.*;
import java.text.Format;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.knowm.xchart.internal.Utils;
Expand Down Expand Up @@ -39,6 +43,8 @@ public abstract class AxisTickCalculator_ implements AxisTickCalculator {

Format axisFormat;

int yIndex;

private static final int MAX_LABEL_LENGTH = 20;

/**
Expand Down Expand Up @@ -157,14 +163,13 @@ boolean willLabelsFitInTickSpaceHint(List<String> tickLabels, int tickSpacingHin

AffineTransform rot = null;
if (styler.getXAxisLabelRotation() != 0) {
rot = AffineTransform.getRotateInstance(
-1 * Math.toRadians(styler.getXAxisLabelRotation()));
rot = AffineTransform.getRotateInstance(-1 * Math.toRadians(styler.getXAxisLabelRotation()));
}

Shape shape = textLayout.getOutline(rot);
Rectangle2D rectangle = shape.getBounds();
double largestLabelWidth = Direction.X.equals(this.axisDirection)
? rectangle.getWidth() : rectangle.getHeight();
double largestLabelWidth =
Direction.X.equals(this.axisDirection) ? rectangle.getWidth() : rectangle.getHeight();

return (largestLabelWidth * 1.1 < tickSpacingHint);
}
Expand All @@ -174,6 +179,18 @@ public Format getAxisFormat() {
return axisFormat;
}

private boolean hasYAxisGroupSpacingHint() {
return axisDirection == Direction.Y && styler.getYAxisGroupTickMarkSpacingHint(yIndex) != null;
}

private int getTickMarkSpacingHint() {
if (axisDirection == Direction.X) {
return styler.getXAxisTickMarkSpacingHint();
}
Integer groupHint = styler.getYAxisGroupTickMarkSpacingHint(yIndex);
return groupHint != null ? groupHint : styler.getYAxisTickMarkSpacingHint();
}

protected void calculate() {

// System.out.println("calculate");
Expand All @@ -199,7 +216,17 @@ protected void calculate() {
if (axisDirection == Direction.X && tickSpace < styler.getXAxisTickMarkSpacingHint()) {
return;
}
if (axisDirection == Direction.Y && tickSpace < styler.getYAxisTickMarkSpacingHint()) {
if (axisDirection == Direction.Y && tickSpace < getTickMarkSpacingHint()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a per-group hint this early return becomes much easier to hit. The whole point of a group hint for a 0/1 axis is to set it large, and as soon as the hint exceeds the available tick space the axis renders with no ticks and no labels at all.

I rendered the chart from TestForIssue797 at a few heights:

chart height plot height hint group 1 labels
600 523 400 0, 1
500 423 400 (none)
400 323 400 (none)
300 223 200 0, 1

So the demo works at its built-in size and goes blank the moment the window is shrunk a little. That is pre-existing behavior for the global hint, but a user setting a group hint has explicitly asked for sparse ticks, not for none. When a group hint is set and tickSpace < hint, could we fall back to ticks at minValue and maxValue instead of returning empty? Alternatively clamp the effective hint to tickSpace before the gridStep calculation. Either way, a test for the "hint larger than plot" case would be good.

// An explicit group hint requests sparse ticks, not an empty axis after resizing.
if (hasYAxisGroupSpacingHint()
&& tickSpace > 0
&& !isNumberFormatChoppingDecimals(maxValue, minValue)) {
double margin = Utils.getTickStartOffset(workingSpace, tickSpace);
tickLabels.add(getAxisFormat().format(minValue));
tickLocations.add(margin);
tickLabels.add(getAxisFormat().format(maxValue));
tickLocations.add(margin + tickSpace);
}
return;
}

Expand All @@ -212,28 +239,22 @@ protected void calculate() {
}

// where the tick should begin in the working space in pixels
double margin =
Utils.getTickStartOffset(
workingSpace,
tickSpace); // in plot space double gridStep = getGridStepForDecimal(tickSpace);
double margin = Utils.getTickStartOffset(workingSpace, tickSpace); // in plot space
// the span of the data
double span = Math.abs(Math.min((maxValue - minValue), Double.MAX_VALUE - 1)); // in data space

if (axisValues != null && areValuesEquallySpaced(axisValues)) {
// Explicit group spacing takes precedence over data-driven, equally spaced ticks.
if (axisValues != null && areValuesEquallySpaced(axisValues) && !hasYAxisGroupSpacingHint()) {
calculateForEquallySpacedAxisValues(tickSpace, margin);
return;
}

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

int tickSpacingHint =
(axisDirection == Direction.X
? styler.getXAxisTickMarkSpacingHint()
: styler.getYAxisTickMarkSpacingHint())
- 5;
int tickSpacingHint = getTickMarkSpacingHint() - 5;

// for very short plots, squeeze some more ticks in than normal into the Y-Axis
if (axisDirection == Direction.Y && tickSpace < 160) {
// Squeeze more ticks into short Y-axes unless the caller explicitly set group spacing.
if (axisDirection == Direction.Y && tickSpace < 160 && !hasYAxisGroupSpacingHint()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that an explicit group hint should win over the small-plot squeeze. A one-line comment here saying so would help the next reader, since both this and the equally-spaced bypass above are silent behavior changes that only kick in when a group hint exists.

tickSpacingHint = 25 - 5;
}

Expand Down Expand Up @@ -367,14 +388,10 @@ protected void calculate() {
} while (!areAllTickLabelsUnique(tickLabels)
|| !willLabelsFitInTickSpaceHint(tickLabels, gridStepInChartSpace));

// Prune ticks that fall outside the actual data band [minValue, maxValue]. The generation loop
// above intentionally overshoots by up to a grid step on each end (getFirstPosition starts one
// grid step below minValue and the loop bound runs two grid steps past maxValue); those overshoot
// ticks must not become visible labels. Gridlines and tick marks already clip to the plot bounds,
// but the tick-label renderer clips to the taller axis-column bounds, so an out-of-range label --
// e.g. a negative label when all data is positive -- can leak into the plot margin. Issue #634.
// A tick value v maps to pixel margin + (v - minValue) / (maxValue - minValue) * tickSpace, so
// v is within [minValue, maxValue] exactly when its pixel is within [margin, margin + tickSpace].
// Prune ticks outside [minValue, maxValue]. Generation intentionally overshoots the
// data range by a grid step. Tick marks and gridlines clip to the plot, but labels clip
// to the taller axis column and can leak into the margin (issue #634).
// Data values in this range map to pixels in [margin, margin + tickSpace].
double bandLow = margin - 1e-6;
double bandHigh = margin + tickSpace + 1e-6;
List<String> keptLabels = new ArrayList<>(tickLabels.size());
Expand All @@ -386,8 +403,7 @@ protected void calculate() {
keptLocations.add(loc);
}
}
// Only apply the pruning if at least one tick survives, so a degenerate range can never blank the
// axis entirely (falls back to the pre-prune behavior).
// Keep the original ticks if pruning a degenerate range would empty the axis.
if (!keptLocations.isEmpty() && keptLocations.size() < tickLocations.size()) {
tickLabels.clear();
tickLabels.addAll(keptLabels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.util.List;
import java.util.function.Function;

import org.knowm.xchart.internal.chartpart.Axis_.Direction;
import org.knowm.xchart.style.AxesChartStyler;

Expand All @@ -29,7 +28,19 @@ public AxisTickCalculator_Callback(
double maxValue,
AxesChartStyler styler) {

this(formattingCallback, axisDirection, workingSpace, minValue, maxValue, styler, 0);
}

AxisTickCalculator_Callback(
Function<Double, String> formattingCallback,
Direction axisDirection,
double workingSpace,
double minValue,
double maxValue,
AxesChartStyler styler,
int yIndex) {
super(axisDirection, workingSpace, minValue, maxValue, styler);
this.yIndex = yIndex;
axisFormat = new Formatter_Custom(formattingCallback);
calculate();
}
Expand All @@ -42,7 +53,21 @@ public AxisTickCalculator_Callback(
double maxValue,
List<Double> axisValues,
AxesChartStyler styler) {
this(
formattingCallback, axisDirection, workingSpace, minValue, maxValue, axisValues, styler, 0);
}

AxisTickCalculator_Callback(
Function<Double, String> formattingCallback,
Direction axisDirection,
double workingSpace,
double minValue,
double maxValue,
List<Double> axisValues,
AxesChartStyler styler,
int yIndex) {
super(axisDirection, workingSpace, minValue, maxValue, axisValues, styler);
this.yIndex = yIndex;
axisFormat = new Formatter_Custom(formattingCallback);
calculate();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,20 @@ public AxisTickCalculator_Number(
double maxValue,
List<Double> axisValues,
AxesChartStyler styler) {
this(axisDirection, workingSpace, minValue, maxValue, axisValues, styler, 0);
}

AxisTickCalculator_Number(
Direction axisDirection,
double workingSpace,
double minValue,
double maxValue,
List<Double> axisValues,
AxesChartStyler styler,
int yIndex) {
super(axisDirection, workingSpace, minValue, maxValue, axisValues, styler);
formatterNumber = new Formatter_Number(styler, axisDirection, minValue, maxValue);
this.yIndex = yIndex;
formatterNumber = new Formatter_Number(styler, axisDirection, minValue, maxValue, yIndex);
axisFormat = formatterNumber;
calculate();
}
Expand All @@ -66,6 +78,7 @@ public AxisTickCalculator_Number(
int yIndex) {

super(axisDirection, workingSpace, minValue, maxValue, styler);
this.yIndex = yIndex;
formatterNumber = new Formatter_Number(styler, axisDirection, minValue, maxValue, yIndex);
axisFormat = formatterNumber;
calculate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.knowm.xchart.CategoryChart;
import org.knowm.xchart.CategorySeries;
import org.knowm.xchart.HeatMapChart;
import org.knowm.xchart.HorizontalBarChart;
import org.knowm.xchart.HorizontalBarSeries;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYSeries;
import org.knowm.xchart.internal.series.AxesChartSeries;
import org.knowm.xchart.internal.series.Series.DataType;
Expand Down Expand Up @@ -306,15 +302,17 @@ && getDataType() != DataType.Date) {
min,
max,
yData,
axesChartStyler);
axesChartStyler,
getYIndex());
}
return new AxisTickCalculator_Callback(
axesChartStyler.getYAxisTickLabelsFormattingFunction(),
Axis_.Direction.Y,
workingSpace,
min,
max,
axesChartStyler);
axesChartStyler,
getYIndex());

} else if (axesChartStyler.isYAxisLogarithmic() && getDataType() != DataType.Date) {

Expand Down Expand Up @@ -342,7 +340,7 @@ && getDataType() != DataType.Date) {
} else {
if (!yData.isEmpty()) {
return new AxisTickCalculator_Number(
Axis_.Direction.Y, workingSpace, min, max, yData, axesChartStyler);
Axis_.Direction.Y, workingSpace, min, max, yData, axesChartStyler, getYIndex());
}
return new AxisTickCalculator_Number(
Axis_.Direction.Y, workingSpace, min, max, axesChartStyler, getYIndex());
Expand Down Expand Up @@ -438,8 +436,7 @@ public double getChartValue(double screenPoint) {
return minVal;
}

double margin =
org.knowm.xchart.internal.Utils.getTickStartOffset(workingSpace, tickSpace);
double margin = org.knowm.xchart.internal.Utils.getTickStartOffset(workingSpace, tickSpace);

minVal = isLog ? Math.log10(minVal) : minVal;
maxVal = isLog ? Math.log10(maxVal) : maxVal;
Expand Down
Loading
Loading