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 * @defaultValue 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 * @return true if animation is enabled and the node is visible and in a scene 313 */ 314 protected final boolean shouldAnimate(){ 315 return getAnimated() && NodeHelper.isTreeShowing(this); 316 } 317 318 /** 319 * Called to update and layout the chart children available from getChartChildren() 320 * 321 * @param top The top offset from the origin to account for any padding on the chart content 322 * @param left The left offset from the origin to account for any padding on the chart content 323 * @param width The width of the area to layout the chart within 324 * @param height The height of the area to layout the chart within 325 */ 326 protected abstract void layoutChartChildren(double top, double left, double width, double height); 327 328 /** 329 * Invoked during the layout pass to layout this chart and all its content. 330 */ 331 @Override protected void layoutChildren() { 332 double top = snappedTopInset(); 333 double left = snappedLeftInset(); 334 double bottom = snappedBottomInset(); 335 double right = snappedRightInset(); 336 final double width = getWidth(); 337 final double height = getHeight(); 338 // layout title 339 if (getTitle() != null) { 340 titleLabel.setVisible(true); 341 if (getTitleSide().equals(Side.TOP)) { 342 final double titleHeight = snapSizeY(titleLabel.prefHeight(width-left-right)); 343 titleLabel.resizeRelocate(left,top,width-left-right,titleHeight); 344 top += titleHeight; 345 } else if (getTitleSide().equals(Side.BOTTOM)) { 346 final double titleHeight = snapSizeY(titleLabel.prefHeight(width-left-right)); 347 titleLabel.resizeRelocate(left,height-bottom-titleHeight,width-left-right,titleHeight); 348 bottom += titleHeight; 349 } else if (getTitleSide().equals(Side.LEFT)) { 350 final double titleWidth = snapSizeX(titleLabel.prefWidth(height-top-bottom)); 351 titleLabel.resizeRelocate(left,top,titleWidth,height-top-bottom); 352 left += titleWidth; 353 } else if (getTitleSide().equals(Side.RIGHT)) { 354 final double titleWidth = snapSizeX(titleLabel.prefWidth(height-top-bottom)); 355 titleLabel.resizeRelocate(width-right-titleWidth,top,titleWidth,height-top-bottom); 356 right += titleWidth; 357 } 358 } else { 359 titleLabel.setVisible(false); 360 } 361 // layout legend 362 final Node legend = getLegend(); 363 if (legend != null) { 364 boolean shouldShowLegend = isLegendVisible(); 365 if (shouldShowLegend) { 366 if (getLegendSide() == Side.TOP) { 367 final double legendHeight = snapSizeY(legend.prefHeight(width-left-right)); 368 final double legendWidth = Utils.boundedSize(snapSizeX(legend.prefWidth(legendHeight)), 0, width - left - right); 369 legend.resizeRelocate(left + (((width - left - right)-legendWidth)/2), top, legendWidth, legendHeight); 370 if ((height - bottom - top - legendHeight) < MIN_HEIGHT_TO_LEAVE_FOR_CHART_CONTENT) { 371 shouldShowLegend = false; 372 } else { 373 top += legendHeight; 374 } 375 } else if (getLegendSide() == Side.BOTTOM) { 376 final double legendHeight = snapSizeY(legend.prefHeight(width-left-right)); 377 final double legendWidth = Utils.boundedSize(snapSizeX(legend.prefWidth(legendHeight)), 0, width - left - right); 378 legend.resizeRelocate(left + (((width - left - right)-legendWidth)/2), height-bottom-legendHeight, legendWidth, legendHeight); 379 if ((height - bottom - top - legendHeight) < MIN_HEIGHT_TO_LEAVE_FOR_CHART_CONTENT) { 380 shouldShowLegend = false; 381 } else { 382 bottom += legendHeight; 383 } 384 } else if (getLegendSide() == Side.LEFT) { 385 final double legendWidth = snapSizeX(legend.prefWidth(height-top-bottom)); 386 final double legendHeight = Utils.boundedSize(snapSizeY(legend.prefHeight(legendWidth)), 0, height - top - bottom); 387 legend.resizeRelocate(left,top +(((height-top-bottom)-legendHeight)/2),legendWidth,legendHeight); 388 if ((width - left - right - legendWidth) < MIN_WIDTH_TO_LEAVE_FOR_CHART_CONTENT) { 389 shouldShowLegend = false; 390 } else { 391 left += legendWidth; 392 } 393 } else if (getLegendSide() == Side.RIGHT) { 394 final double legendWidth = snapSizeX(legend.prefWidth(height-top-bottom)); 395 final double legendHeight = Utils.boundedSize(snapSizeY(legend.prefHeight(legendWidth)), 0, height - top - bottom); 396 legend.resizeRelocate(width-right-legendWidth,top +(((height-top-bottom)-legendHeight)/2),legendWidth,legendHeight); 397 if ((width - left - right - legendWidth) < MIN_WIDTH_TO_LEAVE_FOR_CHART_CONTENT) { 398 shouldShowLegend = false; 399 } else { 400 right += legendWidth; 401 } 402 } 403 } 404 legend.setVisible(shouldShowLegend); 405 } 406 // whats left is for the chart content 407 chartContent.resizeRelocate(left,top,width-left-right,height-top-bottom); 408 } 409 410 /** 411 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So minimum 412 * height is a constant 150. 413 */ 414 @Override protected double computeMinHeight(double width) { return 150; } 415 416 /** 417 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So minimum 418 * width is a constant 200. 419 */ 420 @Override protected double computeMinWidth(double height) { return 200; } 421 422 /** 423 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So preferred 424 * width is a constant 500. 425 */ 426 @Override protected double computePrefWidth(double height) { return 500.0; } 427 428 /** 429 * Charts are sized outside in, user tells chart how much space it has and chart draws inside that. So preferred 430 * height is a constant 400. 431 */ 432 @Override protected double computePrefHeight(double width) { return 400.0; } 433 434 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 435 436 private static class StyleableProperties { 437 private static final CssMetaData<Chart,Side> TITLE_SIDE = 438 new CssMetaData<Chart,Side>("-fx-title-side", 439 new EnumConverter<Side>(Side.class), 440 Side.TOP) { 441 442 @Override 443 public boolean isSettable(Chart node) { 444 return node.titleSide == null || !node.titleSide.isBound(); 445 } 446 447 @Override 448 public StyleableProperty<Side> getStyleableProperty(Chart node) { 449 return (StyleableProperty<Side>)(WritableValue<Side>)node.titleSideProperty(); 450 } 451 }; 452 453 private static final CssMetaData<Chart,Side> LEGEND_SIDE = 454 new CssMetaData<Chart,Side>("-fx-legend-side", 455 new EnumConverter<Side>(Side.class), 456 Side.BOTTOM) { 457 458 @Override 459 public boolean isSettable(Chart node) { 460 return node.legendSide == null || !node.legendSide.isBound(); 461 } 462 463 @Override 464 public StyleableProperty<Side> getStyleableProperty(Chart node) { 465 return (StyleableProperty<Side>)(WritableValue<Side>)node.legendSideProperty(); 466 } 467 }; 468 469 private static final CssMetaData<Chart,Boolean> LEGEND_VISIBLE = 470 new CssMetaData<Chart,Boolean>("-fx-legend-visible", 471 BooleanConverter.getInstance(), Boolean.TRUE) { 472 473 @Override 474 public boolean isSettable(Chart node) { 475 return node.legendVisible == null || !node.legendVisible.isBound(); 476 } 477 478 @Override 479 public StyleableProperty<Boolean> getStyleableProperty(Chart node) { 480 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.legendVisibleProperty(); 481 } 482 }; 483 484 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 485 static { 486 final List<CssMetaData<? extends Styleable, ?>> styleables = 487 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData()); 488 styleables.add(TITLE_SIDE); 489 styleables.add(LEGEND_VISIBLE); 490 styleables.add(LEGEND_SIDE); 491 STYLEABLES = Collections.unmodifiableList(styleables); 492 } 493 } 494 495 /** 496 * @return The CssMetaData associated with this class, which may include the 497 * CssMetaData of its superclasses. 498 * @since JavaFX 8.0 499 */ 500 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 501 return StyleableProperties.STYLEABLES; 502 } 503 504 /** 505 * {@inheritDoc} 506 * @since JavaFX 8.0 507 */ 508 @Override 509 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 510 return getClassCssMetaData(); 511 } 512 513 } 514 515