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