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

de.gsi.chart.XYChart Maven / Gradle / Ivy

package de.gsi.chart;

import java.security.InvalidParameterException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.canvas.GraphicsContext;
import javafx.util.Duration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.gsi.chart.axes.Axis;
import de.gsi.chart.renderer.PolarTickStep;
import de.gsi.chart.renderer.Renderer;
import de.gsi.chart.renderer.spi.ErrorDataSetRenderer;
import de.gsi.chart.renderer.spi.GridRenderer;
import de.gsi.chart.renderer.spi.LabelledMarkerRenderer;
import de.gsi.chart.ui.geometry.Side;
import de.gsi.chart.utils.FXUtils;
import de.gsi.dataset.DataSet;
import de.gsi.dataset.utils.AssertUtils;

/**
 * Chart designed primarily to display data traces using DataSet interfaces which are more flexible and efficient than
 * the observable lists used by XYChart. Brief history: original design inspired by Oracle, extended by CERN (i.e.
 * plugin concept/zoomer), modified to mitigate JavaFX performance issues and extended renderer
 * concept/canvas-concept/interfaces/+more plugins by GSI. Refactored and re-write in 2018 to make it compatible with
 * Apache 2.0 which -- in the spirit of 'Ship of Theseus' -- makes it de-facto a new development. Contributions,
 * bug-fixes, and modifications are welcome. Hope you find this library useful and enjoy!
 *
 * @author braeun
 * @author rstein
 */
public class XYChart extends Chart {
    private static final Logger LOGGER = LoggerFactory.getLogger(XYChart.class);
    protected static final int BURST_LIMIT_MS = 15;
    protected final BooleanProperty polarPlot = new SimpleBooleanProperty(this, "polarPlot", false);
    private final ObjectProperty polarStepSize = new SimpleObjectProperty<>(PolarTickStep.THIRTY);
    private final GridRenderer gridRenderer = new GridRenderer();
    protected final ChangeListener gridLineVisibilitychange = (ob, o, n) -> requestLayout();
    private long lastCanvasUpdate;
    private boolean callCanvasUpdateLater;
    private final ChangeListener axisSideChangeListener = this::axisSideChanged;

    /**
     * Construct a new XYChart with the given axes.
     *
     */
    public XYChart() {
        this(new Axis[] {}); // NOPMD NOSONAR
        // N.B. this constructor is needed since JavaFX seems to instantiate fxml using reflection to find the corresponding constructor
    }

    /**
     * Construct a new XYChart with the given axes.
     *
     * @param axes All axes to be added to the chart
     */
    public XYChart(final Axis... axes) {
        super(axes);

        for (int dim = 0; dim < axes.length; dim++) {
            final Axis axis = axes[dim];
            if (axis == null) {
                continue;
            }
            switch (dim) {
            case DataSet.DIM_X:
                axis.setSide(Side.BOTTOM);
                break;
            case DataSet.DIM_Y:
                axis.setSide(Side.LEFT);
                break;
            default:
                axis.setSide(Side.RIGHT);
                break;
            }
            getAxes().add(axis);
        }

        gridRenderer.horizontalGridLinesVisibleProperty().addListener(gridLineVisibilitychange);
        gridRenderer.verticalGridLinesVisibleProperty().addListener(gridLineVisibilitychange);
        gridRenderer.getHorizontalMinorGrid().visibleProperty().addListener(gridLineVisibilitychange);
        gridRenderer.getVerticalMinorGrid().visibleProperty().addListener(gridLineVisibilitychange);
        gridRenderer.drawOnTopProperty().addListener(gridLineVisibilitychange);

        this.setAnimated(false);
        getRenderers().addListener(this::rendererChanged);

        getRenderers().add(new ErrorDataSetRenderer());
    }

    /**
     * @return datasets attached to the chart and datasets attached to all renderers
     */
    @Override
    public ObservableList getAllDatasets() {
        if (getRenderers() == null) {
            return allDataSets;
        }

        allDataSets.clear();
        allDataSets.addAll(getDatasets());
        getRenderers().stream().filter(renderer -> !(renderer instanceof LabelledMarkerRenderer)).forEach(renderer -> allDataSets.addAll(renderer.getDatasets()));

        return allDataSets;
    }

