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 com.sun.javafx.collections.NonIterableChange; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.Iterator; 032import java.util.List; 033 034import javafx.animation.Interpolator; 035import javafx.animation.KeyFrame; 036import javafx.animation.KeyValue; 037import javafx.beans.property.BooleanProperty; 038import javafx.beans.property.ObjectProperty; 039import javafx.beans.property.ObjectPropertyBase; 040import javafx.beans.property.ReadOnlyObjectProperty; 041import javafx.beans.property.ReadOnlyObjectWrapper; 042import javafx.beans.property.SimpleObjectProperty; 043import javafx.beans.property.StringProperty; 044import javafx.beans.property.StringPropertyBase; 045import javafx.beans.value.ChangeListener; 046import javafx.beans.value.ObservableValue; 047import javafx.collections.FXCollections; 048import javafx.collections.ListChangeListener; 049import javafx.collections.ListChangeListener.Change; 050import javafx.collections.ObservableList; 051import javafx.geometry.Side; 052import javafx.scene.Group; 053import javafx.scene.Node; 054import javafx.scene.layout.Region; 055import javafx.scene.shape.ClosePath; 056import javafx.scene.shape.Line; 057import javafx.scene.shape.LineTo; 058import javafx.scene.shape.MoveTo; 059import javafx.scene.shape.Path; 060import javafx.scene.shape.Rectangle; 061import javafx.util.Duration; 062import java.util.BitSet; 063import javafx.css.StyleableBooleanProperty; 064import javafx.css.CssMetaData; 065import com.sun.javafx.css.converters.BooleanConverter; 066import java.util.HashMap; 067import java.util.Map; 068import javafx.css.Styleable; 069import javafx.css.StyleableProperty; 070 071/** 072 * Chart base class for all 2 axis charts. It is responsible for drawing the two 073 * axes and the plot content. It contains a list of all content in the plot and 074 * implementations of XYChart can add nodes to this list that need to be rendered. 075 * 076 * <p>It is possible to install Tooltips on data items / symbols. 077 * For example the following code snippet installs Tooltip on the 1st data item. 078 * 079 * <pre><code> 080 * XYChart.Data item = ( XYChart.Data)series.getData().get(0); 081 * Tooltip.install(item.getNode(), new Tooltip("Symbol-0")); 082 * </code></pre> 083 * 084 */ 085public abstract class XYChart<X,Y> extends Chart { 086 087 // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- 088 089 // to indicate which colors are being used for the series 090 BitSet colorBits = new BitSet(8); 091 static String DEFAULT_COLOR = "default-color"; 092 Map<Series, Integer> seriesColorMap = new HashMap<Series, Integer>(); 093 private boolean rangeValid = false; 094 private final Line verticalZeroLine = new Line(); 095 private final Line horizontalZeroLine = new Line(); 096 private final Path verticalGridLines = new Path(); 097 private final Path horizontalGridLines = new Path(); 098 private final Path horizontalRowFill = new Path(); 099 private final Path verticalRowFill = new Path(); 100 private final Region plotBackground = new Region(); 101 private final Group plotArea = new Group(){ 102 @Override public void requestLayout() {} // suppress layout requests 103 }; 104 private final Group plotContent = new Group(); 105 private final Rectangle plotAreaClip = new Rectangle(); 106 /* start pointer of a series linked list. */ 107 Series<X,Y> begin = null; 108 /** This is called when a series is added or removed from the chart */ 109 private final ListChangeListener<Series<X,Y>> seriesChanged = new ListChangeListener<Series<X,Y>>() { 110 @Override public void onChanged(Change<? extends Series<X,Y>> c) { 111 while (c.next()) { 112 if (c.getRemoved().size() > 0) updateLegend(); 113 for (Series<X,Y> series : c.getRemoved()) { 114 series.setChart(null); 115 seriesRemoved(series); 116// seriesDefaultColorIndex --; 117 } 118 for(int i=c.getFrom(); i<c.getTo() && !c.wasPermutated(); i++) { 119 final Series<X,Y> series = c.getList().get(i); 120 // add new listener to data 121 series.setChart(XYChart.this); 122 // update linkedList Pointers for series 123 if (XYChart.this.begin == null) { 124 XYChart.this.begin = getData().get(i); 125 XYChart.this.begin.next = null; 126 } else { 127 if (i == 0) { 128 getData().get(0).next = XYChart.this.begin; 129 begin = getData().get(0); 130 } else { 131 Series ptr = begin; 132 for (int j = 0; j < i -1 && ptr!=null ; j++) { 133 ptr = ptr.next; 134 } 135 if (ptr != null) { 136 getData().get(i).next = ptr.next; 137 ptr.next = getData().get(i); 138 } 139 140 } 141 } 142 // update default color style class 143 int nextClearBit = colorBits.nextClearBit(0); 144 colorBits.set(nextClearBit, true); 145 series.defaultColorStyleClass = DEFAULT_COLOR+(nextClearBit%8); 146 seriesColorMap.put(series, nextClearBit%8); 147 // inform sub-classes of series added 148 seriesAdded(series, i); 149 } 150 if (c.getFrom() < c.getTo()) updateLegend(); 151 seriesChanged(c); 152 // RT-12069, linked list pointers should update when list is permutated. 153 if (c.wasPermutated() && getData().size() > 0) { 154 XYChart.this.begin = getData().get(0); 155 Series<X,Y> ptr = begin; 156 for(int k = 1; k < getData().size() && ptr != null; k++) { 157 ptr.next = getData().get(k); 158 ptr = ptr.next; 159 } 160 ptr.next = null; 161 } 162 } 163 // update axis ranges 164 invalidateRange(); 165 // lay everything out 166 requestChartLayout(); 167 } 168 }; 169 170 // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- 171 172 private final Axis<X> xAxis; 173 /** Get the X axis, by default it is along the bottom of the plot */ 174 public Axis<X> getXAxis() { return xAxis; } 175 176 private final Axis<Y> yAxis; 177 /** Get the Y axis, by default it is along the left of the plot */ 178 public Axis<Y> getYAxis() { return yAxis; } 179 180 /** XYCharts data */ 181 private ObjectProperty<ObservableList<Series<X,Y>>> data = new ObjectPropertyBase<ObservableList<Series<X,Y>>>() { 182 private ObservableList<Series<X,Y>> old; 183 @Override protected void invalidated() { 184 final ObservableList<Series<X,Y>> current = getValue(); 185 int saveAnimationState = -1; 186 // add remove listeners 187 if(old != null) { 188 old.removeListener(seriesChanged); 189 // Set animated to false so we don't animate both remove and add 190 // at the same time. RT-14163 191 // RT-21295 - disable animated only when current is also not null. 192 if (current != null && old.size() > 0) { 193 saveAnimationState = (old.get(0).getChart().getAnimated()) ? 1 : 2; 194 old.get(0).getChart().setAnimated(false); 195 } 196 } 197 if(current != null) current.addListener(seriesChanged); 198 // fire series change event if series are added or removed 199 if(old != null || current != null) { 200 final List<Series<X,Y>> removed = (old != null) ? old : Collections.<Series<X,Y>>emptyList(); 201 final int toIndex = (current != null) ? current.size() : 0; 202 // let series listener know all old series have been removed and new that have been added 203 if (toIndex > 0 || !removed.isEmpty()) { 204 seriesChanged.onChanged(new NonIterableChange<Series<X,Y>>(0, toIndex, current){ 205 @Override public List<Series<X,Y>> getRemoved() { return removed; } 206 @Override protected int[] getPermutation() { 207 return new int[0]; 208 } 209 }); 210 } 211 } else if (old != null && old.size() > 0) { 212 // let series listener know all old series have been removed 213 seriesChanged.onChanged(new NonIterableChange<Series<X,Y>>(0, 0, current){ 214 @Override public List<Series<X,Y>> getRemoved() { return old; } 215 @Override protected int[] getPermutation() { 216 return new int[0]; 217 } 218 }); 219 } 220 // restore animated on chart. 221 if (current != null && current.size() > 0 && saveAnimationState != -1) { 222 current.get(0).getChart().setAnimated((saveAnimationState == 1) ? true : false); 223 } 224 old = current; 225 } 226 227 public Object getBean() { 228 return XYChart.this; 229 } 230 231 public String getName() { 232 return "data"; 233 } 234 }; 235 public final ObservableList<Series<X,Y>> getData() { return data.getValue(); } 236 public final void setData(ObservableList<Series<X,Y>> value) { data.setValue(value); } 237 public final ObjectProperty<ObservableList<Series<X,Y>>> dataProperty() { return data; } 238 239 /** True if vertical grid lines should be drawn */ 240 private BooleanProperty verticalGridLinesVisible = new StyleableBooleanProperty(true) { 241 @Override protected void invalidated() { 242 requestChartLayout(); 243 } 244 245 @Override 246 public Object getBean() { 247 return XYChart.this; 248 } 249 250 @Override 251 public String getName() { 252 return "verticalGridLinesVisible"; 253 } 254 255 @Override 256 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 257 return StyleableProperties.VERTICAL_GRID_LINE_VISIBLE; 258 } 259 }; 260 /** 261 * Indicates whether vertical grid lines are visible or not. 262 * 263 * @return true if verticalGridLines are visible else false. 264 * @see #verticalGridLinesVisible 265 */ 266 public final boolean getVerticalGridLinesVisible() { return verticalGridLinesVisible.get(); } 267 public final void setVerticalGridLinesVisible(boolean value) { verticalGridLinesVisible.set(value); } 268 public final BooleanProperty verticalGridLinesVisibleProperty() { return verticalGridLinesVisible; } 269 270 /** True if horizontal grid lines should be drawn */ 271 private BooleanProperty horizontalGridLinesVisible = new StyleableBooleanProperty(true) { 272 @Override protected void invalidated() { 273 requestChartLayout(); 274 } 275 276 @Override 277 public Object getBean() { 278 return XYChart.this; 279 } 280 281 @Override 282 public String getName() { 283 return "horizontalGridLinesVisible"; 284 } 285 286 @Override 287 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 288 return StyleableProperties.HORIZONTAL_GRID_LINE_VISIBLE; 289 } 290 }; 291 public final boolean isHorizontalGridLinesVisible() { return horizontalGridLinesVisible.get(); } 292 public final void setHorizontalGridLinesVisible(boolean value) { horizontalGridLinesVisible.set(value); } 293 public final BooleanProperty horizontalGridLinesVisibleProperty() { return horizontalGridLinesVisible; } 294 295 /** If true then alternative vertical columns will have fills */ 296 private BooleanProperty alternativeColumnFillVisible = new StyleableBooleanProperty(false) { 297 @Override protected void invalidated() { 298 requestChartLayout(); 299 } 300 301 @Override 302 public Object getBean() { 303 return XYChart.this; 304 } 305 306 @Override 307 public String getName() { 308 return "alternativeColumnFillVisible"; 309 } 310 311 @Override 312 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 313 return StyleableProperties.ALTERNATIVE_COLUMN_FILL_VISIBLE; 314 } 315 }; 316 public final boolean isAlternativeColumnFillVisible() { return alternativeColumnFillVisible.getValue(); } 317 public final void setAlternativeColumnFillVisible(boolean value) { alternativeColumnFillVisible.setValue(value); } 318 public final BooleanProperty alternativeColumnFillVisibleProperty() { return alternativeColumnFillVisible; } 319 320 /** If true then alternative horizontal rows will have fills */ 321 private BooleanProperty alternativeRowFillVisible = new StyleableBooleanProperty(true) { 322 @Override protected void invalidated() { 323 requestChartLayout(); 324 } 325 326 @Override 327 public Object getBean() { 328 return XYChart.this; 329 } 330 331 @Override 332 public String getName() { 333 return "alternativeRowFillVisible"; 334 } 335 336 @Override 337 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 338 return StyleableProperties.ALTERNATIVE_ROW_FILL_VISIBLE; 339 } 340 }; 341 public final boolean isAlternativeRowFillVisible() { return alternativeRowFillVisible.getValue(); } 342 public final void setAlternativeRowFillVisible(boolean value) { alternativeRowFillVisible.setValue(value); } 343 public final BooleanProperty alternativeRowFillVisibleProperty() { return alternativeRowFillVisible; } 344 345 /** 346 * If this is true and the vertical axis has both positive and negative values then a additional axis line 347 * will be drawn at the zero point 348 * 349 * @defaultValue true 350 */ 351 private BooleanProperty verticalZeroLineVisible = new StyleableBooleanProperty(true) { 352 @Override protected void invalidated() { 353 requestChartLayout(); 354 } 355 356 @Override 357 public Object getBean() { 358 return XYChart.this; 359 } 360 361 @Override 362 public String getName() { 363 return "verticalZeroLineVisible"; 364 } 365 366 @Override 367 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 368 return StyleableProperties.VERTICAL_ZERO_LINE_VISIBLE; 369 } 370 }; 371 public final boolean isVerticalZeroLineVisible() { return verticalZeroLineVisible.get(); } 372 public final void setVerticalZeroLineVisible(boolean value) { verticalZeroLineVisible.set(value); } 373 public final BooleanProperty verticalZeroLineVisibleProperty() { return verticalZeroLineVisible; } 374 375 /** 376 * If this is true and the horizontal axis has both positive and negative values then a additional axis line 377 * will be drawn at the zero point 378 * 379 * @defaultValue true 380 */ 381 private BooleanProperty horizontalZeroLineVisible = new StyleableBooleanProperty(true) { 382 @Override protected void invalidated() { 383 requestChartLayout(); 384 } 385 386 @Override 387 public Object getBean() { 388 return XYChart.this; 389 } 390 391 @Override 392 public String getName() { 393 return "horizontalZeroLineVisible"; 394 } 395 396 @Override 397 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 398 return StyleableProperties.HORIZONTAL_ZERO_LINE_VISIBLE; 399 } 400 }; 401 public final boolean isHorizontalZeroLineVisible() { return horizontalZeroLineVisible.get(); } 402 public final void setHorizontalZeroLineVisible(boolean value) { horizontalZeroLineVisible.set(value); } 403 public final BooleanProperty horizontalZeroLineVisibleProperty() { return horizontalZeroLineVisible; } 404 405 // -------------- PROTECTED PROPERTIES ----------------------------------------------------------------------------- 406 407 /** 408 * Modifiable and observable list of all content in the plot. This is where implementations of XYChart should add 409 * any nodes they use to draw their plot. 410 * 411 * @return Observable list of plot children 412 */ 413 protected ObservableList<Node> getPlotChildren() { 414 return plotContent.getChildren(); 415 } 416 417 // -------------- CONSTRUCTOR -------------------------------------------------------------------------------------- 418 419 /** 420 * Constructs a XYChart given the two axes. The initial content for the chart 421 * plot background and plot area that includes vertical and horizontal grid 422 * lines and fills, are added. 423 * 424 * @param xAxis X Axis for this XY chart 425 * @param yAxis Y Axis for this XY chart 426 */ 427 public XYChart(Axis<X> xAxis, Axis<Y> yAxis) { 428 this.xAxis = xAxis; 429 if(xAxis.getSide() == null) xAxis.setSide(Side.BOTTOM); 430 this.yAxis = yAxis; 431 if(yAxis.getSide() == null) yAxis.setSide(Side.LEFT); 432 // RT-23123 autoranging leads to charts incorrect appearance. 433 xAxis.autoRangingProperty().addListener(new ChangeListener<Boolean>() { 434 public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) { 435 updateAxisRange(); 436 } 437 }); 438 yAxis.autoRangingProperty().addListener(new ChangeListener<Boolean>() { 439 public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) { 440 updateAxisRange(); 441 } 442 }); 443 // add initial content to chart content 444 getChartChildren().addAll(plotBackground,plotArea,xAxis,yAxis); 445 // We don't want plotArea or plotContent to autoSize or do layout 446 plotArea.setAutoSizeChildren(false); 447 plotContent.setAutoSizeChildren(false); 448 // setup clipping on plot area 449 plotAreaClip.setSmooth(false); 450 plotArea.setClip(plotAreaClip); 451 // add children to plot area 452 plotArea.getChildren().addAll( 453 verticalRowFill, horizontalRowFill, 454 verticalGridLines, horizontalGridLines, 455 verticalZeroLine, horizontalZeroLine, 456 plotContent); 457 // setup css style classes 458 plotContent.getStyleClass().setAll("plot-content"); 459 plotBackground.getStyleClass().setAll("chart-plot-background"); 460 verticalRowFill.getStyleClass().setAll("chart-alternative-column-fill"); 461 horizontalRowFill.getStyleClass().setAll("chart-alternative-row-fill"); 462 verticalGridLines.getStyleClass().setAll("chart-vertical-grid-lines"); 463 horizontalGridLines.getStyleClass().setAll("chart-horizontal-grid-lines"); 464 verticalZeroLine.getStyleClass().setAll("chart-vertical-zero-line"); 465 horizontalZeroLine.getStyleClass().setAll("chart-horizontal-zero-line"); 466 // mark plotContent as unmanaged as its preferred size changes do not effect our layout 467 plotContent.setManaged(false); 468 plotArea.setManaged(false); 469 // listen to animation on/off and sync to axis 470 animatedProperty().addListener(new ChangeListener<Boolean>() { 471 @Override public void changed(ObservableValue<? extends Boolean> valueModel, Boolean oldValue, Boolean newValue) { 472 if(getXAxis() != null) getXAxis().setAnimated(newValue); 473 if(getYAxis() != null) getYAxis().setAnimated(newValue); 474 } 475 }); 476 } 477 478 // -------------- METHODS ------------------------------------------------------------------------------------------ 479 480 @Override public void requestLayout() { 481 super.requestLayout(); 482 // RT-22726 Charts legend does not resize correctly 483 Node legend = getLegend(); 484 if (legend != null && legend instanceof Region) { 485 ((Region)legend).requestLayout(); 486 } 487 } 488 489 /** 490 * Gets the size of the data returning 0 if the data is null 491 * 492 * @return The number of items in data, or null if data is null 493 */ 494 final int getDataSize() { 495 final ObservableList<Series<X,Y>> data = getData(); 496 return (data!=null) ? data.size() : 0; 497 } 498 499 /** Called when a series's name has changed */ 500 private void seriesNameChanged() { 501 updateLegend(); 502 requestChartLayout(); 503 } 504 505 @SuppressWarnings({"UnusedParameters"}) 506 private void dataItemsChanged(Series<X,Y> series, List<Data<X,Y>> removed, int addedFrom, int addedTo, boolean permutation) { 507 for (Data<X,Y> item : removed) { 508 dataItemRemoved(item, series); 509 } 510 for(int i=addedFrom; i<addedTo; i++) { 511 Data<X,Y> item = series.getData().get(i); 512 dataItemAdded(series, i, item); 513 } 514 invalidateRange(); 515 requestChartLayout(); 516 } 517 518 private void dataXValueChanged(Data<X,Y> item) { 519 if(item.getCurrentX() != item.getXValue()) invalidateRange(); 520 dataItemChanged(item); 521 if (shouldAnimate()) { 522 animate( 523 new KeyFrame(Duration.ZERO, new KeyValue(item.currentXProperty(), item.getCurrentX())), 524 new KeyFrame(Duration.millis(700), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) 525 ); 526 } else { 527 item.setCurrentX(item.getXValue()); 528 requestChartLayout(); 529 } 530 } 531 532 private void dataYValueChanged(Data<X,Y> item) { 533 if(item.getCurrentY() != item.getYValue()) invalidateRange(); 534 dataItemChanged(item); 535 if (shouldAnimate()) { 536 animate( 537 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY())), 538 new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH)) 539 ); 540 } else { 541 item.setCurrentY(item.getYValue()); 542 requestChartLayout(); 543 } 544 } 545 546 private void dataExtraValueChanged(Data<X,Y> item) { 547 if(item.getCurrentY() != item.getYValue()) invalidateRange(); 548 dataItemChanged(item); 549 if (shouldAnimate()) { 550 animate( 551 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY())), 552 new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH)) 553 ); 554 } else { 555 item.setCurrentY(item.getYValue()); 556 requestChartLayout(); 557 } 558 } 559 560 /** 561 * This is called whenever a series is added or removed and the legend needs to be updated 562 */ 563 protected void updateLegend(){} 564 565 /** 566 * Called when a data item has been added to a series. This is where implementations of XYChart can create/add new 567 * nodes to getPlotChildren to represent this data item. They also may animate that data add with a fade in or 568 * similar if animated = true. 569 * 570 * @param series The series the data item was added to 571 * @param itemIndex The index of the new item within the series 572 * @param item The new data item that was added 573 */ 574 protected abstract void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item); 575 576 /** 577 * Called when a data item has been removed from data model but it is still visible on the chart. Its still visible 578 * so that you can handle animation for removing it in this method. After you are done animating the data item you 579 * must call removeDataItemFromDisplay() to remove the items node from being displayed on the chart. 580 * 581 * @param item The item that has been removed from the series 582 * @param series The series the item was removed from 583 */ 584 protected abstract void dataItemRemoved(Data<X, Y> item, Series<X, Y> series); 585 586 /** 587 * Called when a data item has changed, ie its xValue, yValue or extraValue has changed. 588 * 589 * @param item The data item who was changed 590 */ 591 protected abstract void dataItemChanged(Data<X, Y> item); 592 /** 593 * A series has been added to the charts data model. This is where implementations of XYChart can create/add new 594 * nodes to getPlotChildren to represent this series. Also you have to handle adding any data items that are 595 * already in the series. You may simply call dataItemAdded() for each one or provide some different animation for 596 * a whole series being added. 597 * 598 * @param series The series that has been added 599 * @param seriesIndex The index of the new series 600 */ 601 protected abstract void seriesAdded(Series<X, Y> series, int seriesIndex); 602 603 /** 604 * A series has been removed from the data model but it is still visible on the chart. Its still visible 605 * so that you can handle animation for removing it in this method. After you are done animating the data item you 606 * must call removeSeriesFromDisplay() to remove the series from the display list. 607 * 608 * @param series The series that has been removed 609 */ 610 protected abstract void seriesRemoved(Series<X,Y> series); 611 612 /** Called when each atomic change is made to the list of series for this chart */ 613 protected void seriesChanged(Change<? extends Series> c) {} 614 615 /** 616 * This is called when a data change has happened that may cause the range to be invalid. 617 */ 618 private void invalidateRange() { 619 rangeValid = false; 620 } 621 622 /** 623 * This is called when the range has been invalidated and we need to update it. If the axis are auto 624 * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the 625 * axis passing it that data. 626 */ 627 protected void updateAxisRange() { 628 final Axis<X> xa = getXAxis(); 629 final Axis<Y> ya = getYAxis(); 630 List<X> xData = null; 631 List<Y> yData = null; 632 if(xa.isAutoRanging()) xData = new ArrayList<X>(); 633 if(ya.isAutoRanging()) yData = new ArrayList<Y>(); 634 if(xData != null || yData != null) { 635 for(Series<X,Y> series : getData()) { 636 for(Data<X,Y> data: series.getData()) { 637 if(xData != null) xData.add(data.getXValue()); 638 if(yData != null) yData.add(data.getYValue()); 639 } 640 } 641 if(xData != null) xa.invalidateRange(xData); 642 if(yData != null) ya.invalidateRange(yData); 643 } 644 } 645 646 /** 647 * Called to update and layout the plot children. This should include all work to updates nodes representing 648 * 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 649 * can be got by getting the width of the x axis and its height from the height of the y axis. 650 */ 651 protected abstract void layoutPlotChildren(); 652 653 /** @inheritDoc */ 654 @Override protected final void layoutChartChildren(double top, double left, double width, double height) { 655 if(getData() == null) return; 656 if (!rangeValid) { 657 rangeValid = true; 658 if(getData() != null) updateAxisRange(); 659 } 660 // snap top and left to pixels 661 top = snapPosition(top); 662 left = snapPosition(left); 663 // get starting stuff 664 final Axis<X> xa = getXAxis(); 665 final ObservableList<Axis.TickMark<X>> xaTickMarks = xa.getTickMarks(); 666 final Axis<Y> ya = getYAxis(); 667 final ObservableList<Axis.TickMark<Y>> yaTickMarks = ya.getTickMarks(); 668 // check we have 2 axises and know their sides 669 if (xa == null || ya == null || xa.getSide() == null || ya.getSide() == null) return; 670 // try and work out width and height of axises 671 double xAxisWidth = 0; 672 double xAxisHeight = 30; // guess x axis height to start with 673 double yAxisWidth = 0; 674 double yAxisHeight = 0; 675 for (int count=0; count<5; count ++) { 676 yAxisHeight = height-xAxisHeight; 677 yAxisWidth = ya.prefWidth(yAxisHeight); 678 xAxisWidth = width - yAxisWidth; 679 double newXAxisHeight = xa.prefHeight(xAxisWidth); 680 if (newXAxisHeight == xAxisHeight) break; 681 xAxisHeight = newXAxisHeight; 682 } 683 // round axis sizes up to whole integers to snap to pixel 684 xAxisWidth = Math.ceil(xAxisWidth); 685 xAxisHeight = Math.ceil(xAxisHeight); 686 yAxisWidth = Math.ceil(yAxisWidth); 687 yAxisHeight = Math.ceil(yAxisHeight); 688 // calc xAxis height 689 double xAxisY = 0; 690 if (xa.getSide().equals(Side.TOP)) { 691 xa.setVisible(true); 692 xAxisY = top+1; 693 top += xAxisHeight; 694 } else if (xa.getSide().equals(Side.BOTTOM)) { 695 xa.setVisible(true); 696 xAxisY = top + yAxisHeight; 697 } else { 698 // X axis should never be left or right so hide 699 xa.setVisible(false); 700 xAxisHeight = 0; 701 } 702 // calc yAxis width 703 double yAxisX = 0; 704 if (ya.getSide().equals(Side.LEFT)) { 705 ya.setVisible(true); 706 yAxisX = left +1; 707 left += yAxisWidth; 708 } else if (ya.getSide().equals(Side.RIGHT)) { 709 ya.setVisible(true); 710 yAxisX = left + xAxisWidth; 711 } else { 712 // Y axis should never be top or bottom so hide 713 ya.setVisible(false); 714 yAxisWidth = 0; 715 } 716 // resize axises 717 xa.resizeRelocate(left, xAxisY, xAxisWidth, xAxisHeight); 718 ya.resizeRelocate(yAxisX, top, yAxisWidth, yAxisHeight); 719 // When the chart is resized, need to specifically call out the axises 720 // to lay out as they are unmanaged. 721 xa.requestAxisLayout(); 722 xa.layout(); 723 ya.requestAxisLayout(); 724 ya.layout(); 725 // layout plot content 726 layoutPlotChildren(); 727 // get axis zero points 728 final double xAxisZero = xa.getZeroPosition(); 729 final double yAxisZero = ya.getZeroPosition(); 730 // position vertical and horizontal zero lines 731 if(Double.isNaN(xAxisZero) || !isVerticalZeroLineVisible()) { 732 verticalZeroLine.setVisible(false); 733 } else { 734 verticalZeroLine.setStartX(left+xAxisZero+0.5); 735 verticalZeroLine.setStartY(top); 736 verticalZeroLine.setEndX(left+xAxisZero+0.5); 737 verticalZeroLine.setEndY(top+yAxisHeight); 738 verticalZeroLine.setVisible(true); 739 } 740 if(Double.isNaN(yAxisZero) || !isHorizontalZeroLineVisible()) { 741 horizontalZeroLine.setVisible(false); 742 } else { 743 horizontalZeroLine.setStartX(left); 744 horizontalZeroLine.setStartY(top+yAxisZero+0.5); 745 horizontalZeroLine.setEndX(left+xAxisWidth); 746 horizontalZeroLine.setEndY(top+yAxisZero+0.5); 747 horizontalZeroLine.setVisible(true); 748 } 749 // layout plot background 750 plotBackground.resizeRelocate(left, top, xAxisWidth, yAxisHeight); 751 // update clip 752 plotAreaClip.setX(left); 753 plotAreaClip.setY(top); 754 plotAreaClip.setWidth(xAxisWidth+1); 755 plotAreaClip.setHeight(yAxisHeight+1); 756// plotArea.setClip(new Rectangle(left, top, xAxisWidth, yAxisHeight)); 757 // position plot group, its origin is the bottom left corner of the plot area 758 plotContent.setLayoutX(left); 759 plotContent.setLayoutY(top); 760 plotContent.requestLayout(); // Note: not sure this is right, maybe plotContent should be resizeable 761 // update vertical grid lines 762 verticalGridLines.getElements().clear(); 763 if(getVerticalGridLinesVisible()) { 764 for(int i=0; i < xaTickMarks.size(); i++) { 765 Axis.TickMark<X> tick = xaTickMarks.get(i); 766 double pixelOffset = (i==(xaTickMarks.size()-1)) ? -0.5 : 0.5; 767 final double x = xa.getDisplayPosition(tick.getValue()); 768 if ((x!=xAxisZero || !isVerticalZeroLineVisible()) && x > 0 && x <= xAxisWidth) { 769 verticalGridLines.getElements().add(new MoveTo(left+x+pixelOffset,top)); 770 verticalGridLines.getElements().add(new LineTo(left+x+pixelOffset,top+yAxisHeight)); 771 } 772 } 773 } 774 // update horizontal grid lines 775 horizontalGridLines.getElements().clear(); 776 if(isHorizontalGridLinesVisible()) { 777 for(int i=0; i < yaTickMarks.size(); i++) { 778 Axis.TickMark<Y> tick = yaTickMarks.get(i); 779 double pixelOffset = (i==(yaTickMarks.size()-1)) ? -0.5 : 0.5; 780 final double y = ya.getDisplayPosition(tick.getValue()); 781 if ((y!=yAxisZero || !isHorizontalZeroLineVisible()) && y >= 0 && y < yAxisHeight) { 782 horizontalGridLines.getElements().add(new MoveTo(left,top+y+pixelOffset)); 783 horizontalGridLines.getElements().add(new LineTo(left+xAxisWidth,top+y+pixelOffset)); 784 } 785 } 786 } 787 // Note: is there a more efficient way to calculate horizontal and vertical row fills? 788 // update vertical row fill 789 verticalRowFill.getElements().clear(); 790 if (isAlternativeColumnFillVisible()) { 791 // tick marks are not sorted so get all the positions and sort them 792 final List<Double> tickPositionsPositive = new ArrayList<Double>(); 793 final List<Double> tickPositionsNegative = new ArrayList<Double>(); 794 for(int i=0; i < xaTickMarks.size(); i++) { 795 double pos = xa.getDisplayPosition((X) xaTickMarks.get(i).getValue()); 796 if (pos == xAxisZero) { 797 tickPositionsPositive.add(pos); 798 tickPositionsNegative.add(pos); 799 } else if (pos < xAxisZero) { 800 tickPositionsPositive.add(pos); 801 } else { 802 tickPositionsNegative.add(pos); 803 } 804 } 805 Collections.sort(tickPositionsPositive); 806 Collections.sort(tickPositionsNegative); 807 // iterate over every pair of positive tick marks and create fill 808 for(int i=1; i < tickPositionsPositive.size(); i+=2) { 809 if((i+1) < tickPositionsPositive.size()) { 810 final double x1 = tickPositionsPositive.get(i); 811 final double x2 = tickPositionsPositive.get(i+1); 812 verticalRowFill.getElements().addAll( 813 new MoveTo(left+x1,top), 814 new LineTo(left+x1,top+yAxisHeight), 815 new LineTo(left+x2,top+yAxisHeight), 816 new LineTo(left+x2,top), 817 new ClosePath()); 818 } 819 } 820 // iterate over every pair of positive tick marks and create fill 821 for(int i=0; i < tickPositionsNegative.size(); i+=2) { 822 if((i+1) < tickPositionsNegative.size()) { 823 final double x1 = tickPositionsNegative.get(i); 824 final double x2 = tickPositionsNegative.get(i+1); 825 verticalRowFill.getElements().addAll( 826 new MoveTo(left+x1,top), 827 new LineTo(left+x1,top+yAxisHeight), 828 new LineTo(left+x2,top+yAxisHeight), 829 new LineTo(left+x2,top), 830 new ClosePath()); 831 } 832 } 833 } 834 // update horizontal row fill 835 horizontalRowFill.getElements().clear(); 836 if (isAlternativeRowFillVisible()) { 837 // tick marks are not sorted so get all the positions and sort them 838 final List<Double> tickPositionsPositive = new ArrayList<Double>(); 839 final List<Double> tickPositionsNegative = new ArrayList<Double>(); 840 for(int i=0; i < yaTickMarks.size(); i++) { 841 double pos = ya.getDisplayPosition((Y) yaTickMarks.get(i).getValue()); 842 if (pos == yAxisZero) { 843 tickPositionsPositive.add(pos); 844 tickPositionsNegative.add(pos); 845 } else if (pos < yAxisZero) { 846 tickPositionsPositive.add(pos); 847 } else { 848 tickPositionsNegative.add(pos); 849 } 850 } 851 Collections.sort(tickPositionsPositive); 852 Collections.sort(tickPositionsNegative); 853 // iterate over every pair of positive tick marks and create fill 854 for(int i=1; i < tickPositionsPositive.size(); i+=2) { 855 if((i+1) < tickPositionsPositive.size()) { 856 final double y1 = tickPositionsPositive.get(i); 857 final double y2 = tickPositionsPositive.get(i+1); 858 horizontalRowFill.getElements().addAll( 859 new MoveTo(left, top + y1), 860 new LineTo(left + xAxisWidth, top + y1), 861 new LineTo(left + xAxisWidth, top + y2), 862 new LineTo(left, top + y2), 863 new ClosePath()); 864 } 865 } 866 // iterate over every pair of positive tick marks and create fill 867 for(int i=0; i < tickPositionsNegative.size(); i+=2) { 868 if((i+1) < tickPositionsNegative.size()) { 869 final double y1 = tickPositionsNegative.get(i); 870 final double y2 = tickPositionsNegative.get(i+1); 871 horizontalRowFill.getElements().addAll( 872 new MoveTo(left, top + y1), 873 new LineTo(left + xAxisWidth, top + y1), 874 new LineTo(left + xAxisWidth, top + y2), 875 new LineTo(left, top + y2), 876 new ClosePath()); 877 } 878 } 879 } 880// 881 } 882 883 /** 884 * Get the index of the series in the series linked list. 885 * 886 * @param series The series to find index for 887 * @return index of the series in series list 888 */ 889 int getSeriesIndex(Series series) { 890 int itemIndex = 0; 891 for (Series s = XYChart.this.begin; s != null; s = s.next) { 892 if (s == series) break; 893 itemIndex++; 894 } 895 return itemIndex; 896 } 897 898 /** 899 * Computes the size of series linked list 900 * @return size of series linked list 901 */ 902 int getSeriesSize() { 903 int count = 0; 904 for (Series d = XYChart.this.begin; d != null; d = d.next) { 905 count++; 906 } 907 return count; 908 } 909 910 /** 911 * This should be called from seriesRemoved() when you are finished with any animation for deleting the series from 912 * the chart. It will remove the series from showing up in the Iterator returned by getDisplayedSeriesIterator(). 913 * 914 * @param series The series to remove 915 */ 916 protected final void removeSeriesFromDisplay(Series<X, Y> series) { 917 if (begin == series) { 918 begin = series.next; 919 } else { 920 Series ptr = begin; 921 while(ptr != null && ptr.next != series) { 922 ptr = ptr.next; 923 } 924 if (ptr != null) 925 ptr.next = series.next; 926 } 927 } 928 929 /** 930 * XYChart maintains a list of all series currently displayed this includes all current series + any series that 931 * have recently been deleted that are in the process of being faded(animated) out. This creates and returns a 932 * iterator over that list. This is what implementations of XYChart should use when plotting data. 933 * 934 * @return iterator over currently displayed series 935 */ 936 protected final Iterator<Series<X,Y>> getDisplayedSeriesIterator() { 937 return new Iterator<Series<X, Y>>() { 938 private boolean start = true; 939 private Series<X,Y> current = begin; 940 @Override public boolean hasNext() { 941 if (start) { 942 return current != null; 943 } else { 944 return current.next != null; 945 } 946 } 947 @Override public Series<X, Y> next() { 948 if (start) { 949 start = false; 950 } else if (current!=null) { 951 current = current.next; 952 } 953 return current; 954 } 955 @Override public void remove() { 956 throw new UnsupportedOperationException("We don't support removing items from the displayed series list."); 957 } 958 }; 959 } 960 961 /** 962 * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is 963 * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot 964 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 965 * to animate when data is added or removed. 966 */ 967 protected final X getCurrentDisplayedXValue(Data<X,Y> item) { return item.getCurrentX(); } 968 969 /** Set the current displayed data value plotted on X axis. 970 * 971 * @param item The XYChart.Data item from which the current X axis data value is obtained. 972 * @see #getCurrentDisplayedXValue 973 */ 974 protected final void setCurrentDisplayedXValue(Data<X,Y> item, X value) { item.setCurrentX(value); } 975 976 /** The current displayed data value property that is plotted on X axis. 977 * 978 * @param item The XYChart.Data item from which the current X axis data value property object is obtained. 979 * @return The current displayed X data value ObjectProperty. 980 * @see #getCurrentDisplayedXValue 981 */ 982 protected final ObjectProperty<X> currentDisplayedXValueProperty(Data<X,Y> item) { return item.currentXProperty(); } 983 984 /** 985 * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is 986 * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot 987 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 988 * to animate when data is added or removed. 989 */ 990 protected final Y getCurrentDisplayedYValue(Data<X,Y> item) { return item.getCurrentY(); } 991 992 /** 993 * Set the current displayed data value plotted on Y axis. 994 * 995 * @param item The XYChart.Data item from which the current Y axis data value is obtained. 996 * @see #getCurrentDisplayedYValue 997 */ 998 protected final void setCurrentDisplayedYValue(Data<X,Y> item, Y value) { item.setCurrentY(value); } 999 1000 /** The current displayed data value property that is plotted on Y axis. 1001 * 1002 * @param item The XYChart.Data item from which the current Y axis data value property object is obtained. 1003 * @return The current displayed Y data value ObjectProperty. 1004 * @see #getCurrentDisplayedYValue 1005 */ 1006 protected final ObjectProperty<Y> currentDisplayedYValueProperty(Data<X,Y> item) { return item.currentYProperty(); } 1007 1008 /** 1009 * The current displayed data extra value. This may be the same as extraValue or different. It is 1010 * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot 1011 * in any custom XYChart implementations. 1012 */ 1013 protected final Object getCurrentDisplayedExtraValue(Data<X,Y> item) { return item.getCurrentExtraValue(); } 1014 1015 /** 1016 * Set the current displayed data extra value. 1017 * 1018 * @param item The XYChart.Data item from which the current extra value is obtained. 1019 * @see #getCurrentDisplayedExtraValue 1020 */ 1021 protected final void setCurrentDisplayedExtraValue(Data<X,Y> item, Object value) { item.setCurrentExtraValue(value); } 1022 1023 /** 1024 * The current displayed extra value property. 1025 * 1026 * @param item The XYChart.Data item from which the current extra value property object is obtained. 1027 * @return ObjectProperty<Object> The current extra value ObjectProperty 1028 * @see #getCurrentDisplayedExtraValue 1029 */ 1030 protected final ObjectProperty<Object> currentDisplayedExtraValueProperty(Data<X,Y> item) { return item.currentExtraValueProperty(); } 1031 1032 /** 1033 * XYChart maintains a list of all items currently displayed this includes all current data + any data items 1034 * recently deleted that are in the process of being faded out. This creates and returns a iterator over 1035 * that list. This is what implementations of XYChart should use when plotting data. 1036 * 1037 * @param series The series to get displayed data for 1038 * @return iterator over currently displayed items from this series 1039 */ 1040 protected final Iterator<Data<X,Y>> getDisplayedDataIterator(final Series<X,Y> series) { 1041 return new Iterator<Data<X, Y>>() { 1042 private boolean start = true; 1043 private Data<X,Y> current = series.begin; 1044 @Override public boolean hasNext() { 1045 if (start) { 1046 return current != null; 1047 } else { 1048 return current.next != null; 1049 } 1050 } 1051 @Override public Data<X, Y> next() { 1052 if (start) { 1053 start = false; 1054 } else if (current!=null) { 1055 current = current.next; 1056 } 1057 return current; 1058 } 1059 @Override public void remove() { 1060 throw new UnsupportedOperationException("We don't support removing items from the displayed data list."); 1061 } 1062 }; 1063 } 1064 1065 /** 1066 * This should be called from dataItemRemoved() when you are finished with any animation for deleting the item from the 1067 * chart. It will remove the data item from showing up in the Iterator returned by getDisplayedDataIterator(). 1068 * 1069 * @param series The series to remove 1070 * @param item The item to remove from series's display list 1071 */ 1072 protected final void removeDataItemFromDisplay(Series<X, Y> series, Data<X, Y> item) { 1073 series.removeDataItemRef(item); 1074 } 1075 1076 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 1077 1078 private static class StyleableProperties { 1079 private static final CssMetaData<XYChart<?,?>,Boolean> HORIZONTAL_GRID_LINE_VISIBLE = 1080 new CssMetaData<XYChart<?,?>,Boolean>("-fx-horizontal-grid-lines-visible", 1081 BooleanConverter.getInstance(), Boolean.TRUE) { 1082 1083 @Override 1084 public boolean isSettable(XYChart<?,?> node) { 1085 return node.horizontalGridLinesVisible == null || 1086 !node.horizontalGridLinesVisible.isBound(); 1087 } 1088 1089 @Override 1090 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1091 return (StyleableProperty<Boolean>)node.horizontalGridLinesVisibleProperty(); 1092 } 1093 }; 1094 1095 private static final CssMetaData<XYChart<?,?>,Boolean> HORIZONTAL_ZERO_LINE_VISIBLE = 1096 new CssMetaData<XYChart<?,?>,Boolean>("-fx-horizontal-zero-line-visible", 1097 BooleanConverter.getInstance(), Boolean.TRUE) { 1098 1099 @Override 1100 public boolean isSettable(XYChart<?,?> node) { 1101 return node.horizontalZeroLineVisible == null || 1102 !node.horizontalZeroLineVisible.isBound(); 1103 } 1104 1105 @Override 1106 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1107 return (StyleableProperty<Boolean>)node.horizontalZeroLineVisibleProperty(); 1108 } 1109 }; 1110 1111 private static final CssMetaData<XYChart<?,?>,Boolean> ALTERNATIVE_ROW_FILL_VISIBLE = 1112 new CssMetaData<XYChart<?,?>,Boolean>("-fx-alternative-row-fill-visible", 1113 BooleanConverter.getInstance(), Boolean.TRUE) { 1114 1115 @Override 1116 public boolean isSettable(XYChart<?,?> node) { 1117 return node.alternativeRowFillVisible == null || 1118 !node.alternativeRowFillVisible.isBound(); 1119 } 1120 1121 @Override 1122 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1123 return (StyleableProperty<Boolean>)node.alternativeRowFillVisibleProperty(); 1124 } 1125 }; 1126 1127 private static final CssMetaData<XYChart<?,?>,Boolean> VERTICAL_GRID_LINE_VISIBLE = 1128 new CssMetaData<XYChart<?,?>,Boolean>("-fx-vertical-grid-lines-visible", 1129 BooleanConverter.getInstance(), Boolean.TRUE) { 1130 1131 @Override 1132 public boolean isSettable(XYChart<?,?> node) { 1133 return node.verticalGridLinesVisible == null || 1134 !node.verticalGridLinesVisible.isBound(); 1135 } 1136 1137 @Override 1138 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1139 return (StyleableProperty<Boolean>)node.verticalGridLinesVisibleProperty(); 1140 } 1141 }; 1142 1143 private static final CssMetaData<XYChart<?,?>,Boolean> VERTICAL_ZERO_LINE_VISIBLE = 1144 new CssMetaData<XYChart<?,?>,Boolean>("-fx-vertical-zero-line-visible", 1145 BooleanConverter.getInstance(), Boolean.TRUE) { 1146 1147 @Override 1148 public boolean isSettable(XYChart<?,?> node) { 1149 return node.verticalZeroLineVisible == null || 1150 !node.verticalZeroLineVisible.isBound(); 1151 } 1152 1153 @Override 1154 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1155 return (StyleableProperty<Boolean>)node.verticalZeroLineVisibleProperty(); 1156 } 1157 }; 1158 1159 private static final CssMetaData<XYChart<?,?>,Boolean> ALTERNATIVE_COLUMN_FILL_VISIBLE = 1160 new CssMetaData<XYChart<?,?>,Boolean>("-fx-alternative-column-fill-visible", 1161 BooleanConverter.getInstance(), Boolean.TRUE) { 1162 1163 @Override 1164 public boolean isSettable(XYChart<?,?> node) { 1165 return node.alternativeColumnFillVisible == null || 1166 !node.alternativeColumnFillVisible.isBound(); 1167 } 1168 1169 @Override 1170 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1171 return (StyleableProperty<Boolean>)node.alternativeColumnFillVisibleProperty(); 1172 } 1173 }; 1174 1175 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1176 static { 1177 final List<CssMetaData<? extends Styleable, ?>> styleables = 1178 new ArrayList<CssMetaData<? extends Styleable, ?>>(Chart.getClassCssMetaData()); 1179 styleables.add(HORIZONTAL_GRID_LINE_VISIBLE); 1180 styleables.add(HORIZONTAL_ZERO_LINE_VISIBLE); 1181 styleables.add(ALTERNATIVE_ROW_FILL_VISIBLE); 1182 styleables.add(VERTICAL_GRID_LINE_VISIBLE); 1183 styleables.add(VERTICAL_ZERO_LINE_VISIBLE); 1184 styleables.add(ALTERNATIVE_COLUMN_FILL_VISIBLE); 1185 STYLEABLES = Collections.unmodifiableList(styleables); 1186 } 1187 } 1188 1189 /** 1190 * @return The CssMetaData associated with this class, which may include the 1191 * CssMetaData of its super classes. 1192 */ 1193 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1194 return StyleableProperties.STYLEABLES; 1195 } 1196 1197 /** 1198 * {@inheritDoc} 1199 */ 1200 @Override 1201 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 1202 return getClassCssMetaData(); 1203 } 1204 1205 // -------------- INNER CLASSES ------------------------------------------------------------------------------------ 1206 1207 /** 1208 * A single data item with data for 2 axis charts 1209 */ 1210 public final static class Data<X,Y> { 1211 // -------------- PUBLIC PROPERTIES ---------------------------------------- 1212 1213 private boolean setToRemove = false; 1214 /** The series this data belongs to */ 1215 private Series<X,Y> series; 1216 void setSeries(Series<X,Y> series) { 1217 this.series = series; 1218 } 1219 1220 /** The generic data value to be plotted on the X axis */ 1221 private ObjectProperty<X> xValue = new ObjectPropertyBase<X>() { 1222 @Override protected void invalidated() { 1223 // Note: calling get to make non-lazy, replace with change listener when available 1224 get(); 1225 if (series!=null) { 1226 XYChart<X,Y> chart = series.getChart(); 1227 if(chart!=null) chart.dataXValueChanged(Data.this); 1228 } else { 1229 // data has not been added to series yet : 1230 // so currentX and X should be the same 1231 setCurrentX(get()); 1232 } 1233 } 1234 1235 @Override 1236 public Object getBean() { 1237 return Data.this; 1238 } 1239 1240 @Override 1241 public String getName() { 1242 return "XValue"; 1243 } 1244 }; 1245 /** 1246 * Gets the generic data value to be plotted on the X axis. 1247 * @return the generic data value to be plotted on the X axis. 1248 */ 1249 public final X getXValue() { return xValue.get(); } 1250 /** 1251 * Sets the generic data value to be plotted on the X axis. 1252 * @param value the generic data value to be plotted on the X axis. 1253 */ 1254 public final void setXValue(X value) { 1255 xValue.set(value); 1256 // handle the case where this is a init because the default constructor was used 1257 if (currentX.get() == null) currentX.setValue(value); 1258 } 1259 /** 1260 * The generic data value to be plotted on the X axis. 1261 * @return The XValue property 1262 */ 1263 public final ObjectProperty<X> XValueProperty() { return xValue; } 1264 1265 /** The generic data value to be plotted on the Y axis */ 1266 private ObjectProperty<Y> yValue = new ObjectPropertyBase<Y>() { 1267 @Override protected void invalidated() { 1268 // Note: calling get to make non-lazy, replace with change listener when available 1269 get(); 1270 if (series!=null) { 1271 XYChart<X,Y> chart = series.getChart(); 1272 if(chart!=null) chart.dataYValueChanged(Data.this); 1273 } else { 1274 // data has not been added to series yet : 1275 // so currentY and Y should be the same 1276 setCurrentY(get()); 1277 } 1278 } 1279 1280 @Override 1281 public Object getBean() { 1282 return Data.this; 1283 } 1284 1285 @Override 1286 public String getName() { 1287 return "YValue"; 1288 } 1289 }; 1290 /** 1291 * Gets the generic data value to be plotted on the Y axis. 1292 * @return the generic data value to be plotted on the Y axis. 1293 */ 1294 public final Y getYValue() { return yValue.get(); } 1295 /** 1296 * Sets the generic data value to be plotted on the Y axis. 1297 * @param value the generic data value to be plotted on the Y axis. 1298 */ 1299 public final void setYValue(Y value) { 1300 yValue.set(value); 1301 // handle the case where this is a init because the default constructor was used 1302 if (currentY.get() == null) currentY.setValue(value); 1303 } 1304 /** 1305 * The generic data value to be plotted on the Y axis. 1306 * @return the YValue property 1307 */ 1308 public final ObjectProperty<Y> YValueProperty() { return yValue; } 1309 1310 /** 1311 * The generic data value to be plotted in any way the chart needs. For example used as the radius 1312 * for BubbleChart. 1313 */ 1314 private ObjectProperty<Object> extraValue = new ObjectPropertyBase<Object>() { 1315 @Override protected void invalidated() { 1316 // Note: calling get to make non-lazy, replace with change listener when available 1317 get(); 1318 if (series!=null) { 1319 XYChart<X,Y> chart = series.getChart(); 1320 if(chart!=null) chart.dataExtraValueChanged(Data.this); 1321 } 1322 } 1323 1324 @Override 1325 public Object getBean() { 1326 return Data.this; 1327 } 1328 1329 @Override 1330 public String getName() { 1331 return "extraValue"; 1332 } 1333 }; 1334 public final Object getExtraValue() { return extraValue.get(); } 1335 public final void setExtraValue(Object value) { extraValue.set(value); } 1336 public final ObjectProperty<Object> extraValueProperty() { return extraValue; } 1337 1338 /** 1339 * The node to display for this data item. You can either create your own node and set it on the data item 1340 * before you add the item to the chart. Otherwise the chart will create a node for you that has the default 1341 * representation for the chart type. This node will be set as soon as the data is added to the chart. You can 1342 * then get it to add mouse listeners etc. Charts will do their best to position and size the node 1343 * appropriately, for example on a Line or Scatter chart this node will be positioned centered on the data 1344 * values position. For a bar chart this is positioned and resized as the bar for this data item. 1345 */ 1346 private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node"); 1347 public final Node getNode() { return node.get(); } 1348 public final void setNode(Node value) { node.set(value); } 1349 public final ObjectProperty<Node> nodeProperty() { return node; } 1350 1351 /** 1352 * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is 1353 * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot 1354 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 1355 * to animate when data is added or removed. 1356 */ 1357 private ObjectProperty<X> currentX = new SimpleObjectProperty<X>(this, "currentX"); 1358 final X getCurrentX() { return currentX.get(); } 1359 final void setCurrentX(X value) { currentX.set(value); } 1360 final ObjectProperty<X> currentXProperty() { return currentX; } 1361 1362 /** 1363 * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is 1364 * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot 1365 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 1366 * to animate when data is added or removed. 1367 */ 1368 private ObjectProperty<Y> currentY = new SimpleObjectProperty<Y>(this, "currentY"); 1369 final Y getCurrentY() { return currentY.get(); } 1370 final void setCurrentY(Y value) { currentY.set(value); } 1371 final ObjectProperty<Y> currentYProperty() { return currentY; } 1372 1373 /** 1374 * The current displayed data extra value. This may be the same as extraValue or different. It is 1375 * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot 1376 * in any custom XYChart implementations. 1377 */ 1378 private ObjectProperty<Object> currentExtraValue = new SimpleObjectProperty<Object>(this, "currentExtraValue"); 1379 final Object getCurrentExtraValue() { return currentExtraValue.getValue(); } 1380 final void setCurrentExtraValue(Object value) { currentExtraValue.setValue(value); } 1381 final ObjectProperty<Object> currentExtraValueProperty() { return currentExtraValue; } 1382 1383 /** 1384 * Next pointer for the next data item. We maintain a linkedlist of the 1385 * data items so even after the data is deleted from the list, 1386 * we have a reference to it 1387 */ 1388 protected Data<X,Y> next = null; 1389 1390 // -------------- CONSTRUCTOR ------------------------------------------------- 1391 1392 /** 1393 * Creates an empty XYChart.Data object. 1394 */ 1395 public Data() {} 1396 1397 /** 1398 * Creates an instance of XYChart.Data object and initializes the X,Y 1399 * data values. 1400 * 1401 * @param xValue The X axis data value 1402 * @param yValue The Y axis data value 1403 */ 1404 public Data(X xValue, Y yValue) { 1405 setXValue(xValue); 1406 setYValue(yValue); 1407 setCurrentX(xValue); 1408 setCurrentY(yValue); 1409 } 1410 1411 /** 1412 * Creates an instance of XYChart.Data object and initializes the X,Y 1413 * data values and extraValue. 1414 * 1415 * @param xValue The X axis data value. 1416 * @param yValue The Y axis data value. 1417 * @param extraValue Chart extra value. 1418 */ 1419 public Data(X xValue, Y yValue, Object extraValue) { 1420 setXValue(xValue); 1421 setYValue(yValue); 1422 setExtraValue(extraValue); 1423 setCurrentX(xValue); 1424 setCurrentY(yValue); 1425 setCurrentExtraValue(extraValue); 1426 } 1427 1428 // -------------- PUBLIC METHODS ---------------------------------------------- 1429 1430 /** 1431 * Returns a string representation of this {@code Data} object. 1432 * @return a string representation of this {@code Data} object. 1433 */ 1434 @Override public String toString() { 1435 return "Data["+getXValue()+","+getYValue()+","+getExtraValue()+"]"; 1436 } 1437 1438 } 1439 1440 /** 1441 * A named series of data items 1442 */ 1443 public static final class Series<X,Y> { 1444 1445 // -------------- PRIVATE PROPERTIES ---------------------------------------- 1446 1447 /** the style class for default color for this series */ 1448 String defaultColorStyleClass; 1449 1450 Data<X,Y> begin = null; // start pointer of a data linked list. 1451 /* 1452 * Next pointer for the next series. We maintain a linkedlist of the 1453 * serieses so even after the series is deleted from the list, 1454 * we have a reference to it - needed by BarChart e.g. 1455 */ 1456 Series<X,Y> next = null; 1457 1458 private final ListChangeListener<Data<X,Y>> dataChangeListener = new ListChangeListener<Data<X, Y>>() { 1459 @Override public void onChanged(Change<? extends Data<X, Y>> c) { 1460 while (c.next()) { 1461 // RT-25187 Probably a sort happened, just reorder the pointers and return. 1462 if (c.wasPermutated()) { 1463 Series<X,Y> series = Series.this; 1464 if (series == null || series.getData() == null) return; 1465 Data<X,Y> ptr = begin; 1466 for(int i = 0; i < series.getData().size(); i++) { 1467 Data<X,Y> item = series.getData().get(i); 1468 if (i == 0) { 1469 begin = item; 1470 ptr = begin; 1471 begin.next = null; 1472 } else { 1473 ptr.next = item; 1474 item.next = null; 1475 ptr = item; 1476 } 1477 } 1478 return; 1479 } 1480 // update data items reference to series 1481 for (Data<X,Y> item : c.getRemoved()) { 1482 item.setToRemove = true; 1483 } 1484 if (c.getAddedSize() > 0) { 1485 for (Data<X,Y> itemPtr = begin; itemPtr != null; itemPtr = itemPtr.next) { 1486 if (itemPtr.setToRemove) { 1487 removeDataItemRef(itemPtr); 1488 } 1489 } 1490 } 1491 for(int i=c.getFrom(); i<c.getTo(); i++) { 1492 getData().get(i).setSeries(Series.this); 1493 // update linkedList Pointers for data in this series 1494 if (begin == null) { 1495 begin = getData().get(i); 1496 begin.next = null; 1497 } else { 1498 if (i == 0) { 1499 getData().get(0).next = begin; 1500 begin = getData().get(0); 1501 } else { 1502 Data<X,Y> ptr = begin; 1503 for (int j = 0; j < i -1 ; j++) { 1504 ptr = ptr.next; 1505 } 1506 getData().get(i).next = ptr.next; 1507 ptr.next = getData().get(i); 1508 } 1509 } 1510 } 1511 // check cycle in the data list 1512 // if cycle exists, and the data is not set to be removed, 1513 // eliminate loop and throw exception stating operation not permitted. 1514 // RT-28880 : infinite loop when same data is added to two charts. 1515 Data<X,Y> cycle = checkCycleInList(); 1516 if ( cycle != null) { 1517 if (!cycle.setToRemove) { 1518 eliminateLoop(cycle); 1519 throw new IllegalArgumentException( 1520 "Duplicate data added or same data added to more than one chart "); 1521 } 1522 } 1523 // inform chart 1524 XYChart<X,Y> chart = getChart(); 1525 if(chart!=null) chart.dataItemsChanged(Series.this, 1526 (List<Data<X,Y>>)c.getRemoved(), c.getFrom(), c.getTo(), c.wasPermutated()); 1527 } 1528 } 1529 }; 1530 1531 private Data<X,Y> checkCycleInList() { 1532 Data<X,Y> slow = null; 1533 Data<X,Y> fast = null; 1534 slow = fast = begin; 1535 while (slow != null && fast != null) { 1536 fast = fast.next; 1537 if (fast == slow) return slow; 1538 if (fast == null) return null; 1539 fast = fast.next; 1540 if (fast == slow) return fast; 1541 slow = slow.next; 1542 } 1543 return null; 1544 } 1545 1546 private void eliminateLoop(Data<X,Y> cycle) { 1547 Data<X,Y> slow = cycle; 1548 // Identify the data that is the start of the loop 1549 Data<X,Y> fast = begin; 1550 //until both the refs are one short of the common element which is the start of the loop 1551 while(fast.next != slow.next) { 1552 fast = fast.next; 1553 slow = slow.next; 1554 } 1555 Data<X,Y>start = fast.next; 1556 //Eliminate loop by setting next pointer of last element to null 1557 fast = start; 1558 while(fast.next != start) { 1559 fast = fast.next; 1560 } 1561 fast.next = null; //break the loop 1562 } 1563 1564 // -------------- PUBLIC PROPERTIES ---------------------------------------- 1565 1566 /** Reference to the chart this series belongs to */ 1567 private final ReadOnlyObjectWrapper<XYChart<X,Y>> chart = new ReadOnlyObjectWrapper<XYChart<X,Y>>(this, "chart"); 1568 public final XYChart<X,Y> getChart() { return chart.get(); } 1569 private void setChart(XYChart<X,Y> value) { chart.set(value); } 1570 public final ReadOnlyObjectProperty<XYChart<X,Y>> chartProperty() { return chart.getReadOnlyProperty(); } 1571 1572 /** The user displayable name for this series */ 1573 private final StringProperty name = new StringPropertyBase() { 1574 @Override protected void invalidated() { 1575 get(); // make non-lazy 1576 if(getChart() != null) getChart().seriesNameChanged(); 1577 } 1578 1579 @Override 1580 public Object getBean() { 1581 return Series.this; 1582 } 1583 1584 @Override 1585 public String getName() { 1586 return "name"; 1587 } 1588 }; 1589 public final String getName() { return name.get(); } 1590 public final void setName(String value) { name.set(value); } 1591 public final StringProperty nameProperty() { return name; } 1592 1593 /** 1594 * The node to display for this series. This is created by the chart if it uses nodes to represent the whole 1595 * series. For example line chart uses this for the line but scatter chart does not use it. This node will be 1596 * set as soon as the series is added to the chart. You can then get it to add mouse listeners etc. 1597 */ 1598 private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node"); 1599 public final Node getNode() { return node.get(); } 1600 public final void setNode(Node value) { node.set(value); } 1601 public final ObjectProperty<Node> nodeProperty() { return node; } 1602 1603 /** ObservableList of data items that make up this series */ 1604 private final ObjectProperty<ObservableList<Data<X,Y>>> data = new ObjectPropertyBase<ObservableList<Data<X,Y>>>() { 1605 private ObservableList<Data<X,Y>> old; 1606 @Override protected void invalidated() { 1607 final ObservableList<Data<X,Y>> current = getValue(); 1608 // add remove listeners 1609 if(old != null) old.removeListener(dataChangeListener); 1610 if(current != null) current.addListener(dataChangeListener); 1611 // fire data change event if series are added or removed 1612 if(old != null || current != null) { 1613 final List<Data<X,Y>> removed = (old != null) ? old : Collections.<Data<X,Y>>emptyList(); 1614 final int toIndex = (current != null) ? current.size() : 0; 1615 // let data listener know all old data have been removed and new data that has been added 1616 if (toIndex > 0 || !removed.isEmpty()) { 1617 dataChangeListener.onChanged(new NonIterableChange<Data<X,Y>>(0, toIndex, current){ 1618 @Override public List<Data<X,Y>> getRemoved() { return removed; } 1619 1620 @Override protected int[] getPermutation() { 1621 return new int[0]; 1622 } 1623 }); 1624 } 1625 } else if (old != null && old.size() > 0) { 1626 // let series listener know all old series have been removed 1627 dataChangeListener.onChanged(new NonIterableChange<Data<X,Y>>(0, 0, current){ 1628 @Override public List<Data<X,Y>> getRemoved() { return old; } 1629 @Override protected int[] getPermutation() { 1630 return new int[0]; 1631 } 1632 }); 1633 } 1634 old = current; 1635 } 1636 1637 @Override 1638 public Object getBean() { 1639 return Series.this; 1640 } 1641 1642 @Override 1643 public String getName() { 1644 return "data"; 1645 } 1646 }; 1647 public final ObservableList<Data<X,Y>> getData() { return data.getValue(); } 1648 public final void setData(ObservableList<Data<X,Y>> value) { data.setValue(value); } 1649 public final ObjectProperty<ObservableList<Data<X,Y>>> dataProperty() { return data; } 1650 1651 // -------------- CONSTRUCTORS ---------------------------------------------- 1652 1653 /** 1654 * Construct a empty series 1655 */ 1656 public Series() { 1657 this(FXCollections.<Data<X,Y>>observableArrayList()); 1658 } 1659 1660 /** 1661 * Constructs a Series and populates it with the given {@link ObservableList} data. 1662 * 1663 * @param data ObservableList of XYChart.Data 1664 */ 1665 public Series(ObservableList<Data<X,Y>> data) { 1666 setData(data); 1667 for(Data<X,Y> item:data) item.setSeries(this); 1668 } 1669 1670 /** 1671 * Constructs a named Series and populates it with the given {@link ObservableList} data. 1672 * 1673 * @param name a name for the series 1674 * @param data ObservableList of XYChart.Data 1675 */ 1676 public Series(String name, ObservableList<Data<X,Y>> data) { 1677 this(data); 1678 setName(name); 1679 } 1680 1681 // -------------- PUBLIC METHODS ---------------------------------------------- 1682 1683 /** 1684 * Returns a string representation of this {@code Series} object. 1685 * @return a string representation of this {@code Series} object. 1686 */ 1687 @Override public String toString() { 1688 return "Series["+getName()+"]"; 1689 } 1690 1691 // -------------- PRIVATE/PROTECTED METHODS ----------------------------------- 1692 1693 /* 1694 * The following methods are for manipulating the pointers in the linked list 1695 * when data is deleted. 1696 */ 1697 private void removeDataItemRef(Data<X,Y> item) { 1698 if (begin == item) { 1699 begin = item.next; 1700 } else { 1701 Data<X,Y> ptr = begin; 1702 while(ptr != null && ptr.next != item) { 1703 ptr = ptr.next; 1704 } 1705 if(ptr != null) ptr.next = item.next; 1706 } 1707 } 1708 1709 int getItemIndex(Data<X,Y> item) { 1710 int itemIndex = 0; 1711 for (Data<X,Y> d = begin; d != null; d = d.next) { 1712 if (d == item) break; 1713 itemIndex++; 1714 } 1715 return itemIndex; 1716 } 1717 1718 int getDataSize() { 1719 int count = 0; 1720 for (Data<X,Y> d = begin; d != null; d = d.next) { 1721 count++; 1722 } 1723 return count; 1724 } 1725 } 1726 1727}