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