de.gsi.chart.axes.spi.AbstractAxisParameter Maven / Gradle / Ivy
Show all versions of chartfx-chart Show documentation
package de.gsi.chart.axes.spi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.css.converters.PaintConverter;
import com.sun.javafx.css.converters.SizeConverter;
import de.gsi.chart.Chart;
import de.gsi.chart.axes.Axis;
import de.gsi.chart.axes.AxisLabelOverlapPolicy;
import de.gsi.chart.ui.css.StylishBooleanProperty;
import de.gsi.chart.ui.css.StylishDoubleProperty;
import de.gsi.chart.ui.css.StylishIntegerProperty;
import de.gsi.chart.ui.css.StylishObjectProperty;
import de.gsi.chart.ui.css.StylishStringProperty;
import de.gsi.chart.ui.geometry.Side;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.FontCssMetaData;
import javafx.css.PseudoClass;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Path;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.util.StringConverter;
/**
* Class containing the properties, getters and setters for the AbstractNumericAxis class
*
* intention is to move the boiler-plate code here for better readability of the AbstractNumericAxis class
*
* @author rstein
*/
public abstract class AbstractAxisParameter extends Pane implements Axis {
private static final String CHART_CSS = Chart.class.getResource("chart.css").toExternalForm();
// N.B. number of divisions, minor tick mark is not drawn if minorTickMark
// == majorTickMark
protected static final int DEFAULT_MINOR_TICK_COUNT = 10;
/**
* Paths used for css-type styling. Not used for actual drawing. Used as a storage contained for the settings
* applied to GraphicsContext which allow much faster (and less complex) drawing routines but do no not allow
* CSS-type styling.
*/
protected final Path majorTickStyle = new Path();
protected final Path minorTickStyle = new Path();
protected final AxisLabel axisLabel = new AxisLabel();
/**
* @return the majorTickStyle for custom user-code based styling
*/
public Path getMajorTickStyle() {
return majorTickStyle;
}
/**
* @return the minorTickStyle for custom user-code based styling
*/
public Path getMinorTickStyle() {
return minorTickStyle;
}
/**
* @return the axisLabel
*/
public Text getAxisLabel() {
return axisLabel;
}
/**
* This is the minimum/maximum current data value and it is used while auto ranging. Package private solely for test
* purposes TODO: replace concept with 'actual range', 'user-defined range', 'auto-range' (+min, max range limit for
* auto).... actual is used to compute tick marks and defined by either user or auto (ie. auto axis is always being
* computed), ALSO add maybe a zoom range (ie. limited by user-set/auto-range range)
*/
protected double oldAxisLength = -1;
protected boolean rangeValid = false;
protected boolean measureInvalid = false;
protected boolean tickLabelsVisibleInvalid = false;
protected final ObservableList majorTickMarks = FXCollections.observableArrayList();
protected final ObservableList minorTickMarkValues = FXCollections.observableArrayList();
/**
* @return observable list containing of each major TickMark on this axis
*/
@Override
public ObservableList getTickMarks() {
return majorTickMarks;
}
/**
* @return observable list containing of each minor TickMark on this axis
*/
@Override
public ObservableList getMinorTickMarks() {
return minorTickMarkValues;
}
/** if available (last) auto-range that has been computed */
protected AxisRange autoRange = new AxisRange();
/** user-specified range (ie. limits based on [lower,upper]Bound) */
protected AxisRange userRange = new AxisRange();
@Override
public AxisRange getAutoRange() {
return autoRange;
}
@Override
public AxisRange getUserRange() {
return userRange;
}
/**
* on auto-ranging this returns getAutoRange(), otherwise the user-specified range getUserRange() (ie. limits based
* on [lower,upper]Bound)
*
* @return actual range that is being used.
*/
@Override
public AxisRange getRange() {
if (isAutoRanging() || isAutoGrowRanging()) {
return getAutoRange();
}
return getUserRange();
}
/**
* The side of the plot which this axis is being drawn on
*
* default axis orientation is BOTTOM, can be set latter to another side
*
*/
private final ObjectProperty side = new StyleableObjectProperty(Side.BOTTOM) {
@Override
protected void invalidated() {
// cause refreshTickMarks
final Side edge = get();
pseudoClassStateChanged(AbstractAxisParameter.TOP_PSEUDOCLASS_STATE, edge == Side.TOP);
pseudoClassStateChanged(AbstractAxisParameter.RIGHT_PSEUDOCLASS_STATE, edge == Side.RIGHT);
pseudoClassStateChanged(AbstractAxisParameter.BOTTOM_PSEUDOCLASS_STATE, edge == Side.BOTTOM);
pseudoClassStateChanged(AbstractAxisParameter.LEFT_PSEUDOCLASS_STATE, edge == Side.LEFT);
pseudoClassStateChanged(AbstractAxisParameter.CENTRE_HOR_PSEUDOCLASS_STATE, edge == Side.CENTER_HOR);
pseudoClassStateChanged(AbstractAxisParameter.CENTRE_VER_PSEUDOCLASS_STATE, edge == Side.CENTER_VER);
requestAxisLayout();
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.SIDE;
}
@Override
public Object getBean() {
return AbstractAxisParameter.this;
}
@Override
public String getName() {
return "side";
}
};
@Override
public Side getSide() {
return sideProperty().get();
}
@Override
public void setSide(final Side value) {
sideProperty().set(value);
}
@Override
public ObjectProperty sideProperty() {
return side;
}
/** The side of the plot which this axis is being drawn on */
private final ObjectProperty overlapPolicy = new StyleableObjectProperty(
AxisLabelOverlapPolicy.SKIP_ALT) {
@Override
protected void invalidated() {
requestAxisLayout();
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.OVERLAP_POLICY;
}
@Override
public Object getBean() {
return AbstractAxisParameter.this;
}
@Override
public String getName() {
return "overlapPolcy";
}
};
public AxisLabelOverlapPolicy getOverlapPolicy() {
return overlapPolicyProperty().get();
}
public void setOverlapPolicy(final AxisLabelOverlapPolicy value) {
overlapPolicyProperty().set(value);
}
public ObjectProperty overlapPolicyProperty() {
return overlapPolicy;
}
/**
* The relative alignment (N.B. clamped to [0.0,1.0]) of the axis if drawn on top of the main canvas (N.B. side ==
* CENTER_HOR or CENTER_VER
*/
private final DoubleProperty centerAxisPosition = new StylishDoubleProperty(
StyleableProperties.CENTER_AXIS_POSITION, this, "centerAxisPosition", 0.5, this::requestAxisLayout) {
@Override
public void set(final double value) {
super.set(Math.max(0.0, Math.min(value, 1.0)));
}
};
public double getCenterAxisPosition() {
return centerAxisPositionProperty().get();
}
public void setAxisCentrePosition(final double value) {
centerAxisPositionProperty().set(value);
}
public DoubleProperty centerAxisPositionProperty() {
return centerAxisPosition;
}
/** axis label alignment */
private final ObjectProperty axisLabelTextAlignment = new StylishObjectProperty<>(
StyleableProperties.AXIS_LABEL_ALIGNMENT, this, "axisLabelTextAlignment", TextAlignment.CENTER,
this::requestAxisLayout);
public TextAlignment getaAxisLabelTextAlignment() {
return axisLabelTextAlignmentProperty().get();
}
public void setAxisLabelAlignment(final TextAlignment value) {
axisLabelTextAlignmentProperty().set(value);
}
public ObjectProperty axisLabelTextAlignmentProperty() {
// return axisLabel.textAlignmentProperty();
return axisLabelTextAlignment;
}
/** The axis label */
private final StringProperty label = new StylishStringProperty(StyleableProperties.AXIS_LABEL, this, "label", null,
this::requestAxisLayout);
@Override
public String getLabel() {
return labelProperty().get();
}
@Override
public void setLabel(final String value) {
labelProperty().set(value);
}
@Override
public StringProperty labelProperty() {
return label;
}
/** true if tick marks should be displayed */
private final BooleanProperty tickMarkVisible = new StylishBooleanProperty(StyleableProperties.TICK_MARK_VISIBLE,
this, "tickMarkVisible", true, this::requestAxisLayout);
public boolean isTickMarkVisible() {
return tickMarkVisible.get();
}
public void setTickMarkVisible(final boolean value) {
tickMarkVisible.set(value);
}
public BooleanProperty tickMarkVisibleProperty() {
return tickMarkVisible;
}
/** true if tick mark labels should be displayed */
private final BooleanProperty tickLabelsVisible = new StylishBooleanProperty(
StyleableProperties.TICK_LABELS_VISIBLE, this, "tickLabelsVisible", true, () -> { // update
// textNode
// visibility
// for
// each
// tick
for (final TickMark tick : majorTickMarks) {
tick.setVisible(this.tickLabelsVisible.get());
}
tickLabelsVisibleInvalid = true;
requestAxisLayout();
});
public boolean isTickLabelsVisible() {
return tickLabelsVisible.get();
}
public void setTickLabelsVisible(final boolean value) {
tickLabelsVisible.set(value);
}
public BooleanProperty tickLabelsVisibleProperty() {
return tickLabelsVisible;
}
/** The length of tick mark lines */
private final DoubleProperty axisPadding = new StylishDoubleProperty(StyleableProperties.AXIS_PADDING, this,
"axisPadding", 15.0, this::requestAxisLayout);
public double getAxisPadding() {
return axisPaddingProperty().get();
}
public void setAxisPadding(final double value) {
axisPaddingProperty().set(value);
}
public DoubleProperty axisPaddingProperty() {
return axisPadding;
}
/** The length of tick mark lines */
private final DoubleProperty tickLength = new StylishDoubleProperty(StyleableProperties.TICK_LENGTH, this,
"tickLength", 8.0, this::requestAxisLayout);
public double getTickLength() {
return tickLength.get();
}
public void setTickLength(final double value) {
tickLength.set(value);
}
public DoubleProperty tickLengthProperty() {
return tickLength;
}
/**
* This is true when the axis determines its range from the data automatically
*/
private final BooleanProperty autoRanging = new StylishBooleanProperty(StyleableProperties.AUTO_RANGING, this,
"autoRanging", true, this::requestAxisLayout);
@Override
public boolean isAutoRanging() {
return autoRanging.get();
}
@Override
public void setAutoRanging(final boolean value) {
autoRanging.set(value);
}
@Override
public BooleanProperty autoRangingProperty() {
return autoRanging;
}
/** The font for all tick labels */
private final ObjectProperty tickLabelFont = new StylishObjectProperty<>(StyleableProperties.TICK_LABEL_FONT,
this, "tickLabelFont", Font.font("System", 8), () -> {
// TODO: remove once verified that measure isn't needed anymore
final Font f = this.tickLabelFontProperty().get();
for (final TickMark tm : getTickMarks()) {
tm.setFont(f);
}
measureInvalid = true;
requestAxisLayout();
});
@Override
public Font getTickLabelFont() {
return tickLabelFont.get();
}
public void setTickLabelFont(final Font value) {
tickLabelFont.set(value);
}
public ObjectProperty tickLabelFontProperty() {
return tickLabelFont;
}
/** The fill for all tick labels */
private final ObjectProperty tickLabelFill = new StylishObjectProperty<>(StyleableProperties.TICK_LABEL_FILL,
this, "tickLabelFill", Color.BLACK, this::requestAxisLayout);
@Override
public Paint getTickLabelFill() {
return tickLabelFill.get();
}
public void setTickLabelFill(final Paint value) {
tickLabelFill.set(value);
}
public ObjectProperty tickLabelFillProperty() {
return tickLabelFill;
}
/** The gap between tick labels and the tick mark lines */
private final DoubleProperty tickLabelGap = new StylishDoubleProperty(StyleableProperties.TICK_LABEL_TICK_GAP, this,
"tickLabelGap", 3.0, this::requestAxisLayout);
@Override
public double getTickLabelGap() {
return axisLabelGap.get();
}
public void setTickLabelGap(final double value) {
tickLabelGap.set(value);
}
public DoubleProperty tickLabelGapProperty() {
return tickLabelGap;
}
/** The gap between tick labels and the axis label */
private final DoubleProperty axisLabelGap = new StylishDoubleProperty(StyleableProperties.AXIS_LABEL_TICK_GAP, this,
"axisLabelGap", 3.0, this::requestAxisLayout);
public double getAxisLabelGap() {
return axisLabelGap.get();
}
public void setAxisLabelGap(final double value) {
axisLabelGap.set(value);
}
public DoubleProperty axisLabelGapProperty() {
return axisLabelGap;
}
/** The animation duration in MS */
private final IntegerProperty animationDuration = new StylishIntegerProperty(StyleableProperties.ANIMATION_DURATION,
this, "animationDuration", 250, this::requestAxisLayout);
public int getAnimationDuration() {
return animationDuration.get();
}
public void setAnimationDuration(final int value) {
animationDuration.set(value);
}
public IntegerProperty animationDurationProperty() {
return animationDuration;
}
/**
* When true any changes to the axis and its range will be animated.
*/
private final BooleanProperty animated = new SimpleBooleanProperty(this, "animated", false);
/**
* Indicates whether the changes to axis range will be animated or not.
*
* @return true if axis range changes will be animated and false otherwise
*/
public boolean getAnimated() {
return animated.get();
}
@Override
public void setAnimated(final boolean value) {
animated.set(value);
}
public BooleanProperty animatedProperty() {
return animated;
}
/**
* Rotation in degrees of tick mark labels from their normal horizontal.
*/
protected final DoubleProperty tickLabelRotation = new StylishDoubleProperty(
StyleableProperties.TICK_LABEL_ROTATION, this, "tickLabelRotation", 0.0, this::requestAxisLayout);
public double getTickLabelRotation() {
return tickLabelRotation.getValue();
}
public void setTickLabelRotation(final double value) {
tickLabelRotation.setValue(value);
}
public DoubleProperty tickLabelRotationProperty() {
return tickLabelRotation;
}
/** true if minor tick marks should be displayed */
private final BooleanProperty minorTickVisible = new StylishBooleanProperty(StyleableProperties.MINOR_TICK_VISIBLE,
this, "minorTickVisible", true, this::requestAxisLayout);
public boolean isMinorTickVisible() {
return minorTickVisible.get();
}
public void setMinorTickVisible(final boolean value) {
minorTickVisible.set(value);
}
public BooleanProperty minorTickVisibleProperty() {
return minorTickVisible;
}
/**
* @return axis length in pixel
*/
@Override
public double getLength() {
if (getSide() == null) {
return Double.NaN;
}
return getSide().isHorizontal() ? getWidth() : getHeight();
}
/** The scale factor from data units to visual units */
private final ReadOnlyDoubleWrapper scale = new ReadOnlyDoubleWrapper(this, "scale", 1) {
@Override
protected void invalidated() {
requestAxisLayout();
measureInvalid = true;
}
};
public double getScale() {
return scale.get();
}
protected void setScale(final double scale) {
this.scale.set(scale);
}
public ReadOnlyDoubleProperty scaleProperty() {
return scale.getReadOnlyProperty();
}
ReadOnlyDoubleWrapper scalePropertyImpl() {
return scale;
}
/**
* The value for the upper bound of this axis, ie max value. This is automatically set if auto ranging is on.
*/
protected final DoubleProperty upperBound = new SimpleDoubleProperty(this, "upperBound", 100) {
@Override
public void set(double newValue) {
double oldValue = get();
if (oldValue != newValue) {
super.set(newValue);
requestAxisLayout();
}
}
};
@Override
public double getUpperBound() {
return upperBound.get();
}
@Override
public void setUpperBound(final double value) {
upperBound.set(value);
}
@Override
public DoubleProperty upperBoundProperty() {
return upperBound;
}
/**
* The value for the lower bound of this axis, ie min value. This is automatically set if auto ranging is on.
*/
protected final DoubleProperty lowerBound = new SimpleDoubleProperty(this, "lowerBound", 0) {
@Override
public void set(double newValue) {
double oldValue = get();
if (oldValue != newValue) {
super.set(newValue);
requestAxisLayout();
}
}
};
@Override
public double getLowerBound() {
return lowerBound.get();
}
@Override
public void setLowerBound(final double value) {
lowerBound.set(value);
}
@Override
public DoubleProperty lowerBoundProperty() {
return lowerBound;
}
/**
* StringConverter used to format tick mark labels. If null a default will be used
*/
private final ObjectProperty> tickLabelFormatter = new ObjectPropertyBase>(
null) {
@Override
protected void invalidated() {
invalidateRange();
requestAxisLayout();
}
@Override
public Object getBean() {
return AbstractAxisParameter.this;
}
@Override
public String getName() {
return "tickLabelFormatter";
}
};
@Override
public StringConverter getTickLabelFormatter() {
return tickLabelFormatter.getValue();
}
public void setTickLabelFormatter(final StringConverter value) {
tickLabelFormatter.setValue(value);
}
public ObjectProperty> tickLabelFormatterProperty() {
return tickLabelFormatter;
}
/**
* The length of minor tick mark lines. Set to 0 to not display minor tick marks.
*/
private final DoubleProperty minorTickLength = new StylishDoubleProperty(StyleableProperties.MINOR_TICK_LENGTH,
this, "minorTickLength", 5.0, this::requestAxisLayout);
public double getMinorTickLength() {
return minorTickLength.get();
}
public void setMinorTickLength(final double value) {
minorTickLength.set(value);
}
public DoubleProperty minorTickLengthProperty() {
return minorTickLength;
}
/**
* The number of minor tick divisions to be displayed between each major tick mark. The number of actual minor tick
* marks will be one less than this. N.B. number of divisions, minor tick mark is not drawn if minorTickMark ==
* majorTickMark
*/
private final IntegerProperty minorTickCount = new StylishIntegerProperty(StyleableProperties.MINOR_TICK_COUNT,
this, "minorTickCount", DEFAULT_MINOR_TICK_COUNT, this::requestAxisLayout);
@Override
public int getMinorTickCount() {
return minorTickCount.get();
}
public void setMinorTickCount(final int value) {
minorTickCount.set(value);
}
public IntegerProperty minorTickCountProperty() {
return minorTickCount;
}
/**
* Used to update scale property in AbstractAxisParameter (that is read-only) TODO: remove is possible
*/
protected final DoubleProperty scaleBinding = new SimpleDoubleProperty(this, "scaleBinding", getScale()) {
@Override
protected void invalidated() {
setScale(get());
}
};
private final BooleanProperty autoGrowRanging = new StylishBooleanProperty(StyleableProperties.AUTO_GROW_RANGING,
this, "autoGrowRanging", false, this::requestAxisLayout);
/**
* This is true when the axis determines its range from the data automatically and grows it if necessary
*
* @return true if axis shall be updated to the optimal data range
*/
@Override
public boolean isAutoGrowRanging() {
return autoGrowRanging.get();
}
/**
* This is true when the axis determines its range from the data automatically and grows it if necessary
*
* @param state true if axis shall be updated to the optimal data range and grows it if necessary
*/
@Override
public void setAutoGrowRanging(final boolean state) {
if (state) {
setAutoRanging(false);
requestAxisLayout();
}
autoGrowRanging.set(state);
}
/**
* This is true when the axis determines its range from the data automatically
*
* @return property
*/
@Override
public BooleanProperty autoGrowRangingProperty() {
return autoGrowRanging;
}
protected boolean isInvertedAxis = false; // internal use (for performance
// reason)
private final BooleanProperty invertAxis = new SimpleBooleanProperty(this, "invertAxis", false) {
@Override
protected void invalidated() {
isInvertedAxis = get();
invalidateRange();
// layoutChildren();
// layout();
requestAxisLayout();
}
};
protected boolean isTimeAxis = false; // internal use (for performance
// reason)
private final BooleanProperty timeAxis = new SimpleBooleanProperty(this, "timeAxis", false) {
@Override
protected void invalidated() {
isTimeAxis = get();
if (isTimeAxis) {
setMinorTickCount(0);
} else {
setMinorTickCount(AbstractAxisParameter.DEFAULT_MINOR_TICK_COUNT);
}
invalidateRange();
// layoutChildren();
// layout();
requestAxisLayout();
}
};
private final BooleanProperty autoRangeRounding = new StylishBooleanProperty(
StyleableProperties.AUTO_RANGE_ROUNDING, this, "autoRangeRounding", false, this::requestAxisLayout);
// TODO: check default (normally true)
/**
* With {@link #autoRangingProperty()} on, defines if the range should be extended to the major tick unit value. For
* example with range [3, 74] and major tick unit [5], the range will be extended to [0, 75].
*
* Default value: {@code true}
*
*
* @return autoRangeRounding property
*/
public BooleanProperty autoRangeRoundingProperty() {
return autoRangeRounding;
}
/**
* Returns the value of the {@link #autoRangeRoundingProperty()}.
*
* @return the auto range rounding flag
*/
public boolean isAutoRangeRounding() {
return autoRangeRoundingProperty().get();
}
/**
* Sets the value of the {@link #autoRangeRoundingProperty()}
*
* @param round if {@code true}, lower and upper bound will be adjusted to the tick unit value
*/
public void setAutoRangeRounding(final boolean round) {
autoRangeRoundingProperty().set(round);
}
/**
* This is {@code true} when the axis labels and data point order should be inverted
*
* @return {@code true} if axis shall be inverted (i.e. drawn from 'max->min', rather than the normal 'min->max')
*/
@Override
public boolean isInvertedAxis() {
return invertAxis.get();
}
/**
* This is {@code true} when the axis labels and data point order should be inverted
*
* @param value {@code true} if axis shall be inverted (i.e. drawn from 'max->min', rather than the normal
* 'min->max')
*/
@Override
public void invertAxis(final boolean value) {
invertAxis.set(value);
}
/**
* This is {@code true} when the axis labels and data point order should be inverted
*
* @return property
*/
@Override
public BooleanProperty invertAxisProperty() {
return invertAxis;
}
/**
* This is true when the axis corresponds to a time-axis
*
* @return true if axis is a time scale
*/
@Override
public boolean isTimeAxis() {
return timeAxis.get();
}
/**
* This is {@code true} when the axis labels and data point should be plotted according to some time-axis definition
*
* @param value {@code true} if axis shall be drawn with time-axis labels
*/
@Override
public void setTimeAxis(final boolean value) {
timeAxis.set(value);
}
/**
* This is {@code true} when the axis labels and data point should be plotted according to some time-axis definition
*
* @return time axis property reference
*/
@Override
public BooleanProperty timeAxisProperty() {
return timeAxis;
}
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
*/
public DoubleProperty autoRangePaddingProperty() {
return autoRangePadding;
}
/**
* Returns the value of the {@link #autoRangePaddingProperty()}.
*
* @return the auto range padding
*/
public double getAutoRangePadding() {
return autoRangePaddingProperty().get();
}
/**
* Sets the value of the {@link #autoRangePaddingProperty()}
*
* @param padding padding factor
*/
public void setAutoRangePadding(final double padding) {
autoRangePaddingProperty().set(padding);
}
protected double decadeRange() {
final double range = Math.abs(getUpperBound() - getLowerBound());
if (range <= 0) {
return 1;
}
return Math.log10(range);
}
protected void updateScaleAndUnitPrefix() {
final double range = Math.abs(getUpperBound() - getLowerBound());
final double logRange = Math.log10(range);
final double power3Upper = 3.0 * Math.ceil(logRange / 3.0);
final double power3Lower = 3.0 * Math.floor(logRange / 3.0);
// TODO: check whether smaller to -INF or closest to '0' should be
// chosen
// System.err.println(" range = " + range + " p3U = " + power3Upper + "
// p3L = " + power3Lower);
final double a = power3Upper > power3Lower ? power3Lower : power3Upper;
final double power = Math.pow(10, a);
final double oldPower = getUnitScaling();
if (power != oldPower && power != 0) {
this.setUnitScaling(power);
}
setTickUnit(range / getMinorTickCount());
}
protected void updateAxisLabelAndUnit() {
final String axisPrimaryLabel = getLabel();
String axisUnit = getUnit();
final boolean isAutoScaling = getAutoUnitScaling();
if (isAutoScaling) {
updateScaleAndUnitPrefix();
}
final String axisPrefix = MetricPrefix.getShortPrefix(getUnitScaling());
if ((axisUnit == null) && (axisPrefix != null)) {
axisUnit = " a.u.";
}
if (axisUnit == null) {
axisLabel.setText(axisPrimaryLabel);
} else {
axisLabel.setText(new StringBuilder().append(axisPrimaryLabel).append(" [").append(axisPrefix)
.append(axisUnit).append("]").toString());
}
axisLabel.applyCss();
}
/** The axis unit label */
private final ObjectProperty unitLabel = new ObjectPropertyBase() {
@Override
protected void invalidated() {
updateAxisLabelAndUnit();
requestAxisLayout();
}
@Override
public Object getBean() {
return AbstractAxisParameter.this;
}
@Override
public String getName() {
return "unitLabel";
}
};
@Override
public String getUnit() {
return unitLabel.get();
}
@Override
public void setUnit(final String value) {
unitLabel.set(value);
}
@Override
public ObjectProperty unitProperty() {
return unitLabel;
}
/** The axis unit label */
private final BooleanProperty autoUnitScaling = new SimpleBooleanProperty(this, "autoUnitScaling", false) {
@Override
protected void invalidated() {
updateAxisLabelAndUnit();
requestAxisLayout();
}
};
@Override
public boolean getAutoUnitScaling() {
return autoUnitScaling.get();
}
@Override
public void setAutoUnitScaling(final boolean value) {
autoUnitScaling.set(value);
}
@Override
public BooleanProperty autoUnitScalingProperty() {
return autoUnitScaling;
}
/** The axis unit label */
private final DoubleProperty unitScaling = new SimpleDoubleProperty(this, "unitScaling", 1.0) {
@Override
protected void invalidated() {
updateAxisLabelAndUnit();
requestAxisLayout();
}
};
@Override
public double getUnitScaling() {
return unitScaling.get();
}
@Override
public void setUnitScaling(final double value) {
if (!Double.isFinite(value) || value == 0) {
throw new IllegalArgumentException("provided number is not finite and/or zero: " + value);
}
setTickUnit(value);
unitScaling.set(value);
}
@Override
public void setUnitScaling(final MetricPrefix prefix) {
unitScaling.set(prefix.getPower());
}
@Override
public DoubleProperty unitScalingProperty() {
return unitScaling;
}
protected final SimpleStyleableDoubleProperty tickUnit = new SimpleStyleableDoubleProperty(
StyleableProperties.TICK_UNIT, this, "tickUnit", 5d) {
@Override
protected void invalidated() {
if (isAutoRanging() || isAutoGrowRanging()) {
return;
}
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);
}
/**
* Create a auto-ranging AbstractAxisParameter
*/
public AbstractAxisParameter() {
super();
getStylesheets().add(AbstractAxisParameter.CHART_CSS);
getStyleClass().setAll("axis");
majorTickStyle.getStyleClass().add("axis-tick-mark");
minorTickStyle.getStyleClass().add("axis-minor-tick-mark");
getChildren().addAll(axisLabel, majorTickStyle, minorTickStyle);
autoRangingProperty().addListener(ch -> {
// disable auto grow if auto range is enabled
if (isAutoRanging()) {
setAutoGrowRanging(false);
}
});
autoGrowRangingProperty().addListener(ch -> {
// disable auto grow if auto range is enabled
if (isAutoGrowRanging()) {
setAutoRanging(false);
}
});
labelProperty().addListener(e -> {
updateAxisLabelAndUnit();
requestAxisLayout();
});
final ChangeListener autoRangeChangeListener = (ch, oldValue, newValue) -> {
if (oldValue.equals(newValue)) {
return;
}
if (getAutoUnitScaling()) {
updateAxisLabelAndUnit();
}
};
upperBoundProperty().addListener(autoRangeChangeListener);
lowerBoundProperty().addListener(autoRangeChangeListener);
axisLabel.textAlignmentProperty().bindBidirectional(this.axisLabelTextAlignmentProperty());
// bind limits to user-specified axis range
// userRange.set
ChangeListener userLimitChangeListener = (ch, o, n) -> {
this.userRange.set(getLowerBound(), getUpperBound());
// axis range has been set manually -> disable auto-ranging
// TODO: enable once the new scheme checks out
// setAutoRanging(false);
// setAutoGrowRanging(false);
if (!isAutoRanging() && !isAutoGrowRanging()) {
requestAxisLayout();
}
};
this.lowerBoundProperty().addListener(userLimitChangeListener);
this.upperBoundProperty().addListener(userLimitChangeListener);
majorTickStyle.applyCss();
minorTickStyle.applyCss();
axisLabel.applyCss();
}
// -------------- UPDATER ROUTINE PROTOTYPES
/**
* Mark the current range invalid, this will cause anything that depends on the range to be recalculated on the next
* layout.
*/
protected void invalidateRange() {
rangeValid = false;
}
/**
* Request that the axis is laid out in the next layout pass. This replaces requestLayout() as it has been
* overridden to do nothing so that changes to children's bounds etc do not cause a layout. This was done as a
* optimisation as the Axis knows the exact minimal set of changes that really need layout to be updated. So we only
* want to request layout then, not on any child change.
*/
@Override
public abstract void requestAxisLayout();
public abstract void fireInvalidated();
// -------------- STYLESHEET HANDLING
/*
*
* TODO: follow-up idea of simplifying CSS handling: add/make css collector
* to StylishXXXProperty handling and put below code inside
*
*/
private static class StyleableProperties {
private static final CssMetaData SIDE = new CssMetaData(
"-fx-side", new EnumConverter<>(Side.class)) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.side == null || !n.side.isBound();
}
@SuppressWarnings("unchecked") // sideProperty() is
// StyleableProperty
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.sideProperty();
}
};
private static final CssMetaData OVERLAP_POLICY = new CssMetaData(
"-fx-overlap-policy", new EnumConverter<>(AxisLabelOverlapPolicy.class)) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.overlapPolicy == null || !n.overlapPolicy.isBound();
}
@SuppressWarnings("unchecked") // sideProperty() is
// StyleableProperty
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.overlapPolicyProperty();
}
};
private static final CssMetaData AXIS_LABEL_ALIGNMENT = new CssMetaData(
"-fx-axis-label-alignment", new EnumConverter<>(TextAlignment.class)) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.axisLabelTextAlignment == null || !n.axisLabelTextAlignment.isBound();
}
@SuppressWarnings("unchecked") // class type matched by design
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.axisLabelTextAlignmentProperty();
}
};
private static final CssMetaData AXIS_LABEL = new CssMetaData(
"-fx-axis-label", com.sun.javafx.css.converters.StringConverter.getInstance()) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.label == null || !n.label.isBound();
}
@SuppressWarnings("unchecked") // class type matched by design
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.labelProperty();
}
};
private static final CssMetaData CENTER_AXIS_POSITION = new CssMetaData(
"-fx-centre-axis-position", SizeConverter.getInstance(), 0.5) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.centerAxisPosition == null || !n.centerAxisPosition.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.centerAxisPositionProperty();
}
};
private static final CssMetaData AXIS_PADDING = new CssMetaData(
"-fx-axis-padding", SizeConverter.getInstance(), 15.0) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.axisPadding == null || !n.axisPadding.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.axisPaddingProperty();
}
};
private static final CssMetaData TICK_LENGTH = new CssMetaData(
"-fx-tick-length", SizeConverter.getInstance(), 8.0) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickLength == null || !n.tickLength.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickLengthProperty();
}
};
private static final CssMetaData TICK_LABEL_FONT = new FontCssMetaData(
"-fx-tick-label-font", Font.font("system", 8.0)) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickLabelFont == null || !n.tickLabelFont.isBound();
}
@SuppressWarnings("unchecked") // tickLabelFontProperty() is
// StyleableProperty
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickLabelFontProperty();
}
};
private static final CssMetaData TICK_LABEL_FILL = new CssMetaData(
"-fx-tick-label-fill", PaintConverter.getInstance(), Color.BLACK) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickLabelFill == null | !n.tickLabelFill.isBound();
}
@SuppressWarnings("unchecked") // tickLabelFillProperty() is
// StyleableProperty
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickLabelFillProperty();
}
};
private static final CssMetaData TICK_LABEL_TICK_GAP = new CssMetaData(
"-fx-tick-label-gap", SizeConverter.getInstance(), 3.0) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickLabelGap == null || !n.tickLabelGap.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickLabelGapProperty();
}
};
private static final CssMetaData AXIS_LABEL_TICK_GAP = new CssMetaData(
"-fx-axis-label-gap", SizeConverter.getInstance(), 3.0) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickLabelGap == null || !n.tickLabelGap.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickLabelGapProperty();
}
};
private static final CssMetaData ANIMATION_DURATION = new CssMetaData(
"-fx-axis-animation-duration", SizeConverter.getInstance(), 250) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.animationDuration == null || !n.animationDuration.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.animationDurationProperty();
}
};
private static final CssMetaData TICK_MARK_VISIBLE = new CssMetaData(
"-fx-tick-mark-visible", BooleanConverter.getInstance(), Boolean.TRUE) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickMarkVisible == null || !n.tickMarkVisible.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickMarkVisibleProperty();
}
};
private static final CssMetaData TICK_LABELS_VISIBLE = new CssMetaData(
"-fx-tick-labels-visible", BooleanConverter.getInstance(), Boolean.TRUE) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.tickLabelsVisible == null || !n.tickLabelsVisible.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.tickLabelsVisibleProperty();
}
};
private static final CssMetaData TICK_UNIT = new CssMetaData(
"-fx-tick-unit", SizeConverter.getInstance(), 5.0) {
@Override
public boolean isSettable(final DefaultNumericAxis axis) {
return axis.tickUnit == null || !axis.tickUnit.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final DefaultNumericAxis axis) {
return (StyleableProperty) axis.tickUnitProperty();
}
};
private static final CssMetaData TICK_LABEL_ROTATION = new CssMetaData(
"-fx-tick-rotation", SizeConverter.getInstance(), 0.0) {
@Override
public boolean isSettable(final DefaultNumericAxis axis) {
return axis.tickLabelRotation == null || !axis.tickLabelRotation.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final DefaultNumericAxis axis) {
return (StyleableProperty) axis.tickLabelRotationProperty();
}
};
private static final CssMetaData MINOR_TICK_LENGTH = new CssMetaData(
"-fx-minor-tick-length", SizeConverter.getInstance(), 5.0) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.minorTickLength == null || !n.minorTickLength.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.minorTickLengthProperty();
}
};
private static final CssMetaData MINOR_TICK_COUNT = new CssMetaData(
"-fx-minor-tick-count", SizeConverter.getInstance(), 5) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.minorTickCount == null || !n.minorTickCount.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.minorTickCountProperty();
}
};
private static final CssMetaData MINOR_TICK_VISIBLE = new CssMetaData(
"-fx-minor-tick-visible", BooleanConverter.getInstance(), Boolean.TRUE) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.minorTickVisible == null || !n.minorTickVisible.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.minorTickVisibleProperty();
}
};
private static final CssMetaData AUTO_RANGING = new CssMetaData(
"-fx-auto-ranging", BooleanConverter.getInstance(), Boolean.TRUE) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.autoRanging == null || !n.autoRanging.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.autoRangingProperty();
}
};
private static final CssMetaData AUTO_GROW_RANGING = new CssMetaData(
"-fx-auto-grow-ranging", BooleanConverter.getInstance(), Boolean.FALSE) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.autoGrowRanging == null || !n.autoGrowRanging.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.autoGrowRangingProperty();
}
};
private static final CssMetaData AUTO_RANGE_ROUNDING = new CssMetaData(
"-fx-auto-range-rounding", BooleanConverter.getInstance(), Boolean.FALSE) {
@Override
public boolean isSettable(final AbstractAxisParameter n) {
return n.autoRangeRounding == null || !n.autoRangeRounding.isBound();
}
@SuppressWarnings("unchecked")
@Override
public StyleableProperty getStyleableProperty(final AbstractAxisParameter n) {
return (StyleableProperty) n.autoRangeRoundingProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables = new ArrayList<>(Region.getClassCssMetaData());
styleables.add(StyleableProperties.SIDE);
styleables.add(StyleableProperties.CENTER_AXIS_POSITION);
styleables.add(StyleableProperties.AXIS_PADDING);
styleables.add(StyleableProperties.AXIS_LABEL_TICK_GAP);
styleables.add(StyleableProperties.AXIS_LABEL_ALIGNMENT);
styleables.add(StyleableProperties.AXIS_LABEL);
styleables.add(StyleableProperties.ANIMATION_DURATION);
styleables.add(StyleableProperties.TICK_LENGTH);
styleables.add(StyleableProperties.TICK_LABEL_FONT);
styleables.add(StyleableProperties.TICK_LABEL_FILL);
styleables.add(StyleableProperties.TICK_LABEL_TICK_GAP);
styleables.add(StyleableProperties.TICK_MARK_VISIBLE);
styleables.add(StyleableProperties.TICK_LABELS_VISIBLE);
styleables.add(StyleableProperties.TICK_LABEL_ROTATION);
styleables.add(StyleableProperties.TICK_UNIT);
styleables.add(StyleableProperties.MINOR_TICK_COUNT);
styleables.add(StyleableProperties.MINOR_TICK_LENGTH);
styleables.add(StyleableProperties.MINOR_TICK_VISIBLE);
styleables.add(AUTO_RANGING);
styleables.add(AUTO_GROW_RANGING);
styleables.add(AUTO_RANGE_ROUNDING);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* @return The CssMetaData associated with this class, which may include the CssMetaData of its super classes.
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
*/
@Override
public List> getCssMetaData() {
return AbstractAxisParameter.getClassCssMetaData();
}
/** pseudo-class indicating this is a vertical Top side Axis. */
private static final PseudoClass TOP_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("top");
/** pseudo-class indicating this is a vertical Bottom side Axis. */
private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("bottom");
/** pseudo-class indicating this is a vertical Left side Axis. */
private static final PseudoClass LEFT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("left");
/** pseudo-class indicating this is a vertical Right side Axis. */
private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("right");
/** pseudo-class indicating this is a horizontal centre Axis. */
private static final PseudoClass CENTRE_HOR_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("horCentre");
/** pseudo-class indicating this is a vertical centre Axis. */
private static final PseudoClass CENTRE_VER_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("verCentre");
}