com.kostikiadis.charts.MultiAxisChart Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opentdk-gui Show documentation
Show all versions of opentdk-gui Show documentation
The Open Tool Development Kit provides packages and classes for easy implementation of Java tools or applications. Originally designed for test supporting software.
The newest version!
/*
* Copyright (c) 2010, 2014, 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 com.kostikiadis.charts;
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 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.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.StyleableBooleanProperty;
import javafx.geometry.Side;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.Chart;
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 javafx.scene.chart.Axis;
public abstract class MultiAxisChart extends Chart {
public static final int Y1_AXIS = 0;
public static final int Y2_AXIS = 1;
// 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();
protected final List> displayedSeries = new ArrayList<>();
/** This is called when a series is added or removed from the chart */
private final ListChangeListener> seriesChanged = c -> {
ObservableList extends MultiAxisChart.Series> series = c.getList();
while (c.next()) {
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 (MultiAxisChart.Series d : c.getAddedSubList()) {
if (!dupCheck.add(d)) {
throw new IllegalArgumentException("Duplicate series added");
}
}
for (MultiAxisChart.Series s : c.getRemoved()) {
s.setToRemove = true;
seriesRemoved(s);
int idx = seriesColorMap.remove(s);
colorBits.clear(idx);
}
for (int i = c.getFrom(); i < c.getTo() && !c.wasPermutated(); i++) {
final MultiAxisChart.Series s = c.getList().get(i);
// add new listener to data
s.setChart(MultiAxisChart.this);
if (s.setToRemove) {
s.setToRemove = false;
s.getChart().seriesBeingRemovedIsAdded(s);
}
// 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 com.lk.javafx.chart.Axis customXAxis;
//
// /** Get the X axis, by default it is along the bottom of the plot */
// public com.lk.javafx.chart.Axis getCustomXAxis() {
// return customXAxis;
// }
private final Axis xAxis;
/** Get the X axis, by default it is along the bottom of the plot
* @return the abscissa */
public Axis getXAxis() {
return xAxis;
}
private final Axis y1Axis;
/** Get the Y axis, by default it is along the left of the plot
* @return the first ordinate */
public Axis getY1Axis() {
return y1Axis;
}
private final Axis y2Axis;
/** Get the Y axis, by default it is along the left of the plot
* @return the second ordinate */
public Axis getY2Axis() {
return y2Axis;
}
/** MultiAxisCharts data */
protected ObjectProperty>> data = new ObjectPropertyBase>>() {
private ObservableList> old;
@SuppressWarnings("unchecked")
@Override
protected void invalidated() {
final ObservableList> 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> 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;
}
@Override
public Object getBean() {
return MultiAxisChart.this;
}
@Override
public String getName() {
return "data";
}
};
public final ObservableList> getData() {
return data.getValue();
}
public final void setData(ObservableList> value) {
data.setValue(value);
}
public final ObjectProperty>> dataProperty() {
return data;
}
/** True if vertical grid lines should be drawn */
private BooleanProperty verticalGridLinesVisible = new StyleableBooleanProperty(true) {
@Override
protected void invalidated() {
requestChartLayout();
}
@Override
public Object getBean() {
return MultiAxisChart.this;
}
@Override
public String getName() {
return "verticalGridLinesVisible";
}
@Override
public CssMetaData, Boolean> getCssMetaData() {
return null;
}
};
/**
* 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 MultiAxisChart.this;
}
@Override
public String getName() {
return "horizontalGridLinesVisible";
}
@Override
public CssMetaData, Boolean> getCssMetaData() {
return null;
}
};
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 MultiAxisChart.this;
}
@Override
public String getName() {
return "alternativeColumnFillVisible";
}
@Override
public CssMetaData, Boolean> getCssMetaData() {
return null;
}
};
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 MultiAxisChart.this;
}
@Override
public String getName() {
return "alternativeRowFillVisible";
}
@Override
public CssMetaData, Boolean> getCssMetaData() {
return null;
}
};
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 MultiAxisChart.this;
}
@Override
public String getName() {
return "verticalZeroLineVisible";
}
@Override
public CssMetaData, Boolean> getCssMetaData() {
return null;
}
};
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 MultiAxisChart.this;
}
@Override
public String getName() {
return "horizontalZeroLineVisible";
}
@Override
public CssMetaData, Boolean> getCssMetaData() {
return null;
}
};
public final boolean isHorizontalZeroLineVisible() {
return horizontalZeroLineVisible.get();
}
public final void setHorizontalZeroLineVisible(boolean value) {
horizontalZeroLineVisible.set(value);
}
public final BooleanProperty horizontalZeroLineVisibleProperty() {
return horizontalZeroLineVisible;
}
/**
* 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) };
}
// -------------- PROTECTED PROPERTIES ------------------------
/**
* Modifiable and observable list of all content in the plot. This is where
* implementations of MultiAxisChart 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 MultiAxisChart 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 y1Axis Y1 Axis for this XY chart
* @param y2Axis Y2 Axis for this XY chart
*/
public MultiAxisChart(Axis xAxis, Axis y1Axis, Axis y2Axis) {
// this.customXAxis = null;
this.xAxis = xAxis;
if (xAxis.getSide() == null)
xAxis.setSide(Side.BOTTOM);
this.y1Axis = y1Axis;
if (y1Axis.getSide() == null)
y1Axis.setSide(Side.LEFT);
this.y2Axis = y2Axis;
if (y2Axis != null && y2Axis.getSide() == null)
y2Axis.setSide(Side.RIGHT);
if (y2Axis != null) {
y2Axis.visibleProperty().addListener(e -> {
layoutPlotChildren();
});
y2Axis.autoRangingProperty().addListener((ov, t, t1) -> {
updateAxisRange();
});
getChartChildren().add(y2Axis);
}
// RT-23123 autoranging leads to charts incorrect appearance.
xAxis.autoRangingProperty().addListener((ov, t, t1) -> {
updateAxisRange();
});
y1Axis.autoRangingProperty().addListener((ov, t, t1) -> {
updateAxisRange();
});
// add initial content to chart content
getChartChildren().addAll(plotBackground, plotArea, xAxis, y1Axis);
// 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 (getY1Axis() != null)
getY1Axis().setAnimated(newValue);
if (getY2Axis() != null)
getY2Axis().setAnimated(newValue);
});
}
// -------------- METHODS -------------------------------------
protected final Iterator> getDisplayedDataIterator(final MultiAxisChart.Series series) {
return Collections.unmodifiableList(series.displayedData).iterator();
}
protected final void removeDataItemFromDisplay(MultiAxisChart.Series series, Data item) {
series.removeDataItemRef(item);
}
protected final void removeSeriesFromDisplay(MultiAxisChart.Series series) {
if (series != null)
series.setToRemove = false;
series.setChart(null);
displayedSeries.remove(series);
}
/**
* Gets the size of the data returning 0 if the data is null
*
* @return The number of items in data, or null if data is null
*/
public 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(MultiAxisChart.Series series, List> removed, int addedFrom,
int addedTo, boolean permutation) {
for (Data item : removed) {
dataItemRemoved(item, series);
}
for (int i = addedFrom; i < addedTo; i++) {
Data item = series.getData().get(i);
dataItemAdded(series, i, item);
}
invalidateRange();
requestChartLayout();
}
private void dataXValueChanged(Data item) {
if (item.getCurrentX() != item.getXValue()) {
invalidateRange();
}
dataItemChanged(item);
item.setCurrentX(item.getXValue());
requestChartLayout();
}
private void dataYValueChanged(Data item) {
if (item.getCurrentY() != item.getYValue())
invalidateRange();
dataItemChanged(item);
item.setCurrentY(item.getYValue());
requestChartLayout();
}
private void dataExtraValueChanged(Data item) {
if (item.getCurrentY() != item.getYValue())
invalidateRange();
dataItemChanged(item);
item.setCurrentY(item.getYValue());
requestChartLayout();
}
/**
* 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();
/**
* This is called whenever a series is added or removed and the legend needs to
* be updated
*/
protected void updateLegend() {
}
/**
* 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 the object that stores all data to display in the chart
*/
void seriesBeingRemovedIsAdded(MultiAxisChart.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 item one specific data object in the chart
* @param series the object that stores all data to display in the chart
*/
void dataBeingRemovedIsAdded(Data item, MultiAxisChart.Series series) {
}
/**
* Called when a data item has been added to a series. This is where
* implementations of MultiAxisChart 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(MultiAxisChart.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, MultiAxisChart.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 MultiAxisChart 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(MultiAxisChart.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(MultiAxisChart.Series series);
/**
* Called when each atomic change is made to the list of series for this chart
* @param c TODO
*/
protected void seriesChanged(Change extends MultiAxisChart.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 com.lk.javafx.chart.Axis customXa = getCustomXAxis();
final Axis y1a = getY1Axis();
final Axis y2a = getY2Axis();
List xData = null;
List y1Data = null;
List y2Data = null;
//
// if (xa == null) {
// if (customXa.isAutoRanging()) {
// xData = new ArrayList();
// }
// } else {
if (xa.isAutoRanging()) {
xData = new ArrayList();
}
// }
if (y1a.isAutoRanging())
y1Data = new ArrayList();
if (y2a != null && y2a.isAutoRanging())
y2Data = new ArrayList();
if (xData != null || y1Data != null) {
for (MultiAxisChart.Series series : getData()) {
for (Data data : series.getData()) {
if (xData != null)
xData.add(data.getXValue());
if (y1Data != null && (data.getExtraValue() == null || (int) data.getExtraValue() == Y1_AXIS)) {
y1Data.add(data.getYValue());
} else if (y2Data != null) {
if (y2a == null)
throw new NullPointerException("Y2 Axis is not defined.");
y2Data.add(data.getYValue());
}
}
}
if (xData != null)
xa.invalidateRange(xData);
if (y1Data != null)
y1a.invalidateRange(y1Data);
if (y2Data != null)
y2a.invalidateRange(y2Data);
}
}
@SuppressWarnings("deprecation")
@Override
protected 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 xa = getXAxis();
// final com.lk.javafx.chart.Axis customXa = getCustomXAxis();
ObservableList> xaTickMarks = null;
// ObservableList> customXaTickMarks = null;
// if (customXa != null) {
// customXaTickMarks = customXa.getTickMarks();
// } else {
xaTickMarks = xa.getTickMarks();
// }
final Axis ya = getY1Axis();
final ObservableList> yaTickMarks = ya.getTickMarks();
final Axis y2a = getY2Axis();
// check we have 2 axises and know their sides
// if ((xa == null && customXa == null) || ya == null)
// return;
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;
double y2AxisWidth = 0;
double y2AxisHeight = 0;
for (int count = 0; count < 5; count++) {
yAxisHeight = snapSize(height - xAxisHeight);
if (yAxisHeight < 0) {
yAxisHeight = 0;
}
yAxisWidth = ya.prefWidth(yAxisHeight);
y2AxisHeight = snapSize(height - xAxisHeight);
if (y2AxisHeight < 0) {
y2AxisHeight = 0;
}
if (y2a != null)
y2AxisWidth = y2a.prefWidth(y2AxisHeight);
xAxisWidth = snapSize(width - yAxisWidth - y2AxisWidth);
if (xAxisWidth < 0) {
xAxisWidth = 0;
}
double newXAxisHeight;
// if (customXa != null) {
// newXAxisHeight = customXa.prefHeight(xAxisWidth);
// } else {
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);
y2AxisWidth = Math.ceil(y2AxisWidth);
y2AxisHeight = Math.ceil(y2AxisHeight);
// calc xAxis height
double xAxisY = 0;
// if (customXa != null) {
// customXa.setVisible(true);
// } else {
xa.setVisible(true);
// }
xAxisY = top + yAxisHeight;
// calc yAxis width
double yAxisX = 0;
ya.setVisible(true);
yAxisX = left + 1;
left += yAxisWidth;
xAxisWidth = width - y2AxisWidth - left;
// TODO : Check again the approach below
// resize axises
// if (customXa != null) {
// customXa.resizeRelocate(left, xAxisY, xAxisWidth, xAxisHeight);
// } else {
xa.resizeRelocate(left, xAxisY, xAxisWidth, xAxisHeight);
// }
ya.resizeRelocate(yAxisX, top, yAxisWidth, yAxisHeight);
if (y2a != null)
y2a.resizeRelocate(width - y2AxisWidth, top, y2AxisWidth, y2AxisHeight);
// When the chart is resized, need to specifically call out the axises
// to lay out as they are unmanaged.
// if (customXa != null) {
// customXa.requestAxisLayout();
// customXa.layout();
// } else {
xa.requestAxisLayout();
xa.layout();
// }
ya.requestAxisLayout();
ya.layout();
if (y2a != null) {
y2a.requestAxisLayout();
y2a.layout();
}
// layout plot content
layoutPlotChildren();
// get axis zero points
final double xAxisZero;
// if (customXa != null) {
// xAxisZero = customXa.getZeroPosition();
// } else {
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 (customXa != null) {
// if (getVerticalGridLinesVisible()) {
// for (int i = 0; i < customXaTickMarks.size(); i++) {
// javafx.scene.chart.Axis.TickMark tick = customXaTickMarks.get(i);
// final double x = customXa.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));
// }
// }
// }
// } else {
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 position
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();
// if(customXa != null) {
// for (int i = 0; i < customXaTickMarks.size(); i++) {
// double pos = customXa.getDisplayPosition((X) customXaTickMarks.get(i).getValue());
//
// if (pos == xAxisZero) {
// tickPositionsPositive.add(pos);
// tickPositionsNegative.add(pos);
// } else if (pos < xAxisZero) {
// tickPositionsPositive.add(pos);
// } else {
// tickPositionsNegative.add(pos);
// }
// }
// } else {
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 tickPositionsPositive = new ArrayList();
final List tickPositionsNegative = new ArrayList();
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());
}
}
}
}
/**
* Computes the size of series linked list
*
* @return size of series linked list
*/
int getSeriesSize() {
return displayedSeries.size();
}
/**
* 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();
}
/**
* A single data item with data for 2 axis charts
* @param the data type of the first axis
* @param the data type of the second axis
*
* @since JavaFX 2.0
*/
public final static class Data {
// -------------- PUBLIC PROPERTIES ----------------------------------------
private boolean setToRemove = false;
/** The series this data belongs to */
private MultiAxisChart.Series series;
void setSeries(MultiAxisChart.Series series) {
this.series = series;
}
/** The generic data value to be plotted on the X axis */
private ObjectProperty xValue = new ObjectPropertyBase() {
@Override
protected void invalidated() {
// Note: calling get to make non-lazy, replace with change listener when
// available
get();
if (series != null) {
MultiAxisChart 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
// and the case when series is not associated to a chart due to a remove series
if (currentX.get() == null || (series != null && series.getChart() == null))
currentX.setValue(value);
}
/**
* The generic data value to be plotted on the X axis.
*
* @return The XValue property
*/
public final ObjectProperty XValueProperty() {
return xValue;
}
/** The generic data value to be plotted on the Y axis */
private ObjectProperty yValue = new ObjectPropertyBase() {
@Override
protected void invalidated() {
// Note: calling get to make non-lazy, replace with change listener when
// available
get();
if (series != null) {
MultiAxisChart 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
// and the case when series is not associated to a chart due to a remove series
if (currentY.get() == null || (series != null && series.getChart() == null))
currentY.setValue(value);
}
/**
* The generic data value to be plotted on the Y axis.
*
* @return the YValue property
*/
public final ObjectProperty YValueProperty() {
return yValue;
}
/**
* The generic data value to be plotted in any way the chart needs. For example
* used as the radius for BubbleChart.
*/
private ObjectProperty
© 2015 - 2025 Weber Informatics LLC | Privacy Policy