javafx.scene.chart.BubbleChart Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.chart;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Ellipse;
import javafx.util.Duration;
import com.sun.javafx.charts.Legend.LegendItem;
/**
* Chart type that plots bubbles for the data points in a series. The extra value property of Data is used to represent
* the radius of the bubble it should be a java.lang.Number.
* @since JavaFX 2.0
*/
public class BubbleChart extends XYChart {
// -------------- CONSTRUCTORS ----------------------------------------------
/**
* Construct a new BubbleChart with the given axis. BubbleChart does not use a Category Axis.
* Both X and Y axes should be of type NumberAxis.
*
* @param xAxis The x axis to use
* @param yAxis The y axis to use
*/
public BubbleChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis) {
this(xAxis, yAxis, FXCollections.>observableArrayList());
}
/**
* Construct a new BubbleChart with the given axis and data. BubbleChart does not
* use a Category Axis. Both X and Y axes should be of type NumberAxis.
*
* @param xAxis The x axis to use
* @param yAxis The y axis to use
* @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
*/
public BubbleChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis, @NamedArg("data") ObservableList> data) {
super(xAxis, yAxis);
if (!(xAxis instanceof ValueAxis && yAxis instanceof ValueAxis)) {
throw new IllegalArgumentException("Axis type incorrect, X and Y should both be NumberAxis");
}
setData(data);
}
// -------------- METHODS ------------------------------------------------------------------------------------------
/**
* Used to get a double value from a object that can be a Number object or null
*
* @param number Object possibly a instance of Number
* @param nullDefault What value to return if the number object is null or not a Number
* @return number converted to double or nullDefault
*/
private static double getDoubleValue(Object number, double nullDefault) {
return !(number instanceof Number) ? nullDefault : ((Number)number).doubleValue();
}
/** {@inheritDoc} */
@Override protected void layoutPlotChildren() {
// update bubble positions
for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
Series series = getData().get(seriesIndex);
// for (Data item = series.begin; item != null; item = item.next) {
Iterator> iter = getDisplayedDataIterator(series);
while(iter.hasNext()) {
Data item = iter.next();
double x = getXAxis().getDisplayPosition(item.getCurrentX());
double y = getYAxis().getDisplayPosition(item.getCurrentY());
if (Double.isNaN(x) || Double.isNaN(y)) {
continue;
}
Node bubble = item.getNode();
Ellipse ellipse;
if (bubble != null) {
if (bubble instanceof StackPane) {
StackPane region = (StackPane)item.getNode();
if (region.getShape() == null) {
ellipse = new Ellipse(getDoubleValue(item.getExtraValue(), 1), getDoubleValue(item.getExtraValue(), 1));
} else if (region.getShape() instanceof Ellipse) {
ellipse = (Ellipse)region.getShape();
} else {
return;
}
ellipse.setRadiusX(getDoubleValue(item.getExtraValue(), 1) * ((getXAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getXAxis()).getScale()) : 1));
ellipse.setRadiusY(getDoubleValue(item.getExtraValue(), 1) * ((getYAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getYAxis()).getScale()) : 1));
// Note: workaround for RT-7689 - saw this in ProgressControlSkin
// The region doesn't update itself when the shape is mutated in place, so we
// null out and then restore the shape in order to force invalidation.
region.setShape(null);
region.setShape(ellipse);
region.setScaleShape(false);
region.setCenterShape(false);
region.setCacheShape(false);
// position the bubble
bubble.setLayoutX(x);
bubble.setLayoutY(y);
}
}
}
}
}
@Override protected void dataItemAdded(Series series, int itemIndex, Data item) {
Node bubble = createBubble(series, getData().indexOf(series), item, itemIndex);
if (shouldAnimate()) {
// fade in new bubble
bubble.setOpacity(0);
getPlotChildren().add(bubble);
FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
ft.setToValue(1);
ft.play();
} else {
getPlotChildren().add(bubble);
}
}
@Override protected void dataItemRemoved(final Data item, final Series series) {
final Node bubble = item.getNode();
if (shouldAnimate()) {
// fade out old bubble
FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
ft.setToValue(0);
ft.setOnFinished(actionEvent -> {
getPlotChildren().remove(bubble);
removeDataItemFromDisplay(series, item);
bubble.setOpacity(1.0);
});
ft.play();
} else {
getPlotChildren().remove(bubble);
removeDataItemFromDisplay(series, item);
}
}
/** {@inheritDoc} */
@Override protected void dataItemChanged(Data item) {
}
@Override protected void seriesAdded(Series series, int seriesIndex) {
// handle any data already in series
for (int j=0; j item = series.getData().get(j);
Node bubble = createBubble(series, seriesIndex, item, j);
if (shouldAnimate()) {
bubble.setOpacity(0);
getPlotChildren().add(bubble);
// fade in new bubble
FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
ft.setToValue(1);
ft.play();
} else {
getPlotChildren().add(bubble);
}
}
}
@Override protected void seriesRemoved(final Series series) {
// remove all bubble nodes
if (shouldAnimate()) {
ParallelTransition pt = new ParallelTransition();
pt.setOnFinished(event -> {
removeSeriesFromDisplay(series);
});
for (XYChart.Data d : series.getData()) {
final Node bubble = d.getNode();
// fade out old bubble
FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
ft.setToValue(0);
ft.setOnFinished(actionEvent -> {
getPlotChildren().remove(bubble);
bubble.setOpacity(1.0);
});
pt.getChildren().add(ft);
}
pt.play();
} else {
for (XYChart.Data d : series.getData()) {
final Node bubble = d.getNode();
getPlotChildren().remove(bubble);
}
removeSeriesFromDisplay(series);
}
}
/**
* Create a Bubble for a given data item if it doesn't already have a node
*
*
* @param series
* @param seriesIndex The index of the series containing the item
* @param item The data item to create node for
* @param itemIndex The index of the data item in the series
* @return Node used for given data item
*/
private Node createBubble(Series series, int seriesIndex, final Data item, int itemIndex) {
Node bubble = item.getNode();
// check if bubble has already been created
if (bubble == null) {
bubble = new StackPane() {
@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case TEXT: {
String accText = getAccessibleText();
if (item.getExtraValue() == null) {
return accText;
} else {
return accText + " Bubble radius is " + item.getExtraValue();
}
}
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
};
bubble.setAccessibleRole(AccessibleRole.TEXT);
bubble.setAccessibleRoleDescription("Bubble");
bubble.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
item.setNode(bubble);
}
// set bubble styles
bubble.getStyleClass().setAll("chart-bubble", "series" + seriesIndex, "data" + itemIndex,
series.defaultColorStyleClass);
return bubble;
}
/**
* 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.
*/
@Override protected void updateAxisRange() {
// For bubble chart we need to override this method as we need to let the axis know that they need to be able
// to cover the whole area occupied by the bubble not just its center data value
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();
final boolean xIsCategory = xa instanceof CategoryAxis;
final boolean yIsCategory = ya instanceof CategoryAxis;
if(xData != null || yData != null) {
for(Series series : getData()) {
for(Data data: series.getData()) {
if(xData != null) {
if(xIsCategory) {
xData.add(data.getXValue());
} else {
xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) + getDoubleValue(data.getExtraValue(), 0)));
xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) - getDoubleValue(data.getExtraValue(), 0)));
}
}
if(yData != null){
if(yIsCategory) {
yData.add(data.getYValue());
} else {
yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) + getDoubleValue(data.getExtraValue(), 0)));
yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) - getDoubleValue(data.getExtraValue(), 0)));
}
}
}
}
if(xData != null) xa.invalidateRange(xData);
if(yData != null) ya.invalidateRange(yData);
}
}
@Override
LegendItem createLegendItemForSeries(Series series, int seriesIndex) {
LegendItem legendItem = new LegendItem(series.getName());
legendItem.getSymbol().getStyleClass().addAll("series" + seriesIndex, "chart-bubble",
"bubble-legend-symbol", series.defaultColorStyleClass);
return legendItem;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy