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

javafx.scene.chart.XYChart Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2024, 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 com.sun.javafx.charts.Legend;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.sun.javafx.scene.control.skin.resources.ControlResources;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectPropertyBase;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Side;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;

import com.sun.javafx.collections.NonIterableChange;
import javafx.css.converter.BooleanConverter;



/**
 * Chart base class for all 2 axis charts. It is responsible for drawing the two
 * axes and the plot content. It contains a list of all content in the plot and
 * implementations of XYChart can add nodes to this list that need to be rendered.
 *
 * 

It is possible to install Tooltips on data items / symbols. * For example the following code snippet installs Tooltip on the 1st data item. * *


 *  XYChart.Data item = ( XYChart.Data)series.getData().get(0);
 *  Tooltip.install(item.getNode(), new Tooltip("Symbol-0"));
 * 
* * @param the X axis value type * @param the Y axis value type * @since JavaFX 2.0 */ public abstract class XYChart extends Chart { // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- // to indicate which colors are being used for the series private final BitSet colorBits = new BitSet(8); static String DEFAULT_COLOR = "default-color"; final Map, Integer> seriesColorMap = new HashMap<>(); private boolean rangeValid = false; private final Line verticalZeroLine = new Line(); private final Line horizontalZeroLine = new Line(); private final Path verticalGridLines = new Path(); private final Path horizontalGridLines = new Path(); private final Path horizontalRowFill = new Path(); private final Path verticalRowFill = new Path(); private final Region plotBackground = new Region(); private final Group plotArea = new Group(){ @Override public void requestLayout() {} // suppress layout requests }; private final Group plotContent = new Group(); private final Rectangle plotAreaClip = new Rectangle(); private final List> displayedSeries = new ArrayList<>(); private Legend legend = new Legend(); /** This is called when a series is added or removed from the chart */ private final ListChangeListener> seriesChanged = c -> { ObservableList> series = c.getList(); while (c.next()) { // RT-12069, linked list pointers should update when list is permutated. if (c.wasPermutated()) { displayedSeries.sort((o1, o2) -> series.indexOf(o2) - series.indexOf(o1)); } if (c.getRemoved().size() > 0) updateLegend(); Set> dupCheck = new HashSet<>(displayedSeries); dupCheck.removeAll(c.getRemoved()); for (Series d : c.getAddedSubList()) { if (!dupCheck.add(d) && !d.setToRemove) { throw new IllegalArgumentException("Duplicate series added"); } } for (Series s : c.getRemoved()) { s.setToRemove = true; seriesRemoved(s); } for(int i=c.getFrom(); i s = c.getList().get(i); if (s.setToRemove) { s.setToRemove = false; s.getChart().seriesBeingRemovedIsAdded(s); } // add new listener to data s.setChart(XYChart.this); // update linkedList Pointers for series displayedSeries.add(s); // update default color style class int nextClearBit = colorBits.nextClearBit(0); colorBits.set(nextClearBit, true); s.defaultColorStyleClass = DEFAULT_COLOR+(nextClearBit%8); seriesColorMap.put(s, nextClearBit%8); // inform sub-classes of series added seriesAdded(s, i); } if (c.getFrom() < c.getTo()) updateLegend(); seriesChanged(c); } // update axis ranges invalidateRange(); // lay everything out requestChartLayout(); }; // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- private final Axis xAxis; private ReadOnlyObjectProperty> xAxisProperty = new ReadOnlyObjectPropertyBase>() { @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "xAxis"; } @Override public Axis get() { return xAxis; } }; /** * Get the X axis, by default it is along the bottom of the plot * @return the X axis of the chart */ public Axis getXAxis() { return xAxis; } private ObservableValue> xAxisProperty() { return xAxisProperty; } private final Axis yAxis; private ReadOnlyObjectProperty> yAxisProperty = new ReadOnlyObjectPropertyBase>() { @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "yAxis"; } @Override public Axis get() { return yAxis; } }; /** * Get the Y axis, by default it is along the left of the plot * @return the Y axis of this chart */ public Axis getYAxis() { return yAxis; } private ObservableValue> yAxisProperty() { return yAxisProperty; } /** XYCharts data */ private ObjectProperty>> data = new ObjectPropertyBase<>() { private ObservableList> old; @Override protected void invalidated() { final ObservableList> current = getValue(); if (current == old) return; int saveAnimationState = -1; // add remove listeners if(old != null) { old.removeListener(seriesChanged); // Set animated to false so we don't animate both remove and add // at the same time. RT-14163 // RT-21295 - disable animated only when current is also not null. if (current != null && old.size() > 0) { saveAnimationState = (old.get(0).getChart().getAnimated()) ? 1 : 2; old.get(0).getChart().setAnimated(false); } } if(current != null) current.addListener(seriesChanged); // fire series change event if series are added or removed if(old != null || current != null) { final List> removed = (old != null) ? old : Collections.>emptyList(); final int toIndex = (current != null) ? current.size() : 0; // let series listener know all old series have been removed and new that have been added if (toIndex > 0 || !removed.isEmpty()) { seriesChanged.onChanged(new NonIterableChange<>(0, toIndex, current){ @Override public List> getRemoved() { return removed; } @Override protected int[] getPermutation() { return new int[0]; } }); } } else if (old != null && old.size() > 0) { // let series listener know all old series have been removed seriesChanged.onChanged(new NonIterableChange<>(0, 0, current){ @Override public List> getRemoved() { return old; } @Override protected int[] getPermutation() { return new int[0]; } }); } // restore animated on chart. if (current != null && current.size() > 0 && saveAnimationState != -1) { current.get(0).getChart().setAnimated((saveAnimationState == 1) ? true : false); } old = current; } public Object getBean() { return XYChart.this; } public String getName() { return "data"; } }; public final ObservableList> getData() { return data.getValue(); } public final void setData(ObservableList> value) { data.setValue(value); } public final ObjectProperty>> dataProperty() { return data; } /** True if vertical grid lines should be drawn */ private BooleanProperty verticalGridLinesVisible = new StyleableBooleanProperty(true) { @Override protected void invalidated() { requestChartLayout(); } @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "verticalGridLinesVisible"; } @Override public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.VERTICAL_GRID_LINE_VISIBLE; } }; /** * Indicates whether vertical grid lines are visible or not. * * @return true if verticalGridLines are visible else false. * @see #verticalGridLinesVisibleProperty() */ public final boolean getVerticalGridLinesVisible() { return verticalGridLinesVisible.get(); } public final void setVerticalGridLinesVisible(boolean value) { verticalGridLinesVisible.set(value); } public final BooleanProperty verticalGridLinesVisibleProperty() { return verticalGridLinesVisible; } /** True if horizontal grid lines should be drawn */ private BooleanProperty horizontalGridLinesVisible = new StyleableBooleanProperty(true) { @Override protected void invalidated() { requestChartLayout(); } @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "horizontalGridLinesVisible"; } @Override public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.HORIZONTAL_GRID_LINE_VISIBLE; } }; public final boolean isHorizontalGridLinesVisible() { return horizontalGridLinesVisible.get(); } public final void setHorizontalGridLinesVisible(boolean value) { horizontalGridLinesVisible.set(value); } public final BooleanProperty horizontalGridLinesVisibleProperty() { return horizontalGridLinesVisible; } /** If true then alternative vertical columns will have fills */ private BooleanProperty alternativeColumnFillVisible = new StyleableBooleanProperty(false) { @Override protected void invalidated() { requestChartLayout(); } @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "alternativeColumnFillVisible"; } @Override public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.ALTERNATIVE_COLUMN_FILL_VISIBLE; } }; public final boolean isAlternativeColumnFillVisible() { return alternativeColumnFillVisible.getValue(); } public final void setAlternativeColumnFillVisible(boolean value) { alternativeColumnFillVisible.setValue(value); } public final BooleanProperty alternativeColumnFillVisibleProperty() { return alternativeColumnFillVisible; } /** If true then alternative horizontal rows will have fills */ private BooleanProperty alternativeRowFillVisible = new StyleableBooleanProperty(true) { @Override protected void invalidated() { requestChartLayout(); } @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "alternativeRowFillVisible"; } @Override public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.ALTERNATIVE_ROW_FILL_VISIBLE; } }; public final boolean isAlternativeRowFillVisible() { return alternativeRowFillVisible.getValue(); } public final void setAlternativeRowFillVisible(boolean value) { alternativeRowFillVisible.setValue(value); } public final BooleanProperty alternativeRowFillVisibleProperty() { return alternativeRowFillVisible; } /** * If this is true and the vertical axis has both positive and negative values then a additional axis line * will be drawn at the zero point * * @defaultValue true */ private BooleanProperty verticalZeroLineVisible = new StyleableBooleanProperty(true) { @Override protected void invalidated() { requestChartLayout(); } @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "verticalZeroLineVisible"; } @Override public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.VERTICAL_ZERO_LINE_VISIBLE; } }; public final boolean isVerticalZeroLineVisible() { return verticalZeroLineVisible.get(); } public final void setVerticalZeroLineVisible(boolean value) { verticalZeroLineVisible.set(value); } public final BooleanProperty verticalZeroLineVisibleProperty() { return verticalZeroLineVisible; } /** * If this is true and the horizontal axis has both positive and negative values then a additional axis line * will be drawn at the zero point * * @defaultValue true */ private BooleanProperty horizontalZeroLineVisible = new StyleableBooleanProperty(true) { @Override protected void invalidated() { requestChartLayout(); } @Override public Object getBean() { return XYChart.this; } @Override public String getName() { return "horizontalZeroLineVisible"; } @Override public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.HORIZONTAL_ZERO_LINE_VISIBLE; } }; public final boolean isHorizontalZeroLineVisible() { return horizontalZeroLineVisible.get(); } public final void setHorizontalZeroLineVisible(boolean value) { horizontalZeroLineVisible.set(value); } public final BooleanProperty horizontalZeroLineVisibleProperty() { return horizontalZeroLineVisible; } // -------------- PROTECTED PROPERTIES ----------------------------------------------------------------------------- /** * Modifiable and observable list of all content in the plot. This is where implementations of XYChart should add * any nodes they use to draw their plot. * * @return Observable list of plot children */ protected ObservableList getPlotChildren() { return plotContent.getChildren(); } // -------------- CONSTRUCTOR -------------------------------------------------------------------------------------- /** * Constructs a XYChart given the two axes. The initial content for the chart * plot background and plot area that includes vertical and horizontal grid * lines and fills, are added. * * @param xAxis X Axis for this XY chart * @param yAxis Y Axis for this XY chart */ public XYChart(Axis xAxis, Axis yAxis) { this.xAxis = xAxis; if (xAxis.getSide() == null) xAxis.setSide(Side.BOTTOM); xAxis.setEffectiveOrientation(Orientation.HORIZONTAL); this.yAxis = yAxis; if (yAxis.getSide() == null) yAxis.setSide(Side.LEFT); yAxis.setEffectiveOrientation(Orientation.VERTICAL); // RT-23123 autoranging leads to charts incorrect appearance. xAxis.autoRangingProperty().addListener((ov, t, t1) -> { updateAxisRange(); }); yAxis.autoRangingProperty().addListener((ov, t, t1) -> { updateAxisRange(); }); // add initial content to chart content getChartChildren().addAll(plotBackground,plotArea,xAxis,yAxis); // We don't want plotArea or plotContent to autoSize or do layout plotArea.setAutoSizeChildren(false); plotContent.setAutoSizeChildren(false); // setup clipping on plot area plotAreaClip.setSmooth(false); plotArea.setClip(plotAreaClip); // add children to plot area plotArea.getChildren().addAll( verticalRowFill, horizontalRowFill, verticalGridLines, horizontalGridLines, verticalZeroLine, horizontalZeroLine, plotContent); // setup css style classes plotContent.getStyleClass().setAll("plot-content"); plotBackground.getStyleClass().setAll("chart-plot-background"); verticalRowFill.getStyleClass().setAll("chart-alternative-column-fill"); horizontalRowFill.getStyleClass().setAll("chart-alternative-row-fill"); verticalGridLines.getStyleClass().setAll("chart-vertical-grid-lines"); horizontalGridLines.getStyleClass().setAll("chart-horizontal-grid-lines"); verticalZeroLine.getStyleClass().setAll("chart-vertical-zero-line"); horizontalZeroLine.getStyleClass().setAll("chart-horizontal-zero-line"); // mark plotContent as unmanaged as its preferred size changes do not effect our layout plotContent.setManaged(false); plotArea.setManaged(false); // listen to animation on/off and sync to axis animatedProperty().addListener((valueModel, oldValue, newValue) -> { if(getXAxis() != null) getXAxis().setAnimated(newValue); if(getYAxis() != null) getYAxis().setAnimated(newValue); }); setLegend(legend); } // -------------- METHODS ------------------------------------------------------------------------------------------ /** * 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 */ final int getDataSize() { final ObservableList> data = getData(); return (data!=null) ? data.size() : 0; } /** Called when a series's name has changed */ private void seriesNameChanged() { updateLegend(); requestChartLayout(); } private void dataItemsChanged(Series series, List> removed, int addedFrom, int addedTo, boolean permutation) { for (Data item : removed) { dataItemRemoved(item, series); } for(int i=addedFrom; i item = series.getData().get(i); dataItemAdded(series, i, item); } invalidateRange(); requestChartLayout(); } private void dataValueChanged(Data item, T newValue, ObjectProperty currentValueProperty) { if (currentValueProperty.get() != newValue) invalidateRange(); dataItemChanged(item); if (shouldAnimate()) { animate( new KeyFrame(Duration.ZERO, new KeyValue(currentValueProperty, currentValueProperty.get())), new KeyFrame(Duration.millis(700), new KeyValue(currentValueProperty, newValue, Interpolator.EASE_BOTH)) ); } else { currentValueProperty.set(newValue); requestChartLayout(); } } /** * This is called whenever a series is added or removed and the legend needs to be updated */ protected void updateLegend() { List legendList = new ArrayList<>(); if (getData() != null) { for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { Series series = getData().get(seriesIndex); legendList.add(createLegendItemForSeries(series, seriesIndex)); } } legend.getItems().setAll(legendList); if (legendList.size() > 0) { if (getLegend() == null) { setLegend(legend); } } else { setLegend(null); } } /** * Called by the updateLegend for each series in the chart in order to * create new legend item * @param series the series for this legend item * @param seriesIndex the index of the series * @return new legend item for this series */ Legend.LegendItem createLegendItemForSeries(Series series, int seriesIndex) { return new Legend.LegendItem(series.getName()); } /** * This method is called when there is an attempt to add series that was * set to be removed, and the removal might not have completed. * @param series */ void seriesBeingRemovedIsAdded(Series series) {} /** * This method is called when there is an attempt to add a Data item that was * set to be removed, and the removal might not have completed. * @param data */ void dataBeingRemovedIsAdded(Data item, Series series) {} /** * Called when a data item has been added to a series. This is where implementations of XYChart can create/add new * nodes to getPlotChildren to represent this data item. They also may animate that data add with a fade in or * similar if animated = true. * * @param series The series the data item was added to * @param itemIndex The index of the new item within the series * @param item The new data item that was added */ protected abstract void dataItemAdded(Series series, int itemIndex, Data item); /** * Called when a data item has been removed from data model but it is still visible on the chart. Its still visible * so that you can handle animation for removing it in this method. After you are done animating the data item you * must call removeDataItemFromDisplay() to remove the items node from being displayed on the chart. * * @param item The item that has been removed from the series * @param series The series the item was removed from */ protected abstract void dataItemRemoved(Data item, Series series); /** * Called when a data item has changed, ie its xValue, yValue or extraValue has changed. * * @param item The data item who was changed */ protected abstract void dataItemChanged(Data item); /** * A series has been added to the charts data model. This is where implementations of XYChart can create/add new * nodes to getPlotChildren to represent this series. Also you have to handle adding any data items that are * already in the series. You may simply call dataItemAdded() for each one or provide some different animation for * a whole series being added. * * @param series The series that has been added * @param seriesIndex The index of the new series */ protected abstract void seriesAdded(Series series, int seriesIndex); /** * A series has been removed from the data model but it is still visible on the chart. Its still visible * so that you can handle animation for removing it in this method. After you are done animating the data item you * must call removeSeriesFromDisplay() to remove the series from the display list. * * @param series The series that has been removed */ protected abstract void seriesRemoved(Series series); /** * Called when each atomic change is made to the list of series for this chart * @param c a Change instance representing the changes to the series */ protected void seriesChanged(Change c) {} /** * This is called when a data change has happened that may cause the range to be invalid. */ private void invalidateRange() { rangeValid = false; } /** * This is called when the range has been invalidated and we need to update it. If the axis are auto * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the * axis passing it that data. */ protected void updateAxisRange() { final Axis xa = getXAxis(); final Axis ya = getYAxis(); List xData = null; List yData = null; if(xa.isAutoRanging()) xData = new ArrayList<>(); if(ya.isAutoRanging()) yData = new ArrayList<>(); if(xData != null || yData != null) { for(Series series : getData()) { for(Data data: series.getData()) { if(xData != null) xData.add(data.getXValue()); if(yData != null) yData.add(data.getYValue()); } } if(xData != null) xa.invalidateRange(xData); if(yData != null) ya.invalidateRange(yData); } } /** * Called to update and layout the plot children. This should include all work to updates nodes representing * the plot on top of the axis and grid lines etc. The origin is the top left of the plot area, the plot area with * can be got by getting the width of the x axis and its height from the height of the y axis. */ protected abstract void layoutPlotChildren(); /** {@inheritDoc} */ @Override protected final void layoutChartChildren(double top, double left, double width, double height) { if(getData() == null) return; if (!rangeValid) { rangeValid = true; if(getData() != null) updateAxisRange(); } // snap top and left to pixels top = snapPositionY(top); left = snapPositionX(left); // get starting stuff final Axis xa = getXAxis(); final ObservableList> xaTickMarks = xa.getTickMarks(); final Axis ya = getYAxis(); final ObservableList> yaTickMarks = ya.getTickMarks(); // check we have 2 axises and know their sides if (xa == null || ya == null) return; // try and work out width and height of axises double xAxisWidth = 0; double xAxisHeight = 30; // guess x axis height to start with double yAxisWidth = 0; double yAxisHeight = 0; for (int count=0; count<5; count ++) { yAxisHeight = snapSizeY(height - xAxisHeight); if (yAxisHeight < 0) { yAxisHeight = 0; } yAxisWidth = ya.prefWidth(yAxisHeight); xAxisWidth = snapSizeX(width - yAxisWidth); if (xAxisWidth < 0) { xAxisWidth = 0; } double newXAxisHeight = xa.prefHeight(xAxisWidth); if (newXAxisHeight == xAxisHeight) break; xAxisHeight = newXAxisHeight; } // round axis sizes up to whole integers to snap to pixel xAxisWidth = Math.ceil(xAxisWidth); xAxisHeight = Math.ceil(xAxisHeight); yAxisWidth = Math.ceil(yAxisWidth); yAxisHeight = Math.ceil(yAxisHeight); // calc xAxis height double xAxisY = 0; switch(xa.getEffectiveSide()) { case TOP: xa.setVisible(true); xAxisY = top+1; top += xAxisHeight; break; case BOTTOM: xa.setVisible(true); xAxisY = top + yAxisHeight; } // calc yAxis width double yAxisX = 0; switch(ya.getEffectiveSide()) { case LEFT: ya.setVisible(true); yAxisX = left +1; left += yAxisWidth; break; case RIGHT: ya.setVisible(true); yAxisX = left + xAxisWidth; } // resize axises xa.resizeRelocate(left, xAxisY, xAxisWidth, xAxisHeight); ya.resizeRelocate(yAxisX, top, yAxisWidth, yAxisHeight); // When the chart is resized, need to specifically call out the axises // to lay out as they are unmanaged. xa.requestAxisLayout(); xa.layout(); ya.requestAxisLayout(); ya.layout(); // layout plot content layoutPlotChildren(); // get axis zero points final double xAxisZero = xa.getZeroPosition(); final double yAxisZero = ya.getZeroPosition(); // position vertical and horizontal zero lines if(Double.isNaN(xAxisZero) || !isVerticalZeroLineVisible()) { verticalZeroLine.setVisible(false); } else { verticalZeroLine.setStartX(left+xAxisZero+0.5); verticalZeroLine.setStartY(top); verticalZeroLine.setEndX(left+xAxisZero+0.5); verticalZeroLine.setEndY(top+yAxisHeight); verticalZeroLine.setVisible(true); } if(Double.isNaN(yAxisZero) || !isHorizontalZeroLineVisible()) { horizontalZeroLine.setVisible(false); } else { horizontalZeroLine.setStartX(left); horizontalZeroLine.setStartY(top+yAxisZero+0.5); horizontalZeroLine.setEndX(left+xAxisWidth); horizontalZeroLine.setEndY(top+yAxisZero+0.5); horizontalZeroLine.setVisible(true); } // layout plot background plotBackground.resizeRelocate(left, top, xAxisWidth, yAxisHeight); // update clip plotAreaClip.setX(left); plotAreaClip.setY(top); plotAreaClip.setWidth(xAxisWidth+1); plotAreaClip.setHeight(yAxisHeight+1); // plotArea.setClip(new Rectangle(left, top, xAxisWidth, yAxisHeight)); // position plot group, its origin is the bottom left corner of the plot area plotContent.setLayoutX(left); plotContent.setLayoutY(top); plotContent.requestLayout(); // Note: not sure this is right, maybe plotContent should be resizeable // update vertical grid lines verticalGridLines.getElements().clear(); if(getVerticalGridLinesVisible()) { for(int i=0; i < xaTickMarks.size(); i++) { Axis.TickMark tick = xaTickMarks.get(i); final double x = xa.getDisplayPosition(tick.getValue()); if ((x!=xAxisZero || !isVerticalZeroLineVisible()) && x > 0 && x <= xAxisWidth) { verticalGridLines.getElements().add(new MoveTo(left+x+0.5,top)); verticalGridLines.getElements().add(new LineTo(left+x+0.5,top+yAxisHeight)); } } } // update horizontal grid lines horizontalGridLines.getElements().clear(); if(isHorizontalGridLinesVisible()) { for(int i=0; i < yaTickMarks.size(); i++) { Axis.TickMark tick = yaTickMarks.get(i); final double y = ya.getDisplayPosition(tick.getValue()); if ((y!=yAxisZero || !isHorizontalZeroLineVisible()) && y >= 0 && y < yAxisHeight) { horizontalGridLines.getElements().add(new MoveTo(left,top+y+0.5)); horizontalGridLines.getElements().add(new LineTo(left+xAxisWidth,top+y+0.5)); } } } // Note: is there a more efficient way to calculate horizontal and vertical row fills? // update vertical row fill verticalRowFill.getElements().clear(); if (isAlternativeColumnFillVisible()) { // tick marks are not sorted so get all the positions and sort them final List tickPositionsPositive = new ArrayList<>(); final List tickPositionsNegative = new ArrayList<>(); for(int i=0; i < xaTickMarks.size(); i++) { double pos = xa.getDisplayPosition(xaTickMarks.get(i).getValue()); if (pos == xAxisZero) { tickPositionsPositive.add(pos); tickPositionsNegative.add(pos); } else if (pos < xAxisZero) { tickPositionsPositive.add(pos); } else { tickPositionsNegative.add(pos); } } Collections.sort(tickPositionsPositive); Collections.sort(tickPositionsNegative); // iterate over every pair of positive tick marks and create fill for(int i=1; i < tickPositionsPositive.size(); i+=2) { if((i+1) < tickPositionsPositive.size()) { final double x1 = tickPositionsPositive.get(i); final double x2 = tickPositionsPositive.get(i+1); verticalRowFill.getElements().addAll( new MoveTo(left+x1,top), new LineTo(left+x1,top+yAxisHeight), new LineTo(left+x2,top+yAxisHeight), new LineTo(left+x2,top), new ClosePath()); } } // iterate over every pair of positive tick marks and create fill for(int i=0; i < tickPositionsNegative.size(); i+=2) { if((i+1) < tickPositionsNegative.size()) { final double x1 = tickPositionsNegative.get(i); final double x2 = tickPositionsNegative.get(i+1); verticalRowFill.getElements().addAll( new MoveTo(left+x1,top), new LineTo(left+x1,top+yAxisHeight), new LineTo(left+x2,top+yAxisHeight), new LineTo(left+x2,top), new ClosePath()); } } } // update horizontal row fill horizontalRowFill.getElements().clear(); if (isAlternativeRowFillVisible()) { // tick marks are not sorted so get all the positions and sort them final List tickPositionsPositive = new ArrayList<>(); final List tickPositionsNegative = new ArrayList<>(); for(int i=0; i < yaTickMarks.size(); i++) { double pos = ya.getDisplayPosition(yaTickMarks.get(i).getValue()); if (pos == yAxisZero) { tickPositionsPositive.add(pos); tickPositionsNegative.add(pos); } else if (pos < yAxisZero) { tickPositionsPositive.add(pos); } else { tickPositionsNegative.add(pos); } } Collections.sort(tickPositionsPositive); Collections.sort(tickPositionsNegative); // iterate over every pair of positive tick marks and create fill for(int i=1; i < tickPositionsPositive.size(); i+=2) { if((i+1) < tickPositionsPositive.size()) { final double y1 = tickPositionsPositive.get(i); final double y2 = tickPositionsPositive.get(i+1); horizontalRowFill.getElements().addAll( new MoveTo(left, top + y1), new LineTo(left + xAxisWidth, top + y1), new LineTo(left + xAxisWidth, top + y2), new LineTo(left, top + y2), new ClosePath()); } } // iterate over every pair of positive tick marks and create fill for(int i=0; i < tickPositionsNegative.size(); i+=2) { if((i+1) < tickPositionsNegative.size()) { final double y1 = tickPositionsNegative.get(i); final double y2 = tickPositionsNegative.get(i+1); horizontalRowFill.getElements().addAll( new MoveTo(left, top + y1), new LineTo(left + xAxisWidth, top + y1), new LineTo(left + xAxisWidth, top + y2), new LineTo(left, top + y2), new ClosePath()); } } } // } /** * Get the index of the series in the series linked list. * * @param series The series to find index for * @return index of the series in series list */ int getSeriesIndex(Series series) { return displayedSeries.indexOf(series); } /** * Computes the size of series linked list * @return size of series linked list */ int getSeriesSize() { return displayedSeries.size(); } /** * This should be called from seriesRemoved() when you are finished with any animation for deleting the series from * the chart. It will remove the series from showing up in the Iterator returned by getDisplayedSeriesIterator(). * * @param series The series to remove */ protected final void removeSeriesFromDisplay(Series series) { if (series != null) series.setToRemove = false; series.setChart(null); displayedSeries.remove(series); int idx = seriesColorMap.remove(series); colorBits.clear(idx); } /** * XYChart maintains a list of all series currently displayed this includes all current series + any series that * have recently been deleted that are in the process of being faded(animated) out. This creates and returns a * iterator over that list. This is what implementations of XYChart should use when plotting data. * * @return iterator over currently displayed series */ protected final Iterator> getDisplayedSeriesIterator() { return Collections.unmodifiableList(displayedSeries).iterator(); } /** * Creates an array of KeyFrames for fading out nodes representing a series * * @param series The series to remove * @param fadeOutTime Time to fade out, in milliseconds * @return array of two KeyFrames from zero to fadeOutTime */ final KeyFrame[] createSeriesRemoveTimeLine(Series series, long fadeOutTime) { final List nodes = new ArrayList<>(); nodes.add(series.getNode()); for (Data d : series.getData()) { if (d.getNode() != null) { nodes.add(d.getNode()); } } // fade out series node and symbols KeyValue[] startValues = new KeyValue[nodes.size()]; KeyValue[] endValues = new KeyValue[nodes.size()]; for (int j = 0; j < nodes.size(); j++) { startValues[j] = new KeyValue(nodes.get(j).opacityProperty(), 1); endValues[j] = new KeyValue(nodes.get(j).opacityProperty(), 0); } return new KeyFrame[] { new KeyFrame(Duration.ZERO, startValues), new KeyFrame(Duration.millis(fadeOutTime), actionEvent -> { getPlotChildren().removeAll(nodes); removeSeriesFromDisplay(series); }, endValues) }; } /** * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this * to animate when data is added or removed. * @param item The XYChart.Data item from which the current X axis data value is obtained * @return The current displayed X data value */ protected final X getCurrentDisplayedXValue(Data item) { return item.getCurrentX(); } /** Set the current displayed data value plotted on X axis. * * @param item The XYChart.Data item from which the current X axis data value is obtained. * @param value The X axis data value * @see #getCurrentDisplayedXValue(Data) */ protected final void setCurrentDisplayedXValue(Data item, X value) { item.setCurrentX(value); } /** The current displayed data value property that is plotted on X axis. * * @param item The XYChart.Data item from which the current X axis data value property object is obtained. * @return The current displayed X data value ObjectProperty. * @see #getCurrentDisplayedXValue(Data) */ protected final ObjectProperty currentDisplayedXValueProperty(Data item) { return item.currentXProperty(); } /** * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this * to animate when data is added or removed. * @param item The XYChart.Data item from which the current Y axis data value is obtained * @return The current displayed Y data value */ protected final Y getCurrentDisplayedYValue(Data item) { return item.getCurrentY(); } /** * Set the current displayed data value plotted on Y axis. * * @param item The XYChart.Data item from which the current Y axis data value is obtained. * @param value The Y axis data value * @see #getCurrentDisplayedYValue(Data) */ protected final void setCurrentDisplayedYValue(Data item, Y value) { item.setCurrentY(value); } /** The current displayed data value property that is plotted on Y axis. * * @param item The XYChart.Data item from which the current Y axis data value property object is obtained. * @return The current displayed Y data value ObjectProperty. * @see #getCurrentDisplayedYValue(Data) */ protected final ObjectProperty currentDisplayedYValueProperty(Data item) { return item.currentYProperty(); } /** * The current displayed data extra value. This may be the same as extraValue or different. It is * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot * in any custom XYChart implementations. * @param item The XYChart.Data item from which the current extra value is obtained * @return The current extra value */ protected final Object getCurrentDisplayedExtraValue(Data item) { return item.getCurrentExtraValue(); } /** * Set the current displayed data extra value. * * @param item The XYChart.Data item from which the current extra value is obtained. * @param value The extra value * @see #getCurrentDisplayedExtraValue(Data) */ protected final void setCurrentDisplayedExtraValue(Data item, Object value) { item.setCurrentExtraValue(value); } /** * The current displayed extra value property. * * @param item The XYChart.Data item from which the current extra value property object is obtained. * @return {@literal ObjectProperty The current extra value ObjectProperty} * @see #getCurrentDisplayedExtraValue(Data) */ protected final ObjectProperty currentDisplayedExtraValueProperty(Data item) { return item.currentExtraValueProperty(); } /** * XYChart maintains a list of all items currently displayed this includes all current data + any data items * recently deleted that are in the process of being faded out. This creates and returns a iterator over * that list. This is what implementations of XYChart should use when plotting data. * * @param series The series to get displayed data for * @return iterator over currently displayed items from this series */ protected final Iterator> getDisplayedDataIterator(final Series series) { return Collections.unmodifiableList(series.displayedData).iterator(); } /** * This should be called from dataItemRemoved() when you are finished with any animation for deleting the item from the * chart. It will remove the data item from showing up in the Iterator returned by getDisplayedDataIterator(). * * @param series The series to remove * @param item The item to remove from series's display list */ protected final void removeDataItemFromDisplay(Series series, Data item) { series.removeDataItemRef(item); } // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ private static class StyleableProperties { private static final CssMetaData,Boolean> HORIZONTAL_GRID_LINE_VISIBLE = new CssMetaData<>("-fx-horizontal-grid-lines-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.horizontalGridLinesVisible == null || !node.horizontalGridLinesVisible.isBound(); } @Override public StyleableProperty getStyleableProperty(XYChart node) { return (StyleableProperty)node.horizontalGridLinesVisibleProperty(); } }; private static final CssMetaData,Boolean> HORIZONTAL_ZERO_LINE_VISIBLE = new CssMetaData<>("-fx-horizontal-zero-line-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.horizontalZeroLineVisible == null || !node.horizontalZeroLineVisible.isBound(); } @Override public StyleableProperty getStyleableProperty(XYChart node) { return (StyleableProperty)node.horizontalZeroLineVisibleProperty(); } }; private static final CssMetaData,Boolean> ALTERNATIVE_ROW_FILL_VISIBLE = new CssMetaData<>("-fx-alternative-row-fill-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.alternativeRowFillVisible == null || !node.alternativeRowFillVisible.isBound(); } @Override public StyleableProperty getStyleableProperty(XYChart node) { return (StyleableProperty)node.alternativeRowFillVisibleProperty(); } }; private static final CssMetaData,Boolean> VERTICAL_GRID_LINE_VISIBLE = new CssMetaData<>("-fx-vertical-grid-lines-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.verticalGridLinesVisible == null || !node.verticalGridLinesVisible.isBound(); } @Override public StyleableProperty getStyleableProperty(XYChart node) { return (StyleableProperty)node.verticalGridLinesVisibleProperty(); } }; private static final CssMetaData,Boolean> VERTICAL_ZERO_LINE_VISIBLE = new CssMetaData<>("-fx-vertical-zero-line-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.verticalZeroLineVisible == null || !node.verticalZeroLineVisible.isBound(); } @Override public StyleableProperty getStyleableProperty(XYChart node) { return (StyleableProperty)node.verticalZeroLineVisibleProperty(); } }; private static final CssMetaData,Boolean> ALTERNATIVE_COLUMN_FILL_VISIBLE = new CssMetaData<>("-fx-alternative-column-fill-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.alternativeColumnFillVisible == null || !node.alternativeColumnFillVisible.isBound(); } @Override public StyleableProperty getStyleableProperty(XYChart node) { return (StyleableProperty)node.alternativeColumnFillVisibleProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList<>(Chart.getClassCssMetaData()); styleables.add(HORIZONTAL_GRID_LINE_VISIBLE); styleables.add(HORIZONTAL_ZERO_LINE_VISIBLE); styleables.add(ALTERNATIVE_ROW_FILL_VISIBLE); styleables.add(VERTICAL_GRID_LINE_VISIBLE); styleables.add(VERTICAL_ZERO_LINE_VISIBLE); styleables.add(ALTERNATIVE_COLUMN_FILL_VISIBLE); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * Gets the {@code CssMetaData} associated with this class, which may include the * {@code CssMetaData} of its superclasses. * @return the {@code CssMetaData} * @since JavaFX 8.0 */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * @since JavaFX 8.0 */ @Override public List> getCssMetaData() { return getClassCssMetaData(); } // -------------- INNER CLASSES ------------------------------------------------------------------------------------ /** * A single data item with data for 2 axis charts * * @param the data X value type * @param the data Y value type * @since JavaFX 2.0 */ public final static class Data { // -------------- PUBLIC PROPERTIES ---------------------------------------- private boolean setToRemove = false; /** The series this data belongs to */ private Series series; private ObjectProperty> seriesProperty = new SimpleObjectProperty<>(); void setSeries(Series series) { this.series = series; this.seriesProperty.set(series); } /** The generic data value to be plotted on the X axis */ private ObjectProperty xValue = new SimpleObjectProperty<>(Data.this, "XValue") { @Override protected void invalidated() { if (series!=null) { XYChart chart = series.getChart(); if(chart!=null) chart.dataValueChanged(Data.this, get(), currentXProperty()); } else { // data has not been added to series yet : // so currentX and X should be the same setCurrentX(get()); } } }; /** * Gets the generic data value to be plotted on the X axis. * @return the generic data value to be plotted on the X axis. */ public final X getXValue() { return xValue.get(); } /** * Sets the generic data value to be plotted on the X axis. * @param value the generic data value to be plotted on the X axis. */ public final void setXValue(X value) { xValue.set(value); // handle the case where this is a init because the default constructor was used // and the case when series is not associated to a chart due to a remove series if (currentX.get() == null || (series != null && series.getChart() == null)) currentX.setValue(value); } /** * The generic data value to be plotted on the X axis. * @return The XValue property */ public final ObjectProperty XValueProperty() { return xValue; } /** The generic data value to be plotted on the Y axis */ private ObjectProperty yValue = new SimpleObjectProperty<>(Data.this, "YValue") { @Override protected void invalidated() { if (series!=null) { XYChart chart = series.getChart(); if(chart!=null) chart.dataValueChanged(Data.this, get(), currentYProperty()); } else { // data has not been added to series yet : // so currentY and Y should be the same setCurrentY(get()); } } }; /** * Gets the generic data value to be plotted on the Y axis. * @return the generic data value to be plotted on the Y axis. */ public final Y getYValue() { return yValue.get(); } /** * Sets the generic data value to be plotted on the Y axis. * @param value the generic data value to be plotted on the Y axis. */ public final void setYValue(Y value) { yValue.set(value); // handle the case where this is a init because the default constructor was used // and the case when series is not associated to a chart due to a remove series if (currentY.get() == null || (series != null && series.getChart() == null)) currentY.setValue(value); } /** * The generic data value to be plotted on the Y axis. * @return the YValue property */ public final ObjectProperty YValueProperty() { return yValue; } /** * The generic data value to be plotted in any way the chart needs. For example used as the radius * for BubbleChart. */ private ObjectProperty extraValue = new SimpleObjectProperty<>(Data.this, "extraValue") { @Override protected void invalidated() { if (series!=null) { XYChart chart = series.getChart(); if(chart!=null) chart.dataValueChanged(Data.this, get(), currentExtraValueProperty()); } } }; public final Object getExtraValue() { return extraValue.get(); } public final void setExtraValue(Object value) { extraValue.set(value); } public final ObjectProperty extraValueProperty() { return extraValue; } /** * The node to display for this data item. You can either create your own node and set it on the data item * before you add the item to the chart. Otherwise the chart will create a node for you that has the default * representation for the chart type. This node will be set as soon as the data is added to the chart. You can * then get it to add mouse listeners etc. Charts will do their best to position and size the node * appropriately, for example on a Line or Scatter chart this node will be positioned centered on the data * values position. For a bar chart this is positioned and resized as the bar for this data item. */ private ObjectProperty node = new SimpleObjectProperty<>(this, "node") { protected void invalidated() { Node node = get(); if (node != null) { node.accessibleTextProperty().unbind(); ObservableValue seriesLabel = seriesProperty .flatMap(Series::nameProperty) .orElse(""); ObservableValue xAxisLabel= seriesProperty .flatMap(Series::chartProperty) .flatMap(XYChart::xAxisProperty) .flatMap(Axis::labelProperty) .orElse(ControlResources.getString("XYChart.series.xaxis")); ObservableValue yAxisLabel = seriesProperty .flatMap(Series::chartProperty) .flatMap(XYChart::yAxisProperty) .flatMap(Axis::labelProperty) .orElse(ControlResources.getString("XYChart.series.yaxis")); node.accessibleTextProperty().bind(new StringBinding() { { bind(currentXProperty(), currentYProperty(), seriesLabel, xAxisLabel, yAxisLabel); } @Override protected String computeValue() { String seriesName = seriesLabel.getValue(); String xAxisName = xAxisLabel.getValue(); String yAxisName = yAxisLabel.getValue(); String format = ControlResources.getString("XYChart.series.accessibleText"); MessageFormat mf = new MessageFormat(format); Object[] args = {seriesName, xAxisName, getCurrentX(), yAxisName, getCurrentY()}; String retVal = mf.format(args); return retVal; } }); } } }; public final Node getNode() { return node.get(); } public final void setNode(Node value) { node.set(value); } public final ObjectProperty nodeProperty() { return node; } /** * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this * to animate when data is added or removed. */ private ObjectProperty currentX = new SimpleObjectProperty<>(this, "currentX"); final X getCurrentX() { return currentX.get(); } final void setCurrentX(X value) { currentX.set(value); } final ObjectProperty currentXProperty() { return currentX; } /** * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this * to animate when data is added or removed. */ private ObjectProperty currentY = new SimpleObjectProperty<>(this, "currentY"); final Y getCurrentY() { return currentY.get(); } final void setCurrentY(Y value) { currentY.set(value); } final ObjectProperty currentYProperty() { return currentY; } /** * The current displayed data extra value. This may be the same as extraValue or different. It is * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot * in any custom XYChart implementations. */ private ObjectProperty currentExtraValue = new SimpleObjectProperty<>(this, "currentExtraValue"); final Object getCurrentExtraValue() { return currentExtraValue.getValue(); } final void setCurrentExtraValue(Object value) { currentExtraValue.setValue(value); } final ObjectProperty currentExtraValueProperty() { return currentExtraValue; } // -------------- CONSTRUCTOR ------------------------------------------------- /** * Creates an empty XYChart.Data object. */ public Data() {} /** * Creates an instance of XYChart.Data object and initializes the X,Y * data values. * * @param xValue The X axis data value * @param yValue The Y axis data value */ public Data(X xValue, Y yValue) { setXValue(xValue); setYValue(yValue); setCurrentX(xValue); setCurrentY(yValue); } /** * Creates an instance of XYChart.Data object and initializes the X,Y * data values and extraValue. * * @param xValue The X axis data value. * @param yValue The Y axis data value. * @param extraValue Chart extra value. */ public Data(X xValue, Y yValue, Object extraValue) { setXValue(xValue); setYValue(yValue); setExtraValue(extraValue); setCurrentX(xValue); setCurrentY(yValue); setCurrentExtraValue(extraValue); } // -------------- PUBLIC METHODS ---------------------------------------------- /** * Returns a string representation of this {@code Data} object. * @return a string representation of this {@code Data} object. */ @Override public String toString() { return "Data["+getXValue()+","+getYValue()+","+getExtraValue()+"]"; } } /** * A named series of data items * * @param the series X value type * @param the series Y value type * @since JavaFX 2.0 */ public static final class Series { // -------------- PRIVATE PROPERTIES ---------------------------------------- /** the style class for default color for this series */ String defaultColorStyleClass; boolean setToRemove = false; private List> displayedData = new ArrayList<>(); private final ListChangeListener> dataChangeListener = new ListChangeListener<>() { @Override public void onChanged(Change> c) { ObservableList> data = c.getList(); final XYChart chart = getChart(); while (c.next()) { if (chart != null) { // RT-25187 Probably a sort happened, just reorder the pointers and return. if (c.wasPermutated()) { displayedData.sort((o1, o2) -> data.indexOf(o2) - data.indexOf(o1)); return; } Set> dupCheck = new HashSet<>(displayedData); dupCheck.removeAll(c.getRemoved()); for (Data d : c.getAddedSubList()) { if (!dupCheck.add(d)) { throw new IllegalArgumentException("Duplicate data added"); } } // update data items reference to series for (Data item : c.getRemoved()) { item.setToRemove = true; } if (c.getAddedSize() > 0) { for (Data itemPtr : c.getAddedSubList()) { if (itemPtr.setToRemove) { if (chart != null) chart.dataBeingRemovedIsAdded(itemPtr, Series.this); itemPtr.setToRemove = false; } } for (Data d : c.getAddedSubList()) { d.setSeries(Series.this); } if (c.getFrom() == 0) { displayedData.addAll(0, c.getAddedSubList()); } else { displayedData.addAll(displayedData.indexOf(data.get(c.getFrom() - 1)) + 1, c.getAddedSubList()); } } // inform chart chart.dataItemsChanged(Series.this, (List>) c.getRemoved(), c.getFrom(), c.getTo(), c.wasPermutated()); } else { Set> dupCheck = new HashSet<>(); for (Data d : data) { if (!dupCheck.add(d)) { throw new IllegalArgumentException("Duplicate data added"); } } for (Data d : c.getAddedSubList()) { d.setSeries(Series.this); } } } } }; // -------------- PUBLIC PROPERTIES ---------------------------------------- /** Reference to the chart this series belongs to */ private final ReadOnlyObjectWrapper> chart = new ReadOnlyObjectWrapper<>(this, "chart") { @Override protected void invalidated() { if (get() == null) { displayedData.clear(); } else { displayedData.addAll(getData()); } } }; public final XYChart getChart() { return chart.get(); } private void setChart(XYChart value) { chart.set(value); } public final ReadOnlyObjectProperty> chartProperty() { return chart.getReadOnlyProperty(); } /** The user displayable name for this series */ private final StringProperty name = new StringPropertyBase() { @Override protected void invalidated() { get(); // make non-lazy if(getChart() != null) getChart().seriesNameChanged(); } @Override public Object getBean() { return Series.this; } @Override public String getName() { return "name"; } }; public final String getName() { return name.get(); } public final void setName(String value) { name.set(value); } public final StringProperty nameProperty() { return name; } /** * The node to display for this series. This is created by the chart if it uses nodes to represent the whole * series. For example line chart uses this for the line but scatter chart does not use it. This node will be * set as soon as the series is added to the chart. You can then get it to add mouse listeners etc. */ private ObjectProperty node = new SimpleObjectProperty<>(this, "node"); public final Node getNode() { return node.get(); } public final void setNode(Node value) { node.set(value); } public final ObjectProperty nodeProperty() { return node; } /** ObservableList of data items that make up this series */ private final ObjectProperty>> data = new ObjectPropertyBase<>() { private ObservableList> old; @Override protected void invalidated() { final ObservableList> current = getValue(); // add remove listeners if(old != null) old.removeListener(dataChangeListener); if(current != null) current.addListener(dataChangeListener); // fire data change event if series are added or removed if(old != null || current != null) { final List> removed = (old != null) ? old : Collections.>emptyList(); final int toIndex = (current != null) ? current.size() : 0; // let data listener know all old data have been removed and new data that has been added if (toIndex > 0 || !removed.isEmpty()) { dataChangeListener.onChanged(new NonIterableChange<>(0, toIndex, current){ @Override public List> getRemoved() { return removed; } @Override protected int[] getPermutation() { return new int[0]; } }); } } else if (old != null && old.size() > 0) { // let series listener know all old series have been removed dataChangeListener.onChanged(new NonIterableChange<>(0, 0, current){ @Override public List> getRemoved() { return old; } @Override protected int[] getPermutation() { return new int[0]; } }); } old = current; } @Override public Object getBean() { return Series.this; } @Override public String getName() { return "data"; } }; public final ObservableList> getData() { return data.getValue(); } public final void setData(ObservableList> value) { data.setValue(value); } public final ObjectProperty>> dataProperty() { return data; } // -------------- CONSTRUCTORS ---------------------------------------------- /** * Construct a empty series */ public Series() { this(FXCollections.>observableArrayList()); } /** * Constructs a Series and populates it with the given {@link ObservableList} data. * * @param data ObservableList of XYChart.Data */ public Series(ObservableList> data) { setData(data); for(Data item:data) item.setSeries(this); } /** * Constructs a named Series and populates it with the given {@link ObservableList} data. * * @param name a name for the series * @param data ObservableList of XYChart.Data */ public Series(String name, ObservableList> data) { this(data); setName(name); } // -------------- PUBLIC METHODS ---------------------------------------------- /** * Returns a string representation of this {@code Series} object. * @return a string representation of this {@code Series} object. */ @Override public String toString() { return "Series["+getName()+"]"; } // -------------- PRIVATE/PROTECTED METHODS ----------------------------------- /* * The following methods are for manipulating the pointers in the linked list * when data is deleted. */ private void removeDataItemRef(Data item) { if (item != null) item.setToRemove = false; displayedData.remove(item); } int getItemIndex(Data item) { return displayedData.indexOf(item); } Data getItem(int i) { return displayedData.get(i); } int getDataSize() { return displayedData.size(); } } }