    /**
     * @return datasets attached to the chart and datasets attached to all renderers TODO: change to change listener
     *         that add/remove datasets from a global observable list
     */
    public ObservableList getAllShownDatasets() {
        final ObservableList ret = FXCollections.observableArrayList();
        ret.addAll(getDatasets());
        getRenderers().stream().filter(Renderer::showInLegend).forEach(renderer -> ret.addAll(renderer.getDatasets()));
        return ret;
    }

    /**
     * @return nomen est omen
     */
    public GridRenderer getGridRenderer() {
        return gridRenderer;
    }

    public PolarTickStep getPolarStepSize() {
        return polarStepSizeProperty().get();
    }

    /**
     * Returns the x axis.
     *
     * @return x axis
     */
    public Axis getXAxis() {
        return getFirstAxis(Orientation.HORIZONTAL);
    }

    /**
     * Returns the y axis.
     *
     * @return y axis
     */
    public Axis getYAxis() {
        return getFirstAxis(Orientation.VERTICAL);
    }

    /**
     * Indicates whether horizontal grid lines are visible or not.
     *
     * @return horizontalGridLinesVisible property
     */
    public final BooleanProperty horizontalGridLinesVisibleProperty() {
        return gridRenderer.horizontalGridLinesVisibleProperty();
    }

    /**
     * Indicates whether horizontal grid lines are visible.
     *
     * @return {@code true} if horizontal grid lines are visible else {@code false}.
     */
    public final boolean isHorizontalGridLinesVisible() {
        return horizontalGridLinesVisibleProperty().get();
    }

    /**
     * whether renderer should use polar coordinates (x -> interpreted as phi, y as radial coordinate)
     *
     * @return true if renderer is plotting in polar coordinates
     */
    public final boolean isPolarPlot() {
        return polarPlotProperty().get();
    }

    /**
     * Indicates whether vertical grid lines are visible.
     *
     * @return {@code true} if vertical grid lines are visible else {@code false}.
     */
    public final boolean isVerticalGridLinesVisible() {
        return verticalGridLinesVisibleProperty().get();
    }

    /**
     * Sets whether renderer should use polar coordinates (x -> interpreted as phi, y as radial coordinate)
     *
     * @return true if renderer is plotting in polar coordinates
     */
    public final BooleanProperty polarPlotProperty() {
        return polarPlot;
    }

    public ObjectProperty polarStepSizeProperty() {
        return polarStepSize;
    }

    /**
     * Sets the value of the {@link #verticalGridLinesVisibleProperty()}.
     *
     * @param value {@code true} to make vertical lines visible
     */
    public final void setHorizontalGridLinesVisible(final boolean value) {
        horizontalGridLinesVisibleProperty().set(value);
    }

    /**
     * Sets whether renderer should use polar coordinates (x -> interpreted as phi, y as radial coordinate)
     *
     * @param state true if renderer is parallelising sub-functionalities
     * @return itself (fluent design)
     */
    public final XYChart setPolarPlot(final boolean state) {
        polarPlotProperty().set(state);
        return this;
    }

    public void setPolarStepSize(final PolarTickStep step) {
        polarStepSizeProperty().set(step);
    }

    /**
     * Sets the value of the {@link #verticalGridLinesVisibleProperty()}.
     *
     * @param value {@code true} to make vertical lines visible
     */
    public final void setVerticalGridLinesVisible(final boolean value) {
        verticalGridLinesVisibleProperty().set(value);
    }

    @Override
    public void updateAxisRange() {
        if (isDataEmpty()) {
            return;
        }

        // lock datasets to prevent writes while updating the axes
        ObservableList dataSets = this.getAllDatasets();
        // check that all registered data sets have proper ranges defined
        dataSets.parallelStream()
                .forEach(dataset -> dataset.getAxisDescriptions().parallelStream().filter(axisD -> !axisD.isDefined()).forEach(axisDescription -> dataset.lock().writeLockGuard(() -> dataset.recomputeLimits(axisDescription.getDimIndex()))));

        final ArrayDeque lockQueue = new ArrayDeque<>(dataSets);
        recursiveLockGuard(lockQueue, () -> getAxes().forEach(chartAxis -> {
            final List dataSetForAxis = getDataSetForAxis(chartAxis);
            updateNumericAxis(chartAxis, dataSetForAxis);
            // chartAxis.requestAxisLayout()
        }));
    }

