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