/*
 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.chart;

import java.util.*;

import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.AccessibleRole;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
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;
import javafx.scene.shape.StrokeLineJoin;
import javafx.util.Duration;

import com.sun.javafx.charts.Legend.LegendItem;
import javafx.css.converter.BooleanConverter;
import javafx.beans.property.BooleanProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableProperty;
import javafx.scene.chart.LineChart.SortingPolicy;

AreaChart - Plots the area between the line that connects the data points and the 0 line on the Y axis.
Since:JavaFX 2.0
/** * AreaChart - Plots the area between the line that connects the data points and * the 0 line on the Y axis. * @since JavaFX 2.0 */
public class AreaChart<X,Y> extends XYChart<X,Y> { // -------------- PRIVATE FIELDS ------------------------------------------
A multiplier for teh Y values that we store for each series, it is used to animate in a new series
/** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */
private Map<Series<X,Y>, DoubleProperty> seriesYMultiplierMap = new HashMap<>(); // -------------- PUBLIC PROPERTIES ----------------------------------------
When true, CSS styleable symbols are created for any data items that don't have a symbol node specified.
Since:JavaFX 8.0
/** * When true, CSS styleable symbols are created for any data items that don't have a symbol node specified. * @since JavaFX 8.0 */
private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { @Override protected void invalidated() { for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex ++) { Series<X,Y> series = getData().get(seriesIndex); for (int itemIndex=0; itemIndex < series.getData().size(); itemIndex ++) { Data<X,Y> item = series.getData().get(itemIndex); Node symbol = item.getNode(); if(get() && symbol == null) { // create any symbols symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); if (null != symbol) { getPlotChildren().add(symbol); } } else if (!get() && symbol != null) { // remove symbols getPlotChildren().remove(symbol); symbol = null; item.setNode(null); } } } requestChartLayout(); } public Object getBean() { return this; } public String getName() { return "createSymbols"; } public CssMetaData<AreaChart<?, ?>,Boolean> getCssMetaData() { return StyleableProperties.CREATE_SYMBOLS; } };
Indicates whether symbols for data points will be created or not.
Returns:true if symbols for data points will be created and false otherwise.
Since:JavaFX 8.0
/** * Indicates whether symbols for data points will be created or not. * * @return true if symbols for data points will be created and false otherwise. * @since JavaFX 8.0 */
public final boolean getCreateSymbols() { return createSymbols.getValue(); } public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } public final BooleanProperty createSymbolsProperty() { return createSymbols; } // -------------- CONSTRUCTORS ----------------------------------------------
Construct a new Area Chart with the given axis
Params:
  • xAxis – The x axis to use
  • yAxis – The y axis to use
/** * Construct a new Area Chart with the given axis * * @param xAxis The x axis to use * @param yAxis The y axis to use */
public AreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) { this(xAxis,yAxis, FXCollections.<Series<X,Y>>observableArrayList()); }
Construct a new Area Chart with the given axis and data
Params:
  • xAxis – The x axis to use
  • yAxis – The y axis to use
  • data – The data to use, this is the actual list used so any changes to it will be reflected in the chart
/** * Construct a new Area Chart with the given axis and data * * @param xAxis The x axis to use * @param yAxis The y axis to use * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart */
public AreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) { super(xAxis,yAxis); setData(data); } // -------------- METHODS ------------------------------------------------------------------------------------------ private static double doubleValue(Number number) { return doubleValue(number, 0); } private static double doubleValue(Number number, double nullDefault) { return (number == null) ? nullDefault : number.doubleValue(); }
{@inheritDoc}
/** {@inheritDoc} */
@Override protected void updateAxisRange() { final Axis<X> xa = getXAxis(); final Axis<Y> ya = getYAxis(); List<X> xData = null; List<Y> yData = null; if(xa.isAutoRanging()) xData = new ArrayList<X>(); if(ya.isAutoRanging()) yData = new ArrayList<Y>(); if(xData != null || yData != null) { for(Series<X,Y> series : getData()) { for(Data<X,Y> data: series.getData()) { if(xData != null) xData.add(data.getXValue()); if(yData != null) yData.add(data.getYValue()); } } if(xData != null && !(xData.size() == 1 && getXAxis().toNumericValue(xData.get(0)) == 0)) { xa.invalidateRange(xData); } if(yData != null && !(yData.size() == 1 && getYAxis().toNumericValue(yData.get(0)) == 0)) { ya.invalidateRange(yData); } } } @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) { final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); if (shouldAnimate()) { boolean animate = false; if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { animate = true; Data<X,Y> p1 = series.getData().get(itemIndex - 1); Data<X,Y> p2 = series.getData().get(itemIndex + 1); double x1 = getXAxis().toNumericValue(p1.getXValue()); double y1 = getYAxis().toNumericValue(p1.getYValue()); double x3 = getXAxis().toNumericValue(p2.getXValue()); double y3 = getYAxis().toNumericValue(p2.getYValue()); double x2 = getXAxis().toNumericValue(item.getXValue()); double y2 = getYAxis().toNumericValue(item.getYValue()); // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); item.setCurrentY(getYAxis().toRealValue(y)); item.setCurrentX(getXAxis().toRealValue(x2)); //2. we can simply use the midpoint on the line as well.. // double x = (x3 + x1)/2; // double y = (y3 + y1)/2; // item.setCurrentX(x); // item.setCurrentY(y); } else if (itemIndex == 0 && series.getData().size() > 1) { animate = true; item.setCurrentX(series.getData().get(1).getXValue()); item.setCurrentY(series.getData().get(1).getYValue()); } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { animate = true; int last = series.getData().size() - 2; item.setCurrentX(series.getData().get(last).getXValue()); item.setCurrentY(series.getData().get(last).getYValue()); } if (symbol != null) { // fade in new symbol symbol.setOpacity(0); getPlotChildren().add(symbol); FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); ft.setToValue(1); ft.play(); } if (animate) { animate( new KeyFrame(Duration.ZERO, (e) -> { if (symbol != null && !getPlotChildren().contains(symbol)) { getPlotChildren().add(symbol); } }, new KeyValue(item.currentYProperty(), item.getCurrentY()), new KeyValue(item.currentXProperty(), item.getCurrentX()) ), new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) ); } } else if (symbol != null) { getPlotChildren().add(symbol); } } @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { final Node symbol = item.getNode(); if (symbol != null) { symbol.focusTraversableProperty().unbind(); } // remove item from sorted list int itemIndex = series.getItemIndex(item); if (shouldAnimate()) { boolean animate = false; // dataSize represents size of currently visible data. After this operation, the number will decrement by 1 final int dataSize = series.getDataSize(); // This is the size of current data list in Series. Note that it might be totaly different from dataSize as // some big operation might have happened on the list. final int dataListSize = series.getData().size(); if (itemIndex > 0 && itemIndex < dataSize -1) { animate = true; Data<X,Y> p1 = series.getItem(itemIndex - 1); Data<X,Y> p2 = series.getItem(itemIndex + 1); double x1 = getXAxis().toNumericValue(p1.getXValue()); double y1 = getYAxis().toNumericValue(p1.getYValue()); double x3 = getXAxis().toNumericValue(p2.getXValue()); double y3 = getYAxis().toNumericValue(p2.getYValue()); double x2 = getXAxis().toNumericValue(item.getXValue()); double y2 = getYAxis().toNumericValue(item.getYValue()); // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); item.setCurrentX(getXAxis().toRealValue(x2)); item.setCurrentY(getYAxis().toRealValue(y2)); item.setXValue(getXAxis().toRealValue(x2)); item.setYValue(getYAxis().toRealValue(y)); //2. we can simply use the midpoint on the line as well.. // double x = (x3 + x1)/2; // double y = (y3 + y1)/2; // item.setCurrentX(x); // item.setCurrentY(y); } else if (itemIndex == 0 && dataListSize > 1) { animate = true; item.setXValue(series.getData().get(0).getXValue()); item.setYValue(series.getData().get(0).getYValue()); } else if (itemIndex == (dataSize - 1) && dataListSize > 1) { animate = true; int last = dataListSize - 1; item.setXValue(series.getData().get(last).getXValue()); item.setYValue(series.getData().get(last).getYValue()); } else if (symbol != null) { // fade out symbol symbol.setOpacity(0); FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); ft.setToValue(0); ft.setOnFinished(actionEvent -> { getPlotChildren().remove(symbol); removeDataItemFromDisplay(series, item); }); ft.play(); } else { item.setSeries(null); removeDataItemFromDisplay(series, item); } if (animate) { animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY()), new KeyValue(item.currentXProperty(), item.getCurrentX())), new KeyFrame(Duration.millis(800), actionEvent -> { item.setSeries(null); getPlotChildren().remove(symbol); removeDataItemFromDisplay(series, item); }, new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) ); } } else { item.setSeries(null); getPlotChildren().remove(symbol); removeDataItemFromDisplay(series, item); } //Note: better animation here, point should move from old position to new position at center point between prev and next symbols }
{@inheritDoc}
/** {@inheritDoc} */
@Override protected void dataItemChanged(Data<X, Y> item) { } @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { // Update style classes for all series lines and symbols // Note: is there a more efficient way of doing this? for (int i = 0; i < getDataSize(); i++) { final Series<X,Y> s = getData().get(i); Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); for (int j=0; j < s.getData().size(); j++) { final Data<X,Y> item = s.getData().get(j); final Node node = item.getNode(); if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); } } } @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { // create new paths for series Path seriesLine = new Path(); Path fillPath = new Path(); seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); Group areaGroup = new Group(fillPath,seriesLine); series.setNode(areaGroup); // create series Y multiplier DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); seriesYMultiplierMap.put(series, seriesYAnimMultiplier); // handle any data already in series if (shouldAnimate()) { seriesYAnimMultiplier.setValue(0d); } else { seriesYAnimMultiplier.setValue(1d); } getPlotChildren().add(areaGroup); List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); if (shouldAnimate()) { // animate in new series keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(areaGroup.opacityProperty(), 0), new KeyValue(seriesYAnimMultiplier, 0) )); keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(areaGroup.opacityProperty(), 1) )); keyFrames.add(new KeyFrame(Duration.millis(500), new KeyValue(seriesYAnimMultiplier, 1) )); } for (int j=0; j<series.getData().size(); j++) { Data<X,Y> item = series.getData().get(j); final Node symbol = createSymbol(series, seriesIndex, item, j); if (symbol != null) { if (shouldAnimate()) { symbol.setOpacity(0); getPlotChildren().add(symbol); // fade in new symbol keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); } else { getPlotChildren().add(symbol); } } } if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); } @Override protected void seriesRemoved(final Series<X,Y> series) { // remove series Y multiplier seriesYMultiplierMap.remove(series); // remove all symbol nodes if (shouldAnimate()) { Timeline tl = new Timeline(createSeriesRemoveTimeLine(series, 400)); tl.play(); } else { getPlotChildren().remove(series.getNode()); for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); removeSeriesFromDisplay(series); } }
{@inheritDoc}
/** {@inheritDoc} */
@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); final ObservableList<Node> children = ((Group) series.getNode()).getChildren(); Path fillPath = (Path) children.get(0); Path linePath = (Path) children.get(1); makePaths(this, series, constructedPath, fillPath, linePath, seriesYAnimMultiplier.get(), SortingPolicy.X_AXIS); } } static <X,Y> void makePaths(XYChart<X, Y> chart, Series<X, Y> series, List<LineTo> constructedPath, Path fillPath, Path linePath, double yAnimMultiplier, SortingPolicy sortAxis) { final Axis<X> axisX = chart.getXAxis(); final Axis<Y> axisY = chart.getYAxis(); final double hlw = linePath.getStrokeWidth() / 2.0; final boolean sortX = (sortAxis == SortingPolicy.X_AXIS); final boolean sortY = (sortAxis == SortingPolicy.Y_AXIS); final double dataXMin = sortX ? -hlw : Double.NEGATIVE_INFINITY; final double dataXMax = sortX ? axisX.getWidth() + hlw : Double.POSITIVE_INFINITY; final double dataYMin = sortY ? -hlw : Double.NEGATIVE_INFINITY; final double dataYMax = sortY ? axisY.getHeight() + hlw : Double.POSITIVE_INFINITY; LineTo prevDataPoint = null; LineTo nextDataPoint = null; constructedPath.clear(); for (Iterator<Data<X, Y>> it = chart.getDisplayedDataIterator(series); it.hasNext(); ) { Data<X, Y> item = it.next(); double x = axisX.getDisplayPosition(item.getCurrentX()); double y = axisY.getDisplayPosition( axisY.toRealValue(axisY.toNumericValue(item.getCurrentY()) * yAnimMultiplier)); boolean skip = (Double.isNaN(x) || Double.isNaN(y)); Node symbol = item.getNode(); if (symbol != null) { final double w = symbol.prefWidth(-1); final double h = symbol.prefHeight(-1); if (skip) { symbol.resizeRelocate(-w*2, -h*2, w, h); } else { symbol.resizeRelocate(x-(w/2), y-(h/2), w, h); } } if (skip) continue; if (x < dataXMin || y < dataYMin) { if (prevDataPoint == null) { prevDataPoint = new LineTo(x, y); } else if ((sortX && prevDataPoint.getX() <= x) || (sortY && prevDataPoint.getY() <= y)) { prevDataPoint.setX(x); prevDataPoint.setY(y); } } else if (x <= dataXMax && y <= dataYMax) { constructedPath.add(new LineTo(x, y)); } else { if (nextDataPoint == null) { nextDataPoint = new LineTo(x, y); } else if ((sortX && x <= nextDataPoint.getX()) || (sortY && y <= nextDataPoint.getY())) { nextDataPoint.setX(x); nextDataPoint.setY(y); } } } if (!constructedPath.isEmpty() || prevDataPoint != null || nextDataPoint != null) { if (sortX) { Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX())); } else if (sortY) { Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getY(), e2.getY())); } else { // assert prevDataPoint == null && nextDataPoint == null } if (prevDataPoint != null) { constructedPath.add(0, prevDataPoint); } if (nextDataPoint != null) { constructedPath.add(nextDataPoint); } // assert !constructedPath.isEmpty() LineTo first = constructedPath.get(0); LineTo last = constructedPath.get(constructedPath.size()-1); final double displayYPos = first.getY(); ObservableList<PathElement> lineElements = linePath.getElements(); lineElements.clear(); lineElements.add(new MoveTo(first.getX(), displayYPos)); lineElements.addAll(constructedPath); if (fillPath != null) { ObservableList<PathElement> fillElements = fillPath.getElements(); fillElements.clear(); double yOrigin = axisY.getDisplayPosition(axisY.toRealValue(0.0)); fillElements.add(new MoveTo(first.getX(), yOrigin)); fillElements.addAll(constructedPath); fillElements.add(new LineTo(last.getX(), yOrigin)); fillElements.add(new ClosePath()); } } } private Node createSymbol(Series<X,Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) { Node symbol = item.getNode(); // check if symbol has already been created if (symbol == null && getCreateSymbols()) { symbol = new StackPane(); symbol.setAccessibleRole(AccessibleRole.TEXT); symbol.setAccessibleRoleDescription("Point"); symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); item.setNode(symbol); } // set symbol styles // Note: not sure if we want to add or check, ie be more careful and efficient here if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, series.defaultColorStyleClass); return symbol; } @Override LegendItem createLegendItemForSeries(Series<X, Y> series, int seriesIndex) { LegendItem legendItem = new LegendItem(series.getName()); legendItem.getSymbol().getStyleClass().addAll("chart-area-symbol", "series" + seriesIndex, "area-legend-symbol", series.defaultColorStyleClass); return legendItem; } // -------------- STYLESHEET HANDLING -------------------------------------- private static class StyleableProperties { private static final CssMetaData<AreaChart<?,?>,Boolean> CREATE_SYMBOLS = new CssMetaData<AreaChart<?,?>,Boolean>("-fx-create-symbols", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(AreaChart<?,?> node) { return node.createSymbols == null || !node.createSymbols.isBound(); } @Override public StyleableProperty<Boolean> getStyleableProperty(AreaChart<?,?> node) { return (StyleableProperty<Boolean>)node.createSymbolsProperty(); } }; private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; static { final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); styleables.add(CREATE_SYMBOLS); STYLEABLES = Collections.unmodifiableList(styleables); } }
Returns:The CssMetaData associated with this class, which may include the CssMetaData of its superclasses.
Since:JavaFX 8.0
/** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its superclasses. * @since JavaFX 8.0 */
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { return StyleableProperties.STYLEABLES; }
{@inheritDoc}
Since:JavaFX 8.0
/** * {@inheritDoc} * @since JavaFX 8.0 */
@Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { return getClassCssMetaData(); } }