1 /* 2 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.chart; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 32 import javafx.animation.Animation; 33 import javafx.animation.FadeTransition; 34 import javafx.animation.Interpolator; 35 import javafx.animation.KeyFrame; 36 import javafx.animation.KeyValue; 37 import javafx.animation.Timeline; 38 import javafx.application.Platform; 39 import javafx.beans.binding.StringBinding; 40 import javafx.beans.property.BooleanProperty; 41 import javafx.beans.property.DoubleProperty; 42 import javafx.beans.property.DoublePropertyBase; 43 import javafx.beans.property.ObjectProperty; 44 import javafx.beans.property.ObjectPropertyBase; 45 import javafx.beans.property.ReadOnlyObjectProperty; 46 import javafx.beans.property.ReadOnlyObjectWrapper; 47 import javafx.beans.property.SimpleDoubleProperty; 48 import javafx.beans.property.StringProperty; 49 import javafx.beans.property.StringPropertyBase; 50 import javafx.beans.value.WritableValue; 51 import javafx.collections.FXCollections; 52 import javafx.collections.ListChangeListener; 53 import javafx.collections.ObservableList; 54 import javafx.event.ActionEvent; 55 import javafx.event.EventHandler; 56 import javafx.geometry.NodeOrientation; 57 import javafx.geometry.Side; 58 import javafx.scene.AccessibleRole; 59 import javafx.scene.Node; 60 import javafx.scene.layout.Region; 61 import javafx.scene.shape.Arc; 62 import javafx.scene.shape.ArcTo; 63 import javafx.scene.shape.ArcType; 64 import javafx.scene.shape.ClosePath; 65 import javafx.scene.shape.LineTo; 66 import javafx.scene.shape.MoveTo; 67 import javafx.scene.shape.Path; 68 import javafx.scene.text.Text; 69 import javafx.scene.transform.Scale; 70 import javafx.util.Duration; 71 72 import com.sun.javafx.charts.Legend; 73 import com.sun.javafx.charts.Legend.LegendItem; 74 import com.sun.javafx.collections.NonIterableChange; 75 76 import javafx.css.StyleableBooleanProperty; 77 import javafx.css.StyleableDoubleProperty; 78 import javafx.css.CssMetaData; 79 80 import javafx.css.converter.BooleanConverter; 81 import javafx.css.converter.SizeConverter; 82 import java.util.BitSet; 83 84 import javafx.css.Styleable; 85 import javafx.css.StyleableProperty; 86 87 /** 88 * Displays a PieChart. The chart content is populated by pie slices based on 89 * data set on the PieChart. 90 * <p> The clockwise property is set to true by default, which means slices are 91 * placed in the clockwise order. The labelsVisible property is used to either display 92 * pie slice labels or not. 93 * 94 * @since JavaFX 2.0 95 */ 96 public class PieChart extends Chart { 97 98 // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- 99 private static final int MIN_PIE_RADIUS = 25; 100 private static final double LABEL_TICK_GAP = 6; 101 private static final double LABEL_BALL_RADIUS = 2; 102 private BitSet colorBits = new BitSet(8); 103 private double centerX; 104 private double centerY; 105 private double pieRadius; 106 private Data begin = null; 107 private final Path labelLinePath = new Path() { 108 @Override public boolean usesMirroring() { 109 return false; 110 } 111 }; 112 private Legend legend = new Legend(); 113 private Data dataItemBeingRemoved = null; 114 private Timeline dataRemoveTimeline = null; 115 private final ListChangeListener<Data> dataChangeListener = c -> { 116 while (c.next()) { 117 // RT-28090 Probably a sort happened, just reorder the pointers. 118 if (c.wasPermutated()) { 119 Data ptr = begin; 120 for (int i = 0; i < getData().size(); i++) { 121 Data item = getData().get(i); 122 updateDataItemStyleClass(item, i); 123 if (i == 0) { 124 begin = item; 125 ptr = begin; 126 begin.next = null; 127 } else { 128 ptr.next = item; 129 item.next = null; 130 ptr = item; 131 } 132 } 133 // update legend style classes 134 if (isLegendVisible()) { 135 updateLegend(); 136 } 137 requestChartLayout(); 138 return; 139 } 140 // recreate linked list & set chart on new data 141 for (int i = c.getFrom(); i < c.getTo(); i++) { 142 Data item = getData().get(i); 143 item.setChart(PieChart.this); 144 if (begin == null) { 145 begin = item; 146 begin.next = null; 147 } else { 148 if (i == 0) { 149 item.next = begin; 150 begin = item; 151 } else { 152 Data ptr = begin; 153 for (int j = 0; j < i -1 ; j++) { 154 ptr = ptr.next; 155 } 156 item.next = ptr.next; 157 ptr.next = item; 158 } 159 } 160 } 161 // call data added/removed methods 162 for (Data item : c.getRemoved()) { 163 dataItemRemoved(item); 164 } 165 for (int i = c.getFrom(); i < c.getTo(); i++) { 166 Data item = getData().get(i); 167 // assign default color to the added slice 168 // TODO: check nearby colors 169 item.defaultColorIndex = colorBits.nextClearBit(0); 170 colorBits.set(item.defaultColorIndex); 171 dataItemAdded(item, i); 172 } 173 if (c.wasRemoved() || c.wasAdded()) { 174 for (int i = 0; i < getData().size(); i++) { 175 Data item = getData().get(i); 176 updateDataItemStyleClass(item, i); 177 } 178 // update legend if any data has changed 179 if (isLegendVisible()) { 180 updateLegend(); 181 } 182 } 183 } 184 // re-layout everything 185 requestChartLayout(); 186 }; 187 188 // -------------- PUBLIC PROPERTIES ---------------------------------------- 189 190 /** PieCharts data */ 191 private ObjectProperty<ObservableList<Data>> data = new ObjectPropertyBase<ObservableList<Data>>() { 192 private ObservableList<Data> old; 193 @Override protected void invalidated() { 194 final ObservableList<Data> current = getValue(); 195 // add remove listeners 196 if(old != null) old.removeListener(dataChangeListener); 197 if(current != null) current.addListener(dataChangeListener); 198 // fire data change event if series are added or removed 199 if(old != null || current != null) { 200 final List<Data> removed = (old != null) ? old : Collections.<Data>emptyList(); 201 final int toIndex = (current != null) ? current.size() : 0; 202 // let data listener know all old data have been removed and new data that has been added 203 if (toIndex > 0 || !removed.isEmpty()) { 204 dataChangeListener.onChanged(new NonIterableChange<Data>(0, toIndex, current){ 205 @Override public List<Data> getRemoved() { return removed; } 206 @Override public boolean wasPermutated() { return false; } 207 @Override protected int[] getPermutation() { 208 return new int[0]; 209 } 210 }); 211 } 212 } else if (old != null && old.size() > 0) { 213 // let series listener know all old series have been removed 214 dataChangeListener.onChanged(new NonIterableChange<Data>(0, 0, current){ 215 @Override public List<Data> getRemoved() { return old; } 216 @Override public boolean wasPermutated() { return false; } 217 @Override protected int[] getPermutation() { 218 return new int[0]; 219 } 220 }); 221 } 222 old = current; 223 } 224 225 public Object getBean() { 226 return PieChart.this; 227 } 228 229 public String getName() { 230 return "data"; 231 } 232 }; 233 public final ObservableList<Data> getData() { return data.getValue(); } 234 public final void setData(ObservableList<Data> value) { data.setValue(value); } 235 public final ObjectProperty<ObservableList<Data>> dataProperty() { return data; } 236 237 /** The angle to start the first pie slice at */ 238 private DoubleProperty startAngle = new StyleableDoubleProperty(0) { 239 @Override public void invalidated() { 240 get(); 241 requestChartLayout(); 242 } 243 244 @Override 245 public Object getBean() { 246 return PieChart.this; 247 } 248 249 @Override 250 public String getName() { 251 return "startAngle"; 252 } 253 254 public CssMetaData<PieChart,Number> getCssMetaData() { 255 return StyleableProperties.START_ANGLE; 256 } 257 }; 258 public final double getStartAngle() { return startAngle.getValue(); } 259 public final void setStartAngle(double value) { startAngle.setValue(value); } 260 public final DoubleProperty startAngleProperty() { return startAngle; } 261 262 /** When true we start placing slices clockwise from the startAngle */ 263 private BooleanProperty clockwise = new StyleableBooleanProperty(true) { 264 @Override public void invalidated() { 265 get(); 266 requestChartLayout(); 267 } 268 269 @Override 270 public Object getBean() { 271 return PieChart.this; 272 } 273 274 @Override 275 public String getName() { 276 return "clockwise"; 277 } 278 279 public CssMetaData<PieChart,Boolean> getCssMetaData() { 280 return StyleableProperties.CLOCKWISE; 281 } 282 }; 283 public final void setClockwise(boolean value) { clockwise.setValue(value);} 284 public final boolean isClockwise() { return clockwise.getValue(); } 285 public final BooleanProperty clockwiseProperty() { return clockwise; } 286 287 288 /** The length of the line from the outside of the pie to the slice labels. */ 289 private DoubleProperty labelLineLength = new StyleableDoubleProperty(20d) { 290 @Override public void invalidated() { 291 get(); 292 requestChartLayout(); 293 } 294 295 @Override 296 public Object getBean() { 297 return PieChart.this; 298 } 299 300 @Override 301 public String getName() { 302 return "labelLineLength"; 303 } 304 305 public CssMetaData<PieChart,Number> getCssMetaData() { 306 return StyleableProperties.LABEL_LINE_LENGTH; 307 } 308 }; 309 public final double getLabelLineLength() { return labelLineLength.getValue(); } 310 public final void setLabelLineLength(double value) { labelLineLength.setValue(value); } 311 public final DoubleProperty labelLineLengthProperty() { return labelLineLength; } 312 313 /** When true pie slice labels are drawn */ 314 private BooleanProperty labelsVisible = new StyleableBooleanProperty(true) { 315 @Override public void invalidated() { 316 get(); 317 requestChartLayout(); 318 } 319 320 @Override 321 public Object getBean() { 322 return PieChart.this; 323 } 324 325 @Override 326 public String getName() { 327 return "labelsVisible"; 328 } 329 330 public CssMetaData<PieChart,Boolean> getCssMetaData() { 331 return StyleableProperties.LABELS_VISIBLE; 332 } 333 }; 334 public final void setLabelsVisible(boolean value) { labelsVisible.setValue(value);} 335 336 /** 337 * Indicates whether pie slice labels are drawn or not 338 * @return true if pie slice labels are visible and false otherwise. 339 */ 340 public final boolean getLabelsVisible() { return labelsVisible.getValue(); } 341 public final BooleanProperty labelsVisibleProperty() { return labelsVisible; } 342 343 // -------------- CONSTRUCTOR ---------------------------------------------- 344 345 /** 346 * Construct a new empty PieChart. 347 */ 348 public PieChart() { 349 this(FXCollections.<Data>observableArrayList()); 350 } 351 352 /** 353 * Construct a new PieChart with the given data 354 * 355 * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart 356 */ 357 public PieChart(ObservableList<PieChart.Data> data) { 358 getChartChildren().add(labelLinePath); 359 labelLinePath.getStyleClass().add("chart-pie-label-line"); 360 setLegend(legend); 361 setData(data); 362 // set chart content mirroring to be always false i.e. chartContent mirrorring is not done 363 // when node orientation is right-to-left for PieChart. 364 useChartContentMirroring = false; 365 } 366 367 // -------------- METHODS -------------------------------------------------- 368 369 private void dataNameChanged(Data item) { 370 item.textNode.setText(item.getName()); 371 requestChartLayout(); 372 updateLegend(); 373 } 374 375 private void dataPieValueChanged(Data item) { 376 if (shouldAnimate()) { 377 animate( 378 new KeyFrame(Duration.ZERO, new KeyValue(item.currentPieValueProperty(), 379 item.getCurrentPieValue())), 380 new KeyFrame(Duration.millis(500),new KeyValue(item.currentPieValueProperty(), 381 item.getPieValue(), Interpolator.EASE_BOTH)) 382 ); 383 } else { 384 item.setCurrentPieValue(item.getPieValue()); 385 requestChartLayout(); // RT-23091 386 } 387 } 388 389 private Node createArcRegion(Data item) { 390 Node arcRegion = item.getNode(); 391 // check if symbol has already been created 392 if (arcRegion == null) { 393 arcRegion = new Region(); 394 arcRegion.setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); 395 arcRegion.setPickOnBounds(false); 396 item.setNode(arcRegion); 397 } 398 return arcRegion; 399 } 400 401 private Text createPieLabel(Data item) { 402 Text text = item.textNode; 403 text.setText(item.getName()); 404 return text; 405 } 406 407 private void updateDataItemStyleClass(final Data item, int index) { 408 Node node = item.getNode(); 409 if (node != null) { 410 // Note: not sure if we want to add or check, ie be more careful and efficient here 411 node.getStyleClass().setAll("chart-pie", "data" + index, 412 "default-color" + item.defaultColorIndex % 8); 413 if (item.getPieValue() < 0) { 414 node.getStyleClass().add("negative"); 415 } 416 } 417 } 418 419 private void dataItemAdded(final Data item, int index) { 420 // create shape 421 Node shape = createArcRegion(item); 422 final Text text = createPieLabel(item); 423 item.getChart().getChartChildren().add(shape); 424 if (shouldAnimate()) { 425 // if the same data item is being removed, first stop the remove animation, 426 // remove the item and then start the add animation. 427 if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) { 428 if (dataItemBeingRemoved == item) { 429 dataRemoveTimeline.stop(); 430 dataRemoveTimeline = null; 431 getChartChildren().remove(item.textNode); 432 getChartChildren().remove(shape); 433 removeDataItemRef(item); 434 } 435 } 436 animate( 437 new KeyFrame(Duration.ZERO, 438 new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue()), 439 new KeyValue(item.radiusMultiplierProperty(), item.getRadiusMultiplier())), 440 new KeyFrame(Duration.millis(500), 441 actionEvent -> { 442 text.setOpacity(0); 443 // RT-23597 : item's chart might have been set to null if 444 // this item is added and removed before its add animation finishes. 445 if (item.getChart() == null) item.setChart(PieChart.this); 446 item.getChart().getChartChildren().add(text); 447 FadeTransition ft = new FadeTransition(Duration.millis(150),text); 448 ft.setToValue(1); 449 ft.play(); 450 }, 451 new KeyValue(item.currentPieValueProperty(), item.getPieValue(), Interpolator.EASE_BOTH), 452 new KeyValue(item.radiusMultiplierProperty(), 1, Interpolator.EASE_BOTH)) 453 ); 454 } else { 455 getChartChildren().add(text); 456 item.setRadiusMultiplier(1); 457 item.setCurrentPieValue(item.getPieValue()); 458 } 459 460 // we sort the text nodes to always be at the end of the children list, so they have a higher z-order 461 // (Fix for RT-34564) 462 for (int i = 0; i < getChartChildren().size(); i++) { 463 Node n = getChartChildren().get(i); 464 if (n instanceof Text) { 465 n.toFront(); 466 } 467 } 468 } 469 470 private void removeDataItemRef(Data item) { 471 if (begin == item) { 472 begin = item.next; 473 } else { 474 Data ptr = begin; 475 while(ptr != null && ptr.next != item) { 476 ptr = ptr.next; 477 } 478 if(ptr != null) ptr.next = item.next; 479 } 480 } 481 482 private Timeline createDataRemoveTimeline(final Data item) { 483 final Node shape = item.getNode(); 484 Timeline t = new Timeline(); 485 t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, 486 new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue()), 487 new KeyValue(item.radiusMultiplierProperty(), item.getRadiusMultiplier())), 488 new KeyFrame(Duration.millis(500), 489 actionEvent -> { 490 // removing item 491 colorBits.clear(item.defaultColorIndex); 492 getChartChildren().remove(shape); 493 // fade out label 494 FadeTransition ft = new FadeTransition(Duration.millis(150),item.textNode); 495 ft.setFromValue(1); 496 ft.setToValue(0); 497 ft.setOnFinished(new EventHandler<ActionEvent>() { 498 @Override public void handle(ActionEvent actionEvent) { 499 getChartChildren().remove(item.textNode); 500 // remove chart references from old data - RT-22553 501 item.setChart(null); 502 removeDataItemRef(item); 503 item.textNode.setOpacity(1.0); 504 } 505 }); 506 ft.play(); 507 }, 508 new KeyValue(item.currentPieValueProperty(), 0, Interpolator.EASE_BOTH), 509 new KeyValue(item.radiusMultiplierProperty(), 0)) 510 ); 511 return t; 512 } 513 514 private void dataItemRemoved(final Data item) { 515 final Node shape = item.getNode(); 516 if (shouldAnimate()) { 517 dataRemoveTimeline = createDataRemoveTimeline(item); 518 dataItemBeingRemoved = item; 519 animate(dataRemoveTimeline); 520 } else { 521 colorBits.clear(item.defaultColorIndex); 522 getChartChildren().remove(item.textNode); 523 getChartChildren().remove(shape); 524 // remove chart references from old data 525 item.setChart(null); 526 removeDataItemRef(item); 527 } 528 } 529 530 /** @inheritDoc */ 531 @Override protected void layoutChartChildren(double top, double left, double contentWidth, double contentHeight) { 532 centerX = contentWidth/2 + left; 533 centerY = contentHeight/2 + top; 534 double total = 0.0; 535 for (Data item = begin; item != null; item = item.next) { 536 total+= Math.abs(item.getCurrentPieValue()); 537 } 538 double scale = (total != 0) ? 360 / total : 0; 539 540 labelLinePath.getElements().clear(); 541 // calculate combined bounds of all labels & pie radius 542 double[] labelsX = null; 543 double[] labelsY = null; 544 double[] labelAngles = null; 545 double labelScale = 1; 546 ArrayList<LabelLayoutInfo> fullPie = null; 547 boolean shouldShowLabels = getLabelsVisible(); 548 if(getLabelsVisible()) { 549 550 double xPad = 0d; 551 double yPad = 0d; 552 553 labelsX = new double[getDataSize()]; 554 labelsY = new double[getDataSize()]; 555 labelAngles = new double[getDataSize()]; 556 fullPie = new ArrayList<LabelLayoutInfo>(); 557 int index = 0; 558 double start = getStartAngle(); 559 for (Data item = begin; item != null; item = item.next) { 560 // remove any scale on the text node 561 item.textNode.getTransforms().clear(); 562 563 double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue())); 564 labelAngles[index] = normalizeAngle(start + (size / 2)); 565 final double sproutX = calcX(labelAngles[index], getLabelLineLength(), 0); 566 final double sproutY = calcY(labelAngles[index], getLabelLineLength(), 0); 567 labelsX[index] = sproutX; 568 labelsY[index] = sproutY; 569 xPad = Math.max(xPad, 2 * (item.textNode.getLayoutBounds().getWidth() + LABEL_TICK_GAP + Math.abs(sproutX))); 570 if (sproutY > 0) { // on bottom 571 yPad = Math.max(yPad, 2 * Math.abs(sproutY+item.textNode.getLayoutBounds().getMaxY())); 572 } else { // on top 573 yPad = Math.max(yPad, 2 * Math.abs(sproutY + item.textNode.getLayoutBounds().getMinY())); 574 } 575 start+= size; 576 index++; 577 } 578 pieRadius = Math.min(contentWidth - xPad, contentHeight - yPad) / 2; 579 // check if this makes the pie too small 580 if (pieRadius < MIN_PIE_RADIUS ) { 581 // calculate scale for text to fit labels in 582 final double roomX = contentWidth-MIN_PIE_RADIUS-MIN_PIE_RADIUS; 583 final double roomY = contentHeight-MIN_PIE_RADIUS-MIN_PIE_RADIUS; 584 labelScale = Math.min( 585 roomX/xPad, 586 roomY/yPad 587 ); 588 // hide labels if pie radius is less than minimum 589 if ((begin == null && labelScale < 0.7) || ((begin.textNode.getFont().getSize()*labelScale) < 9)) { 590 shouldShowLabels = false; 591 labelScale = 1; 592 } else { 593 // set pieRadius to minimum 594 pieRadius = MIN_PIE_RADIUS; 595 // apply scale to all label positions 596 for(int i=0; i< labelsX.length; i++) { 597 labelsX[i] = labelsX[i] * labelScale; 598 labelsY[i] = labelsY[i] * labelScale; 599 } 600 } 601 } 602 } 603 604 if(!shouldShowLabels) { 605 pieRadius = Math.min(contentWidth,contentHeight) / 2; 606 } 607 608 if (getChartChildren().size() > 0) { 609 int index = 0; 610 for (Data item = begin; item != null; item = item.next) { 611 // layout labels for pie slice 612 item.textNode.setVisible(shouldShowLabels); 613 if (shouldShowLabels) { 614 double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue())); 615 final boolean isLeftSide = !(labelAngles[index] > -90 && labelAngles[index] < 90); 616 617 double sliceCenterEdgeX = calcX(labelAngles[index], pieRadius, centerX); 618 double sliceCenterEdgeY = calcY(labelAngles[index], pieRadius, centerY); 619 double xval = isLeftSide ? 620 (labelsX[index] + sliceCenterEdgeX - item.textNode.getLayoutBounds().getMaxX() - LABEL_TICK_GAP) : 621 (labelsX[index] + sliceCenterEdgeX - item.textNode.getLayoutBounds().getMinX() + LABEL_TICK_GAP); 622 double yval = labelsY[index] + sliceCenterEdgeY - (item.textNode.getLayoutBounds().getMinY()/2) -2; 623 624 // do the line (Path)for labels 625 double lineEndX = sliceCenterEdgeX +labelsX[index]; 626 double lineEndY = sliceCenterEdgeY +labelsY[index]; 627 LabelLayoutInfo info = new LabelLayoutInfo(sliceCenterEdgeX, 628 sliceCenterEdgeY,lineEndX, lineEndY, xval, yval, item.textNode, Math.abs(size)); 629 fullPie.add(info); 630 631 // set label scales 632 if (labelScale < 1) { 633 item.textNode.getTransforms().add( 634 new Scale( 635 labelScale, labelScale, 636 isLeftSide ? item.textNode.getLayoutBounds().getWidth() : 0, 637 // 0, 638 0 639 ) 640 ); 641 } 642 } 643 index++; 644 } 645 646 // Check for collision and resolve by hiding the label of the smaller pie slice 647 resolveCollision(fullPie); 648 649 // update/draw pie slices 650 double sAngle = getStartAngle(); 651 for (Data item = begin; item != null; item = item.next) { 652 Node node = item.getNode(); 653 Arc arc = null; 654 if (node != null) { 655 if (node instanceof Region) { 656 Region arcRegion = (Region)node; 657 if( arcRegion.getShape() == null) { 658 arc = new Arc(); 659 arcRegion.setShape(arc); 660 } else { 661 arc = (Arc)arcRegion.getShape(); 662 } 663 arcRegion.setShape(null); 664 arcRegion.setShape(arc); 665 arcRegion.setScaleShape(false); 666 arcRegion.setCenterShape(false); 667 arcRegion.setCacheShape(false); 668 } 669 } 670 double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue())); 671 // update slice arc size 672 arc.setStartAngle(sAngle); 673 arc.setLength(size); 674 arc.setType(ArcType.ROUND); 675 arc.setRadiusX(pieRadius * item.getRadiusMultiplier()); 676 arc.setRadiusY(pieRadius * item.getRadiusMultiplier()); 677 node.setLayoutX(centerX); 678 node.setLayoutY(centerY); 679 sAngle += size; 680 } 681 // finally draw the text and line 682 if (fullPie != null) { 683 for (LabelLayoutInfo info : fullPie) { 684 if (info.text.isVisible()) drawLabelLinePath(info); 685 } 686 } 687 } 688 } 689 690 // We check for pie slice label collision and if collision is detected, we then 691 // compare the size of the slices, and hide the label of the smaller slice. 692 private void resolveCollision(ArrayList<LabelLayoutInfo> list) { 693 int boxH = (begin != null) ? (int)begin.textNode.getLayoutBounds().getHeight() : 0; 694 int i; int j; 695 for (i = 0, j = 1; list != null && j < list.size(); j++ ) { 696 LabelLayoutInfo box1 = list.get(i); 697 LabelLayoutInfo box2 = list.get(j); 698 if ((box1.text.isVisible() && box2.text.isVisible()) && 699 (fuzzyGT(box2.textY, box1.textY) ? fuzzyLT((box2.textY - boxH - box1.textY), 2) : 700 fuzzyLT((box1.textY - boxH - box2.textY), 2)) && 701 (fuzzyGT(box1.textX, box2.textX) ? fuzzyLT((box1.textX - box2.textX), box2.text.prefWidth(-1)) : 702 fuzzyLT((box2.textX - box1.textX), box1.text.prefWidth(-1)))) { 703 if (fuzzyLT(box1.size, box2.size)) { 704 box1.text.setVisible(false); 705 i = j; 706 } else { 707 box2.text.setVisible(false); 708 } 709 } else { 710 i = j; 711 } 712 } 713 } 714 715 private int fuzzyCompare(double o1, double o2) { 716 double fuzz = 0.00001; 717 return (((Math.abs(o1 - o2)) < fuzz) ? 0 : ((o1 < o2) ? -1 : 1)); 718 } 719 720 private boolean fuzzyGT(double o1, double o2) { 721 return (fuzzyCompare(o1, o2) == 1) ? true: false; 722 } 723 724 private boolean fuzzyLT(double o1, double o2) { 725 return (fuzzyCompare(o1, o2) == -1) ? true : false; 726 } 727 728 private void drawLabelLinePath(LabelLayoutInfo info) { 729 info.text.setLayoutX(info.textX); 730 info.text.setLayoutY(info.textY); 731 labelLinePath.getElements().add(new MoveTo(info.startX, info.startY)); 732 labelLinePath.getElements().add(new LineTo(info.endX, info.endY)); 733 734 labelLinePath.getElements().add(new MoveTo(info.endX-LABEL_BALL_RADIUS,info.endY)); 735 labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 736 90, info.endX,info.endY-LABEL_BALL_RADIUS, false, true)); 737 labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 738 90, info.endX+LABEL_BALL_RADIUS,info.endY, false, true)); 739 labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 740 90, info.endX,info.endY+LABEL_BALL_RADIUS, false, true)); 741 labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 742 90, info.endX-LABEL_BALL_RADIUS,info.endY, false, true)); 743 labelLinePath.getElements().add(new ClosePath()); 744 } 745 /** 746 * This is called whenever a series is added or removed and the legend needs to be updated 747 */ 748 private void updateLegend() { 749 Node legendNode = getLegend(); 750 if (legendNode != null && legendNode != legend) return; // RT-23569 dont update when user has set legend. 751 legend.setVertical(getLegendSide().equals(Side.LEFT) || getLegendSide().equals(Side.RIGHT)); 752 legend.getItems().clear(); 753 if (getData() != null) { 754 for (Data item : getData()) { 755 LegendItem legenditem = new LegendItem(item.getName()); 756 legenditem.getSymbol().getStyleClass().addAll(item.getNode().getStyleClass()); 757 legenditem.getSymbol().getStyleClass().add("pie-legend-symbol"); 758 legend.getItems().add(legenditem); 759 } 760 } 761 if (legend.getItems().size() > 0) { 762 if (legendNode == null) { 763 setLegend(legend); 764 } 765 } else { 766 setLegend(null); 767 } 768 } 769 770 private int getDataSize() { 771 int count = 0; 772 for (Data d = begin; d != null; d = d.next) { 773 count++; 774 } 775 return count; 776 } 777 778 private static double calcX(double angle, double radius, double centerX) { 779 return (double)(centerX + radius * Math.cos(Math.toRadians(-angle))); 780 } 781 782 private static double calcY(double angle, double radius, double centerY) { 783 return (double)(centerY + radius * Math.sin(Math.toRadians(-angle))); 784 } 785 786 /** Normalize any angle into -180 to 180 deg range */ 787 private static double normalizeAngle(double angle) { 788 double a = angle % 360; 789 if (a <= -180) a += 360; 790 if (a > 180) a -= 360; 791 return a; 792 } 793 794 // -------------- INNER CLASSES -------------------------------------------- 795 796 // Class holding label line layout info for collision detection and removal 797 final static class LabelLayoutInfo { 798 double startX; 799 double startY; 800 double endX; 801 double endY; 802 double textX; 803 double textY; 804 Text text; 805 double size; 806 807 public LabelLayoutInfo(double startX, double startY, double endX, double endY, 808 double textX, double textY, Text text, double size) { 809 this.startX = startX; 810 this.startY = startY; 811 this.endX = endX; 812 this.endY = endY; 813 this.textX = textX; 814 this.textY = textY; 815 this.text = text; 816 this.size = size; 817 } 818 } 819 820 /** 821 * PieChart Data Item, represents one slice in the PieChart 822 * 823 * @since JavaFX 2.0 824 */ 825 public final static class Data { 826 827 private Text textNode = new Text(); 828 /** 829 * Next pointer for the next data item : so we can do animation on data delete. 830 */ 831 private Data next = null; 832 833 /** 834 * Default color index for this slice. 835 */ 836 private int defaultColorIndex; 837 838 // -------------- PUBLIC PROPERTIES ------------------------------------ 839 840 /** 841 * The chart which this data belongs to. 842 */ 843 private ReadOnlyObjectWrapper<PieChart> chart = new ReadOnlyObjectWrapper<PieChart>(this, "chart"); 844 845 public final PieChart getChart() { 846 return chart.getValue(); 847 } 848 849 private void setChart(PieChart value) { 850 chart.setValue(value); 851 } 852 853 public final ReadOnlyObjectProperty<PieChart> chartProperty() { 854 return chart.getReadOnlyProperty(); 855 } 856 857 /** 858 * The name of the pie slice 859 */ 860 private StringProperty name = new StringPropertyBase() { 861 @Override 862 protected void invalidated() { 863 if (getChart() != null) getChart().dataNameChanged(Data.this); 864 } 865 866 @Override 867 public Object getBean() { 868 return Data.this; 869 } 870 871 @Override 872 public String getName() { 873 return "name"; 874 } 875 }; 876 877 public final void setName(java.lang.String value) { 878 name.setValue(value); 879 } 880 881 public final java.lang.String getName() { 882 return name.getValue(); 883 } 884 885 public final StringProperty nameProperty() { 886 return name; 887 } 888 889 /** 890 * The value of the pie slice 891 */ 892 private DoubleProperty pieValue = new DoublePropertyBase() { 893 @Override 894 protected void invalidated() { 895 if (getChart() != null) getChart().dataPieValueChanged(Data.this); 896 } 897 898 @Override 899 public Object getBean() { 900 return Data.this; 901 } 902 903 @Override 904 public String getName() { 905 return "pieValue"; 906 } 907 }; 908 909 public final double getPieValue() { 910 return pieValue.getValue(); 911 } 912 913 public final void setPieValue(double value) { 914 pieValue.setValue(value); 915 } 916 917 public final DoubleProperty pieValueProperty() { 918 return pieValue; 919 } 920 921 /** 922 * The current pie value, used during animation. This will be the last data value, new data value or 923 * anywhere in between 924 */ 925 private DoubleProperty currentPieValue = new SimpleDoubleProperty(this, "currentPieValue"); 926 927 private double getCurrentPieValue() { 928 return currentPieValue.getValue(); 929 } 930 931 private void setCurrentPieValue(double value) { 932 currentPieValue.setValue(value); 933 } 934 935 private DoubleProperty currentPieValueProperty() { 936 return currentPieValue; 937 } 938 939 /** 940 * Multiplier that is used to animate the radius of the pie slice 941 */ 942 private DoubleProperty radiusMultiplier = new SimpleDoubleProperty(this, "radiusMultiplier"); 943 944 private double getRadiusMultiplier() { 945 return radiusMultiplier.getValue(); 946 } 947 948 private void setRadiusMultiplier(double value) { 949 radiusMultiplier.setValue(value); 950 } 951 952 private DoubleProperty radiusMultiplierProperty() { 953 return radiusMultiplier; 954 } 955 956 /** 957 * Readonly access to the node that represents the pie slice. You can use this to add mouse event listeners etc. 958 */ 959 private ReadOnlyObjectWrapper<Node> node = new ReadOnlyObjectWrapper<>(this, "node"); 960 961 /** 962 * Returns the node that represents the pie slice. You can use this to 963 * add mouse event listeners etc. 964 */ 965 public Node getNode() { 966 return node.getValue(); 967 } 968 969 private void setNode(Node value) { 970 node.setValue(value); 971 } 972 973 public ReadOnlyObjectProperty<Node> nodeProperty() { 974 return node.getReadOnlyProperty(); 975 } 976 977 // -------------- CONSTRUCTOR ------------------------------------------------- 978 979 /** 980 * Constructs a PieChart.Data object with the given name and value. 981 * 982 * @param name name for Pie 983 * @param value pie value 984 */ 985 public Data(java.lang.String name, double value) { 986 setName(name); 987 setPieValue(value); 988 textNode.getStyleClass().addAll("text", "chart-pie-label"); 989 textNode.setAccessibleRole(AccessibleRole.TEXT); 990 textNode.setAccessibleRoleDescription("slice"); 991 textNode.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 992 textNode.accessibleTextProperty().bind( new StringBinding() { 993 {bind(nameProperty(), currentPieValueProperty());} 994 @Override protected String computeValue() { 995 return getName() + " represents " + getCurrentPieValue() + " percent"; 996 } 997 }); 998 } 999 1000 // -------------- PUBLIC METHODS ---------------------------------------------- 1001 1002 /** 1003 * Returns a string representation of this {@code Data} object. 1004 * 1005 * @return a string representation of this {@code Data} object. 1006 */ 1007 @Override 1008 public java.lang.String toString() { 1009 return "Data[" + getName() + "," + getPieValue() + "]"; 1010 } 1011 } 1012 1013 // -------------- STYLESHEET HANDLING -------------------------------------- 1014 1015 /** 1016 * Super-lazy instantiation pattern from Bill Pugh. 1017 * @treatAsPrivate implementation detail 1018 */ 1019 private static class StyleableProperties { 1020 private static final CssMetaData<PieChart,Boolean> CLOCKWISE = 1021 new CssMetaData<PieChart,Boolean>("-fx-clockwise", 1022 BooleanConverter.getInstance(), Boolean.TRUE) { 1023 1024 @Override 1025 public boolean isSettable(PieChart node) { 1026 return node.clockwise == null || !node.clockwise.isBound(); 1027 } 1028 1029 @Override 1030 public StyleableProperty<Boolean> getStyleableProperty(PieChart node) { 1031 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.clockwiseProperty(); 1032 } 1033 }; 1034 1035 private static final CssMetaData<PieChart,Boolean> LABELS_VISIBLE = 1036 new CssMetaData<PieChart,Boolean>("-fx-pie-label-visible", 1037 BooleanConverter.getInstance(), Boolean.TRUE) { 1038 1039 @Override 1040 public boolean isSettable(PieChart node) { 1041 return node.labelsVisible == null || !node.labelsVisible.isBound(); 1042 } 1043 1044 @Override 1045 public StyleableProperty<Boolean> getStyleableProperty(PieChart node) { 1046 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.labelsVisibleProperty(); 1047 } 1048 }; 1049 1050 private static final CssMetaData<PieChart,Number> LABEL_LINE_LENGTH = 1051 new CssMetaData<PieChart,Number>("-fx-label-line-length", 1052 SizeConverter.getInstance(), 20d) { 1053 1054 @Override 1055 public boolean isSettable(PieChart node) { 1056 return node.labelLineLength == null || !node.labelLineLength.isBound(); 1057 } 1058 1059 @Override 1060 public StyleableProperty<Number> getStyleableProperty(PieChart node) { 1061 return (StyleableProperty<Number>)(WritableValue<Number>)node.labelLineLengthProperty(); 1062 } 1063 }; 1064 1065 private static final CssMetaData<PieChart,Number> START_ANGLE = 1066 new CssMetaData<PieChart,Number>("-fx-start-angle", 1067 SizeConverter.getInstance(), 0d) { 1068 1069 @Override 1070 public boolean isSettable(PieChart node) { 1071 return node.startAngle == null || !node.startAngle.isBound(); 1072 } 1073 1074 @Override 1075 public StyleableProperty<Number> getStyleableProperty(PieChart node) { 1076 return (StyleableProperty<Number>)(WritableValue<Number>)node.startAngleProperty(); 1077 } 1078 }; 1079 1080 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1081 static { 1082 1083 final List<CssMetaData<? extends Styleable, ?>> styleables = 1084 new ArrayList<CssMetaData<? extends Styleable, ?>>(Chart.getClassCssMetaData()); 1085 styleables.add(CLOCKWISE); 1086 styleables.add(LABELS_VISIBLE); 1087 styleables.add(LABEL_LINE_LENGTH); 1088 styleables.add(START_ANGLE); 1089 STYLEABLES = Collections.unmodifiableList(styleables); 1090 } 1091 } 1092 1093 /** 1094 * @return The CssMetaData associated with this class, which may include the 1095 * CssMetaData of its super classes. 1096 * @since JavaFX 8.0 1097 */ 1098 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1099 return StyleableProperties.STYLEABLES; 1100 } 1101 1102 /** 1103 * {@inheritDoc} 1104 * @since JavaFX 8.0 1105 */ 1106 @Override 1107 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 1108 return getClassCssMetaData(); 1109 } 1110 1111 } 1112 1113