    protected void recursiveLockGuard(final Deque queue, final Runnable runnable) { // NOPMD
        if (queue.isEmpty()) {
            runnable.run();
        } else {
            queue.pop().lock().readLockGuard(() -> recursiveLockGuard(queue, runnable));
        }
    }

    /**
     * Indicates whether vertical grid lines are visible or not.
     *
     * @return verticalGridLinesVisible property
     */
    public final BooleanProperty verticalGridLinesVisibleProperty() {
        return gridRenderer.verticalGridLinesVisibleProperty();
    }

    private boolean isDataEmpty() {
        return getAllDatasets() == null || getAllDatasets().isEmpty();
    }

    /**
     * add XYChart specific axis handling (ie. placement around charts, add new DefaultNumericAxis if one is missing,
     * etc.)
     *
     * @param change the new axis change that is being added
     */
    @Override
    protected void axesChanged(final ListChangeListener.Change change) {
        while (change.next()) {
            change.getRemoved().forEach(axis -> {
                AssertUtils.notNull("to be removed axis is null", axis);
                // check if axis is associated with an existing renderer, if yes
                // -> throw an exception
                // remove from axis.side property side listener
                removeFromAllAxesPanes(axis);
                axis.sideProperty().removeListener(axisSideChangeListener);
            });

            change.getAddedSubList().forEach(axis -> {
                // check if axis is associated with an existing renderer,
                // if yes -> throw an exception
                AssertUtils.notNull("to be added axis is null", axis);

                final Side side = axis.getSide();
                if (side == null) {
                    throw new InvalidParameterException("axis '" + axis.getName() + "' has 'null' as side being set");
                }
                if (axis instanceof Node && !getAxesPane(axis.getSide()).getChildren().contains(axis)) {
                    getAxesPane(axis.getSide()).getChildren().add((Node) axis);
                }

                axis.sideProperty().addListener(axisSideChangeListener);
            });
        }

        requestLayout();
    }

    protected void axisSideChanged(final ObservableValue change, final Side oldValue, final Side newValue) {
        if (newValue != null && newValue.equals(oldValue)) {
            return;
        }
        // loop through all registered axis
        for (final Axis axis : axesList) {
            if (axis.getSide() == null) {
                // remove axis from all axis panes
                removeFromAllAxesPanes(axis);
            }

            // check if axis is in correct pane
            if (axis instanceof Node && getAxesPane(axis.getSide()).getChildren().contains(axis)) {
                // yes, it is continue with next axis
                continue;
            }
            // axis needs to be moved to new pane location
            // first: remove axis from all axis panes
            removeFromAllAxesPanes(axis);

            // second: add axis to correct axis pane
            getAxesPane(axis.getSide()).getChildren().add((Node) axis);
        }
        requestLayout();
    }

