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