Created
March 18, 2013 20:02
-
-
Save KhaledLela/5190333 to your computer and use it in GitHub Desktop.
Fix Bug on StackedBarChart to be able to add more that one bar (positive & negative) for each category on each series
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2011, 2013, 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. | |
*/ | |
import com.sun.javafx.charts.Legend; | |
import java.util.*; | |
import javafx.animation.*; | |
import javafx.beans.property.DoubleProperty; | |
import javafx.beans.property.SimpleDoubleProperty; | |
import javafx.collections.FXCollections; | |
import javafx.collections.ObservableList; | |
import javafx.event.ActionEvent; | |
import javafx.event.EventHandler; | |
import javafx.geometry.Orientation; | |
import javafx.scene.Node; | |
import javafx.scene.chart.Axis; | |
import javafx.scene.chart.BarChart; | |
import javafx.scene.chart.CategoryAxis; | |
import javafx.scene.chart.ValueAxis; | |
import javafx.scene.chart.XYChart; | |
import javafx.scene.chart.XYChart.Data; | |
import javafx.scene.chart.XYChart.Series; | |
import javafx.scene.layout.StackPane; | |
import javafx.util.Duration; | |
/** | |
* CustomizedStackedBarChart is a variation of {@link BarChart} that plots bars | |
* indicating data values for a category. The bars can be vertical or horizontal | |
* depending on which axis is a category axis. The bar for each series is | |
* stacked on top of the previous series. | |
*/ | |
public class CustomizedStackedBarChart<X, Y> extends XYChart<X, Y> { | |
// -------------- PRIVATE FIELDS ------------------------------------------- | |
private Map<Series, Map<String, List<Data<X, Y>>>> seriesCategoryMap = new HashMap<Series, Map<String, List<Data<X, Y>>>>(); | |
private Legend legend = new Legend(); | |
private final Orientation orientation; | |
private CategoryAxis categoryAxis; | |
private ValueAxis valueAxis; | |
private int seriesDefaultColorIndex = 0; | |
private Map<Series<X, Y>, String> seriesDefaultColorMap = new HashMap<Series<X, Y>, String>(); | |
// -------------- PUBLIC PROPERTIES ---------------------------------------- | |
/** | |
* The gap to leave between bars in separate categories | |
*/ | |
private DoubleProperty categoryGap = new SimpleDoubleProperty(10) { | |
@Override | |
protected void invalidated() { | |
get(); | |
requestChartLayout(); | |
} | |
}; | |
public double getCategoryGap() { | |
return categoryGap.getValue(); | |
} | |
public void setCategoryGap(double value) { | |
categoryGap.setValue(value); | |
} | |
public DoubleProperty categoryGapProperty() { | |
return categoryGap; | |
} | |
// -------------- CONSTRUCTOR ---------------------------------------------- | |
/** | |
* Construct a new CustomizedStackedBarChart with the given axis. The two | |
* axis should be a ValueAxis/NumberAxis and a CategoryAxis, they can be in | |
* either order depending on if you want a horizontal or vertical bar chart. | |
* | |
* @param xAxis The x axis to use | |
* @param yAxis The y axis to use | |
*/ | |
public CustomizedStackedBarChart(Axis<X> xAxis, Axis<Y> yAxis) { | |
this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList()); | |
} | |
/** | |
* Construct a new CustomizedStackedBarChart with the given axis and data. | |
* The two axis should be a ValueAxis/NumberAxis and a CategoryAxis, they | |
* can be in either order depending on if you want a horizontal or vertical | |
* bar chart. | |
* | |
* @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 CustomizedStackedBarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data) { | |
super(xAxis, yAxis); | |
getStyleClass().add("stacked-bar-chart"); | |
setLegend(legend); | |
if (!((xAxis instanceof ValueAxis && yAxis instanceof CategoryAxis) | |
|| (yAxis instanceof ValueAxis && xAxis instanceof CategoryAxis))) { | |
throw new IllegalArgumentException("Axis type incorrect, one of X,Y should be CategoryAxis and the other NumberAxis"); | |
} | |
if (xAxis instanceof CategoryAxis) { | |
categoryAxis = (CategoryAxis) xAxis; | |
valueAxis = (ValueAxis) yAxis; | |
orientation = Orientation.VERTICAL; | |
} else { | |
categoryAxis = (CategoryAxis) yAxis; | |
valueAxis = (ValueAxis) xAxis; | |
orientation = Orientation.HORIZONTAL; | |
} | |
// update css | |
// pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE); | |
// pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE); | |
setData(data); | |
} | |
/** | |
* Construct a new CustomizedStackedBarChart with the given axis and data. | |
* The two axis should be a ValueAxis/NumberAxis and a CategoryAxis, they | |
* can be in either order depending on if you want a horizontal or vertical | |
* bar chart. | |
* | |
* @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 | |
* @param categoryGap The gap to leave between bars in separate categories | |
*/ | |
public CustomizedStackedBarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data, double categoryGap) { | |
this(xAxis, yAxis); | |
setData(data); | |
setCategoryGap(categoryGap); | |
} | |
// -------------- METHODS -------------------------------------------------- | |
@Override | |
protected void dataItemAdded(Series<X, Y> series, int itemIndex, Data<X, Y> item) { | |
String category; | |
if (orientation == Orientation.VERTICAL) { | |
category = (String) item.getXValue(); | |
} else { | |
category = (String) item.getYValue(); | |
} | |
// Don't plot if category does not already exist ? | |
// if (!categoryAxis.getCategories().contains(category)) return; | |
Map<String, List<Data<X, Y>>> categoryMap = seriesCategoryMap.get(series); | |
if (categoryMap == null) { | |
categoryMap = new HashMap<String, List<Data<X, Y>>>(); | |
seriesCategoryMap.put(series, categoryMap); | |
} | |
// list to hold more that one bar "positive and negative" | |
List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>(); | |
itemList.add(item); | |
categoryMap.put(category, itemList); | |
Node bar = createBar(series, getData().indexOf(series), item, itemIndex); | |
if (shouldAnimate()) { | |
animateDataAdd(item, bar); | |
} else { | |
getPlotChildren().add(bar); | |
} | |
} | |
@Override | |
protected void dataItemRemoved(final Data<X, Y> item, final Series<X, Y> series) { | |
final Node bar = item.getNode(); | |
if (shouldAnimate()) { | |
Timeline t = createDataRemoveTimeline(item, bar, series); | |
t.setOnFinished(new EventHandler<ActionEvent>() { | |
public void handle(ActionEvent event) { | |
removeDataItemFromDisplay(series, item); | |
} | |
}); | |
t.play(); | |
} else { | |
getPlotChildren().remove(bar); | |
removeDataItemFromDisplay(series, item); | |
} | |
} | |
/** | |
* @inheritDoc | |
*/ | |
@Override | |
protected void dataItemChanged(Data<X, Y> item) { | |
double barVal; | |
double currentVal; | |
if (orientation == Orientation.VERTICAL) { | |
barVal = ((Number) item.getYValue()).doubleValue(); | |
currentVal = ((Number) getCurrentDisplayedYValue(item)).doubleValue(); | |
} else { | |
barVal = ((Number) item.getXValue()).doubleValue(); | |
currentVal = ((Number) getCurrentDisplayedXValue(item)).doubleValue(); | |
} | |
if (currentVal > 0 && barVal < 0) { // going from positive to negative | |
// add style class negative | |
item.getNode().getStyleClass().add("negative"); | |
} else if (currentVal < 0 && barVal > 0) { // going from negative to positive | |
// remove style class negative | |
item.getNode().getStyleClass().remove("negative"); | |
} | |
} | |
private void animateDataAdd(Data<X, Y> item, Node bar) { | |
double barVal; | |
if (orientation == Orientation.VERTICAL) { | |
barVal = ((Number) item.getYValue()).doubleValue(); | |
if (barVal < 0) { | |
bar.getStyleClass().add("negative"); | |
} | |
item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition())); | |
setCurrentDisplayedYValue(item, getYAxis().toRealValue(getYAxis().getZeroPosition())); | |
getPlotChildren().add(bar); | |
item.setYValue(getYAxis().toRealValue(barVal)); | |
animate( | |
TimelineBuilder.create().keyFrames( | |
new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))), | |
new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH))).build()); | |
} else { | |
barVal = ((Number) item.getXValue()).doubleValue(); | |
if (barVal < 0) { | |
bar.getStyleClass().add("negative"); | |
} | |
item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition())); | |
setCurrentDisplayedXValue(item, getXAxis().toRealValue(getXAxis().getZeroPosition())); | |
getPlotChildren().add(bar); | |
item.setXValue(getXAxis().toRealValue(barVal)); | |
animate( | |
TimelineBuilder.create().keyFrames( | |
new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))), | |
new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH))).build()); | |
} | |
} | |
/** | |
* @inheritDoc | |
*/ | |
@Override | |
protected void seriesAdded(Series<X, Y> series, int seriesIndex) { | |
String defaultColorStyleClass = "default-color" + (seriesDefaultColorIndex % 8); | |
seriesDefaultColorMap.put(series, defaultColorStyleClass); | |
seriesDefaultColorIndex++; | |
// handle any data already in series | |
// create entry in the map | |
Map<String, List<Data<X, Y>>> categoryMap = new HashMap<String, List<Data<X, Y>>>(); | |
for (int j = 0; j < series.getData().size(); j++) { | |
Data<X, Y> item = series.getData().get(j); | |
Node bar = createBar(series, seriesIndex, item, j); | |
String category; | |
if (orientation == Orientation.VERTICAL) { | |
category = (String) item.getXValue(); | |
} else { | |
category = (String) item.getYValue(); | |
} | |
// list of two item positive and negative | |
List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>(); | |
itemList.add(item); | |
categoryMap.put(category, itemList); | |
if (shouldAnimate()) { | |
animateDataAdd(item, bar); | |
} else { | |
double barVal = (orientation == Orientation.VERTICAL) ? ((Number) item.getYValue()).doubleValue() | |
: ((Number) item.getXValue()).doubleValue(); | |
if (barVal < 0) { | |
bar.getStyleClass().add("negative"); | |
} | |
getPlotChildren().add(bar); | |
} | |
} | |
if (categoryMap.size() > 0) { | |
seriesCategoryMap.put(series, categoryMap); | |
} | |
} | |
private Timeline createDataRemoveTimeline(Data<X, Y> item, final Node bar, final Series<X, Y> series) { | |
Timeline t = new Timeline(); | |
if (orientation == Orientation.VERTICAL) { | |
item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition())); | |
t.getKeyFrames().addAll( | |
new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))), | |
new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() { | |
@Override | |
public void handle(ActionEvent actionEvent) { | |
getPlotChildren().remove(bar); | |
} | |
}, | |
new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH))); | |
} else { | |
item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition())); | |
t.getKeyFrames().addAll( | |
new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))), | |
new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() { | |
@Override | |
public void handle(ActionEvent actionEvent) { | |
getPlotChildren().remove(bar); | |
} | |
}, | |
new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH))); | |
} | |
return t; | |
} | |
@Override | |
protected void seriesRemoved(final Series<X, Y> series) { | |
// remove all symbol nodes | |
if (shouldAnimate()) { | |
ParallelTransition pt = new ParallelTransition(); | |
pt.setOnFinished(new EventHandler<ActionEvent>() { | |
public void handle(ActionEvent event) { | |
removeSeriesFromDisplay(series); | |
} | |
}); | |
for (Data<X, Y> d : series.getData()) { | |
final Node bar = d.getNode(); | |
// Animate series deletion | |
if (getSeriesSize() > 1) { | |
for (int j = 0; j < series.getData().size(); j++) { | |
Data<X, Y> item = series.getData().get(j); | |
Timeline t = createDataRemoveTimeline(item, bar, series); | |
pt.getChildren().add(t); | |
} | |
} else { | |
// fade out last series | |
FadeTransition ft = new FadeTransition(Duration.millis(700), bar); | |
ft.setFromValue(1); | |
ft.setToValue(0); | |
ft.setOnFinished(new EventHandler<ActionEvent>() { | |
@Override | |
public void handle(ActionEvent actionEvent) { | |
getPlotChildren().remove(bar); | |
} | |
}); | |
pt.getChildren().add(ft); | |
} | |
} | |
pt.play(); | |
} else { | |
for (Data<X, Y> d : series.getData()) { | |
final Node bar = d.getNode(); | |
getPlotChildren().remove(bar); | |
} | |
removeSeriesFromDisplay(series); | |
} | |
} | |
/** | |
* @inheritDoc | |
*/ | |
@Override | |
protected void updateAxisRange() { | |
// This override is necessary to update axis range based on cumulative Y value for the | |
// Y axis instead of the inherited way where the max value in the data range is used. | |
final Axis<X> xa = getXAxis(); | |
final Axis<Y> ya = getYAxis(); | |
if (xa.isAutoRanging()) { | |
List xData = new ArrayList<Number>(); | |
if (xa instanceof CategoryAxis) { | |
xData.addAll(categoryAxis.getCategories()); | |
} else { | |
int catIndex = 0; | |
for (String category : categoryAxis.getCategories()) { | |
int index = 0; | |
double totalXN = 0; | |
double totalXP = 0; | |
Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); | |
while (seriesIterator.hasNext()) { | |
Series<X, Y> series = seriesIterator.next(); | |
for (final Data<X, Y> item : getDataItem(series, index, catIndex, category)) {; | |
if (item != null) { | |
boolean isNegative = item.getNode().getStyleClass().contains("negative"); | |
if (!isNegative) { | |
totalXP += xa.toNumericValue(item.getXValue()); | |
} else { | |
totalXN += xa.toNumericValue(item.getXValue()); | |
} | |
} | |
} | |
} | |
xData.add(totalXP); | |
xData.add(totalXN); | |
catIndex++; | |
} | |
} | |
xa.invalidateRange(xData); | |
} | |
if (ya.isAutoRanging()) { | |
List yData = new ArrayList<Number>(); | |
if (ya instanceof CategoryAxis) { | |
yData.addAll(categoryAxis.getCategories()); | |
} else { | |
int catIndex = 0; | |
for (String category : categoryAxis.getCategories()) { | |
int index = 0; | |
double totalYP = 0; | |
double totalYN = 0; | |
Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); | |
while (seriesIterator.hasNext()) { | |
Series<X, Y> series = seriesIterator.next(); | |
for (final Data<X, Y> item : getDataItem(series, index, catIndex, category)) {; | |
if (item != null) { | |
boolean isNegative = item.getNode().getStyleClass().contains("negative"); | |
if (!isNegative) { | |
totalYP += ya.toNumericValue(item.getYValue()); | |
} else { | |
totalYN += ya.toNumericValue(item.getYValue()); | |
} | |
} | |
} | |
} | |
yData.add(totalYP); | |
yData.add(totalYN); | |
catIndex++; | |
} | |
} | |
ya.invalidateRange(yData); | |
} | |
} | |
/** | |
* @inheritDoc | |
*/ | |
@Override | |
protected void layoutPlotChildren() { | |
double catSpace = categoryAxis.getCategorySpacing(); | |
// calculate bar spacing | |
final double availableBarSpace = catSpace - getCategoryGap(); | |
final double barWidth = availableBarSpace; | |
final double barOffset = -((catSpace - getCategoryGap()) / 2); | |
final double zeroPos = valueAxis.getZeroPosition(); | |
// update bar positions and sizes | |
int catIndex = 0; | |
for (String category : categoryAxis.getCategories()) { | |
int index = 0; | |
int currentPositiveHeight = 0; | |
int currentNegativeHeight = 0; | |
Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); | |
while (seriesIterator.hasNext()) { | |
Series<X, Y> series = seriesIterator.next(); | |
for (final Data<X, Y> item : getDataItem(series, index, catIndex, category)) {; | |
if (item != null) { | |
final Node bar = item.getNode(); | |
final double categoryPos; | |
final double valPos; | |
if (orientation == Orientation.VERTICAL) { | |
categoryPos = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); | |
valPos = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); | |
} else { | |
categoryPos = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item)); | |
valPos = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item)); | |
} | |
final double bottom; | |
final double top; | |
boolean isNegative = bar.getStyleClass().contains("negative"); | |
if (!isNegative) { | |
bottom = currentPositiveHeight + Math.min(valPos, zeroPos); | |
top = currentPositiveHeight + Math.max(valPos, zeroPos); | |
if (orientation == Orientation.VERTICAL) { | |
currentPositiveHeight -= top - bottom; | |
} else { | |
currentPositiveHeight += top - bottom; | |
} | |
} else { | |
bottom = currentNegativeHeight + Math.min(valPos, zeroPos); | |
top = currentNegativeHeight + Math.max(valPos, zeroPos); | |
if (orientation == Orientation.VERTICAL) { | |
currentNegativeHeight += top - bottom; | |
} else { | |
currentNegativeHeight += top - bottom; | |
} | |
} | |
if (orientation == Orientation.VERTICAL) { | |
bar.resizeRelocate(categoryPos + barOffset, | |
bottom, barWidth, top - bottom); | |
} else { | |
//noinspection SuspiciousNameCombination | |
bar.resizeRelocate(bottom, | |
categoryPos + barOffset, | |
top - bottom, barWidth); | |
} | |
index++; | |
} | |
} | |
} | |
catIndex++; | |
} | |
} | |
/** | |
* Computes the size of series linked list | |
* | |
* @return size of series linked list | |
*/ | |
int getSeriesSize() { | |
int count = 0; | |
Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); | |
while (seriesIterator.hasNext()) { | |
seriesIterator.next(); | |
count++; | |
} | |
return count; | |
} | |
/** | |
* This is called whenever a series is added or removed and the legend needs | |
* to be updated | |
*/ | |
@Override | |
protected void updateLegend() { | |
legend.getItems().clear(); | |
if (getData() != null) { | |
for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { | |
Series series = getData().get(seriesIndex); | |
Legend.LegendItem legenditem = new Legend.LegendItem(series.getName()); | |
String defaultColorStyleClass = seriesDefaultColorMap.get(series); | |
legenditem.getSymbol().getStyleClass().addAll("chart-bar", "series" + seriesIndex, "bar-legend-symbol", | |
defaultColorStyleClass); | |
legend.getItems().add(legenditem); | |
} | |
} | |
if (legend.getItems().size() > 0) { | |
if (getLegend() == null) { | |
setLegend(legend); | |
} | |
} else { | |
setLegend(null); | |
} | |
} | |
private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) { | |
Node bar = item.getNode(); | |
if (bar == null) { | |
bar = new StackPane(); | |
item.setNode(bar); | |
} | |
String defaultColorStyleClass = seriesDefaultColorMap.get(series); | |
bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, defaultColorStyleClass); | |
return bar; | |
} | |
private List<Data<X, Y>> getDataItem(Series<X, Y> series, int seriesIndex, int itemIndex, String category) { | |
Map<String, List<Data<X, Y>>> catmap = seriesCategoryMap.get(series); | |
return catmap != null ? catmap.get(category) != null ? catmap.get(category) : new ArrayList<Data<X, Y>>() : new ArrayList<Data<X, Y>>(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment