Zur13
Zur13

Reputation: 382

JavaFX area chart behavior changed

I have swing application which has panel that contains several JavaFX AreaCharts (using javafx.embed.swing.JFXPanel) with custom styles. We had used a jre 8u20 and jre 8u25 and all worked fine, now I had to update to jre 8u66 and my charts are looks different.

That question describes opposite to my situation: How to add negative values to JavaFx Area Chart?. I've used chart series to color the background for chart based on data (for example I need to color in red background for axis X intervals where the data is absent). The JavaFX 8u20 area chart background was colored to the lower bound of chart line, after update area chart fill background only to axis ignoring part under or above axis.

Now JavaFX area chart works like described in the documentation:

The default behavior

How I can return javaFX area chart the old behavior to get something like that using jre 8u66:

The image was taken here: https://stackoverflow.com/questions/30185035/how-to-add-negative-values-to-javafx-area-chart

Edit:

So I've found the commit which fixed the negative value background fill http://hg.openjdk.java.net/openjfx/8u60/rt/rev/a57b8ba039d0?revcount=480.

It's only a few lines in method and my first idea was to make a quick fix: override this one method in my own class, but I've got problems doing that, the JavaFX classes is not friendly to such modifications many required fields and methods are private or package-private :(

Here is the class I tried to made to alter AreaChart behavior:

public class NegativeBGAreaChart<X,Y> extends AreaChart<X, Y> {
    public NegativeBGAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
        this(xAxis,yAxis, FXCollections.<Series<X,Y>>observableArrayList());
    }

    public NegativeBGAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) {
        super(xAxis,yAxis, data);
    }


    @Override 
    protected void layoutPlotChildren() {
        List<LineTo> constructedPath = new ArrayList<>(getDataSize());
        for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
            Series<X, Y> series = getData().get(seriesIndex);
            DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series);
            double lastX = 0;
            final ObservableList<Node> children = ((Group) series.getNode()).getChildren();
            ObservableList<PathElement> seriesLine = ((Path) children.get(1)).getElements();
            ObservableList<PathElement> fillPath = ((Path) children.get(0)).getElements();
            seriesLine.clear();
            fillPath.clear();
            constructedPath.clear();
            for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext(); ) {
                Data<X, Y> item = it.next();
                double x = getXAxis().getDisplayPosition(item.getCurrentX());
                double y = getYAxis().getDisplayPosition( getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
                constructedPath.add(new LineTo(x, y));
                if (Double.isNaN(x) || Double.isNaN(y)) {
                    continue;
                }
                lastX = x;
                Node symbol = item.getNode();
                if (symbol != null) {
                    final double w = symbol.prefWidth(-1);
                    final double h = symbol.prefHeight(-1);
                    symbol.resizeRelocate(x-(w/2), y-(h/2),w,h);
                }
            }

            if (!constructedPath.isEmpty()) {
                Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX()));
                LineTo first = constructedPath.get(0);

                final double displayYPos = first.getY();
                final double numericYPos = getYAxis().toNumericValue(getYAxis().getValueForDisplay(displayYPos));

                // RT-34626: We can't always use getZeroPosition(), as it may be the case
                // that the zero position of the y-axis is not visible on the chart. In these
                // cases, we need to use the height between the point and the y-axis line.
                final double yAxisZeroPos = getYAxis().getZeroPosition();
                final boolean isYAxisZeroPosVisible = !Double.isNaN(yAxisZeroPos);
                final double yAxisHeight = getYAxis().getHeight();
                final double yFillPos = isYAxisZeroPosVisible ? yAxisZeroPos : numericYPos < 0 ? numericYPos - yAxisHeight : yAxisHeight;

                seriesLine.add(new MoveTo(first.getX(), displayYPos));
                fillPath.add(new MoveTo(first.getX(), yFillPos));

                seriesLine.addAll(constructedPath);
                fillPath.addAll(constructedPath);
                fillPath.add(new LineTo(lastX, yFillPos));
                fillPath.add(new ClosePath());
            }
        }
    }
}

The problems are:

  1. Field of original AreaChart is private and inaccessible

    private Map, DoubleProperty> seriesYMultiplierMap = new HashMap<>();

  2. Some methods are package-private, for example

    javafx.scene.chart.XYChart.Data.getCurrentX()

    javafx.scene.chart.XYChart.Data.getCurrentY()

    javafx.scene.chart.XYChart.getDataSize()

Upvotes: 1

Views: 926

Answers (1)

Zur13
Zur13

Reputation: 382

So as possible workaround I've made this:

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javafx.beans.NamedArg;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.Axis;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;

/**
 * AreaChart - Plots the area between the line that connects the data points     and
 * the 0 line on the Y axis. This implementation Plots the area between the line
 * that connects the data points and the bottom of the chart area.
 * 
 * @since JavaFX 2.0
 */
public class NegativeBGAreaChart<X, Y> extends AreaChart<X, Y> {
    protected Map<Series<X, Y>, DoubleProperty> shadowSeriesYMultiplierMap = new HashMap<>();

    // -------------- CONSTRUCTORS ----------------------------------------------

    public NegativeBGAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
        this(xAxis, yAxis, FXCollections.<Series<X, Y>> observableArrayList());
    }

    public NegativeBGAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X, Y>> data) {
        super(xAxis, yAxis, data);
    }

    // -------------- METHODS ------------------------------------------------------------------------------------------
    @Override
    protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
        DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier");
        shadowSeriesYMultiplierMap.put(series, seriesYAnimMultiplier);
        super.seriesAdded(series, seriesIndex);
    }

    @Override
    protected void seriesRemoved(final Series<X, Y> series) {
        shadowSeriesYMultiplierMap.remove(series);
        super.seriesRemoved(series);
    }

    @Override
    protected void layoutPlotChildren() {
        // super.layoutPlotChildren();
        try {
            List<LineTo> constructedPath = new ArrayList<>(getDataSize());
            for (int seriesIndex = 0; seriesIndex < getDataSize(); seriesIndex++) {
                Series<X, Y> series = getData().get(seriesIndex);
                DoubleProperty seriesYAnimMultiplier = shadowSeriesYMultiplierMap.get(series);
                double lastX = 0;
                final ObservableList<Node> children = ((Group) series.getNode()).getChildren();
                ObservableList<PathElement> seriesLine = ((Path) children.get(1)).getElements();
                ObservableList<PathElement> fillPath = ((Path) children.get(0)).getElements();
                seriesLine.clear();
                fillPath.clear();
                constructedPath.clear();
                for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext();) {
                    Data<X, Y> item = it.next();
                    double x = getXAxis().getDisplayPosition(item.getXValue());// FIXME: here should be used item.getCurrentX()
                    double y = getYAxis().getDisplayPosition(
                        getYAxis().toRealValue(
                                getYAxis().toNumericValue(item.getYValue()) * seriesYAnimMultiplier.getValue()));// FIXME: here should be used item.getCurrentY()
                    constructedPath.add(new LineTo(x, y));
                    if (Double.isNaN(x) || Double.isNaN(y)) {
                        continue;
                    }
                    lastX = x;
                    Node symbol = item.getNode();
                    if (symbol != null) {
                        final double w = symbol.prefWidth(-1);
                        final double h = symbol.prefHeight(-1);
                        symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h);
                    }
                }

                if (!constructedPath.isEmpty()) {
                    Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX()));
                    LineTo first = constructedPath.get(0);

                    seriesLine.add(new MoveTo(first.getX(), first.getY()));
                    fillPath.add(new MoveTo(first.getX(), getYAxis().getHeight()));

                    seriesLine.addAll(constructedPath);
                    fillPath.addAll(constructedPath);
                    fillPath.add(new LineTo(lastX, getYAxis().getHeight()));
                    fillPath.add(new ClosePath());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Gets the size of the data returning 0 if the data is null
     *
     * @return The number of items in data, or null if data is null
     */
    public int getDataSize() {
        final ObservableList<Series<X, Y>> data = getData();
        return (data != null) ? data.size() : 0;
    }
}

