/* * Copyright (c) 2010, 2012, 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.collections.NonIterableChange; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.beans.property.StringPropertyBase; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.beans.value.WritableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; 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.css.StyleableBooleanProperty; import com.sun.javafx.css.StyleableProperty; import com.sun.javafx.css.converters.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. */ @SuppressWarnings({"unchecked","deprecation"}) public abstract class XYChart<X,Y> extends Chart { // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- private int seriesDefaultColorIndex = 0; 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(); /* start pointer of a series linked list. */ Series<X,Y> begin = null; /** This is called when a series is added or removed from the chart */ private final ListChangeListener<Series<X,Y>> seriesChanged = new ListChangeListener<Series<X,Y>>() { @Override public void onChanged(Change<? extends Series<X,Y>> c) { while (c.next()) { if (c.getRemoved().size() > 0) updateLegend(); for (Series<X,Y> series : c.getRemoved()) { series.setChart(null); seriesRemoved(series); seriesDefaultColorIndex --; } for(int i=c.getFrom(); i<c.getTo() && !c.wasPermutated(); i++) { final Series<X,Y> series = c.getList().get(i); // add new listener to data series.setChart(XYChart.this); // update linkedList Pointers for series if (XYChart.this.begin == null) { XYChart.this.begin = getData().get(i); XYChart.this.begin.next = null; } else { if (i == 0) { getData().get(0).next = XYChart.this.begin; begin = getData().get(0); } else { Series ptr = begin; for (int j = 0; j < i -1 && ptr!=null ; j++) { ptr = ptr.next; } if (ptr != null) { getData().get(i).next = ptr.next; ptr.next = getData().get(i); } } } // update default color style class series.defaultColorStyleClass = "default-color"+(seriesDefaultColorIndex % 8); seriesDefaultColorIndex ++; // inform sub-classes of series added seriesAdded(series, i); } if (c.getFrom() < c.getTo()) updateLegend(); seriesChanged(c); // RT-12069, linked list pointers should update when list is permutated. if (c.wasPermutated() && getData().size() > 0) { XYChart.this.begin = getData().get(0); Series<X,Y> ptr = begin; for(int k = 1; k < getData().size() && ptr != null; k++) { ptr.next = getData().get(k); ptr = ptr.next; } ptr.next = null; } } // update axis ranges invalidateRange(); // lay everything out requestChartLayout(); } }; // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- private final Axis<X> xAxis; /** Get the X axis, by default it is along the bottom of the plot */ public Axis<X> getXAxis() { return xAxis; } private final Axis<Y> yAxis; /** Get the Y axis, by default it is along the left of the plot */ public Axis<Y> getYAxis() { return yAxis; } /** XYCharts data */ private ObjectProperty<ObservableList<Series<X,Y>>> data = new ObjectPropertyBase<ObservableList<Series<X,Y>>>() { private ObservableList<Series<X,Y>> old; @Override protected void invalidated() { final ObservableList<Series<X,Y>> current = getValue(); 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<Series<X,Y>> removed = (old != null) ? old : Collections.<Series<X,Y>>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<Series<X,Y>>(0, toIndex, current){ @Override public List<Series<X,Y>> 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<Series<X,Y>>(0, 0, current){ @Override public List<Series<X,Y>> 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<Series<X,Y>> getData() { return data.getValue(); } public final void setData(ObservableList<Series<X,Y>> value) { data.setValue(value); } public final ObjectProperty<ObservableList<Series<X,Y>>> 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 StyleableProperty getStyleableProperty() { return StyleableProperties.VERTICAL_GRID_LINE_VISIBLE; } }; /** * Indicates whether vertical grid lines are visible or not. * * @return true if verticalGridLines are visible else false. * @see #verticalGridLinesVisible */ 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 StyleableProperty getStyleableProperty() { 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 StyleableProperty getStyleableProperty() { 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 StyleableProperty getStyleableProperty() { 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 StyleableProperty getStyleableProperty() { 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 StyleableProperty getStyleableProperty() { 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<Node> 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<X> xAxis, Axis<Y> yAxis) { this.xAxis = xAxis; if(xAxis.getSide() == null) xAxis.setSide(Side.BOTTOM); this.yAxis = yAxis; if(yAxis.getSide() == null) yAxis.setSide(Side.LEFT); // 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(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> valueModel, Boolean oldValue, Boolean newValue) { if(getXAxis() != null) getXAxis().setAnimated(newValue); if(getYAxis() != null) getYAxis().setAnimated(newValue); } }); } // -------------- 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<Series<X,Y>> data = getData(); return (data!=null) ? data.size() : 0; } /** Called when a series's name has changed */ private void seriesNameChanged() { updateLegend(); requestChartLayout(); } @SuppressWarnings({"UnusedParameters"}) private void dataItemsChanged(Series<X,Y> series, List<Data<X,Y>> removed, int addedFrom, int addedTo, boolean permutation) { for (Data<X,Y> item : removed) { dataItemRemoved(item, series); } for(int i=addedFrom; i<addedTo; i++) { Data<X,Y> item = series.getData().get(i); dataItemAdded(series, i, item); } invalidateRange(); requestChartLayout(); } private void dataXValueChanged(Data<X,Y> item) { if(item.getCurrentX() != item.getXValue()) invalidateRange(); dataItemChanged(item); if (shouldAnimate()) { animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentXProperty(), item.getCurrentX())), new KeyFrame(Duration.millis(700), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) ); } else { item.setCurrentX(item.getXValue()); requestChartLayout(); } } private void dataYValueChanged(Data<X,Y> item) { if(item.getCurrentY() != item.getYValue()) invalidateRange(); dataItemChanged(item); if (shouldAnimate()) { animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY())), new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH)) ); } else { item.setCurrentY(item.getYValue()); requestChartLayout(); } } private void dataExtraValueChanged(Data<X,Y> item) { if(item.getCurrentY() != item.getYValue()) invalidateRange(); dataItemChanged(item); if (shouldAnimate()) { animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY())), new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH)) ); } else { item.setCurrentY(item.getYValue()); requestChartLayout(); } } /** * This is called whenever a series is added or removed and the legend needs to be updated */ protected void updateLegend(){} /** * 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<X,Y> series, int itemIndex, Data<X,Y> 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<X, Y> item, Series<X, Y> 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<X, Y> 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<X, Y> 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<X,Y> series); /** Called when each atomic change is made to the list of series for this chart */ protected void seriesChanged(Change<? extends Series> 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<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) 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 = snapPosition(top); left = snapPosition(left); // get starting stuff final Axis<X> xa = getXAxis(); final ObservableList<Axis.TickMark<X>> xaTickMarks = xa.getTickMarks(); final Axis<Y> ya = getYAxis(); final ObservableList<Axis.TickMark<Y>> yaTickMarks = ya.getTickMarks(); // check we have 2 axises and know their sides if (xa == null || ya == null || xa.getSide() == null || ya.getSide() == 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 = height-xAxisHeight; yAxisWidth = ya.prefWidth(yAxisHeight); xAxisWidth = width - yAxisWidth; 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; if (xa.getSide().equals(Side.TOP)) { xa.setVisible(true); xAxisY = top+1; top += xAxisHeight; } else if (xa.getSide().equals(Side.BOTTOM)) { xa.setVisible(true); xAxisY = top + yAxisHeight; } else { // X axis should never be left or right so hide xa.setVisible(false); xAxisHeight = 0; } // calc yAxis width double yAxisX = 0; if (ya.getSide().equals(Side.LEFT)) { ya.setVisible(true); yAxisX = left +1; left += yAxisWidth; } else if (ya.getSide().equals(Side.RIGHT)) { ya.setVisible(true); yAxisX = left + xAxisWidth; } else { // Y axis should never be top or bottom so hide ya.setVisible(false); yAxisWidth = 0; } // 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<X> tick = xaTickMarks.get(i); double pixelOffset = (i==(xaTickMarks.size()-1)) ? -0.5 : 0.5; final double x = xa.getDisplayPosition(tick.getValue()); if ((x!=xAxisZero || !isVerticalZeroLineVisible()) && x > 0 && x <= xAxisWidth) { verticalGridLines.getElements().add(new MoveTo(left+x+pixelOffset,top)); verticalGridLines.getElements().add(new LineTo(left+x+pixelOffset,top+yAxisHeight)); } } } // update horizontal grid lines horizontalGridLines.getElements().clear(); if(isHorizontalGridLinesVisible()) { for(int i=0; i < yaTickMarks.size(); i++) { Axis.TickMark<Y> tick = yaTickMarks.get(i); double pixelOffset = (i==(yaTickMarks.size()-1)) ? -0.5 : 0.5; final double y = ya.getDisplayPosition(tick.getValue()); if ((y!=yAxisZero || !isHorizontalZeroLineVisible()) && y >= 0 && y < yAxisHeight) { horizontalGridLines.getElements().add(new MoveTo(left,top+y+pixelOffset)); horizontalGridLines.getElements().add(new LineTo(left+xAxisWidth,top+y+pixelOffset)); } } } // 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<Double> tickPositionsPositive = new ArrayList<Double>(); final List<Double> tickPositionsNegative = new ArrayList<Double>(); for(int i=0; i < xaTickMarks.size(); i++) { double pos = xa.getDisplayPosition((X) 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<Double> tickPositionsPositive = new ArrayList<Double>(); final List<Double> tickPositionsNegative = new ArrayList<Double>(); for(int i=0; i < yaTickMarks.size(); i++) { double pos = ya.getDisplayPosition((Y) 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) { int itemIndex = 0; for (Series s = XYChart.this.begin; s != null; s = s.next) { if (s == series) break; itemIndex++; } return itemIndex; } /** * Computes the size of series linked list * @return size of series linked list */ int getSeriesSize() { int count = 0; for (Series d = XYChart.this.begin; d != null; d = d.next) { count++; } return count; } /** * 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<X, Y> series) { if (begin == series) { begin = series.next; } else { Series ptr = begin; while(ptr != null && ptr.next != series) { ptr = ptr.next; } if (ptr != null) ptr.next = series.next; } } /** * 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<Series<X,Y>> getDisplayedSeriesIterator() { return new Iterator<Series<X, Y>>() { private boolean start = true; private Series<X,Y> current = begin; @Override public boolean hasNext() { if (start) { return current != null; } else { return current.next != null; } } @Override public Series<X, Y> next() { if (start) { start = false; } else if (current!=null) { current = current.next; } return current; } @Override public void remove() { throw new UnsupportedOperationException("We don't support removing items from the displayed series list."); } }; } /** * 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. */ protected final X getCurrentDisplayedXValue(Data<X,Y> 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. * @see #getCurrentDisplayedXValue */ protected final void setCurrentDisplayedXValue(Data<X,Y> 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 */ protected final ObjectProperty<X> currentDisplayedXValueProperty(Data<X,Y> 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. */ protected final Y getCurrentDisplayedYValue(Data<X,Y> 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. * @see #getCurrentDisplayedYValue */ protected final void setCurrentDisplayedYValue(Data<X,Y> 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 */ protected final ObjectProperty<Y> currentDisplayedYValueProperty(Data<X,Y> 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. */ protected final Object getCurrentDisplayedExtraValue(Data<X,Y> 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. * @see #getCurrentDisplayedExtraValue */ protected final void setCurrentDisplayedExtraValue(Data<X,Y> 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 ObjectProperty<Object> The current extra value ObjectProperty * @see #getCurrentDisplayedExtraValue */ protected final ObjectProperty<Object> currentDisplayedExtraValueProperty(Data<X,Y> 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<Data<X,Y>> getDisplayedDataIterator(final Series<X,Y> series) { return new Iterator<Data<X, Y>>() { private boolean start = true; private Data<X,Y> current = series.begin; @Override public boolean hasNext() { if (start) { return current != null; } else { return current.next != null; } } @Override public Data<X, Y> next() { if (start) { start = false; } else if (current!=null) { current = current.next; } return current; } @Override public void remove() { throw new UnsupportedOperationException("We don't support removing items from the displayed data list."); } }; } /** * 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<X, Y> series, Data<X, Y> item) { series.removeDataItemRef(item); } // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ private static class StyleableProperties { private static final StyleableProperty<XYChart,Boolean> HORIZONTAL_GRID_LINE_VISIBLE = new StyleableProperty<XYChart,Boolean>("-fx-horizontal-grid-lines-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.horizontalGridLinesVisible == null || !node.horizontalGridLinesVisible.isBound(); } @Override public WritableValue<Boolean> getWritableValue(XYChart node) { return node.horizontalGridLinesVisibleProperty(); } }; private static final StyleableProperty<XYChart,Boolean> HORIZONTAL_ZERO_LINE_VISIBLE = new StyleableProperty<XYChart,Boolean>("-fx-horizontal-zero-line-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.horizontalZeroLineVisible == null || !node.horizontalZeroLineVisible.isBound(); } @Override public WritableValue<Boolean> getWritableValue(XYChart node) { return node.horizontalZeroLineVisibleProperty(); } }; private static final StyleableProperty<XYChart,Boolean> ALTERNATIVE_ROW_FILL_VISIBLE = new StyleableProperty<XYChart,Boolean>("-fx-alternative-row-fill-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.alternativeRowFillVisible == null || !node.alternativeRowFillVisible.isBound(); } @Override public WritableValue<Boolean> getWritableValue(XYChart node) { return node.alternativeRowFillVisibleProperty(); } }; private static final StyleableProperty<XYChart,Boolean> VERTICAL_GRID_LINE_VISIBLE = new StyleableProperty<XYChart,Boolean>("-fx-vertical-grid-lines-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.verticalGridLinesVisible == null || !node.verticalGridLinesVisible.isBound(); } @Override public WritableValue<Boolean> getWritableValue(XYChart node) { return node.verticalGridLinesVisibleProperty(); } }; private static final StyleableProperty<XYChart,Boolean> VERTICAL_ZERO_LINE_VISIBLE = new StyleableProperty<XYChart,Boolean>("-fx-vertical-zero-line-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.verticalZeroLineVisible == null || !node.verticalZeroLineVisible.isBound(); } @Override public WritableValue<Boolean> getWritableValue(XYChart node) { return node.verticalZeroLineVisibleProperty(); } }; private static final StyleableProperty<XYChart,Boolean> ALTERNATIVE_COLUMN_FILL_VISIBLE = new StyleableProperty<XYChart,Boolean>("-fx-alternative-column-fill-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(XYChart node) { return node.alternativeColumnFillVisible == null || !node.alternativeColumnFillVisible.isBound(); } @Override public WritableValue<Boolean> getWritableValue(XYChart node) { return node.alternativeColumnFillVisibleProperty(); } }; private static final List<StyleableProperty> STYLEABLES; static { final List<StyleableProperty> styleables = new ArrayList<StyleableProperty>(Chart.impl_CSS_STYLEABLES()); Collections.addAll(styleables, HORIZONTAL_GRID_LINE_VISIBLE, HORIZONTAL_ZERO_LINE_VISIBLE, ALTERNATIVE_ROW_FILL_VISIBLE, VERTICAL_GRID_LINE_VISIBLE, VERTICAL_ZERO_LINE_VISIBLE, ALTERNATIVE_COLUMN_FILL_VISIBLE ); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated public static List<StyleableProperty> impl_CSS_STYLEABLES() { return XYChart.StyleableProperties.STYLEABLES; } /** * RT-19263 * @treatAsPrivate implementation detail * @deprecated This is an experimental API that is not intended for general use and is subject to change in future versions */ @Deprecated public List<StyleableProperty> impl_getStyleableProperties() { return impl_CSS_STYLEABLES(); } // -------------- INNER CLASSES ------------------------------------------------------------------------------------ /** * A single data item with data for 2 axis charts */ public final static class Data<X,Y> { // -------------- PUBLIC PROPERTIES ---------------------------------------- private boolean setToRemove = false; /** The series this data belongs to */ private Series<X,Y> series; private void setSeries(Series<X,Y> series) { this.series = series; } /** The generic data value to be plotted on the X axis */ private ObjectProperty<X> xValue = new ObjectPropertyBase<X>() { @Override protected void invalidated() { // Note: calling get to make non-lazy, replace with change listener when available get(); if (series!=null) { XYChart<X,Y> chart = series.getChart(); if(chart!=null) chart.dataXValueChanged(Data.this); } else { // data has not been added to series yet : // so currentX and X should be the same setCurrentX(get()); } } @Override public Object getBean() { return Data.this; } @Override public String getName() { return "XValue"; } }; /** * 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 if (currentX.get() == null) currentX.setValue(value); } /** * The generic data value to be plotted on the X axis. * @return The XValue property */ public final ObjectProperty<X> XValueProperty() { return xValue; } /** The generic data value to be plotted on the Y axis */ private ObjectProperty<Y> yValue = new ObjectPropertyBase<Y>() { @Override protected void invalidated() { // Note: calling get to make non-lazy, replace with change listener when available get(); if (series!=null) { XYChart<X,Y> chart = series.getChart(); if(chart!=null) chart.dataYValueChanged(Data.this); } else { // data has not been added to series yet : // so currentY and Y should be the same setCurrentY(get()); } } @Override public Object getBean() { return Data.this; } @Override public String getName() { return "YValue"; } }; /** * 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 if (currentY.get() == null) currentY.setValue(value); } /** * The generic data value to be plotted on the Y axis. * @return the YValue property */ public final ObjectProperty<Y> 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<Object> extraValue = new ObjectPropertyBase<Object>() { @Override protected void invalidated() { // Note: calling get to make non-lazy, replace with change listener when available get(); if (series!=null) { XYChart<X,Y> chart = series.getChart(); if(chart!=null) chart.dataExtraValueChanged(Data.this); } } @Override public Object getBean() { return Data.this; } @Override public String getName() { return "extraValue"; } }; public final Object getExtraValue() { return extraValue.get(); } public final void setExtraValue(Object value) { extraValue.set(value); } public final ObjectProperty<Object> 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> node = new SimpleObjectProperty<Node>(this, "node"); public final Node getNode() { return node.get(); } public final void setNode(Node value) { node.set(value); } public final ObjectProperty<Node> 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<X> currentX = new SimpleObjectProperty<X>(this, "currentX"); final X getCurrentX() { return currentX.get(); } final void setCurrentX(X value) { currentX.set(value); } final ObjectProperty<X> 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<Y> currentY = new SimpleObjectProperty<Y>(this, "currentY"); final Y getCurrentY() { return currentY.get(); } final void setCurrentY(Y value) { currentY.set(value); } final ObjectProperty<Y> 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<Object> currentExtraValue = new SimpleObjectProperty<Object>(this, "currentExtraValue"); final Object getCurrentExtraValue() { return currentExtraValue.getValue(); } final void setCurrentExtraValue(Object value) { currentExtraValue.setValue(value); } final ObjectProperty<Object> currentExtraValueProperty() { return currentExtraValue; } /** * Next pointer for the next data item. We maintain a linkedlist of the * data items so even after the data is deleted from the list, * we have a reference to it */ protected Data<X,Y> next = null; // -------------- 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 */ public static final class Series<X,Y> { // HACK! private boolean appendOnly = false; public void setAppendOnly(boolean ao) { appendOnly = ao; } // -------------- PRIVATE PROPERTIES ---------------------------------------- /** the style class for default color for this series */ String defaultColorStyleClass; Data<X,Y> begin = null; // start pointer of a data linked list. /* * Next pointer for the next series. We maintain a linkedlist of the * serieses so even after the series is deleted from the list, * we have a reference to it - needed by BarChart e.g. */ Series<X,Y> next = null; private final ListChangeListener<Data<X,Y>> dataChangeListener = new ListChangeListener<Data<X, Y>>() { @Override public void onChanged(Change<? extends Data<X, Y>> c) { while (c.next()) { // update data items reference to series for (Data<X,Y> item : c.getRemoved()) { item.setSeries(null); item.setToRemove = true; } if (c.getAddedSize() > 0) { for (Data<X,Y> itemPtr = begin; itemPtr != null; itemPtr = itemPtr.next) { if (itemPtr.setToRemove) { removeDataItemRef(itemPtr); } } } for(int i=c.getFrom(); i<c.getTo(); i++) { getData().get(i).setSeries(Series.this); // // HACK ALERT! // The code below is there to allow elements to be removed from the series by // maintinaing a linked list of the data items. Unfortunately the implementation // is horribly inneficient and doesn't recognize the normal case of appending // items to the list. For a 7000 entry list, this code follows ~24M links // The right answer is to fix the code properly (handle the nromal case // efficiently), but for now I'm just going to rely on the fact that I don't // remove elements and therefore don't need to maintain the list. if (!appendOnly) { // update linkedList Pointers for data in this series if (begin == null) { begin = getData().get(i); begin.next = null; } else { if (i == 0) { getData().get(0).next = begin; begin = getData().get(0); } else { Data<X,Y> ptr = begin; for (int j = 0; j < i -1 ; j++) { ptr = ptr.next; } getData().get(i).next = ptr.next; ptr.next = getData().get(i); } } } } // inform chart XYChart<X,Y> chart = getChart(); if(chart!=null) chart.dataItemsChanged(Series.this, (List<Data<X,Y>>)c.getRemoved(), c.getFrom(), c.getTo(), c.wasPermutated()); } } }; // -------------- PUBLIC PROPERTIES ---------------------------------------- /** Reference to the chart this series belongs to */ private final ReadOnlyObjectWrapper<XYChart<X,Y>> chart = new ReadOnlyObjectWrapper<XYChart<X,Y>>(this, "chart"); public final XYChart<X,Y> getChart() { return chart.get(); } private void setChart(XYChart<X,Y> value) { chart.set(value); } public final ReadOnlyObjectProperty<XYChart<X,Y>> 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> node = new SimpleObjectProperty<Node>(this, "node"); public final Node getNode() { return node.get(); } public final void setNode(Node value) { node.set(value); } public final ObjectProperty<Node> nodeProperty() { return node; } /** ObservableList of data items that make up this series */ private final ObjectProperty<ObservableList<Data<X,Y>>> data = new ObjectPropertyBase<ObservableList<Data<X,Y>>>() { private ObservableList<Data<X,Y>> old; @Override protected void invalidated() { final ObservableList<Data<X,Y>> 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<Data<X,Y>> removed = (old != null) ? old : Collections.<Data<X,Y>>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<Data<X,Y>>(0, toIndex, current){ @Override public List<Data<X,Y>> 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<Data<X,Y>>(0, 0, current){ @Override public List<Data<X,Y>> 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<Data<X,Y>> getData() { return data.getValue(); } public final void setData(ObservableList<Data<X,Y>> value) { data.setValue(value); } public final ObjectProperty<ObservableList<Data<X,Y>>> dataProperty() { return data; } // -------------- CONSTRUCTORS ---------------------------------------------- /** * Construct a empty series */ public Series() { this(FXCollections.<Data<X,Y>>observableArrayList()); } /** * Constructs a Series and populates it with the given {@link ObservableList} data. * * @param data ObservableList of XYChart.Data */ public Series(ObservableList<Data<X,Y>> data) { setData(data); for(Data<X,Y> 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<X,Y>> 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<X,Y> item) { if (begin == item) { begin = item.next; } else { Data<X,Y> ptr = begin; while(ptr != null && ptr.next != item) { ptr = ptr.next; } if(ptr != null) ptr.next = item.next; } } int getItemIndex(Data<X,Y> item) { int itemIndex = 0; for (Data<X,Y> d = begin; d != null; d = d.next) { if (d == item) break; itemIndex++; } return itemIndex; } int getDataSize() { int count = 0; for (Data<X,Y> d = begin; d != null; d = d.next) { count++; } return count; } } }