1 /* 2 * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.chart; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 32 import com.sun.javafx.scene.control.skin.Utils; 33 34 import javafx.animation.Animation; 35 import javafx.animation.KeyFrame; 36 import javafx.application.Platform; 37 import javafx.beans.property.BooleanProperty; 38 import javafx.beans.property.ObjectProperty; 39 import javafx.beans.property.ObjectPropertyBase; 40 import javafx.beans.property.SimpleBooleanProperty; 41 import javafx.beans.property.StringProperty; 42 import javafx.beans.property.StringPropertyBase; 43 import javafx.beans.value.WritableValue; 44 import javafx.collections.ObservableList; 45 import javafx.geometry.Pos; 46 import javafx.geometry.Side; 47 import javafx.scene.Node; 48 import javafx.scene.control.Label; 49 import javafx.scene.layout.Pane; 50 import javafx.scene.layout.Region; 51 52 import com.sun.javafx.charts.ChartLayoutAnimator; 53 import com.sun.javafx.charts.Legend; 54 import com.sun.javafx.scene.NodeHelper; 55 56 import javafx.css.StyleableBooleanProperty; 57 import javafx.css.StyleableObjectProperty; 58 import javafx.css.CssMetaData; 59 60 import javafx.css.converter.BooleanConverter; 61 import javafx.css.converter.EnumConverter; 62 63 import javafx.css.Styleable; 64 import javafx.css.StyleableProperty; 65 66 /** 67 * Base class for all charts. It has 3 parts the title, legend and chartContent. The chart content is populated by the 68 * specific subclass of Chart. 69 * 70 * @since JavaFX 2.0 71 */ 72 public abstract class Chart extends Region { 73 74 // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- 75 76 private static final int MIN_WIDTH_TO_LEAVE_FOR_CHART_CONTENT = 200; 77 private static final int MIN_HEIGHT_TO_LEAVE_FOR_CHART_CONTENT = 150; 78 79 /** Title Label */ 80 private final Label titleLabel = new Label(); 81 /** 82 * This is the Pane that Chart subclasses use to contain the chart content, 83 * It is sized to be inside the chart area leaving space for the title and legend. 84 */ 85 private final Pane chartContent = new Pane() { 86 @Override protected void layoutChildren() { 87 final double top = snappedTopInset(); 88 final double left = snappedLeftInset(); 89 final double bottom = snappedBottomInset(); 90 final double right = snappedRightInset(); 91 final double width = getWidth(); 92 final double height = getHeight(); 93 final double contentWidth = snapSizeX(width - (left + right)); 94 final double contentHeight = snapSizeY(height - (top + bottom)); 95 layoutChartChildren(snapPositionY(top), snapPositionX(left), contentWidth, contentHeight); 96 } 97 @Override public boolean usesMirroring() { 98 return useChartContentMirroring; 99 } 100 }; 101 // Determines if chart content should be mirrored if node orientation is right-to-left. 102 boolean useChartContentMirroring = true; 103 104 /** Animator for animating stuff on the chart */ 105 private final ChartLayoutAnimator animator = new ChartLayoutAnimator(chartContent); 106 107 // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- 108 109 /** The chart title */ 110 private StringProperty title = new StringPropertyBase() { 111 @Override protected void invalidated() { 112 titleLabel.setText(get()); 113 } 114 115 @Override 116 public Object getBean() { 117 return Chart.this; 118 } 119 120 @Override 121 public String getName() { 122 return "title"; 123 } 124 }; 125 public final String getTitle() { return title.get(); } 126 public final void setTitle(String value) { title.set(value); } 127 public final StringProperty titleProperty() { return title; } 128 129 /** 130 * The side of the chart where the title is displayed 131 * @default Side.TOP 132 */ 133 private ObjectProperty<Side> titleSide = new StyleableObjectProperty<Side>(Side.TOP) { 134 @Override protected void invalidated() { 135 requestLayout(); 136 } 137 138 @Override 139 public CssMetaData<Chart,Side> getCssMetaData() { 140 return StyleableProperties.TITLE_SIDE; 141 } 142 143 @Override 144 public Object getBean() { 145 return Chart.this; 146 } 147 148 @Override 149 public String getName() { 150 return "titleSide"; 151 } 152 }; 153 public final Side getTitleSide() { return titleSide.get(); } 154 public final void setTitleSide(Side value) { titleSide.set(value); } 155 public final ObjectProperty<Side> titleSideProperty() { return titleSide; } 156 157 /** 158 * The node to display as the Legend. Subclasses can set a node here to be displayed on a side as the legend. If 159 * no legend is wanted then this can be set to null 160 */ 161 private final ObjectProperty<Node> legend = new ObjectPropertyBase<Node>() { 162 private Node old = null; 163 @Override protected void invalidated() { 164 Node newLegend = get(); 165 if (old != null) getChildren().remove(old); 166 if (newLegend != null) { 167 getChildren().add(newLegend); 168 newLegend.setVisible(isLegendVisible()); 169 } 170 old = newLegend; 171 } 172 173 @Override 174 public Object getBean() { 175 return Chart.this; 176 } 177 178 @Override 179 public String getName() { 180 return "legend"; 181 } 182 }; 183 protected final Node getLegend() { return legend.getValue(); } 184 protected final void setLegend(Node value) { legend.setValue(value); } 185 protected final ObjectProperty<Node> legendProperty() { return legend; } 186 187 /** 188 * When true the chart will display a legend if the chart implementation supports a legend. 189 */ 190 private final BooleanProperty legendVisible = new StyleableBooleanProperty(true) { 191 @Override protected void invalidated() { 192 requestLayout(); 193 } 194 195 @Override 196 public CssMetaData<Chart,Boolean> getCssMetaData() { 197 return StyleableProperties.LEGEND_VISIBLE; 198 } 199 200 @Override 201 public Object getBean() { 202 return Chart.this; 203 } 204 205 @Override 206 public String getName() { 207 return "legendVisible"; 208 } 209 }; 210 public final boolean isLegendVisible() { return legendVisible.getValue(); } 211 public final void setLegendVisible(boolean value) { legendVisible.setValue(value); } 212 public final BooleanProperty legendVisibleProperty() { return legendVisible; } 213 214 /** 215 * The side of the chart where the legend should be displayed 216 * 217 * @defaultValue Side.BOTTOM 218 */ 219 private ObjectProperty<Side> legendSide = new StyleableObjectProperty<Side>(Side.BOTTOM) { 220 @Override protected void invalidated() { 221 final Side legendSide = get(); 222 final Node legend = getLegend(); 223 if(legend instanceof Legend) ((Legend)legend).setVertical(Side.LEFT.equals(legendSide) || Side.RIGHT.equals(legendSide)); 224 requestLayout(); 225 } 226 227 @Override 228 public CssMetaData<Chart,Side> getCssMetaData() { 229 return StyleableProperties.LEGEND_SIDE; 230 } 231 232 @Override 233 public Object getBean() { 234 return Chart.this; 235 } 236 237 @Override 238 public String getName() { 239 return "legendSide"; 240 } 241 }; 242 public final Side getLegendSide() { return legendSide.get(); } 243 public final void setLegendSide(Side value) { legendSide.set(value); } 244 public final ObjectProperty<Side> legendSideProperty() { return legendSide; } 245 246 /** When true any data changes will be animated. */ 247 private BooleanProperty animated = new SimpleBooleanProperty(this, "animated", true); 248 249 /** 250 * Indicates whether data changes will be animated or not. 251 * 252 * @return true if data changes will be animated and false otherwise. 253 */ 254 public final boolean getAnimated() { return animated.get(); } 255 public final void setAnimated(boolean value) { animated.set(value); } 256 public final BooleanProperty animatedProperty() { return animated; } 257 258 // -------------- PROTECTED PROPERTIES ----------------------------------------------------------------------------- 259 260 /** 261 * Modifiable and observable list of all content in the chart. This is where implementations of Chart should add 262 * any nodes they use to draw their chart. This excludes the legend and title which are looked after by this class. 263 * 264 * @return Observable list of plot children 265 */ 266 protected ObservableList<Node> getChartChildren() { 267 return chartContent.getChildren(); 268 } 269 270 // -------------- CONSTRUCTOR -------------------------------------------------------------------------------------- 271 272 /** 273 * Creates a new default Chart instance. 274 */ 275 public Chart() { 276 titleLabel.setAlignment(Pos.CENTER); 277 titleLabel.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 278 getChildren().addAll(titleLabel, chartContent); 279 getStyleClass().add("chart"); 280 titleLabel.getStyleClass().add("chart-title"); 281 chartContent.getStyleClass().add("chart-content"); 282 // mark chartContent as unmanaged because any changes to its preferred size shouldn't cause a relayout 283 chartContent.setManaged(false); 284 } 285 286 // -------------- METHODS ------------------------------------------------------------------------------------------ 287 288 /** 289 * Play a animation involving the given keyframes. On every frame of the animation the chart will be relayed out 290 * 291 * @param keyFrames Array of KeyFrames to play 292 */ 293 void animate(KeyFrame...keyFrames) { animator.animate(keyFrames); } 294 295 /** 296 * Play the given animation on every frame of the animation the chart will be relayed out until the animation 297 * finishes. So to add a animation to a chart, create a animation on data model, during layoutChartContent() map 298 * data model to nodes then call this method with the animation. 299 * 300 * @param animation The animation to play 301 */ 302 protected void animate(Animation animation) { animator.animate(animation); } 303 304 /** Call this when you know something has changed that needs the chart to be relayed out. */ 305 protected void requestChartLayout() { 306 chartContent.requestLayout(); 307 } 308 309 /** 310 * This is used to check if any given animation should run. It returns true if animation is enabled and the node 311 * is visible and in a scene. 312 */ 313 protected final boolean shouldAnimate(){ 314 return getAnimated() && NodeHelper.isTreeVisible(this) && getScene() != null; 315 } 316 317 /** 318 * Called to update and layout the chart children available from getChartChildren() 319 * 320 * @param top The top offset from the origin to account for any padding on the chart content 321 * @param left The left offset from the origin to account for any padding on the chart content 322 * @param width The width of the area to layout the chart within 323 * @param height The height of the area to layout the chart within 324 */ 325 protected abstract void layoutChartChildren(double top, double left, double width, double height); 326 327 /** 328 * Invoked during the layout pass to layout this chart and all its content. 329 */ 330 @Override protected void layoutChildren() { 331 double top = snappedTopInset(); 332 double left = snappedLeftInset(); 333 double bottom = snappedBottomInset(); 334 double right = snappedRightInset(); 335 final double width = getWidth(); 336 final double height = getHeight(); 337 // layout title 338 if (getTitle() != null) { 339 titleLabel.setVisible(true); 340 if (getTitleSide().equals(Side.TOP)) { 341 final double titleHeight = snapSizeY(titleLabel.prefHeight(width-left-right)); 342 titleLabel.resizeRelocate(left,top,width-left-right,titleHeight); 343 top += titleHeight; 344 } else if (getTitleSide().equals(Side.BOTTOM)) { 345 final double titleHeight = snapSizeY(titleLabel.prefHeight(width-left-right)); 346 titleLabel.resizeRelocate(left,height-bottom-titleHeight,width-left-right,titleHeight); 347 bottom += titleHeight; 348 } else if (getTitleSide().equals(Side.LEFT)) { 349 final double titleWidth = snapSizeX(titleLabel.prefWidth(height-top-bottom)); 350 titleLabel.resizeRelocate(left,top,titleWidth,height-top-bottom); 351 left += titleWidth; 352 } else if (getTitleSide().equals(Side.RIGHT)) { 353 final double titleWidth = snapSizeX(titleLabel.prefWidth(height-top-bottom)); 354 titleLabel.resizeRelocate(width-right-titleWidth,top,titleWidth,height-top-bottom); 355 right += titleWidth; 356 } 357 } else { 358 titleLabel.setVisible(false); 359 } 360 // layout legend 361 final Node legend = getLegend(); 362 if (legend != null) { 363 boolean shouldShowLegend = isLegendVisible(); 364 if (shouldShowLegend) { 365 if (getLegendSide() == Side.TOP) { 366 final double legendHeight = snapSizeY(legend.prefHeight(width-left-right)); 367 final double legendWidth = Utils.boundedSize(snapSizeX(legend.prefWidth(legendHeight)), 0, width - left - right); 368 legend.resizeRelocate(left + (((width - left - right)-legendWidth)/2), top, legendWidth, legendHeight); 369 if ((height - bottom - top - legendHeight) < MIN_HEIGHT_TO_LEAVE_FOR_CHART_CONTENT) { 370 shouldShowLegend = false; 371 } else { 372 top += legendHeight; 373 } 374 } else if (getLegendSide() == Side.BOTTOM) { 375 final double legendHeight = snapSizeY(legend.prefHeight(width-left-right)); 376 final double legendWidth = Utils.boundedSize(snapSizeX(legend.prefWidth(legendHeight)), 0, width - left - right); 377 legend.resizeRelocate(left + (((width - left - right)-legendWidth)/2), height-bottom-legendHeight, legendWidth, legendHeight); 378 if ((height - bottom - top - legendHeight) < MIN_HEIGHT_TO_LEAVE_FOR_CHART_CONTENT) { 379 shouldShowLegend = false; 380 } else { 381 bottom += legendHeight; 382 } 383 } else if (getLegendSide() == Side.LEFT) { 384 final double legendWidth = snapSizeX(legend.prefWidth(height-top-bottom)); 385 final double legendHeight = Utils.boundedSize(snapSizeY(legend.prefHeight(legendWidth)), 0, height - top - bottom); 386 legend.resizeRelocate(left,top +(((height-top-bottom)-legendHeight)/2),legendWidth,legendHeight); 387 if ((width - left - right - legendWidth) < MIN_WIDTH_TO_LEAVE_FOR_CHART_CONTENT) { 388 shouldShowLegend = false; 389 } else { 390 left += legendWidth; 391 } 392 } else if (getLegendSide() == Side.RIGHT) { 393 final double legendWidth = snapSizeX(legend.prefWidth(height-top-bottom)); 394 final double legendHeight = Utils.boundedSize(snapSizeY(legend.prefHeight(legendWidth)), 0, height - top - bottom); 395 legend.resizeRelocate(width-right-legendWidth,top +(((height-top-bottom)-legendHeight)/2),legendWidth,legendHeight); 396 if ((width - left - right - legendWidth) < MIN_WIDTH_TO_LEAVE_FOR_CHART_CONTENT) { 397 shouldShowLegend = false; 398 } else { 399 right += legendWidth; 400 } 401 } 402 } 403 legend.setVisible(shouldShowLegend); 404 } 405 // whats left is for the chart content 406 chartContent.resizeRelocate(left,top,width-left-right,height-top-bottom); 407 } 408 409 /** 410 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So minimum 411 * height is a constant 150. 412 */ 413 @Override protected double computeMinHeight(double width) { return 150; } 414 415 /** 416 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So minimum 417 * width is a constant 200. 418 */ 419 @Override protected double computeMinWidth(double height) { return 200; } 420 421 /** 422 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So preferred 423 * width is a constant 500. 424 */ 425 @Override protected double computePrefWidth(double height) { return 500.0; } 426 427 /** 428 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So preferred 429 * height is a constant 400. 430 */ 431 @Override protected double computePrefHeight(double width) { return 400.0; } 432 433 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 434 435 private static class StyleableProperties { 436 private static final CssMetaData<Chart,Side> TITLE_SIDE = 437 new CssMetaData<Chart,Side>("-fx-title-side", 438 new EnumConverter<Side>(Side.class), 439 Side.TOP) { 440 441 @Override 442 public boolean isSettable(Chart node) { 443 return node.titleSide == null || !node.titleSide.isBound(); 444 } 445 446 @Override 447 public StyleableProperty<Side> getStyleableProperty(Chart node) { 448 return (StyleableProperty<Side>)(WritableValue<Side>)node.titleSideProperty(); 449 } 450 }; 451 452 private static final CssMetaData<Chart,Side> LEGEND_SIDE = 453 new CssMetaData<Chart,Side>("-fx-legend-side", 454 new EnumConverter<Side>(Side.class), 455 Side.BOTTOM) { 456 457 @Override 458 public boolean isSettable(Chart node) { 459 return node.legendSide == null || !node.legendSide.isBound(); 460 } 461 462 @Override 463 public StyleableProperty<Side> getStyleableProperty(Chart node) { 464 return (StyleableProperty<Side>)(WritableValue<Side>)node.legendSideProperty(); 465 } 466 }; 467 468 private static final CssMetaData<Chart,Boolean> LEGEND_VISIBLE = 469 new CssMetaData<Chart,Boolean>("-fx-legend-visible", 470 BooleanConverter.getInstance(), Boolean.TRUE) { 471 472 @Override 473 public boolean isSettable(Chart node) { 474 return node.legendVisible == null || !node.legendVisible.isBound(); 475 } 476 477 @Override 478 public StyleableProperty<Boolean> getStyleableProperty(Chart node) { 479 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.legendVisibleProperty(); 480 } 481 }; 482 483 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 484 static { 485 final List<CssMetaData<? extends Styleable, ?>> styleables = 486 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData()); 487 styleables.add(TITLE_SIDE); 488 styleables.add(LEGEND_VISIBLE); 489 styleables.add(LEGEND_SIDE); 490 STYLEABLES = Collections.unmodifiableList(styleables); 491 } 492 } 493 494 /** 495 * @return The CssMetaData associated with this class, which may include the 496 * CssMetaData of its super classes. 497 * @since JavaFX 8.0 498 */ 499 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 500 return StyleableProperties.STYLEABLES; 501 } 502 503 /** 504 * {@inheritDoc} 505 * @since JavaFX 8.0 506 */ 507 @Override 508 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 509 return getClassCssMetaData(); 510 } 511 512 } 513 514