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