    /**
     * checks whether renderer has required x and y axes and adds the first x or y from the chart itself if necessary
     * 

* additionally moves axis from Renderer with defined Side that are not yet in the Chart also to the chart's list * * @param renderer to be checked */ protected void checkRendererForRequiredAxes(final Renderer renderer) { if (renderer.getAxes().size() < 2) { // not enough axes present in renderer Optional xAxis = renderer.getAxes().stream().filter(a -> a.getSide().isHorizontal()).findFirst(); Optional yAxis = renderer.getAxes().stream().filter(a -> a.getSide().isVertical()).findFirst(); // search for horizontal/vertical axes in Chart (which creates one if missing) and add to renderer if (xAxis.isEmpty()) { renderer.getAxes().add(getFirstAxis(Orientation.HORIZONTAL)); } if (yAxis.isEmpty()) { // search for horizontal axis in Chart (which creates one if missing) and add to renderer renderer.getAxes().add(getFirstAxis(Orientation.VERTICAL)); } } // check if there are assignable axes not yet present in the Chart's list getAxes().addAll(renderer.getAxes().stream().limit(2).filter(a -> (a.getSide() != null && !getAxes().contains(a))).collect(Collectors.toList())); } protected List getDataSetForAxis(final Axis axis) { final List retVal = new ArrayList<>(); if (axis == null) { return retVal; } retVal.addAll(getDatasets()); getRenderers().forEach(renderer -> renderer.getAxes().stream().filter(axis::equals).forEach(rendererAxis -> retVal.addAll(renderer.getDatasets()))); return retVal; } @Override protected void redrawCanvas() { if (DEBUG && LOGGER.isDebugEnabled()) { LOGGER.debug(" xychart redrawCanvas() - pre"); } setAutoNotification(false); FXUtils.assertJavaFxThread(); final long now = System.nanoTime(); final double diffMillisSinceLastUpdate = TimeUnit.NANOSECONDS.toMillis(now - lastCanvasUpdate); if (diffMillisSinceLastUpdate < XYChart.BURST_LIMIT_MS) { if (!callCanvasUpdateLater) { callCanvasUpdateLater = true; // repaint 20 ms later in case this was just a burst operation final KeyFrame kf1 = new KeyFrame(Duration.millis(20), e -> requestLayout()); final Timeline timeline = new Timeline(kf1); Platform.runLater(timeline::play); } return; } if (DEBUG && LOGGER.isDebugEnabled()) { LOGGER.debug(" xychart redrawCanvas() - executing"); LOGGER.debug(" xychart redrawCanvas() - canvas size = {}", String.format("%fx%f", canvas.getWidth(), canvas.getHeight())); } lastCanvasUpdate = now; callCanvasUpdateLater = false; final GraphicsContext gc = canvas.getGraphicsContext2D(); gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); if (!gridRenderer.isDrawOnTop()) { gridRenderer.render(gc, this, 0, null); } int dataSetOffset = 0; for (final Renderer renderer : getRenderers()) { // check for and add required axes checkRendererForRequiredAxes(renderer); final List drawnDataSets = renderer.render(gc, this, dataSetOffset, getDatasets()); dataSetOffset += drawnDataSets == null ? 0 : drawnDataSets.size(); } if (gridRenderer.isDrawOnTop()) { gridRenderer.render(gc, this, 0, null); } setAutoNotification(true); if (DEBUG && LOGGER.isDebugEnabled()) { LOGGER.debug(" xychart redrawCanvas() - done"); } } protected static void updateNumericAxis(final Axis axis, final List dataSets) { if (dataSets == null || dataSets.isEmpty()) { return; } final boolean oldAutoState = axis.autoNotification().getAndSet(false); final double oldMin = axis.getAutoRange().getMin(); final double oldMax = axis.getAutoRange().getMax(); final double oldLength = axis.getLength(); final boolean isHorizontal = axis.getSide().isHorizontal(); final Side side = axis.getSide(); axis.getAutoRange().clear(); dataSets.stream().filter(DataSet::isVisible).forEach(dataset -> dataset.lock().readLockGuard(() -> { if (dataset.getDimension() > 2 && (side == Side.RIGHT || side == Side.TOP)) { if (!dataset.getAxisDescription(DataSet.DIM_Z).isDefined()) { dataset.recomputeLimits(DataSet.DIM_Z); } axis.getAutoRange().add(dataset.getAxisDescription(DataSet.DIM_Z).getMin()); axis.getAutoRange().add(dataset.getAxisDescription(DataSet.DIM_Z).getMax()); } else { final int nDim = isHorizontal ? DataSet.DIM_X : DataSet.DIM_Y; if (!dataset.getAxisDescription(nDim).isDefined()) { dataset.recomputeLimits(nDim); } axis.getAutoRange().add(dataset.getAxisDescription(nDim).getMin()); axis.getAutoRange().add(dataset.getAxisDescription(nDim).getMax()); } })); // handling of numeric axis and auto-range or auto-grow setting only if (!axis.isAutoRanging() && !axis.isAutoGrowRanging()) { if (oldMin != axis.getMin() || oldMax != axis.getMax() || oldLength != axis.getLength()) { axis.requestAxisLayout(); } axis.autoNotification().set(oldAutoState); return; } if (axis.isAutoGrowRanging()) { axis.getAutoRange().add(oldMin); axis.getAutoRange().add(oldMax); } axis.getAutoRange().setAxisLength(axis.getLength() == 0 ? 1 : axis.getLength(), side); axis.getUserRange().setAxisLength(axis.getLength() == 0 ? 1 : axis.getLength(), side); axis.invalidateRange(null); if (oldMin != axis.getMin() || oldMax != axis.getMax() || oldLength != axis.getLength()) { axis.requestAxisLayout(); } axis.autoNotification().set(oldAutoState); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy