Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.chart;
027
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.List;
031import javafx.animation.Animation;
032import javafx.animation.FadeTransition;
033import javafx.animation.Interpolator;
034import javafx.animation.KeyFrame;
035import javafx.animation.KeyValue;
036import javafx.animation.Timeline;
037import javafx.beans.property.BooleanProperty;
038import javafx.beans.property.DoubleProperty;
039import javafx.beans.property.DoublePropertyBase;
040import javafx.beans.property.ObjectProperty;
041import javafx.beans.property.ObjectPropertyBase;
042import javafx.beans.property.ReadOnlyObjectProperty;
043import javafx.beans.property.ReadOnlyObjectWrapper;
044import javafx.beans.property.SimpleDoubleProperty;
045import javafx.beans.property.SimpleObjectProperty;
046import javafx.beans.property.StringProperty;
047import javafx.beans.property.StringPropertyBase;
048import javafx.collections.FXCollections;
049import javafx.collections.ListChangeListener;
050import javafx.collections.ObservableList;
051import javafx.event.ActionEvent;
052import javafx.event.EventHandler;
053import javafx.geometry.Side;
054import javafx.scene.Node;
055import javafx.scene.layout.Region;
056import javafx.scene.shape.Arc;
057import javafx.scene.shape.ArcTo;
058import javafx.scene.shape.ArcType;
059import javafx.scene.shape.ClosePath;
060import javafx.scene.shape.LineTo;
061import javafx.scene.shape.MoveTo;
062import javafx.scene.shape.Path;
063import javafx.scene.text.Text;
064import javafx.scene.transform.Scale;
065import javafx.util.Duration;
066import com.sun.javafx.charts.Legend;
067import com.sun.javafx.charts.Legend.LegendItem;
068import com.sun.javafx.collections.NonIterableChange;
069import javafx.css.StyleableBooleanProperty;
070import javafx.css.StyleableDoubleProperty;
071import javafx.css.CssMetaData;
072import com.sun.javafx.css.converters.BooleanConverter;
073import com.sun.javafx.css.converters.SizeConverter;
074import javafx.css.Styleable;
075import javafx.css.StyleableProperty;
076
077/**
078 * Displays a PieChart. The chart content is populated by pie slices based on
079 * data set on the PieChart.
080 * <p> The clockwise property is set to true by default, which means slices are
081 * placed in the clockwise order. The labelsVisible property is used to either display
082 * pie slice labels or not.
083 *
084 */
085public class PieChart extends Chart {
086
087    // -------------- PRIVATE FIELDS -----------------------------------------------------------------------------------
088    private static final int MIN_PIE_RADIUS = 25;
089    private int defaultColorIndex = 0;
090    private static final double LABEL_TICK_GAP = 6;
091    private static final double LABEL_BALL_RADIUS = 2;
092    private double centerX;
093    private double centerY;
094    private double pieRadius;
095    private Data begin = null;
096    private final Path labelLinePath = new Path();
097    private Legend legend = new Legend();
098    private Data dataItemBeingRemoved = null;
099    private Timeline dataRemoveTimeline = null;
100    private final ListChangeListener<Data> dataChangeListener = new ListChangeListener<Data>() {
101        @Override public void onChanged(Change<? extends Data> c) {
102            while(c.next()) {
103                // RT-28090 Probably a sort happened, just reorder the pointers.
104                if (c.wasPermutated()) {
105                    Data ptr = begin;
106                    for(int i = 0; i < getData().size(); i++) {
107                        Data item = getData().get(i);
108                        if (i == 0) {
109                            begin = item;
110                            ptr = begin;
111                            begin.next = null;
112                        } else {
113                            ptr.next = item;
114                            item.next = null;
115                            ptr = item;
116                        }
117                    }
118                    requestChartLayout();
119                    return;
120                }
121            // recreate linked list & set chart on new data
122            for(int i=c.getFrom(); i<c.getTo(); i++) {
123                getData().get(i).setChart(PieChart.this);
124                if (begin == null) {
125                    begin = getData().get(i);
126                    begin.next = null;
127                } else {
128                    if (i == 0) {
129                        getData().get(0).next = begin;
130                        begin = getData().get(0);
131                    } else {
132                        Data ptr = begin;
133                        for (int j = 0; j < i -1 ; j++) {
134                            ptr = ptr.next;
135                        }
136                        getData().get(i).next = ptr.next;
137                        ptr.next = getData().get(i);
138                    }
139                }
140            }
141            // call data added/removed methods
142            for (Data item : c.getRemoved()) {
143                dataItemRemoved(item);
144            }
145            for(int i=c.getFrom(); i<c.getTo(); i++) {
146                Data item = getData().get(i);
147                dataItemAdded(i, item);
148            }
149            // update legend if any data has changed
150            if (isLegendVisible() && (c.getRemoved().size() > 0 || c.getFrom() < c.getTo())) updateLegend();
151            // re-layout everything
152            }
153            requestChartLayout();
154        }
155    };
156
157    // -------------- PUBLIC PROPERTIES ----------------------------------------
158
159    /** PieCharts data */
160    private ObjectProperty<ObservableList<Data>> data = new ObjectPropertyBase<ObservableList<Data>>() {
161        private ObservableList<Data> old;
162        @Override protected void invalidated() {
163            final ObservableList<Data> current = getValue();
164            // add remove listeners
165            if(old != null) old.removeListener(dataChangeListener);
166            if(current != null) current.addListener(dataChangeListener);
167            // fire data change event if series are added or removed
168            if(old != null || current != null) {
169                final List<Data> removed = (old != null) ? old : Collections.<Data>emptyList();
170                final int toIndex = (current != null) ? current.size() : 0;
171                // let data listener know all old data have been removed and new data that has been added
172                if (toIndex > 0 || !removed.isEmpty()) {
173                    dataChangeListener.onChanged(new NonIterableChange<Data>(0, toIndex, current){
174                        @Override public List<Data> getRemoved() { return removed; }
175                        @Override public boolean wasPermutated() { return false; }
176                        @Override protected int[] getPermutation() {
177                            return new int[0];
178                        }
179                    });
180                }
181            } else if (old != null && old.size() > 0) {
182                // let series listener know all old series have been removed
183                dataChangeListener.onChanged(new NonIterableChange<Data>(0, 0, current){
184                    @Override public List<Data> getRemoved() { return old; }
185                    @Override public boolean wasPermutated() { return false; }
186                    @Override protected int[] getPermutation() {
187                        return new int[0];
188                    }
189                });
190            }
191            old = current;
192        }
193
194        public Object getBean() {
195            return PieChart.this;
196        }
197
198        public String getName() {
199            return "data";
200        }
201    };
202    public final ObservableList<Data> getData() { return data.getValue(); }
203    public final void setData(ObservableList<Data> value) { data.setValue(value); }
204    public final ObjectProperty<ObservableList<Data>> dataProperty() { return data; }
205
206    /** The angle to start the first pie slice at */
207    private DoubleProperty startAngle = new StyleableDoubleProperty(0) {
208        @Override public void invalidated() {
209            get();
210            requestChartLayout();
211        }
212
213        @Override
214        public Object getBean() {
215            return PieChart.this;
216        }
217
218        @Override
219        public String getName() {
220            return "startAngle";
221        }
222
223        public CssMetaData<PieChart,Number> getCssMetaData() {
224            return StyleableProperties.START_ANGLE;
225        }
226    };
227    public final double getStartAngle() { return startAngle.getValue(); }
228    public final void setStartAngle(double value) { startAngle.setValue(value); }
229    public final DoubleProperty startAngleProperty() { return startAngle; }
230
231    /** When true we start placing slices clockwise from the startAngle */
232    private BooleanProperty clockwise = new StyleableBooleanProperty(true) {
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 "clockwise";
246        }
247
248        public CssMetaData<PieChart,Boolean> getCssMetaData() {
249            return StyleableProperties.CLOCKWISE;
250        }
251    };
252    public final void setClockwise(boolean value) { clockwise.setValue(value);}
253    public final boolean isClockwise() { return clockwise.getValue(); }
254    public final BooleanProperty clockwiseProperty() { return clockwise; }
255
256
257    /** The length of the line from the outside of the pie to the slice labels. */
258    private DoubleProperty labelLineLength = new StyleableDoubleProperty(20d) {
259        @Override public void invalidated() {
260            get();
261            requestChartLayout();
262        }
263
264        @Override
265        public Object getBean() {
266            return PieChart.this;
267        }
268
269        @Override
270        public String getName() {
271            return "labelLineLength";
272        }
273
274        public CssMetaData<PieChart,Number> getCssMetaData() {
275            return StyleableProperties.LABEL_LINE_LENGTH;
276        }
277    };
278    public final double getLabelLineLength() { return labelLineLength.getValue(); }
279    public final void setLabelLineLength(double value) { labelLineLength.setValue(value); }
280    public final DoubleProperty labelLineLengthProperty() { return labelLineLength; }
281
282    /** When true pie slice labels are drawn */
283    private BooleanProperty labelsVisible = new StyleableBooleanProperty(true) {
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 "labelsVisible";
297        }
298
299        public CssMetaData<PieChart,Boolean> getCssMetaData() {
300            return StyleableProperties.LABELS_VISIBLE;
301        }
302    };
303    public final void setLabelsVisible(boolean value) { labelsVisible.setValue(value);}
304
305    /**
306     * Indicates whether pie slice labels are drawn or not
307     * @return true if pie slice labels are visible and false otherwise.
308     */
309    public final boolean getLabelsVisible() { return labelsVisible.getValue(); }
310    public final BooleanProperty labelsVisibleProperty() { return labelsVisible; }
311
312    // -------------- CONSTRUCTOR ----------------------------------------------
313
314    /**
315     * Construct a new empty PieChart.
316     */
317    public PieChart() {
318        this(FXCollections.<Data>observableArrayList());
319    }
320
321    /**
322     * Construct a new PieChart with the given data
323     *
324     * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
325     */
326    public PieChart(ObservableList<PieChart.Data> data) {
327        getChartChildren().add(labelLinePath);
328        labelLinePath.getStyleClass().add("chart-pie-label-line");
329        setLegend(legend);
330        setData(data);
331        // set chart content mirroring to be always false i.e. chartContent mirrorring is not done
332        // when  node orientation is right-to-left for PieChart.
333        useChartContentMirroring = false;
334    }
335
336    // -------------- METHODS --------------------------------------------------
337
338    @Override public void requestLayout() {
339        super.requestLayout();
340        // RT-22986 PieChart legend resize issue
341        if (legend != null) legend.requestLayout();
342    }
343    
344    private void dataNameChanged(Data item) {
345        item.textNode.setText(item.getName());
346        requestChartLayout();
347        updateLegend();
348    }
349
350    private void dataPieValueChanged(Data item) {
351        if (shouldAnimate()) {
352            animate(
353                new KeyFrame(Duration.ZERO, new KeyValue(item.currentPieValueProperty(),
354                        item.getCurrentPieValue())),
355                new KeyFrame(Duration.millis(500),new KeyValue(item.currentPieValueProperty(),
356                        item.getPieValue(), Interpolator.EASE_BOTH))
357            );
358        } else {
359            item.setCurrentPieValue(item.getPieValue());
360            requestChartLayout(); // RT-23091 
361        }
362    }
363
364    private Node createArcRegion(int itemIndex, Data item) {
365        Node arcRegion = item.getNode();
366        // check if symbol has already been created
367        if (arcRegion == null) {
368            arcRegion = new Region();
369            arcRegion.setPickOnBounds(false);
370            item.setNode(arcRegion);
371        }
372        // Note: not sure if we want to add or check, ie be more careful and efficient here
373        arcRegion.getStyleClass().setAll("chart-pie", "data" + itemIndex, item.defaultColorStyleString);
374        if (item.getPieValue() < 0) {
375            arcRegion.getStyleClass().add("negative");
376        }
377        return arcRegion;
378    }
379
380    private Text createPieLabel(int itemIndex, Data item) {
381        Text text = item.textNode;
382        text.setText(item.getName());
383        return text;
384    }
385
386    private void dataItemAdded(int itemIndex, final Data item) {
387        // set default color styleClass
388        item.defaultColorStyleString = "default-color"+(defaultColorIndex % 8);
389        defaultColorIndex ++;
390        // create shape
391        Node shape = createArcRegion(itemIndex, item);
392        final Text text = createPieLabel(itemIndex, item);
393        item.getChart().getChartChildren().add(shape);
394        if (shouldAnimate()) {
395            // if the same data item is being removed, first stop the remove animation,
396            // remove the item and then start the add animation.
397            if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) {
398                if (dataItemBeingRemoved == item) {
399                    dataRemoveTimeline.stop();
400                    dataRemoveTimeline = null;
401                    getChartChildren().remove(item.textNode);
402                    getChartChildren().remove(shape);
403                    removeDataItemRef(item);
404                }
405            }
406            animate(
407                new KeyFrame(Duration.ZERO,
408                    new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue()),
409                    new KeyValue(item.radiusMultiplierProperty(), item.getRadiusMultiplier())),
410                new KeyFrame(Duration.millis(500),
411                    new EventHandler<ActionEvent>() {
412                        @Override public void handle(ActionEvent actionEvent) {
413                            text.setOpacity(0);
414                            // RT-23597 : item's chart might have been set to null if
415                            // this item is added and removed before its add animation finishes.
416                            if (item.getChart() == null) item.setChart(PieChart.this);
417                            item.getChart().getChartChildren().add(text);
418                            FadeTransition ft = new FadeTransition(Duration.millis(150),text);
419                            ft.setToValue(1);
420                            ft.play();
421                        }
422                    },
423                    new KeyValue(item.currentPieValueProperty(), item.getPieValue(), Interpolator.EASE_BOTH),
424                    new KeyValue(item.radiusMultiplierProperty(), 1, Interpolator.EASE_BOTH))
425            );
426        } else {
427            getChartChildren().add(text);
428            item.setRadiusMultiplier(1);
429            item.setCurrentPieValue(item.getPieValue());
430        }
431    }
432
433    private void removeDataItemRef(Data item) {
434        if (begin == item) {
435            begin = item.next;
436        } else {
437            Data ptr = begin;
438            while(ptr != null && ptr.next != item) {
439                ptr = ptr.next;
440            }
441            if(ptr != null) ptr.next = item.next;
442        }
443    }
444    
445    private Timeline createDataRemoveTimeline(final Data item) {
446        final Node shape = item.getNode();
447        Timeline t = new Timeline();
448        t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO,
449                    new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue()),
450                    new KeyValue(item.radiusMultiplierProperty(), item.getRadiusMultiplier())),
451                new KeyFrame(Duration.millis(500),
452                    new EventHandler<ActionEvent>() {
453                        @Override public void handle(ActionEvent actionEvent) {
454                            // removing item
455                            getChartChildren().remove(shape);
456                            // fade out label
457                            FadeTransition ft = new FadeTransition(Duration.millis(150),item.textNode);
458                            ft.setFromValue(1);
459                            ft.setToValue(0);
460                            ft.setOnFinished(new EventHandler<ActionEvent>() {
461                                 @Override public void handle(ActionEvent actionEvent) {
462                                     getChartChildren().remove(item.textNode);
463                                     // remove chart references from old data - RT-22553
464                                     item.setChart(null);
465                                     removeDataItemRef(item);
466                                 }
467                            });
468                            ft.play();
469                        }
470                    },
471                    new KeyValue(item.currentPieValueProperty(), 0, Interpolator.EASE_BOTH),
472                    new KeyValue(item.radiusMultiplierProperty(), 0))
473                );
474        return t;
475    }
476
477    private void dataItemRemoved(final Data item) {
478        final Node shape = item.getNode();
479        if (shouldAnimate()) {
480            dataRemoveTimeline = createDataRemoveTimeline(item);
481            dataItemBeingRemoved = item;
482            animate(dataRemoveTimeline);
483        } else {
484            getChartChildren().remove(item.textNode);
485            getChartChildren().remove(shape);
486            // remove chart references from old data
487            item.setChart(null);
488            removeDataItemRef(item);
489        }
490    }
491
492    /** @inheritDoc */
493    @Override protected void layoutChartChildren(double top, double left, double contentWidth, double contentHeight) {
494        centerX = contentWidth/2 + left;
495        centerY = contentHeight/2 + top;
496        double total = 0.0;
497        for (Data item = begin; item != null; item = item.next) {
498            total+= Math.abs(item.getCurrentPieValue());
499        }
500        double scale = (total != 0) ? 360 / total : 0;
501
502        labelLinePath.getElements().clear();
503         // calculate combined bounds of all labels & pie radius
504        double minX = 0.0d;
505        double minY = 0.0d;
506        double maxX = 0.0d;
507        double maxY = 0.0d;
508        double[] labelsX = null;
509        double[] labelsY = null;
510        double[] labelAngles = null;
511        double labelScale = 1;
512        ArrayList<LabelLayoutInfo> fullPie = null;
513        boolean shouldShowLabels = getLabelsVisible();
514        if(getLabelsVisible()) {
515            labelsX = new double[getDataSize()];
516            labelsY = new double[getDataSize()];
517            labelAngles = new double[getDataSize()];
518            fullPie = new ArrayList<LabelLayoutInfo>();
519            int index = 0;
520            double start = getStartAngle();
521            for (Data item = begin; item != null; item = item.next) {
522                // remove any scale on the text node
523                item.textNode.getTransforms().clear();
524
525                double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue()));
526                labelAngles[index] = normalizeAngle(start + (size / 2));
527                final double sproutX = calcX(labelAngles[index], getLabelLineLength(), 0);
528                final double sproutY = calcY(labelAngles[index], getLabelLineLength(), 0);
529                labelsX[index] = sproutX;
530                labelsY[index] = sproutY;
531                if (sproutX < 0) { // on left
532                    minX = Math.min(minX, sproutX-item.textNode.getLayoutBounds().getWidth()-LABEL_TICK_GAP);
533                } else { // on right
534                    maxX = Math.max(maxX, sproutX+item.textNode.getLayoutBounds().getWidth()+LABEL_TICK_GAP);
535
536                }
537                if (sproutY > 0) { // on bottom
538                    maxY = Math.max(maxY, sproutY+item.textNode.getLayoutBounds().getMaxY());
539                } else { // on top
540                    minY = Math.min(minY, sproutY + item.textNode.getLayoutBounds().getMinY());
541                }
542                start+= size;
543                index++;
544            }
545            double xPad = (Math.max(Math.abs(minX), Math.abs(maxX))) * 2;
546            double yPad = (Math.max(Math.abs(minY), Math.abs(maxY))) * 2;
547            pieRadius = Math.min(contentWidth - xPad, contentHeight - yPad) / 2;
548            // check if this makes the pie too small
549            if (pieRadius < MIN_PIE_RADIUS ) {
550                // calculate scale for text to fit labels in
551                final double roomX = contentWidth-MIN_PIE_RADIUS-MIN_PIE_RADIUS;
552                final double roomY = contentHeight-MIN_PIE_RADIUS-MIN_PIE_RADIUS;
553                labelScale = Math.min(
554                        roomX/xPad,
555                        roomY/yPad
556                );
557                // hide labels if pie radius is less than minimum
558                if ((begin == null && labelScale < 0.7) || ((begin.textNode.getFont().getSize()*labelScale) < 9)) {
559                    shouldShowLabels = false;
560                    labelScale = 1;
561                } else {
562                    // set pieRadius to minimum
563                    pieRadius = MIN_PIE_RADIUS;
564                    // apply scale to all label positions
565                    for(int i=0; i< labelsX.length; i++) {
566                        labelsX[i] =  labelsX[i] * labelScale;
567                        labelsY[i] =  labelsY[i] * labelScale;
568                    }
569                }
570            }
571        }
572
573        if(!shouldShowLabels) {
574            pieRadius = Math.min(contentWidth,contentHeight) / 2;
575        }
576
577        if (getChartChildren().size() > 0) {
578            int index = 0;
579            for (Data item = begin; item != null; item = item.next) {
580                // layout labels for pie slice
581                item.textNode.setVisible(shouldShowLabels);
582                if (shouldShowLabels) {
583                    double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue()));
584                    final boolean isLeftSide = !(labelAngles[index] > -90 && labelAngles[index] < 90);
585                    
586                    double sliceCenterEdgeX = calcX(labelAngles[index], pieRadius, centerX);
587                    double sliceCenterEdgeY = calcY(labelAngles[index], pieRadius, centerY);
588                    double xval = isLeftSide ?
589                        (labelsX[index] + sliceCenterEdgeX - item.textNode.getLayoutBounds().getMaxX() - LABEL_TICK_GAP) :
590                        (labelsX[index] + sliceCenterEdgeX - item.textNode.getLayoutBounds().getMinX() + LABEL_TICK_GAP);
591                    double yval = labelsY[index] + sliceCenterEdgeY - (item.textNode.getLayoutBounds().getMinY()/2) -2;
592
593                    // do the line (Path)for labels
594                    double lineEndX = sliceCenterEdgeX +labelsX[index];
595                    double lineEndY = sliceCenterEdgeY +labelsY[index];
596                    LabelLayoutInfo info = new LabelLayoutInfo(sliceCenterEdgeX,
597                            sliceCenterEdgeY,lineEndX, lineEndY, xval, yval, item.textNode, Math.abs(size));
598                    fullPie.add(info);
599
600                    // set label scales
601                    if (labelScale < 1) {
602                        item.textNode.getTransforms().add(
603                            new Scale(
604                                    labelScale, labelScale,
605                                    isLeftSide ? item.textNode.getLayoutBounds().getWidth() : 0,
606//                                    0,
607                                    0
608                            )
609                        );
610                    }
611                }
612                index++;
613            }
614
615             // Check for collision and resolve by hiding the label of the smaller pie slice
616            resolveCollision(fullPie);
617
618            // update/draw pie slices
619            double sAngle = getStartAngle();
620            for (Data item = begin; item != null; item = item.next) {
621             Node node = item.getNode();
622                Arc arc = null;
623                 if (node != null) {
624                    if (node instanceof Region) {
625                        Region arcRegion = (Region)node;
626                        if( arcRegion.getShape() == null) {
627                            arc = new Arc();
628                            arcRegion.setShape(arc);
629                        } else {
630                            arc = (Arc)arcRegion.getShape();
631                        }
632                        arcRegion.setShape(null);
633                        arcRegion.setShape(arc);
634                        arcRegion.setScaleShape(false);
635                        arcRegion.setCenterShape(false);
636                        arcRegion.setCacheShape(false);
637                    }
638                }
639                double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue()));
640                // update slice arc size
641                arc.setStartAngle(sAngle);
642                arc.setLength(size);
643                arc.setType(ArcType.ROUND);
644                arc.setRadiusX(pieRadius * item.getRadiusMultiplier());
645                arc.setRadiusY(pieRadius * item.getRadiusMultiplier());
646                node.setLayoutX(centerX);
647                node.setLayoutY(centerY);
648                sAngle += size;
649            }
650            // finally draw the text and line
651            if (fullPie != null) {
652                for (LabelLayoutInfo info : fullPie) {
653                    if (info.text.isVisible()) drawLabelLinePath(info);
654                }
655            }
656        }
657    }
658
659    // We check for pie slice label collision and if collision is detected, we then
660    // compare the size of the slices, and hide the label of the smaller slice.
661    private void resolveCollision(ArrayList<LabelLayoutInfo> list) {
662        int boxH = (begin != null) ? (int)begin.textNode.getLayoutBounds().getHeight() : 0;
663        int i; int j;
664        for (i = 0, j = 1; list != null && j < list.size(); j++ ) {
665            LabelLayoutInfo box1 = list.get(i);
666            LabelLayoutInfo box2 = list.get(j);
667            if ((box1.text.isVisible() && box2.text.isVisible()) &&
668                    (fuzzyGT(box2.textY, box1.textY) ? fuzzyLT((box2.textY - boxH - box1.textY), 2) :
669                     fuzzyLT((box1.textY - boxH - box2.textY), 2)) &&
670                    (fuzzyGT(box1.textX, box2.textX) ? fuzzyLT((box1.textX - box2.textX), box2.text.prefWidth(-1)) :
671                        fuzzyLT((box2.textX - box1.textX), box1.text.prefWidth(-1)))) {
672                if (fuzzyLT(box1.size, box2.size)) {
673                    box1.text.setVisible(false);
674                    i = j;
675                } else {
676                    box2.text.setVisible(false);
677                }
678            } else {
679                i = j;
680            }
681        }
682    }
683
684    private int fuzzyCompare(double o1, double o2) {
685       double fuzz = 0.00001;
686       return (((Math.abs(o1 - o2)) < fuzz) ? 0 : ((o1 < o2) ? -1 : 1));
687    }
688
689    private boolean fuzzyGT(double o1, double o2) {
690        return (fuzzyCompare(o1, o2) == 1) ? true: false;
691    }
692
693    private boolean fuzzyLT(double o1, double o2) {
694        return (fuzzyCompare(o1, o2) == -1) ? true : false;
695    }
696
697    private void drawLabelLinePath(LabelLayoutInfo info) {
698        info.text.setLayoutX(info.textX);
699        info.text.setLayoutY(info.textY);
700        labelLinePath.getElements().add(new MoveTo(info.startX, info.startY));
701        labelLinePath.getElements().add(new LineTo(info.endX, info.endY));
702
703        labelLinePath.getElements().add(new MoveTo(info.endX-LABEL_BALL_RADIUS,info.endY));
704        labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS,
705                    90, info.endX,info.endY-LABEL_BALL_RADIUS, false, true));
706        labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS,
707                    90, info.endX+LABEL_BALL_RADIUS,info.endY, false, true));
708        labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS,
709                    90, info.endX,info.endY+LABEL_BALL_RADIUS, false, true));
710        labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS,
711                    90, info.endX-LABEL_BALL_RADIUS,info.endY, false, true));
712        labelLinePath.getElements().add(new ClosePath());
713    }
714    /**
715     * This is called whenever a series is added or removed and the legend needs to be updated
716     */
717    private void updateLegend() {
718        Node legendNode = getLegend();
719        if (legendNode != null && legendNode != legend) return; // RT-23569 dont update when user has set legend.
720        legend.setVertical(getLegendSide().equals(Side.LEFT) || getLegendSide().equals(Side.RIGHT));
721        legend.getItems().clear();
722        if (getData() != null) {
723            for (Data item : getData()) {
724                LegendItem legenditem = new LegendItem(item.getName());
725                legenditem.getSymbol().getStyleClass().addAll(item.getNode().getStyleClass());
726                legenditem.getSymbol().getStyleClass().add("pie-legend-symbol");
727                legend.getItems().add(legenditem);
728            }
729        }
730        if (legend.getItems().size() > 0) {
731            if (legendNode == null) {
732                setLegend(legend);
733            }
734        } else {
735            setLegend(null);
736        }
737    }
738
739    private int getDataSize() {
740        int count = 0;
741        for (Data d = begin; d != null; d = d.next) {
742            count++;
743        }
744        return count;
745    }
746
747    private static double calcX(double angle, double radius, double centerX) {
748        return (double)(centerX + radius * Math.cos(Math.toRadians(-angle)));
749    }
750
751    private static double calcY(double angle, double radius, double centerY) {
752        return (double)(centerY + radius * Math.sin(Math.toRadians(-angle)));
753    }
754
755     /** Normalize any angle into -180 to 180 deg range */
756    private static double normalizeAngle(double angle) {
757        double a = angle % 360;
758        if (a <= -180) a += 360;
759        if (a > 180) a -= 360;
760        return a;
761    }
762
763    // -------------- INNER CLASSES --------------------------------------------
764
765    // Class holding label line layout info for collision detection and removal
766    final static class LabelLayoutInfo {
767        double startX;
768        double startY;
769        double endX;
770        double endY;
771        double textX;
772        double textY;
773        Text text;
774        double size;
775
776        public LabelLayoutInfo(double startX, double startY, double endX, double endY,
777                double textX, double textY, Text text, double size) {
778            this.startX = startX;
779            this.startY = startY;
780            this.endX = endX;
781            this.endY = endY;
782            this.textX = textX;
783            this.textY = textY;
784            this.text = text;
785            this.size = size;
786        }
787    }
788    /**
789     * PieChart Data Item, represents one slice in the PieChart
790     */
791    public final static class Data {
792
793        private Text textNode = new Text();
794        /** Next pointer for the next data item : so we can do animation on data delete. */
795        private Data next = null;
796        private String defaultColorStyleString;
797
798        // -------------- PUBLIC PROPERTIES ------------------------------------
799
800        /** The chart which this data belongs to. */
801        private ReadOnlyObjectWrapper<PieChart> chart = new ReadOnlyObjectWrapper<PieChart>(this, "chart");
802        public final PieChart getChart() { return chart.getValue(); }
803        private void setChart(PieChart value) { chart.setValue(value); }
804        public final ReadOnlyObjectProperty<PieChart> chartProperty() { return chart.getReadOnlyProperty(); }
805
806        /** The name of the pie slice */
807        private StringProperty name = new StringPropertyBase()  {
808            @Override protected void invalidated() {
809                if(getChart()!=null) getChart().dataNameChanged(Data.this);
810            }
811
812            @Override
813            public Object getBean() {
814                return Data.this;
815            }
816
817            @Override
818            public String getName() {
819                return "name";
820            }
821        };
822        public final void setName(java.lang.String value) { name.setValue(value); }
823        public final java.lang.String getName() { return name.getValue(); }
824        public final StringProperty nameProperty() { return name; }
825
826        /** The value of the pie slice */
827        private DoubleProperty pieValue = new DoublePropertyBase() {
828            @Override protected void invalidated() {
829                if(getChart() !=null) getChart().dataPieValueChanged(Data.this);
830            }
831
832            @Override
833            public Object getBean() {
834                return Data.this;
835            }
836
837            @Override
838            public String getName() {
839                return "pieValue";
840            }
841        };
842        public final double getPieValue() { return pieValue.getValue(); }
843        public final void setPieValue(double value) { pieValue.setValue(value); }
844        public final DoubleProperty pieValueProperty() { return pieValue; }
845
846        /**
847         * The current pie value, used during animation. This will be the last data value, new data value or
848         * anywhere in between
849         */
850        private DoubleProperty currentPieValue = new SimpleDoubleProperty(this, "currentPieValue");
851        private double getCurrentPieValue() { return currentPieValue.getValue(); }
852        private void setCurrentPieValue(double value) { currentPieValue.setValue(value); }
853        private DoubleProperty currentPieValueProperty() { return currentPieValue; }
854
855        /** Multiplier that is used to animate the radius of the pie slice */
856        private DoubleProperty radiusMultiplier = new SimpleDoubleProperty(this, "radiusMultiplier");
857        private double getRadiusMultiplier() { return radiusMultiplier.getValue(); }
858        private void setRadiusMultiplier(double value) { radiusMultiplier.setValue(value); }
859        private DoubleProperty radiusMultiplierProperty() { return radiusMultiplier; }
860        
861        /**
862         * Readonly access to the node that represents the pie slice. You can use this to add mouse event listeners etc.
863         */
864        private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node");
865        public Node getNode() { return node.getValue(); }
866        private void setNode(Node value) { node.setValue(value); }
867        private ObjectProperty<Node> nodeProperty() { return node; }
868         
869        // -------------- CONSTRUCTOR -------------------------------------------------
870
871        /**
872         * Constructs a PieChart.Data object with the given name and value.
873         *
874         * @param name name for Pie
875         * @param value pie value
876         */
877        public Data(java.lang.String name, double value) {
878            setName(name);
879            setPieValue(value);
880            textNode.getStyleClass().addAll("text", "chart-pie-label");
881        }
882
883        // -------------- PUBLIC METHODS ----------------------------------------------
884
885        /**
886         * Returns a string representation of this {@code Data} object.
887         * @return a string representation of this {@code Data} object.
888         */ 
889        @Override public java.lang.String toString() {
890            return "Data["+getName()+","+getPieValue()+"]";
891        }
892    }
893
894    // -------------- STYLESHEET HANDLING --------------------------------------
895    
896    /**
897      * Super-lazy instantiation pattern from Bill Pugh.
898      * @treatAsPrivate implementation detail
899      */
900     private static class StyleableProperties {
901         private static final CssMetaData<PieChart,Boolean> CLOCKWISE = 
902             new CssMetaData<PieChart,Boolean>("-fx-clockwise",
903                 BooleanConverter.getInstance(), Boolean.TRUE) {
904
905            @Override
906            public boolean isSettable(PieChart node) {
907                return node.clockwise == null || !node.clockwise.isBound();
908            }
909
910            @Override
911            public StyleableProperty<Boolean> getStyleableProperty(PieChart node) {
912                return (StyleableProperty<Boolean>)node.clockwiseProperty();
913            }
914        };
915         
916         private static final CssMetaData<PieChart,Boolean> LABELS_VISIBLE = 
917             new CssMetaData<PieChart,Boolean>("-fx-pie-label-visible",
918                 BooleanConverter.getInstance(), Boolean.TRUE) {
919
920            @Override
921            public boolean isSettable(PieChart node) {
922                return node.labelsVisible == null || !node.labelsVisible.isBound();
923            }
924
925            @Override
926            public StyleableProperty<Boolean> getStyleableProperty(PieChart node) {
927                return (StyleableProperty<Boolean>)node.labelsVisibleProperty();
928            }
929        };
930         
931         private static final CssMetaData<PieChart,Number> LABEL_LINE_LENGTH = 
932             new CssMetaData<PieChart,Number>("-fx-label-line-length",
933                 SizeConverter.getInstance(), 20d) {
934
935            @Override
936            public boolean isSettable(PieChart node) {
937                return node.labelLineLength == null || !node.labelLineLength.isBound();
938            }
939
940            @Override
941            public StyleableProperty<Number> getStyleableProperty(PieChart node) {
942                return (StyleableProperty<Number>)node.labelLineLengthProperty();
943            }
944        };
945         
946         private static final CssMetaData<PieChart,Number> START_ANGLE = 
947             new CssMetaData<PieChart,Number>("-fx-start-angle",
948                 SizeConverter.getInstance(), 0d) {
949
950            @Override
951            public boolean isSettable(PieChart node) {
952                return node.startAngle == null || !node.startAngle.isBound();
953            }
954
955            @Override
956            public StyleableProperty<Number> getStyleableProperty(PieChart node) {
957                return (StyleableProperty<Number>)node.startAngleProperty();
958            }
959        };
960
961         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
962         static {
963
964            final List<CssMetaData<? extends Styleable, ?>> styleables =
965                new ArrayList<CssMetaData<? extends Styleable, ?>>(Chart.getClassCssMetaData());
966            styleables.add(CLOCKWISE);
967            styleables.add(LABELS_VISIBLE);
968            styleables.add(LABEL_LINE_LENGTH);
969            styleables.add(START_ANGLE);
970            STYLEABLES = Collections.unmodifiableList(styleables);
971         }
972    }
973
974    /**
975     * @return The CssMetaData associated with this class, which may include the
976     * CssMetaData of its super classes.
977     */
978    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
979        return StyleableProperties.STYLEABLES;
980    }
981
982    /**
983     * {@inheritDoc}
984     */
985    @Override
986    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
987        return getClassCssMetaData();
988    }
989
990}
991
992