Java tutorial
/* * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.chart; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.List; import java.util.Objects; import javafx.animation.Animation; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.binding.StringBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.StringProperty; import javafx.beans.property.StringPropertyBase; import javafx.beans.value.WritableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.NodeOrientation; import javafx.geometry.Side; import javafx.scene.AccessibleRole; import javafx.scene.Node; import javafx.scene.layout.Region; import javafx.scene.shape.Arc; import javafx.scene.shape.ArcTo; import javafx.scene.shape.ArcType; import javafx.scene.shape.ClosePath; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; import javafx.scene.text.Text; import javafx.scene.transform.Scale; import javafx.util.Duration; import com.sun.javafx.charts.Legend; import com.sun.javafx.charts.Legend.LegendItem; import com.sun.javafx.collections.NonIterableChange; import javafx.css.StyleableBooleanProperty; import javafx.css.StyleableDoubleProperty; import javafx.css.CssMetaData; import javafx.css.converter.BooleanConverter; import javafx.css.converter.SizeConverter; import javafx.css.Styleable; import javafx.css.StyleableProperty; /** * Displays a PieChart. The chart content is populated by pie slices based on * data set on the PieChart. * <p> The clockwise property is set to true by default, which means slices are * placed in the clockwise order. The labelsVisible property is used to either display * pie slice labels or not. * * @since JavaFX 2.0 */ public class PieChart extends Chart { // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- private static final int MIN_PIE_RADIUS = 25; private static final double LABEL_TICK_GAP = 6; private static final double LABEL_BALL_RADIUS = 2; private BitSet colorBits = new BitSet(8); private double pieRadius; private Data begin = null; private final Path labelLinePath = new Path() { @Override public boolean usesMirroring() { return false; } }; private List<LabelLayoutInfo> labelLayoutInfos = null; private Legend legend = new Legend(); private Data dataItemBeingRemoved = null; private Timeline dataRemoveTimeline = null; private final ListChangeListener<Data> dataChangeListener = c -> { while (c.next()) { // RT-28090 Probably a sort happened, just reorder the pointers. if (c.wasPermutated()) { Data ptr = begin; for (int i = 0; i < getData().size(); i++) { Data item = getData().get(i); updateDataItemStyleClass(item, i); if (i == 0) { begin = item; ptr = begin; begin.next = null; } else { ptr.next = item; item.next = null; ptr = item; } } updateLegend(); requestChartLayout(); return; } // recreate linked list & set chart on new data for (int i = c.getFrom(); i < c.getTo(); i++) { Data item = getData().get(i); item.setChart(PieChart.this); if (begin == null) { begin = item; begin.next = null; } else { if (i == 0) { item.next = begin; begin = item; } else { Data ptr = begin; for (int j = 0; j < i - 1; j++) { ptr = ptr.next; } item.next = ptr.next; ptr.next = item; } } } // call data added/removed methods for (Data item : c.getRemoved()) { dataItemRemoved(item); } for (int i = c.getFrom(); i < c.getTo(); i++) { Data item = getData().get(i); // assign default color to the added slice // TODO: check nearby colors item.defaultColorIndex = colorBits.nextClearBit(0); colorBits.set(item.defaultColorIndex); dataItemAdded(item, i); } if (c.wasRemoved() || c.wasAdded()) { for (int i = 0; i < getData().size(); i++) { Data item = getData().get(i); updateDataItemStyleClass(item, i); } updateLegend(); } } // re-layout everything requestChartLayout(); }; // -------------- PUBLIC PROPERTIES ---------------------------------------- /** PieCharts data */ private ObjectProperty<ObservableList<Data>> data = new ObjectPropertyBase<ObservableList<Data>>() { private ObservableList<Data> old; @Override protected void invalidated() { final ObservableList<Data> current = getValue(); // add remove listeners if (old != null) old.removeListener(dataChangeListener); if (current != null) current.addListener(dataChangeListener); // fire data change event if series are added or removed if (old != null || current != null) { final List<Data> removed = (old != null) ? old : Collections.<Data>emptyList(); final int toIndex = (current != null) ? current.size() : 0; // let data listener know all old data have been removed and new data that has been added if (toIndex > 0 || !removed.isEmpty()) { dataChangeListener.onChanged(new NonIterableChange<Data>(0, toIndex, current) { @Override public List<Data> getRemoved() { return removed; } @Override public boolean wasPermutated() { return false; } @Override protected int[] getPermutation() { return new int[0]; } }); } } else if (old != null && old.size() > 0) { // let series listener know all old series have been removed dataChangeListener.onChanged(new NonIterableChange<Data>(0, 0, current) { @Override public List<Data> getRemoved() { return old; } @Override public boolean wasPermutated() { return false; } @Override protected int[] getPermutation() { return new int[0]; } }); } old = current; } public Object getBean() { return PieChart.this; } public String getName() { return "data"; } }; public final ObservableList<Data> getData() { return data.getValue(); } public final void setData(ObservableList<Data> value) { data.setValue(value); } public final ObjectProperty<ObservableList<Data>> dataProperty() { return data; } /** The angle to start the first pie slice at */ private DoubleProperty startAngle = new StyleableDoubleProperty(0) { @Override public void invalidated() { get(); requestChartLayout(); } @Override public Object getBean() { return PieChart.this; } @Override public String getName() { return "startAngle"; } public CssMetaData<PieChart, Number> getCssMetaData() { return StyleableProperties.START_ANGLE; } }; public final double getStartAngle() { return startAngle.getValue(); } public final void setStartAngle(double value) { startAngle.setValue(value); } public final DoubleProperty startAngleProperty() { return startAngle; } /** When true we start placing slices clockwise from the startAngle */ private BooleanProperty clockwise = new StyleableBooleanProperty(true) { @Override public void invalidated() { get(); requestChartLayout(); } @Override public Object getBean() { return PieChart.this; } @Override public String getName() { return "clockwise"; } public CssMetaData<PieChart, Boolean> getCssMetaData() { return StyleableProperties.CLOCKWISE; } }; public final void setClockwise(boolean value) { clockwise.setValue(value); } public final boolean isClockwise() { return clockwise.getValue(); } public final BooleanProperty clockwiseProperty() { return clockwise; } /** The length of the line from the outside of the pie to the slice labels. */ private DoubleProperty labelLineLength = new StyleableDoubleProperty(20d) { @Override public void invalidated() { get(); requestChartLayout(); } @Override public Object getBean() { return PieChart.this; } @Override public String getName() { return "labelLineLength"; } public CssMetaData<PieChart, Number> getCssMetaData() { return StyleableProperties.LABEL_LINE_LENGTH; } }; public final double getLabelLineLength() { return labelLineLength.getValue(); } public final void setLabelLineLength(double value) { labelLineLength.setValue(value); } public final DoubleProperty labelLineLengthProperty() { return labelLineLength; } /** When true pie slice labels are drawn */ private BooleanProperty labelsVisible = new StyleableBooleanProperty(true) { @Override public void invalidated() { get(); requestChartLayout(); } @Override public Object getBean() { return PieChart.this; } @Override public String getName() { return "labelsVisible"; } public CssMetaData<PieChart, Boolean> getCssMetaData() { return StyleableProperties.LABELS_VISIBLE; } }; public final void setLabelsVisible(boolean value) { labelsVisible.setValue(value); } /** * Indicates whether pie slice labels are drawn or not * @return true if pie slice labels are visible and false otherwise. */ public final boolean getLabelsVisible() { return labelsVisible.getValue(); } public final BooleanProperty labelsVisibleProperty() { return labelsVisible; } // -------------- CONSTRUCTOR ---------------------------------------------- /** * Construct a new empty PieChart. */ public PieChart() { this(FXCollections.<Data>observableArrayList()); } /** * Construct a new PieChart with the given data * * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart */ public PieChart(ObservableList<PieChart.Data> data) { getChartChildren().add(labelLinePath); labelLinePath.getStyleClass().add("chart-pie-label-line"); setLegend(legend); setData(data); // set chart content mirroring to be always false i.e. chartContent mirrorring is not done // when node orientation is right-to-left for PieChart. useChartContentMirroring = false; } // -------------- METHODS -------------------------------------------------- private void dataNameChanged(Data item) { item.textNode.setText(item.getName()); requestChartLayout(); updateLegend(); } private void dataPieValueChanged(Data item) { if (shouldAnimate()) { animate(new KeyFrame(Duration.ZERO, new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue())), new KeyFrame(Duration.millis(500), new KeyValue(item.currentPieValueProperty(), item.getPieValue(), Interpolator.EASE_BOTH))); } else { item.setCurrentPieValue(item.getPieValue()); requestChartLayout(); // RT-23091 } } private Node createArcRegion(Data item) { Node arcRegion = item.getNode(); // check if symbol has already been created if (arcRegion == null) { arcRegion = new Region(); arcRegion.setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); arcRegion.setPickOnBounds(false); item.setNode(arcRegion); } return arcRegion; } private Text createPieLabel(Data item) { Text text = item.textNode; text.setText(item.getName()); return text; } private void updateDataItemStyleClass(final Data item, int index) { Node node = item.getNode(); if (node != null) { // Note: not sure if we want to add or check, ie be more careful and efficient here node.getStyleClass().setAll("chart-pie", "data" + index, "default-color" + item.defaultColorIndex % 8); if (item.getPieValue() < 0) { node.getStyleClass().add("negative"); } } } private void dataItemAdded(final Data item, int index) { // create shape Node shape = createArcRegion(item); final Text text = createPieLabel(item); item.getChart().getChartChildren().add(shape); if (shouldAnimate()) { // if the same data item is being removed, first stop the remove animation, // remove the item and then start the add animation. if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) { if (dataItemBeingRemoved == item) { dataRemoveTimeline.stop(); dataRemoveTimeline = null; getChartChildren().remove(item.textNode); getChartChildren().remove(shape); removeDataItemRef(item); } } animate(new KeyFrame(Duration.ZERO, new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue()), new KeyValue(item.radiusMultiplierProperty(), item.getRadiusMultiplier())), new KeyFrame(Duration.millis(500), actionEvent -> { text.setOpacity(0); // RT-23597 : item's chart might have been set to null if // this item is added and removed before its add animation finishes. if (item.getChart() == null) item.setChart(PieChart.this); item.getChart().getChartChildren().add(text); FadeTransition ft = new FadeTransition(Duration.millis(150), text); ft.setToValue(1); ft.play(); }, new KeyValue(item.currentPieValueProperty(), item.getPieValue(), Interpolator.EASE_BOTH), new KeyValue(item.radiusMultiplierProperty(), 1, Interpolator.EASE_BOTH))); } else { getChartChildren().add(text); item.setRadiusMultiplier(1); item.setCurrentPieValue(item.getPieValue()); } // we sort the text nodes to always be at the end of the children list, so they have a higher z-order // (Fix for RT-34564) for (int i = 0; i < getChartChildren().size(); i++) { Node n = getChartChildren().get(i); if (n instanceof Text) { n.toFront(); } } } private void removeDataItemRef(Data item) { if (begin == item) { begin = item.next; } else { Data ptr = begin; while (ptr != null && ptr.next != item) { ptr = ptr.next; } if (ptr != null) ptr.next = item.next; } } private Timeline createDataRemoveTimeline(final Data item) { final Node shape = item.getNode(); Timeline t = new Timeline(); t.getKeyFrames().addAll( new KeyFrame(Duration.ZERO, new KeyValue(item.currentPieValueProperty(), item.getCurrentPieValue()), new KeyValue(item.radiusMultiplierProperty(), item.getRadiusMultiplier())), new KeyFrame(Duration.millis(500), actionEvent -> { // removing item colorBits.clear(item.defaultColorIndex); getChartChildren().remove(shape); // fade out label FadeTransition ft = new FadeTransition(Duration.millis(150), item.textNode); ft.setFromValue(1); ft.setToValue(0); ft.setOnFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { getChartChildren().remove(item.textNode); // remove chart references from old data - RT-22553 item.setChart(null); removeDataItemRef(item); item.textNode.setOpacity(1.0); } }); ft.play(); }, new KeyValue(item.currentPieValueProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(item.radiusMultiplierProperty(), 0))); return t; } private void dataItemRemoved(final Data item) { final Node shape = item.getNode(); if (shouldAnimate()) { dataRemoveTimeline = createDataRemoveTimeline(item); dataItemBeingRemoved = item; animate(dataRemoveTimeline); } else { colorBits.clear(item.defaultColorIndex); getChartChildren().remove(item.textNode); getChartChildren().remove(shape); // remove chart references from old data item.setChart(null); removeDataItemRef(item); } } /** {@inheritDoc} */ @Override protected void layoutChartChildren(double top, double left, double contentWidth, double contentHeight) { double total = 0.0; for (Data item = begin; item != null; item = item.next) { total += Math.abs(item.getCurrentPieValue()); } double scale = (total != 0) ? 360 / total : 0; // calculate combined bounds of all labels & pie radius double[] labelsX = null; double[] labelsY = null; double[] labelAngles = null; double labelScale = 1; List<LabelLayoutInfo> fullPie = null; boolean shouldShowLabels = getLabelsVisible(); if (shouldShowLabels) { double xPad = 0d; double yPad = 0d; labelsX = new double[getDataSize()]; labelsY = new double[getDataSize()]; labelAngles = new double[getDataSize()]; fullPie = new ArrayList<>(); int index = 0; double start = getStartAngle(); for (Data item = begin; item != null; item = item.next) { // remove any scale on the text node item.textNode.getTransforms().clear(); double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue())); labelAngles[index] = normalizeAngle(start + (size / 2)); final double sproutX = calcX(labelAngles[index], getLabelLineLength(), 0); final double sproutY = calcY(labelAngles[index], getLabelLineLength(), 0); labelsX[index] = sproutX; labelsY[index] = sproutY; xPad = Math.max(xPad, 2 * (item.textNode.getLayoutBounds().getWidth() + LABEL_TICK_GAP + Math.abs(sproutX))); if (sproutY > 0) { // on bottom yPad = Math.max(yPad, 2 * Math.abs(sproutY + item.textNode.getLayoutBounds().getMaxY())); } else { // on top yPad = Math.max(yPad, 2 * Math.abs(sproutY + item.textNode.getLayoutBounds().getMinY())); } start += size; index++; } pieRadius = Math.min(contentWidth - xPad, contentHeight - yPad) / 2; // check if this makes the pie too small if (pieRadius < MIN_PIE_RADIUS) { // calculate scale for text to fit labels in final double roomX = contentWidth - MIN_PIE_RADIUS - MIN_PIE_RADIUS; final double roomY = contentHeight - MIN_PIE_RADIUS - MIN_PIE_RADIUS; labelScale = Math.min(roomX / xPad, roomY / yPad); // hide labels if pie radius is less than minimum if ((begin == null && labelScale < 0.7) || ((begin.textNode.getFont().getSize() * labelScale) < 9)) { shouldShowLabels = false; labelScale = 1; } else { // set pieRadius to minimum pieRadius = MIN_PIE_RADIUS; // apply scale to all label positions for (int i = 0; i < labelsX.length; i++) { labelsX[i] = labelsX[i] * labelScale; labelsY[i] = labelsY[i] * labelScale; } } } } if (!shouldShowLabels) { pieRadius = Math.min(contentWidth, contentHeight) / 2; labelLinePath.getElements().clear(); } if (getChartChildren().size() > 0) { double centerX = contentWidth / 2 + left; double centerY = contentHeight / 2 + top; int index = 0; for (Data item = begin; item != null; item = item.next) { // layout labels for pie slice item.textNode.setVisible(shouldShowLabels); if (shouldShowLabels) { double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue())); final boolean isLeftSide = !(labelAngles[index] > -90 && labelAngles[index] < 90); double sliceCenterEdgeX = calcX(labelAngles[index], pieRadius, centerX); double sliceCenterEdgeY = calcY(labelAngles[index], pieRadius, centerY); double xval = isLeftSide ? (labelsX[index] + sliceCenterEdgeX - item.textNode.getLayoutBounds().getMaxX() - LABEL_TICK_GAP) : (labelsX[index] + sliceCenterEdgeX - item.textNode.getLayoutBounds().getMinX() + LABEL_TICK_GAP); double yval = labelsY[index] + sliceCenterEdgeY - (item.textNode.getLayoutBounds().getMinY() / 2) - 2; // do the line (Path)for labels double lineEndX = sliceCenterEdgeX + labelsX[index]; double lineEndY = sliceCenterEdgeY + labelsY[index]; LabelLayoutInfo info = new LabelLayoutInfo(sliceCenterEdgeX, sliceCenterEdgeY, lineEndX, lineEndY, xval, yval, item.textNode, Math.abs(size)); fullPie.add(info); // set label scales if (labelScale < 1) { item.textNode.getTransforms().add(new Scale(labelScale, labelScale, isLeftSide ? item.textNode.getLayoutBounds().getWidth() : 0, 0)); } } index++; } // update/draw pie slices double sAngle = getStartAngle(); for (Data item = begin; item != null; item = item.next) { Node node = item.getNode(); Arc arc = null; if (node != null) { if (node instanceof Region) { Region arcRegion = (Region) node; if (arcRegion.getShape() == null) { arc = new Arc(); arcRegion.setShape(arc); } else { arc = (Arc) arcRegion.getShape(); } arcRegion.setScaleShape(false); arcRegion.setCenterShape(false); arcRegion.setCacheShape(false); } } double size = (isClockwise()) ? (-scale * Math.abs(item.getCurrentPieValue())) : (scale * Math.abs(item.getCurrentPieValue())); // update slice arc size arc.setStartAngle(sAngle); arc.setLength(size); arc.setType(ArcType.ROUND); arc.setRadiusX(pieRadius * item.getRadiusMultiplier()); arc.setRadiusY(pieRadius * item.getRadiusMultiplier()); node.setLayoutX(centerX); node.setLayoutY(centerY); sAngle += size; } // finally draw the text and line if (fullPie != null) { // Check for collision and resolve by hiding the label of the smaller pie slice resolveCollision(fullPie); if (!fullPie.equals(labelLayoutInfos)) { labelLinePath.getElements().clear(); for (LabelLayoutInfo info : fullPie) { if (info.text.isVisible()) drawLabelLinePath(info); } labelLayoutInfos = fullPie; } } } } // We check for pie slice label collision and if collision is detected, we then // compare the size of the slices, and hide the label of the smaller slice. private void resolveCollision(List<LabelLayoutInfo> list) { int boxH = (begin != null) ? (int) begin.textNode.getLayoutBounds().getHeight() : 0; for (int i = 0; i < list.size(); i++) { for (int j = i + 1; j < list.size(); j++) { LabelLayoutInfo box1 = list.get(i); LabelLayoutInfo box2 = list.get(j); if ((box1.text.isVisible() && box2.text.isVisible()) && (fuzzyGT(box2.textY, box1.textY) ? fuzzyLT((box2.textY - boxH - box1.textY), 2) : fuzzyLT((box1.textY - boxH - box2.textY), 2)) && (fuzzyGT(box1.textX, box2.textX) ? fuzzyLT((box1.textX - box2.textX), box2.text.prefWidth(-1)) : fuzzyLT((box2.textX - box1.textX), box1.text.prefWidth(-1)))) { if (fuzzyLT(box1.size, box2.size)) { box1.text.setVisible(false); } else { box2.text.setVisible(false); } } } } } private int fuzzyCompare(double o1, double o2) { double fuzz = 0.00001; return (((Math.abs(o1 - o2)) < fuzz) ? 0 : ((o1 < o2) ? -1 : 1)); } private boolean fuzzyGT(double o1, double o2) { return fuzzyCompare(o1, o2) == 1; } private boolean fuzzyLT(double o1, double o2) { return fuzzyCompare(o1, o2) == -1; } private void drawLabelLinePath(LabelLayoutInfo info) { info.text.setLayoutX(info.textX); info.text.setLayoutY(info.textY); labelLinePath.getElements().add(new MoveTo(info.startX, info.startY)); labelLinePath.getElements().add(new LineTo(info.endX, info.endY)); labelLinePath.getElements().add(new MoveTo(info.endX - LABEL_BALL_RADIUS, info.endY)); labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 90, info.endX, info.endY - LABEL_BALL_RADIUS, false, true)); labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 90, info.endX + LABEL_BALL_RADIUS, info.endY, false, true)); labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 90, info.endX, info.endY + LABEL_BALL_RADIUS, false, true)); labelLinePath.getElements().add(new ArcTo(LABEL_BALL_RADIUS, LABEL_BALL_RADIUS, 90, info.endX - LABEL_BALL_RADIUS, info.endY, false, true)); labelLinePath.getElements().add(new ClosePath()); } /** * This is called whenever a series is added or removed and the legend needs to be updated */ private void updateLegend() { Node legendNode = getLegend(); if (legendNode != null && legendNode != legend) return; // RT-23596 dont update when user has set legend. legend.setVertical(getLegendSide().equals(Side.LEFT) || getLegendSide().equals(Side.RIGHT)); List<Legend.LegendItem> legendList = new ArrayList<>(); if (getData() != null) { for (Data item : getData()) { LegendItem legenditem = new LegendItem(item.getName()); legenditem.getSymbol().getStyleClass().addAll(item.getNode().getStyleClass()); legenditem.getSymbol().getStyleClass().add("pie-legend-symbol"); legendList.add(legenditem); } } legend.getItems().setAll(legendList); if (legendList.size() > 0) { if (legendNode == null) { setLegend(legend); } } else { setLegend(null); } } private int getDataSize() { int count = 0; for (Data d = begin; d != null; d = d.next) { count++; } return count; } private static double calcX(double angle, double radius, double centerX) { return (double) (centerX + radius * Math.cos(Math.toRadians(-angle))); } private static double calcY(double angle, double radius, double centerY) { return (double) (centerY + radius * Math.sin(Math.toRadians(-angle))); } /** Normalize any angle into -180 to 180 deg range */ private static double normalizeAngle(double angle) { double a = angle % 360; if (a <= -180) a += 360; if (a > 180) a -= 360; return a; } // -------------- INNER CLASSES -------------------------------------------- // Class holding label line layout info for collision detection and removal private final static class LabelLayoutInfo { double startX; double startY; double endX; double endY; double textX; double textY; Text text; double size; LabelLayoutInfo(double startX, double startY, double endX, double endY, double textX, double textY, Text text, double size) { this.startX = startX; this.startY = startY; this.endX = endX; this.endY = endY; this.textX = textX; this.textY = textY; this.text = text; this.size = size; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LabelLayoutInfo that = (LabelLayoutInfo) o; return Double.compare(that.startX, startX) == 0 && Double.compare(that.startY, startY) == 0 && Double.compare(that.endX, endX) == 0 && Double.compare(that.endY, endY) == 0 && Double.compare(that.textX, textX) == 0 && Double.compare(that.textY, textY) == 0 && Double.compare(that.size, size) == 0; } @Override public int hashCode() { return Objects.hash(startX, startY, endX, endY, textX, textY, size); } } /** * PieChart Data Item, represents one slice in the PieChart * * @since JavaFX 2.0 */ public final static class Data { private Text textNode = new Text(); /** * Next pointer for the next data item : so we can do animation on data delete. */ private Data next = null; /** * Default color index for this slice. */ private int defaultColorIndex; // -------------- PUBLIC PROPERTIES ------------------------------------ /** * The chart which this data belongs to. */ private ReadOnlyObjectWrapper<PieChart> chart = new ReadOnlyObjectWrapper<PieChart>(this, "chart"); public final PieChart getChart() { return chart.getValue(); } private void setChart(PieChart value) { chart.setValue(value); } public final ReadOnlyObjectProperty<PieChart> chartProperty() { return chart.getReadOnlyProperty(); } /** * The name of the pie slice */ private StringProperty name = new StringPropertyBase() { @Override protected void invalidated() { if (getChart() != null) getChart().dataNameChanged(Data.this); } @Override public Object getBean() { return Data.this; } @Override public String getName() { return "name"; } }; public final void setName(java.lang.String value) { name.setValue(value); } public final java.lang.String getName() { return name.getValue(); } public final StringProperty nameProperty() { return name; } /** * The value of the pie slice */ private DoubleProperty pieValue = new DoublePropertyBase() { @Override protected void invalidated() { if (getChart() != null) getChart().dataPieValueChanged(Data.this); } @Override public Object getBean() { return Data.this; } @Override public String getName() { return "pieValue"; } }; public final double getPieValue() { return pieValue.getValue(); } public final void setPieValue(double value) { pieValue.setValue(value); } public final DoubleProperty pieValueProperty() { return pieValue; } /** * The current pie value, used during animation. This will be the last data value, new data value or * anywhere in between */ private DoubleProperty currentPieValue = new SimpleDoubleProperty(this, "currentPieValue"); private double getCurrentPieValue() { return currentPieValue.getValue(); } private void setCurrentPieValue(double value) { currentPieValue.setValue(value); } private DoubleProperty currentPieValueProperty() { return currentPieValue; } /** * Multiplier that is used to animate the radius of the pie slice */ private DoubleProperty radiusMultiplier = new SimpleDoubleProperty(this, "radiusMultiplier"); private double getRadiusMultiplier() { return radiusMultiplier.getValue(); } private void setRadiusMultiplier(double value) { radiusMultiplier.setValue(value); } private DoubleProperty radiusMultiplierProperty() { return radiusMultiplier; } /** * Readonly access to the node that represents the pie slice. You can use this to add mouse event listeners etc. */ private ReadOnlyObjectWrapper<Node> node = new ReadOnlyObjectWrapper<>(this, "node"); /** * Returns the node that represents the pie slice. You can use this to * add mouse event listeners etc. * @return the node that represents the pie slice */ public Node getNode() { return node.getValue(); } private void setNode(Node value) { node.setValue(value); } public ReadOnlyObjectProperty<Node> nodeProperty() { return node.getReadOnlyProperty(); } // -------------- CONSTRUCTOR ------------------------------------------------- /** * Constructs a PieChart.Data object with the given name and value. * * @param name name for Pie * @param value pie value */ public Data(java.lang.String name, double value) { setName(name); setPieValue(value); textNode.getStyleClass().addAll("text", "chart-pie-label"); textNode.setAccessibleRole(AccessibleRole.TEXT); textNode.setAccessibleRoleDescription("slice"); textNode.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); textNode.accessibleTextProperty().bind(new StringBinding() { { bind(nameProperty(), currentPieValueProperty()); } @Override protected String computeValue() { return getName() + " represents " + getCurrentPieValue() + " percent"; } }); } // -------------- PUBLIC METHODS ---------------------------------------------- /** * Returns a string representation of this {@code Data} object. * * @return a string representation of this {@code Data} object. */ @Override public java.lang.String toString() { return "Data[" + getName() + "," + getPieValue() + "]"; } } // -------------- STYLESHEET HANDLING -------------------------------------- /* * Super-lazy instantiation pattern from Bill Pugh. */ private static class StyleableProperties { private static final CssMetaData<PieChart, Boolean> CLOCKWISE = new CssMetaData<PieChart, Boolean>( "-fx-clockwise", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(PieChart node) { return node.clockwise == null || !node.clockwise.isBound(); } @Override public StyleableProperty<Boolean> getStyleableProperty(PieChart node) { return (StyleableProperty<Boolean>) (WritableValue<Boolean>) node.clockwiseProperty(); } }; private static final CssMetaData<PieChart, Boolean> LABELS_VISIBLE = new CssMetaData<PieChart, Boolean>( "-fx-pie-label-visible", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(PieChart node) { return node.labelsVisible == null || !node.labelsVisible.isBound(); } @Override public StyleableProperty<Boolean> getStyleableProperty(PieChart node) { return (StyleableProperty<Boolean>) (WritableValue<Boolean>) node.labelsVisibleProperty(); } }; private static final CssMetaData<PieChart, Number> LABEL_LINE_LENGTH = new CssMetaData<PieChart, Number>( "-fx-label-line-length", SizeConverter.getInstance(), 20d) { @Override public boolean isSettable(PieChart node) { return node.labelLineLength == null || !node.labelLineLength.isBound(); } @Override public StyleableProperty<Number> getStyleableProperty(PieChart node) { return (StyleableProperty<Number>) (WritableValue<Number>) node.labelLineLengthProperty(); } }; private static final CssMetaData<PieChart, Number> START_ANGLE = new CssMetaData<PieChart, Number>( "-fx-start-angle", SizeConverter.getInstance(), 0d) { @Override public boolean isSettable(PieChart node) { return node.startAngle == null || !node.startAngle.isBound(); } @Override public StyleableProperty<Number> getStyleableProperty(PieChart node) { return (StyleableProperty<Number>) (WritableValue<Number>) node.startAngleProperty(); } }; private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; static { final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>( Chart.getClassCssMetaData()); styleables.add(CLOCKWISE); styleables.add(LABELS_VISIBLE); styleables.add(LABEL_LINE_LENGTH); styleables.add(START_ANGLE); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its superclasses. * @since JavaFX 8.0 */ public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * @since JavaFX 8.0 */ @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { return getClassCssMetaData(); } }