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.isTreeShowing(this);
 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 superclasses.
 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