de.gsi.chart.axes.spi.NumericAxis Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of chartfx-chart Show documentation
Show all versions of chartfx-chart Show documentation
This charting library ${project.artifactId}- is an extension
in the spirit of Oracle's XYChart and performance/time-proven JDataViewer charting functionalities.
Emphasis was put on plotting performance for both large number of data points and real-time displays,
as well as scientific accuracies leading to error bar/surface plots, and other scientific plotting
features (parameter measurements, fitting, multiple axes, zoom, ...).
package de.gsi.chart.axes.spi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.sun.javafx.css.converters.SizeConverter;
import de.gsi.chart.axes.AxisTransform;
import de.gsi.chart.axes.LogAxisType;
import de.gsi.chart.axes.TickUnitSupplier;
import de.gsi.chart.axes.spi.format.DefaultTickUnitSupplier;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.scene.layout.Region;
/**
* A axis class that plots a range of numbers with major tick marks every
* "tickUnit". You can use any Number type with this axis, Long, Double,
* BigDecimal etc.
*
* Compared to the {@code NumberAxis}, this one has a few additional features:
*
* - Re-calculates tick unit also when the {@link #autoRangingProperty()
* auto-ranging} is off
* - Supports configuration of {@link #autoRangePaddingProperty() auto-range
* padding}
* - Supports configuration of {@link #autoRangeRoundingProperty() auto-range
* rounding}
* - Supports custom {@link #tickUnitSupplierProperty() tick unit
* suppliers}
*
*/
public final class NumericAxis extends AbstractAxis {
private static final int TICK_MARK_GAP = 6;
private static final double NEXT_TICK_UNIT_FACTOR = 1.01;
private static final int MAX_TICK_COUNT = 20;
private static final TickUnitSupplier DEFAULT_TICK_UNIT_SUPPLIER = new DefaultTickUnitSupplier();
private static final int DEFAULT_RANGE_LENGTH = 2;
private static final double DEFAULT_RANGE_PADDING = 0.1;
private double localScale;
private double localCurrentLowerBound;
private double localOffset;
/**
* Creates an {@link #autoRangingProperty() auto-ranging} Axis.
*/
public NumericAxis() {
super();
super.currentLowerBound.addListener((evt, o, n) -> {
localCurrentLowerBound = currentLowerBound.get();
final double zero = super.getDisplayPosition(0);
localOffset = zero + localCurrentLowerBound * scaleProperty().get();
});
super.scaleProperty().addListener((evt, o, n) -> {
localScale = scaleProperty().get();
final double zero = super.getDisplayPosition(0);
localOffset = zero + currentLowerBound.get() * localScale;
});
}
/**
* Creates a {@link #autoRangingProperty() non-auto-ranging} Axis with the
* given upper bound, lower bound and tick unit.
*
* @param lowerBound
* the {@link #lowerBoundProperty() lower bound} of the axis
* @param upperBound
* the {@link #upperBoundProperty() upper bound} of the axis
* @param tickUnit
* the tick unit, i.e. space between tick marks
*/
public NumericAxis(final double lowerBound, final double upperBound, final double tickUnit) {
this(null, lowerBound, upperBound, tickUnit);
}
/**
* Create a {@link #autoRangingProperty() non-auto-ranging} Axis with the
* given upper bound, lower bound and tick unit.
*
* @param axisLabel
* the axis {@link #labelProperty() label}
* @param lowerBound
* the {@link #lowerBoundProperty() lower bound} of the axis
* @param upperBound
* the {@link #upperBoundProperty() upper bound} of the axis
* @param tickUnit
* the tick unit, i.e. space between tick marks
*/
public NumericAxis(final String axisLabel, final double lowerBound, final double upperBound,
final double tickUnit) {
super(lowerBound, upperBound);
this.setLabel(axisLabel);
setTickUnit(tickUnit);
super.currentLowerBound.addListener((evt, o, n) -> localCurrentLowerBound = currentLowerBound.get());
super.scaleProperty().addListener((evt, o, n) -> localScale = scaleProperty().get());
}
private final BooleanProperty forceZeroInRange = new SimpleBooleanProperty(this, "forceZeroInRange", true) {
@Override
protected void invalidated() {
if (isAutoRanging()) {
requestAxisLayout();
invalidateRange();
}
}
};
/**
* When {@code true} zero is always included in the visible range. This only
* has effect if {@link #autoRangingProperty() auto-ranging} is on.
*
* @return forceZeroInRange property
*/
public BooleanProperty forceZeroInRangeProperty() {
return forceZeroInRange;
}
/**
* Returns the value of the {@link #forceZeroInRangeProperty()}.
*
* @return value of the forceZeroInRange property
*/
public boolean isForceZeroInRange() {
return forceZeroInRange.getValue();
}
/**
* Sets the value of the {@link #forceZeroInRangeProperty()}.
*
* @param value
* if {@code true}, zero is always included in the visible range
*/
public void setForceZeroInRange(final boolean value) {
forceZeroInRange.setValue(value);
}
private final DoubleProperty autoRangePadding = new SimpleDoubleProperty(0);
/**
* Fraction of the range to be applied as padding on both sides of the axis
* range. E.g. if set to 0.1 (10%) on axis with data range [10, 20], the new
* automatically calculated range will be [9, 21].
*
* @return autoRangePadding property
*/
@Override
public DoubleProperty autoRangePaddingProperty() {
return autoRangePadding;
}
/**
* Returns the value of the {@link #autoRangePaddingProperty()}.
*
* @return the auto range padding
*/
@Override
public double getAutoRangePadding() {
return autoRangePaddingProperty().get();
}
/**
* Sets the value of the {@link #autoRangePaddingProperty()}
*
* @param padding
* padding factor
*/
@Override
public void setAutoRangePadding(final double padding) {
autoRangePaddingProperty().set(padding);
}
private final SimpleStyleableDoubleProperty tickUnit = new SimpleStyleableDoubleProperty(
StyleableProperties.TICK_UNIT, this, "tickUnit", 5d) {
@Override
protected void invalidated() {
if (!isAutoRanging()) {
invalidateRange();
requestAxisLayout();
}
}
};
/**
* The value between each major tick mark in data units. This is
* automatically set if we are auto-ranging.
*
* @return tickUnit property
*/
@Override
public DoubleProperty tickUnitProperty() {
return tickUnit;
}
/**
* Returns tick unit value expressed in data units.
*
* @return major tick unit value
*/
@Override
public double getTickUnit() {
return tickUnitProperty().get();
}
/**
* Sets the value of the {@link #tickUnitProperty()}.
*
* @param unit
* major tick unit
*/
@Override
public void setTickUnit(final double unit) {
tickUnitProperty().set(unit);
}
private final ObjectProperty tickUnitSupplier = new SimpleObjectProperty<>(this,
"tickUnitSupplier", NumericAxis.DEFAULT_TICK_UNIT_SUPPLIER);
/**
* Strategy to compute major tick unit when auto-range is on or when axis
* bounds change. By default initialized to {@link DefaultTickUnitSupplier}.
*
* See {@link TickUnitSupplier} for more information about the expected
* behavior of the strategy.
*
*
* @return tickUnitSupplier property
*/
public ObjectProperty tickUnitSupplierProperty() {
return tickUnitSupplier;
}
/**
* Returns the value of the {@link #tickUnitSupplierProperty()}.
*
* @return the TickUnitSupplier
*/
public TickUnitSupplier getTickUnitSupplier() {
return tickUnitSupplierProperty().get();
}
/**
* Sets the value of the {@link #tickUnitSupplierProperty()}.
*
* @param supplier
* the tick unit supplier. If {@code null}, the default one will
* be used
*/
public void setTickUnitSupplier(final TickUnitSupplier supplier) {
tickUnitSupplierProperty().set(supplier);
}
@Override
protected void setRange(final AxisRange range, final boolean animate) {
super.setRange(range, animate);
setTickUnit(range.getTickUnit());
}
@Override
protected List calculateMajorTickValues(final double axisLength, final AxisRange range) {
final AxisRange rangeImpl = range;
if (rangeImpl.getLowerBound() == rangeImpl.getUpperBound() || rangeImpl.getTickUnit() <= 0) {
return Arrays.asList(rangeImpl.getLowerBound());
}
final List tickValues = new ArrayList<>();
final double firstTick = NumericAxis.computeFistMajorTick(rangeImpl.getLowerBound(),
rangeImpl.getTickUnit());
for (double major = firstTick; major <= rangeImpl.getUpperBound(); major += rangeImpl.getTickUnit()) {
tickValues.add(major);
}
return tickValues;
}
private static double computeFistMajorTick(final double lowerBound, final double tickUnit) {
return Math.ceil(lowerBound / tickUnit) * tickUnit;
}
@Override
protected List calculateMinorTickValues() {
if (getMinorTickCount() == 0 || getTickUnit() == 0) {
return Collections.emptyList();
}
final List minorTickMarks = new ArrayList<>();
final double lowerBound = getLowerBound();
final double upperBound = getUpperBound();
final double majorUnit = getTickUnit();
final double firstMajorTick = NumericAxis.computeFistMajorTick(lowerBound, majorUnit);
final double minorUnit = majorUnit / getMinorTickCount();
for (double majorTick = firstMajorTick - majorUnit; majorTick < upperBound; majorTick += majorUnit) {
final double nextMajorTick = majorTick + majorUnit;
for (double minorTick = majorTick + minorUnit; minorTick < nextMajorTick; minorTick += minorUnit) {
if (minorTick >= lowerBound && minorTick <= upperBound) {
minorTickMarks.add(minorTick);
}
}
}
return minorTickMarks;
}
@Override
protected AxisRange getAxisRange() {
final AxisRange localRange = super.getAxisRange();
final double lower = localRange.getLowerBound();
final double upper = localRange.getUpperBound();
final double axisLength = localRange.getAxisLength();
final double scale = localRange.getScale();
return new AxisRange(lower, upper, axisLength, scale, getTickUnit());
}
@Override
protected AxisRange autoRange(final double minValue, final double maxValue, final double length,
final double labelSize) {
final double min = minValue > 0 && isForceZeroInRange() ? 0 : minValue;
final double max = maxValue < 0 && isForceZeroInRange() ? 0 : maxValue;
final double padding = NumericAxis.getEffectiveRange(min, max) * getAutoRangePadding();
final double paddedMin = NumericAxis.clampBoundToZero(min - padding, min);
final double paddedMax = NumericAxis.clampBoundToZero(max + padding, max);
return computeRange(paddedMin, paddedMax, length, labelSize);
}
private static double getEffectiveRange(final double min, final double max) {
double effectiveRange = max - min;
if (effectiveRange == 0) {
effectiveRange = min == 0 ? NumericAxis.DEFAULT_RANGE_LENGTH : Math.abs(min);
}
return effectiveRange;
}
/**
* If padding pushed the bound above or below zero - stick it to zero.
*
* @param paddedBound padded bounds
* @param bound computed raw bounds
* @return clamped bounds
*/
private static double clampBoundToZero(final double paddedBound, final double bound) {
if (paddedBound < 0 && bound >= 0 || paddedBound > 0 && bound <= 0) {
return 0;
}
return paddedBound;
}
@Override
protected AxisRange computeRange(final double min, final double max, final double axisLength,
final double labelSize) {
double minValue = min;
double maxValue = max;
if (max - min == 0) {
final double padding = getAutoRangePadding() == 0 ? NumericAxis.DEFAULT_RANGE_PADDING
: getAutoRangePadding();
final double paddedRange = NumericAxis.getEffectiveRange(min, max) * padding;
minValue = min - paddedRange / 2;
maxValue = max + paddedRange / 2;
}
return computeRangeImpl(minValue, maxValue, axisLength, labelSize);
}
private AxisRange computeRangeImpl(final double min, final double max, final double axisLength,
final double labelSize) {
final int numOfFittingLabels = (int) Math.floor(axisLength / labelSize);
final int numOfTickMarks = Math.max(Math.min(numOfFittingLabels, NumericAxis.MAX_TICK_COUNT), 2);
double rawTickUnit = (max - min) / numOfTickMarks;
double prevTickUnitRounded;
double tickUnitRounded = Double.MIN_VALUE;
double minRounded = min;
double maxRounded = max;
int ticksCount;
double reqLength;
do {
if (Double.isNaN(rawTickUnit)) {
throw new IllegalArgumentException("Can't calculate axis range: data contains NaN value");
}
// Here we ignore the tickUnit property, so even if the tick unit
// was specified and the auto-range is off
// we don't use it. When narrowing the range (e.g. zoom-in) - this
// is usually ok, but if one wants
// explicitly change bounds while preserving the specified tickUnit,
// this won't work. Perhaps the usage of
// tickUnit should be independent of the auto-range so we should
// introduce autoTickUnit. The other option is
// to provide custom TickUnitSupplier that always returns the same
// tick unit.
prevTickUnitRounded = tickUnitRounded;
tickUnitRounded = computeTickUnit(rawTickUnit);
if (tickUnitRounded <= prevTickUnitRounded) {
break;
}
double firstMajorTick;
if (isAutoRanging() && isAutoRangeRounding()) {
minRounded = Math.floor(min / tickUnitRounded) * tickUnitRounded;
maxRounded = Math.ceil(max / tickUnitRounded) * tickUnitRounded;
firstMajorTick = minRounded;
} else {
firstMajorTick = Math.ceil(min / tickUnitRounded) * tickUnitRounded;
}
ticksCount = 0;
double maxReqTickGap = 0;
double halfOfLastTickSize = 0;
for (double major = firstMajorTick; major <= maxRounded; major += tickUnitRounded, ticksCount++) {
final double tickMarkSize = measureTickMarkLength(major);
if (major == firstMajorTick) {
halfOfLastTickSize = tickMarkSize / 2;
} else {
maxReqTickGap = Math.max(maxReqTickGap,
halfOfLastTickSize + NumericAxis.TICK_MARK_GAP + tickMarkSize / 2);
}
}
reqLength = (ticksCount - 1) * maxReqTickGap;
rawTickUnit = tickUnitRounded * NumericAxis.NEXT_TICK_UNIT_FACTOR;
} while (numOfTickMarks > 2 && (reqLength > axisLength || ticksCount > NumericAxis.MAX_TICK_COUNT));
final double newScale = calculateNewScale(axisLength, minRounded, maxRounded);
return new AxisRange(minRounded, maxRounded, axisLength, newScale, tickUnitRounded);
}
private double computeTickUnit(final double rawTickUnit) {
TickUnitSupplier unitSupplier = getTickUnitSupplier();
if (unitSupplier == null) {
unitSupplier = NumericAxis.DEFAULT_TICK_UNIT_SUPPLIER;
}
final double majorUnit = unitSupplier.computeTickUnit(rawTickUnit);
if (majorUnit <= 0) {
throw new IllegalArgumentException("The " + unitSupplier.getClass().getName()
+ " computed illegal unit value [" + majorUnit + "] for argument " + rawTickUnit);
}
return majorUnit;
}
/**
* Computes the preferred tick unit based on the upper/lower bounds and the
* length of the axis in screen coordinates.
*
* @param axisLength
* the length in screen coordinates
* @return the tick unit
*/
@Override
public double computePreferredTickUnit(final double axisLength) {
final double labelSize = getTickLabelFont().getSize() * 2;
final int numOfFittingLabels = (int) Math.floor(axisLength / labelSize);
final int numOfTickMarks = Math.max(Math.min(numOfFittingLabels, NumericAxis.MAX_TICK_COUNT), 2);
final double max = upperBoundProperty().get();
final double min = lowerBoundProperty().get();
double rawTickUnit = (max - min) / numOfTickMarks;
double prevTickUnitRounded;
double tickUnitRounded = Double.MIN_VALUE;
double minRounded = min;
double maxRounded = max;
int ticksCount;
double reqLength;
do {
if (Double.isNaN(rawTickUnit)) {
throw new IllegalArgumentException("Can't calculate axis range: data contains NaN value");
}
// Here we ignore the tickUnit property, so even if the tick unit
// was specified and the auto-range is off
// we don't use it. When narrowing the range (e.g. zoom-in) - this
// is usually ok, but if one wants
// explicitly change bounds while preserving the specified tickUnit,
// this won't work. Perhaps the usage of
// tickUnit should be independent of the auto-range so we should
// introduce autoTickUnit. The other option is
// to provide custom TickUnitSupplier that always returns the same
// tick unit.
prevTickUnitRounded = tickUnitRounded;
tickUnitRounded = computeTickUnit(rawTickUnit);
if (tickUnitRounded <= prevTickUnitRounded) {
break;
}
double firstMajorTick;
if ((isAutoRanging() || isAutoGrowRanging()) && isAutoRangeRounding()) {
minRounded = Math.floor(min / tickUnitRounded) * tickUnitRounded;
maxRounded = Math.ceil(max / tickUnitRounded) * tickUnitRounded;
firstMajorTick = minRounded;
} else {
firstMajorTick = Math.ceil(min / tickUnitRounded) * tickUnitRounded;
}
ticksCount = 0;
double maxReqTickGap = 0;
double halfOfLastTickSize = 0;
for (double major = firstMajorTick; major <= maxRounded; major += tickUnitRounded, ticksCount++) {
final double tickMarkSize = measureTickMarkLength(major);
if (major == firstMajorTick) {
halfOfLastTickSize = tickMarkSize / 2;
} else {
maxReqTickGap = Math.max(maxReqTickGap,
halfOfLastTickSize + NumericAxis.TICK_MARK_GAP + tickMarkSize / 2);
}
}
reqLength = (ticksCount - 1) * maxReqTickGap;
rawTickUnit = tickUnitRounded * NumericAxis.NEXT_TICK_UNIT_FACTOR;
} while (numOfTickMarks > 2 && (reqLength > axisLength || ticksCount > NumericAxis.MAX_TICK_COUNT));
return tickUnitRounded;
}
/**
* Get the display position along this axis for a given value. If the value
* is not in the current range, the returned value will be an extrapolation
* of the display position. -- cached double optimised version (shaves of
* 50% on delays)
*
* @param value
* The data value to work out display position for
* @return display position
*/
@Override
public double getDisplayPosition(final double value) {
return localOffset + (value - localCurrentLowerBound) * localScale;
}
/**
* Get the data value for the given display position on this axis. If the
* axis is a CategoryAxis this will be the nearest value. -- cached double
* optimised version (shaves of 50% on delays)
*
* @param displayPosition
* A pixel position on this axis
* @return the nearest data value to the given pixel position or null if not
* on axis;
*/
@Override
public double getValueForDisplay(final double displayPosition) {
return (displayPosition - localOffset) / localScale + localCurrentLowerBound;
}
/**
* Get the display position of the zero line along this axis.
*
* @return display position or Double.NaN if zero is not in current range;
*/
@Override
public double getZeroPosition() {
if (0 < getLowerBound() || 0 > getUpperBound()) {
return Double.NaN;
}
// noinspection unchecked
return getDisplayPosition(0);
}
/**
* Checks if the given value is plottable on this axis
*
* @param value
* The value to check if its on axis
* @return true if the given value is plottable on this axis
*/
@Override
public boolean isValueOnAxis(final double value) {
return value >= getLowerBound() && value <= getUpperBound();
}
// -------------- STYLESHEET HANDLING
// ------------------------------------------------------------------------------
private static class StyleableProperties {
private static final CssMetaData TICK_UNIT = new CssMetaData(
"-fx-tick-unit", SizeConverter.getInstance(), 5.0) {
@Override
public boolean isSettable(final NumericAxis axis) {
return axis.tickUnit == null || !axis.tickUnit.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final NumericAxis axis) {
return (StyleableProperty) axis.tickUnitProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables = new ArrayList<>(
Region.getClassCssMetaData());
styleables.add(StyleableProperties.TICK_UNIT);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
@Override
public List> getCssMetaData() {
return NumericAxis.getClassCssMetaData();
}
// private static class NumericAxisRange extends Range {
// private final double tickUnit;
//
// NumericAxisRange(final Range range, final double tickUnit) {
// this(range.getLowerBound(), range.getUpperBound(), range.getScale(),
// range.getTickFormat(), tickUnit);
// }
//
// NumericAxisRange(final double lowerBound, final double upperBound, final
// double scale, final String tickFormat,
// final double tickUnit) {
// super(lowerBound, upperBound, scale, tickFormat, tickUnit, -1);
// this.tickUnit = tickUnit;
// }
//
// @Override
// public double getTickUnit() {
// return tickUnit;
// }
// }
@Override
public boolean isLogAxis() {
return false;
}
@Override
public LogAxisType getLogAxisType() {
return LogAxisType.LINEAR_SCALE;
}
@Override
public AxisTransform getAxisTransform() {
return null;
}
}