I know it's buggy, but I have no choice right now, I hope some day JavaFX will be changed to be more friendly to external changes.

Edit:

this workaround is fixing the problem described, but it seems that due to the code marked FIXME the series points appear on the wrong coordinates. On the screenshot below points of the series has Y coordinates 3 or -3 but they all are placed on the axis with coordinate 0.

enter image description here

Edit2:

So I've managed to fix it, there was something wrong with animation, so I've disabled animation for chart:

chart.setAnimated(false);

and fixed the method (removed animation multiplier in second FIXME line) so finally I have this:

public class NegativeBGAreaChart<X, Y> extends AreaChart<X, Y> {
    protected Map<Series<X, Y>, DoubleProperty> shadowSeriesYMultiplierMap = new HashMap<>();

    // -------------- CONSTRUCTORS ----------------------------------------------

    public NegativeBGAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
        this(xAxis, yAxis, FXCollections.<Series<X, Y>> observableArrayList());
    }

    public NegativeBGAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X, Y>> data) {
        super(xAxis, yAxis, data);
    }

    // -------------- METHODS ------------------------------------------------------------------------------------------
    @Override
    protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
        DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier");
        shadowSeriesYMultiplierMap.put(series, seriesYAnimMultiplier);
        super.seriesAdded(series, seriesIndex);
    }

    @Override
    protected void seriesRemoved(final Series<X, Y> series) {
        shadowSeriesYMultiplierMap.remove(series);
        super.seriesRemoved(series);
    }

    @Override
    protected void layoutPlotChildren() {
//          super.layoutPlotChildren();
        try {
            List<LineTo> constructedPath = new ArrayList<>(getDataSize());
            for (int seriesIndex = 0; seriesIndex < getDataSize(); seriesIndex++) {
                Series<X, Y> series = getData().get(seriesIndex);
                DoubleProperty seriesYAnimMultiplier = shadowSeriesYMultiplierMap.get(series);
                double lastX = 0;
                final ObservableList<Node> children = ((Group) series.getNode()).getChildren();
                ObservableList<PathElement> seriesLine = ((Path) children.get(1)).getElements();
                ObservableList<PathElement> fillPath = ((Path) children.get(0)).getElements();
                seriesLine.clear();
                fillPath.clear();
                constructedPath.clear();
                for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext();) {
                    Data<X, Y> item = it.next();
                    double x = getXAxis().getDisplayPosition(item.getXValue());// FIXME: here should be used item.getCurrentX()
                    double y = getYAxis().getDisplayPosition(getYAxis().toRealValue(getYAxis().toNumericValue(item.getYValue())));// FIXME: here should be used item.getCurrentY()
                    constructedPath.add(new LineTo(x, y));
                    if (Double.isNaN(x) || Double.isNaN(y)) {
                        continue;
                    }
                    lastX = x;
                    Node symbol = item.getNode();
                    if (symbol != null) {
                        final double w = symbol.prefWidth(-1);
                        final double h = symbol.prefHeight(-1);
                        symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h);
                    }
                }

                if (!constructedPath.isEmpty()) {
                    Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX()));
                    LineTo first = constructedPath.get(0);

                    seriesLine.add(new MoveTo(first.getX(), first.getY()));
                    fillPath.add(new MoveTo(first.getX(), getYAxis().getHeight()));

                    seriesLine.addAll(constructedPath);
                    fillPath.addAll(constructedPath);
                    fillPath.add(new LineTo(lastX, getYAxis().getHeight()));
                    fillPath.add(new ClosePath());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Gets the size of the data returning 0 if the data is null
     *
     * @return The number of items in data, or null if data is null
     */
    public int getDataSize() {
        final ObservableList<Series<X, Y>> data = getData();
        return (data != null) ? data.size() : 0;
    }
}

After the fix all looks as it should (same data set as on previous picture):

enter image description here

Upvotes: 1

Related Questions