Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.chart; 027 028import java.util.ArrayList; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032 033import javafx.animation.FadeTransition; 034import javafx.animation.Interpolator; 035import javafx.animation.KeyFrame; 036import javafx.animation.KeyValue; 037import javafx.animation.Timeline; 038import javafx.animation.Animation; 039import javafx.beans.property.BooleanProperty; 040import javafx.beans.property.DoubleProperty; 041import javafx.beans.property.SimpleDoubleProperty; 042import javafx.collections.FXCollections; 043import javafx.collections.ListChangeListener; 044import javafx.collections.ObservableList; 045import javafx.event.ActionEvent; 046import javafx.event.EventHandler; 047import javafx.scene.Node; 048import javafx.scene.layout.StackPane; 049import javafx.scene.shape.LineTo; 050import javafx.scene.shape.MoveTo; 051import javafx.scene.shape.Path; 052import javafx.scene.shape.StrokeLineJoin; 053import javafx.util.Duration; 054 055import com.sun.javafx.charts.Legend; 056import com.sun.javafx.charts.Legend.LegendItem; 057import javafx.css.StyleableBooleanProperty; 058import javafx.css.CssMetaData; 059import com.sun.javafx.css.converters.BooleanConverter; 060import java.util.*; 061import javafx.css.Styleable; 062import javafx.css.StyleableProperty; 063 064/** 065 * Line Chart plots a line connecting the data points in a series. The data points 066 * themselves can be represented by symbols optionally. Line charts are usually used 067 * to view data trends over time or category. 068 */ 069public class LineChart<X,Y> extends XYChart<X,Y> { 070 071 // -------------- PRIVATE FIELDS ------------------------------------------ 072 073 /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */ 074 private Map<Series, DoubleProperty> seriesYMultiplierMap = new HashMap<Series, DoubleProperty>(); 075 private Legend legend = new Legend(); 076 private Timeline dataRemoveTimeline; 077 private Series<X,Y> seriesOfDataRemoved = null; 078 private Data<X,Y> dataItemBeingRemoved = null; 079 080 // -------------- PUBLIC PROPERTIES ---------------------------------------- 081 082 /** When true, CSS styleable symbols are created for any data items that don't have a symbol node specified. */ 083 private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { 084 @Override protected void invalidated() { 085 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex ++) { 086 Series<X,Y> series = getData().get(seriesIndex); 087 for (int itemIndex=0; itemIndex < series.getData().size(); itemIndex ++) { 088 Data<X,Y> item = series.getData().get(itemIndex); 089 Node symbol = item.getNode(); 090 if(get() && symbol == null) { // create any symbols 091 symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 092 getPlotChildren().add(symbol); 093 } else if (!get() && symbol != null) { // remove symbols 094 getPlotChildren().remove(symbol); 095 symbol = null; 096 item.setNode(null); 097 } 098 } 099 } 100 requestChartLayout(); 101 } 102 103 public Object getBean() { 104 return LineChart.this; 105 } 106 107 public String getName() { 108 return "createSymbols"; 109 } 110 111 public CssMetaData<LineChart<?,?>,Boolean> getCssMetaData() { 112 return StyleableProperties.CREATE_SYMBOLS; 113 } 114 }; 115 116 /** 117 * Indicates whether symbols for data points will be created or not. 118 * 119 * @return true if symbols for data points will be created and false otherwise. 120 */ 121 public final boolean getCreateSymbols() { return createSymbols.getValue(); } 122 public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } 123 public final BooleanProperty createSymbolsProperty() { return createSymbols; } 124 125 // -------------- CONSTRUCTORS ---------------------------------------------- 126 127 /** 128 * Construct a new LineChart with the given axis. 129 * 130 * @param xAxis The x axis to use 131 * @param yAxis The y axis to use 132 */ 133 public LineChart(Axis<X> xAxis, Axis<Y> yAxis) { 134 this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList()); 135 } 136 137 /** 138 * Construct a new LineChart with the given axis and data. 139 * 140 * @param xAxis The x axis to use 141 * @param yAxis The y axis to use 142 * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart 143 */ 144 public LineChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data) { 145 super(xAxis,yAxis); 146 setLegend(legend); 147 setData(data); 148 } 149 150 // -------------- METHODS ------------------------------------------------------------------------------------------ 151 152 @Override protected void dataItemAdded(final Series<X,Y> series, int itemIndex, final Data<X,Y> item) { 153 final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 154 if (shouldAnimate()) { 155 if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) { 156 if (seriesOfDataRemoved == series) { 157 dataRemoveTimeline.stop(); 158 dataRemoveTimeline = null; 159 getPlotChildren().remove(dataItemBeingRemoved.getNode()); 160 removeDataItemFromDisplay(seriesOfDataRemoved, dataItemBeingRemoved); 161 seriesOfDataRemoved = null; 162 dataItemBeingRemoved = null; 163 } 164 } 165 boolean animate = false; 166 if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { 167 animate = true; 168 Data<X,Y> p1 = series.getData().get(itemIndex - 1); 169 Data<X,Y> p2 = series.getData().get(itemIndex + 1); 170 if (p1 != null && p2 != null) { 171 double x1 = getXAxis().toNumericValue(p1.getXValue()); 172 double y1 = getYAxis().toNumericValue(p1.getYValue()); 173 double x3 = getXAxis().toNumericValue(p2.getXValue()); 174 double y3 = getYAxis().toNumericValue(p2.getYValue()); 175 176 double x2 = getXAxis().toNumericValue(item.getXValue()); 177 //double y2 = getYAxis().toNumericValue(item.getYValue()); 178 if (x2 > x1 && x2 < x3) { 179 //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 180 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 181 item.setCurrentY(getYAxis().toRealValue(y)); 182 item.setCurrentX(getXAxis().toRealValue(x2)); 183 } else { 184 //2. we can simply use the midpoint on the line as well.. 185 double x = (x3 + x1)/2; 186 double y = (y3 + y1)/2; 187 item.setCurrentX(getXAxis().toRealValue(x)); 188 item.setCurrentY(getYAxis().toRealValue(y)); 189 } 190 } 191 } else if (itemIndex == 0 && series.getData().size() > 1) { 192 animate = true; 193 item.setCurrentX(series.getData().get(1).getXValue()); 194 item.setCurrentY(series.getData().get(1).getYValue()); 195 } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { 196 animate = true; 197 int last = series.getData().size() - 2; 198 item.setCurrentX(series.getData().get(last).getXValue()); 199 item.setCurrentY(series.getData().get(last).getYValue()); 200 } else if(symbol != null) { 201 // fade in new symbol 202 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 203 ft.setToValue(1); 204 ft.play(); 205 } 206 if(symbol != null) { 207 getPlotChildren().add(symbol); 208 } 209 if (animate) { 210 animate( 211 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 212 item.getCurrentY()), 213 new KeyValue(item.currentXProperty(), 214 item.getCurrentX())), 215 new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), 216 item.getYValue(), Interpolator.EASE_BOTH), 217 new KeyValue(item.currentXProperty(), 218 item.getXValue(), Interpolator.EASE_BOTH)) 219 ); 220 } 221 222 } else { 223 if (symbol != null) getPlotChildren().add(symbol); 224 } 225 } 226 227 @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { 228 final Node symbol = item.getNode(); 229 // remove item from sorted list 230 int itemIndex = series.getItemIndex(item); 231 if (shouldAnimate()) { 232 boolean animate = false; 233 if (itemIndex > 0 && itemIndex < series.getDataSize()) { 234 animate = true; 235 int index=0; Data<X,Y> d; 236 for (d = series.begin; d != null && index != itemIndex - 1; d=d.next) index++; 237 Data<X,Y> p1 = d; 238 Data<X,Y> p2 = (d.next).next; 239 if (p1 != null && p2 != null) { 240 double x1 = getXAxis().toNumericValue(p1.getXValue()); 241 double y1 = getYAxis().toNumericValue(p1.getYValue()); 242 double x3 = getXAxis().toNumericValue(p2.getXValue()); 243 double y3 = getYAxis().toNumericValue(p2.getYValue()); 244 245 double x2 = getXAxis().toNumericValue(item.getXValue()); 246 double y2 = getYAxis().toNumericValue(item.getYValue()); 247 if (x2 > x1 && x2 < x3) { 248 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 249 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 250 item.setCurrentX(getXAxis().toRealValue(x2)); 251 item.setCurrentY(getYAxis().toRealValue(y2)); 252 item.setXValue(getXAxis().toRealValue(x2)); 253 item.setYValue(getYAxis().toRealValue(y)); 254 } else { 255 //2. we can simply use the midpoint on the line as well.. 256 double x = (x3 + x1)/2; 257 double y = (y3 + y1)/2; 258 item.setCurrentX(getXAxis().toRealValue(x)); 259 item.setCurrentY(getYAxis().toRealValue(y)); 260 } 261 } 262 } else if (itemIndex == 0 && series.getDataSize() > 1) { 263 animate = true; 264 Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series); 265 if (iter.hasNext()) { // get first data value 266 Data<X,Y> d = iter.next(); 267 item.setXValue(d.getXValue()); 268 item.setYValue(d.getYValue()); 269 } 270 } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) { 271 animate = true; 272 int last = series.getData().size() - 1; 273 item.setXValue(series.getData().get(last).getXValue()); 274 item.setYValue(series.getData().get(last).getYValue()); 275 } else { 276 // fade out symbol 277 if (symbol != null) { 278 symbol.setOpacity(0); 279 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 280 ft.setToValue(0); 281 ft.setOnFinished(new EventHandler<ActionEvent>() { 282 @Override public void handle(ActionEvent actionEvent) { 283 item.setSeries(null); 284 getPlotChildren().remove(symbol); 285 removeDataItemFromDisplay(series, item); 286 } 287 }); 288 ft.play(); 289 } 290 } 291 if (animate) { 292 dataRemoveTimeline = createDataRemoveTimeline(item, symbol, series); 293 seriesOfDataRemoved = series; 294 dataItemBeingRemoved = item; 295 dataRemoveTimeline.play(); 296 } 297 } else { 298 item.setSeries(null); 299 if (symbol != null) getPlotChildren().remove(symbol); 300 removeDataItemFromDisplay(series, item); 301 } 302 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 303 } 304 305 /** @inheritDoc */ 306 @Override protected void dataItemChanged(Data<X, Y> item) { 307 } 308 309 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 310 // Update style classes for all series lines and symbols 311 // Note: is there a more efficient way of doing this? 312 for (int i = 0; i < getDataSize(); i++) { 313 final Series<X,Y> s = getData().get(i); 314 Node seriesNode = s.getNode(); 315 if(seriesNode != null) seriesNode.getStyleClass().setAll("chart-series-line", "series" + i, s.defaultColorStyleClass); 316 } 317 } 318 319 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 320 // create new path for series 321 Path seriesLine = new Path(); 322 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 323 series.setNode(seriesLine); 324 // create series Y multiplier 325 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 326 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 327 // handle any data already in series 328 if (shouldAnimate()) { 329 seriesLine.setOpacity(0); 330 seriesYAnimMultiplier.setValue(0d); 331 } else { 332 seriesYAnimMultiplier.setValue(1d); 333 } 334 getPlotChildren().add(seriesLine); 335 336 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 337 if (shouldAnimate()) { 338 // animate in new series 339 keyFrames.add(new KeyFrame(Duration.ZERO, 340 new KeyValue(seriesLine.opacityProperty(), 0), 341 new KeyValue(seriesYAnimMultiplier, 0) 342 )); 343 keyFrames.add(new KeyFrame(Duration.millis(200), 344 new KeyValue(seriesLine.opacityProperty(), 1) 345 )); 346 keyFrames.add(new KeyFrame(Duration.millis(500), 347 new KeyValue(seriesYAnimMultiplier, 1) 348 )); 349 } 350 for (int j=0; j<series.getData().size(); j++) { 351 Data item = series.getData().get(j); 352 final Node symbol = createSymbol(series, seriesIndex, item, j); 353 if(symbol != null) { 354 if (shouldAnimate()) symbol.setOpacity(0); 355 getPlotChildren().add(symbol); 356 if (shouldAnimate()) { 357 // fade in new symbol 358 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 359 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 360 } 361 } 362 } 363 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 364 } 365 private void updateDefaultColorIndex(final Series<X,Y> series) { 366 int clearIndex = seriesColorMap.get(series); 367 colorBits.clear(clearIndex); 368 series.getNode().getStyleClass().remove(DEFAULT_COLOR+clearIndex); 369 for (int j=0; j < series.getData().size(); j++) { 370 final Node node = series.getData().get(j).getNode(); 371 if(node!=null) { 372 node.getStyleClass().remove(DEFAULT_COLOR+clearIndex); 373 } 374 } 375 seriesColorMap.remove(series); 376 } 377 378 @Override protected void seriesRemoved(final Series<X,Y> series) { 379 updateDefaultColorIndex(series); 380 // remove all symbol nodes 381 seriesYMultiplierMap.remove(series); 382 if (shouldAnimate()) { 383 // create list of all nodes we need to fade out 384 final List<Node> nodes = new ArrayList<Node>(); 385 nodes.add(series.getNode()); 386 if (getCreateSymbols()) { // RT-22124 387 // done need to fade the symbols if createSymbols is false 388 for (Data d: series.getData()) nodes.add(d.getNode()); 389 } 390 // fade out old and symbols 391 KeyValue[] startValues = new KeyValue[nodes.size()]; 392 KeyValue[] endValues = new KeyValue[nodes.size()]; 393 for (int j=0; j < nodes.size(); j++) { 394 startValues[j] = new KeyValue(nodes.get(j).opacityProperty(),1); 395 endValues[j] = new KeyValue(nodes.get(j).opacityProperty(),0); 396 } 397 Timeline tl = new Timeline(); 398 tl.getKeyFrames().addAll( 399 new KeyFrame(Duration.ZERO,startValues), 400 new KeyFrame(Duration.millis(900), new EventHandler<ActionEvent>() { 401 @Override public void handle(ActionEvent actionEvent) { 402 getPlotChildren().removeAll(nodes); 403 removeSeriesFromDisplay(series); 404 } 405 },endValues) 406 ); 407 tl.play(); 408 } else { 409 getPlotChildren().remove(series.getNode()); 410 for (Data d:series.getData()) getPlotChildren().remove(d.getNode()); 411 removeSeriesFromDisplay(series); 412 } 413 } 414 415 /** @inheritDoc */ 416 @Override protected void layoutPlotChildren() { 417 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { 418 Series<X,Y> series = getData().get(seriesIndex); 419 final DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 420 boolean isFirst = true; 421 if(series.getNode() instanceof Path) { 422 Path seriesLine = (Path)series.getNode(); 423 seriesLine.getElements().clear(); 424 for (Data<X,Y> item = series.begin; item != null; item = item.next) { 425 double x = getXAxis().getDisplayPosition(item.getCurrentX()); 426 double y = getYAxis().getDisplayPosition( 427 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 428 if (isFirst) { 429 isFirst = false; 430 seriesLine.getElements().add(new MoveTo(x, y)); 431 } else { 432 seriesLine.getElements().add(new LineTo(x, y)); 433 } 434 Node symbol = item.getNode(); 435 if (symbol != null) { 436 final double w = symbol.prefWidth(-1); 437 final double h = symbol.prefHeight(-1); 438 symbol.resizeRelocate(x-(w/2), y-(h/2),w,h); 439 } 440 } 441 } 442 } 443 } 444 445 private Timeline createDataRemoveTimeline(final Data<X,Y> item, final Node symbol, final Series<X,Y> series) { 446 Timeline t = new Timeline(); 447 448 t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 449 item.getCurrentY()), new KeyValue(item.currentXProperty(), 450 item.getCurrentX())), 451 new KeyFrame(Duration.millis(500), new EventHandler<ActionEvent>() { 452 @Override public void handle(ActionEvent actionEvent) { 453 if (symbol != null) getPlotChildren().remove(symbol); 454 removeDataItemFromDisplay(series, item); 455 } 456 }, 457 new KeyValue(item.currentYProperty(), 458 item.getYValue(), Interpolator.EASE_BOTH), 459 new KeyValue(item.currentXProperty(), 460 item.getXValue(), Interpolator.EASE_BOTH)) 461 ); 462 return t; 463 } 464 465 private Node createSymbol(Series<X, Y> series, int seriesIndex, final Data item, int itemIndex) { 466 Node symbol = item.getNode(); 467 // check if symbol has already been created 468 if (symbol == null && getCreateSymbols()) { 469 symbol = new StackPane(); 470 item.setNode(symbol); 471 } 472 // set symbol styles 473 if (symbol != null) symbol.getStyleClass().addAll("chart-line-symbol", "series" + seriesIndex, 474 "data" + itemIndex, series.defaultColorStyleClass); 475 return symbol; 476 } 477 478 /** 479 * This is called whenever a series is added or removed and the legend needs to be updated 480 */ 481 @Override protected void updateLegend() { 482 legend.getItems().clear(); 483 if (getData() != null) { 484 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 485 Series<X,Y> series = getData().get(seriesIndex); 486 LegendItem legenditem = new LegendItem(series.getName()); 487 legenditem.getSymbol().getStyleClass().addAll("chart-line-symbol", "series"+seriesIndex, series.defaultColorStyleClass); 488 legend.getItems().add(legenditem); 489 } 490 } 491 if (legend.getItems().size() > 0) { 492 if (getLegend() == null) { 493 setLegend(legend); 494 } 495 } else { 496 setLegend(null); 497 } 498 } 499 500 // -------------- STYLESHEET HANDLING -------------------------------------- 501 502 private static class StyleableProperties { 503 private static final CssMetaData<LineChart<?,?>,Boolean> CREATE_SYMBOLS = 504 new CssMetaData<LineChart<?,?>,Boolean>("-fx-create-symbols", 505 BooleanConverter.getInstance(), Boolean.TRUE) { 506 507 @Override 508 public boolean isSettable(LineChart node) { 509 return node.createSymbols == null || !node.createSymbols.isBound(); 510 } 511 512 @Override 513 public StyleableProperty<Boolean> getStyleableProperty(LineChart node) { 514 return (StyleableProperty<Boolean>)node.createSymbolsProperty(); 515 } 516 }; 517 518 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 519 static { 520 final List<CssMetaData<? extends Styleable, ?>> styleables = 521 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 522 styleables.add(CREATE_SYMBOLS); 523 STYLEABLES = Collections.unmodifiableList(styleables); 524 } 525 } 526 527 /** 528 * @return The CssMetaData associated with this class, which may include the 529 * CssMetaData of its super classes. 530 */ 531 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 532 return StyleableProperties.STYLEABLES; 533 } 534 535 /** 536 * {@inheritDoc} 537 */ 538 @Override 539 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 540 return getClassCssMetaData(); 541 } 542 543}