All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.gsi.chart.axes.spi.LogarithmicAxis Maven / Gradle / Ivy

package de.gsi.chart.axes.spi;

import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;

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 de.gsi.chart.ui.css.CssPropertyFactory;

/**
 * @author rstein
 */
public class LogarithmicAxis extends AbstractAxis {
    private static final CssPropertyFactory CSS = new CssPropertyFactory<>(AbstractAxisParameter.getClassCssMetaData());
    public static final double DEFAULT_LOGARITHM_BASE = 10;
    public static final double DEFAULT_LOG_MIN_VALUE = 1e-6;
    public static final double DB20_LOGARITHM_BASE = Math.exp(Math.log10(10) / 20.0);
    public static final double DB10_LOGARITHM_BASE = Math.exp(Math.log10(10) / 10.0);
    public static final int DEFAULT_SMALL_LOG_AXIS = 4; // [orders of
            // magnitude], e.g. '4'
            // <-> [1,10000]
    private static final int DEFAULT_TICK_COUNT = 9;
    private static final TickUnitSupplier DEFAULT_TICK_UNIT_SUPPLIER = new DefaultTickUnitSupplier();

    private final transient Cache cache = new Cache();

    private boolean isUpdating;

    private final StyleableDoubleProperty tickUnit = CSS.createDoubleProperty(this, "tickUnit", 5d, true, null, () -> {
        if (!isAutoRanging()) {
            invalidate();
            requestAxisLayout();
        }
    });

    private final ObjectProperty tickUnitSupplier = new SimpleObjectProperty<>(this,
            "tickUnitSupplier", LogarithmicAxis.DEFAULT_TICK_UNIT_SUPPLIER);

    private final DoubleProperty logarithmBase = new SimpleDoubleProperty(this, "logarithmBase",
            LogarithmicAxis.DEFAULT_LOGARITHM_BASE) {
        @Override
        protected void invalidated() {
            if (get() <= 1) {
                throw new IllegalArgumentException("logarithmBase must be grater than 1");
            }
            invalidate();
            requestAxisLayout();
        }
    };

    /**
     * Creates an {@link #autoRangingProperty() auto-ranging} LogarithmicAxis.
     */
    public LogarithmicAxis() {
        this("axis label", 0.0, 0.0, 5.0);
    }

    /**
     * Creates a {@link #autoRangingProperty() non-auto-ranging} LogarithmicAxis with the given upper bound, lower bound
     * and tick unit.
     *
     * @param lowerBound the {@link #minProperty() lower bound} of the axis
     * @param upperBound the {@link #maxProperty() upper bound} of the axis
     * @param tickUnit the tick unit, i.e. space between tick marks
     */
    public LogarithmicAxis(final double lowerBound, final double upperBound, final double tickUnit) {
        this(null, lowerBound, upperBound, tickUnit);
    }

    /**
     * Create a {@link #autoRangingProperty() non-auto-ranging} LogarithmicAxis with the given upper bound, lower bound
     * and tick unit.
     *
     * @param axisLabel the axis {@link #nameProperty() label}
     * @param lowerBound the {@link #minProperty() lower bound} of the axis
     * @param upperBound the {@link #maxProperty() upper bound} of the axis
     * @param tickUnit the tick unit, i.e. space between tick marks
     */
    public LogarithmicAxis(final String axisLabel, final double lowerBound, final double upperBound,
            final double tickUnit) {
        super(lowerBound, upperBound);
        this.setName(axisLabel);
        if (lowerBound >= upperBound || lowerBound == 0 || (lowerBound == 0 && upperBound == 0)) {
            setAutoRanging(true);
        }
        setTickUnit(tickUnit);
        setMinorTickCount(LogarithmicAxis.DEFAULT_TICK_COUNT);
        super.minProperty().addListener((evt, o, n) -> cache.updateCachedAxisVariables());
        super.maxProperty().addListener((evt, o, n) -> cache.updateCachedAxisVariables());
        super.scaleProperty().addListener((evt, o, n) -> cache.updateCachedAxisVariables());
        widthProperty().addListener((ch, o, n) -> cache.axisWidth = getWidth());
        heightProperty().addListener((ch, o, n) -> cache.axisHeight = getHeight());

        isUpdating = false;
    }

    /**
     * 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) {
        return tickUnit.get();
    }

    @Override
    public AxisTransform getAxisTransform() {
        return null;
    }

    @Override
    public List> getCssMetaData() {
        return LogarithmicAxis.getClassCssMetaData();
    }

    /**
     * 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) {
        final double valueLogOffset = log(value) - cache.lowerBoundLog;
        if (cache.isVerticalAxis) {
            return cache.axisHeight - valueLogOffset * cache.logScaleLengthInv;
        }
        return valueLogOffset * cache.logScaleLengthInv;
    }

    /**
     * Returns the value of the {@link #logarithmBaseProperty()}.
     *
     * @return base of the logarithm
     */
    public double getLogarithmBase() {
        return logarithmBaseProperty().get();
    }

    /**
     * @return the log axis Type @see LogAxisType
     */
    @Override
    public LogAxisType getLogAxisType() {
        return LogAxisType.LINEAR_SCALE;
    }

    /**
     * Returns tick unit value expressed in data units.
     *
     * @return major tick unit value
     */
    @Override
    public double getTickUnit() {
        return tickUnitProperty().get();
    }

    /**
     * Returns the value of the {@link #tickUnitSupplierProperty()}.
     *
     * @return the TickUnitSupplier
     */
    public TickUnitSupplier getTickUnitSupplier() {
        return tickUnitSupplierProperty().get();
    }

    /**
     * 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) {
        if (cache.isVerticalAxis) {
            final double height = cache.axisHeight;
            return pow(cache.lowerBoundLog + (height - displayPosition) / height * cache.logScaleLength);
        }

        return pow(cache.lowerBoundLog + displayPosition / cache.axisWidth * cache.logScaleLength);
    }

    /**
     * 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() {
        return getDisplayPosition(cache.localCurrentLowerBound);
    }

    /**
     * @return {@code true} if logarithmic axis, {@code false} otherwise
     */
    @Override
    public boolean isLogAxis() {
        return true;
    }

    /**
     * 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 >= getMin() && value <= getMax();
    }

    /**
     * Base of the logarithm used by the axis, must be grater than 1.
     * 

* Default value: 10 *

* * @return base of the logarithm */ public DoubleProperty logarithmBaseProperty() { return logarithmBase; } @Override public void requestAxisLayout() { if (isUpdating) { return; } super.requestAxisLayout(); } /** * Sets value of the {@link #logarithmBaseProperty()}. * * @param value base of the logarithm, value > 1 */ public void setLogarithmBase(final double value) { logarithmBaseProperty().set(value); } /** * Sets the value of the {@link #tickUnitProperty()}. * * @param unit major tick unit */ @Override public void setTickUnit(final double unit) { tickUnitProperty().set(unit); } /** * 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); } /** * 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; } /** * 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; } private double log(final double value) { if (value <= 0) { return Double.NaN; } return Math.log10(value) / cache.logBase; } private double pow(final double value) { return Math.pow(getLogarithmBase(), value); } @Override protected AxisRange autoRange(final double minValue, final double maxValue, final double length, final double labelSize) { double min = minValue; // sanitise range if < 0 if (min <= 0) { min = LogarithmicAxis.DEFAULT_LOG_MIN_VALUE; isUpdating = true; setMin(LogarithmicAxis.DEFAULT_LOG_MIN_VALUE); isUpdating = false; } final double paddingScale = 1.0 + getAutoRangePadding(); final double paddedMin = min / paddingScale; final double paddedMax = maxValue * paddingScale; return computeRange(paddedMin, paddedMax, length, labelSize); } @Override protected List calculateMajorTickValues(final double axisLength, final AxisRange range) { if (range == null) { throw new InvalidParameterException("range is null"); } final List tickValues = new ArrayList<>(); if (range.getLowerBound() >= range.getUpperBound()) { return Collections.singletonList(range.getLowerBound()); } double exp = Math.ceil(log(range.getLowerBound())); for (double tickValue = pow(exp); tickValue <= range.getUpperBound(); tickValue = pow(++exp)) { tickValues.add(tickValue); } return tickValues; } // -------------- STYLESHEET HANDLING // ------------------------------------------------------------------------------ @Override protected List calculateMinorTickValues() { if (getMinorTickCount() <= 0) { return Collections.emptyList(); } final List minorTickMarks = new ArrayList<>(); final double lowerBound = getMin(); final double upperBound = getMax(); double exp = Math.floor(log(lowerBound)); for (double majorTick = pow(exp); majorTick < upperBound; majorTick = pow(++exp)) { final double nextMajorTick = pow(exp + 1); final double minorUnit = (nextMajorTick - majorTick) / getMinorTickCount(); for (double minorTick = majorTick + minorUnit; minorTick < nextMajorTick; minorTick += minorUnit) { if (minorTick >= lowerBound && minorTick <= upperBound) { minorTickMarks.add(minorTick); } } } return minorTickMarks; } @Override protected AxisRange computeRange(final double min, final double max, final double axisLength, final double labelSize) { double minValue = min; double maxValue = max; if ((isAutoRanging() || isAutoGrowRanging()) && isAutoRangeRounding()) { minValue = minValue <= 0 ? LogarithmicAxis.DEFAULT_LOG_MIN_VALUE : pow(Math.floor(log(minValue))); maxValue = pow(Math.ceil(log(maxValue))); } final double newScale = calculateNewScale(axisLength, minValue, maxValue); return new AxisRange(minValue, maxValue, axisLength, newScale, tickUnit.get()); } public static List> getClassCssMetaData() { return CSS.getCssMetaData(); } protected class Cache { protected double localCurrentLowerBound; protected double localCurrentUpperBound; protected double upperBoundLog; protected double lowerBoundLog; protected double logScaleLength; protected double logScaleLengthInv; protected boolean isVerticalAxis; protected double axisWidth; protected double axisHeight; protected double logBase; private void updateCachedAxisVariables() { localCurrentLowerBound = LogarithmicAxis.super.getMin(); localCurrentUpperBound = LogarithmicAxis.super.getMax(); upperBoundLog = log(getMax()); lowerBoundLog = log(getMin()); logScaleLength = upperBoundLog - lowerBoundLog; logScaleLengthInv = 1.0 / logScaleLength; if (getSide() != null) { isVerticalAxis = getSide().isVertical(); } if (isVerticalAxis) { logScaleLengthInv = axisHeight / logScaleLength; } else { logScaleLengthInv = axisWidth / logScaleLength; } logBase = Math.log10(getLogarithmBase()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy