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