javafx.scene.chart.XYChart Maven / Gradle / Ivy
/*
* 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 extends Series> 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 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 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