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 javafx.css.CssMetaData;
  29 import javafx.css.StyleableBooleanProperty;
  30 import javafx.css.StyleableDoubleProperty;
  31 import javafx.css.StyleableIntegerProperty;
  32 
  33 import javafx.css.converter.BooleanConverter;
  34 import javafx.css.converter.SizeConverter;
  35 
  36 import java.util.ArrayList;
  37 import java.util.Collections;
  38 import java.util.List;
  39 
  40 import javafx.beans.property.*;
  41 import javafx.beans.value.WritableValue;
  42 import javafx.css.Styleable;
  43 import javafx.css.StyleableProperty;
  44 import javafx.geometry.Side;
  45 import javafx.scene.shape.LineTo;
  46 import javafx.scene.shape.MoveTo;
  47 import javafx.scene.shape.Path;
  48 import javafx.util.StringConverter;
  49 
  50 
  51 /**
  52  * A axis who's data is defined as Numbers. It can also draw minor
  53  * tick-marks between the major ones.
  54  * @since JavaFX 2.0
  55  */
  56 public abstract class ValueAxis<T extends Number> extends Axis<T> {
  57 
  58     // -------------- PRIVATE FIELDS -----------------------------------------------------------------------------------
  59 
  60     private final Path minorTickPath  = new Path();
  61 
  62     private double offset;
  63     /** This is the minimum current data value and it is used while auto ranging.
  64      *  Package private solely for test purposes */
  65     double dataMinValue;
  66     /** This is the maximum current data value and it is used while auto ranging.
  67      *  Package private solely for test purposes */
  68     double dataMaxValue;
  69     /** List of the values at which there are minor ticks */
  70     private List<T> minorTickMarkValues = null;
  71     // -------------- PRIVATE PROPERTIES -------------------------------------------------------------------------------
  72 
  73     /**
  74      * The current value for the lowerBound of this axis, ie min value.
  75      * This may be the same as lowerBound or different. It is used by NumberAxis to animate the
  76      * lowerBound from the old value to the new value.
  77      */
  78     protected final DoubleProperty currentLowerBound = new SimpleDoubleProperty(this, "currentLowerBound");
  79 
  80     // -------------- PUBLIC PROPERTIES --------------------------------------------------------------------------------
  81 
  82     /** true if minor tick marks should be displayed */
  83     private BooleanProperty minorTickVisible = new StyleableBooleanProperty(true) {
  84         @Override protected void invalidated() {
  85             minorTickPath.setVisible(get());
  86             requestAxisLayout();
  87         }
  88 
  89         @Override
  90         public Object getBean() {
  91             return ValueAxis.this;
  92         }
  93 
  94         @Override
  95         public String getName() {
  96             return "minorTickVisible";
  97         }
  98 
  99         @Override
 100         public CssMetaData<ValueAxis<? extends Number>,Boolean> getCssMetaData() {
 101             return StyleableProperties.MINOR_TICK_VISIBLE;
 102         }
 103     };
 104     public final boolean isMinorTickVisible() { return minorTickVisible.get(); }
 105     public final void setMinorTickVisible(boolean value) { minorTickVisible.set(value); }
 106     public final BooleanProperty minorTickVisibleProperty() { return minorTickVisible; }
 107 
 108 
 109     /** The scale factor from data units to visual units */
 110     private ReadOnlyDoubleWrapper scale = new ReadOnlyDoubleWrapper(this, "scale", 0) {
 111         @Override
 112         protected void invalidated() {
 113             requestAxisLayout();
 114             measureInvalid = true;
 115         }
 116     };
 117     public final double getScale() { return scale.get(); }
 118     protected final void setScale(double scale) { this.scale.set(scale); }
 119     public final ReadOnlyDoubleProperty scaleProperty() { return scale.getReadOnlyProperty(); }
 120     ReadOnlyDoubleWrapper scalePropertyImpl() { return scale; }
 121 
 122     /** The value for the upper bound of this axis, ie max value. This is automatically set if auto ranging is on. */
 123     private DoubleProperty upperBound = new DoublePropertyBase(100) {
 124         @Override protected void invalidated() {
 125             if(!isAutoRanging()) {
 126                 invalidateRange();
 127                 requestAxisLayout();
 128             }
 129         }
 130 
 131         @Override
 132         public Object getBean() {
 133             return ValueAxis.this;
 134         }
 135 
 136         @Override
 137         public String getName() {
 138             return "upperBound";
 139         }
 140     };
 141     public final double getUpperBound() { return upperBound.get(); }
 142     public final void setUpperBound(double value) { upperBound.set(value); }
 143     public final DoubleProperty upperBoundProperty() { return upperBound; }
 144 
 145     /** The value for the lower bound of this axis, ie min value. This is automatically set if auto ranging is on. */
 146     private DoubleProperty lowerBound = new DoublePropertyBase(0) {
 147         @Override protected void invalidated() {
 148             if(!isAutoRanging()) {
 149                 invalidateRange();
 150                 requestAxisLayout();
 151             }
 152         }
 153 
 154         @Override
 155         public Object getBean() {
 156             return ValueAxis.this;
 157         }
 158 
 159         @Override
 160         public String getName() {
 161             return "lowerBound";
 162         }
 163     };
 164     public final double getLowerBound() { return lowerBound.get(); }
 165     public final void setLowerBound(double value) { lowerBound.set(value); }
 166     public final DoubleProperty lowerBoundProperty() { return lowerBound; }
 167 
 168     /** StringConverter used to format tick mark labels. If null a default will be used */
 169     private final ObjectProperty<StringConverter<T>> tickLabelFormatter = new ObjectPropertyBase<StringConverter<T>>(null){
 170         @Override protected void invalidated() {
 171             invalidateRange();
 172             requestAxisLayout();
 173         }
 174 
 175         @Override
 176         public Object getBean() {
 177             return ValueAxis.this;
 178         }
 179 
 180         @Override
 181         public String getName() {
 182             return "tickLabelFormatter";
 183         }
 184     };
 185     public final StringConverter<T> getTickLabelFormatter() { return tickLabelFormatter.getValue(); }
 186     public final void setTickLabelFormatter(StringConverter<T> value) { tickLabelFormatter.setValue(value); }
 187     public final ObjectProperty<StringConverter<T>> tickLabelFormatterProperty() { return tickLabelFormatter; }
 188 
 189     /** The length of minor tick mark lines. Set to 0 to not display minor tick marks. */
 190     private DoubleProperty minorTickLength = new StyleableDoubleProperty(5) {
 191         @Override protected void invalidated() {
 192             requestAxisLayout();
 193         }
 194 
 195         @Override
 196         public Object getBean() {
 197             return ValueAxis.this;
 198         }
 199 
 200         @Override
 201         public String getName() {
 202             return "minorTickLength";
 203         }
 204 
 205         @Override
 206         public CssMetaData<ValueAxis<? extends Number>,Number> getCssMetaData() {
 207             return StyleableProperties.MINOR_TICK_LENGTH;
 208         }
 209     };
 210     public final double getMinorTickLength() { return minorTickLength.get(); }
 211     public final void setMinorTickLength(double value) { minorTickLength.set(value); }
 212     public final DoubleProperty minorTickLengthProperty() { return minorTickLength; }
 213 
 214     /**
 215      * The number of minor tick divisions to be displayed between each major tick mark.
 216      * The number of actual minor tick marks will be one less than this.
 217      */
 218     private IntegerProperty minorTickCount = new StyleableIntegerProperty(5) {
 219         @Override protected void invalidated() {
 220             invalidateRange();
 221             requestAxisLayout();
 222         }
 223 
 224         @Override
 225         public Object getBean() {
 226             return ValueAxis.this;
 227         }
 228 
 229         @Override
 230         public String getName() {
 231             return "minorTickCount";
 232         }
 233 
 234         @Override
 235         public CssMetaData<ValueAxis<? extends Number>,Number> getCssMetaData() {
 236             return StyleableProperties.MINOR_TICK_COUNT;
 237         }
 238     };
 239     public final int getMinorTickCount() { return minorTickCount.get(); }
 240     public final void setMinorTickCount(int value) { minorTickCount.set(value); }
 241     public final IntegerProperty minorTickCountProperty() { return minorTickCount; }
 242 
 243     // -------------- CONSTRUCTORS -------------------------------------------------------------------------------------
 244 
 245     /**
 246      * Create a auto-ranging ValueAxis
 247      */
 248     public ValueAxis() {
 249         minorTickPath.getStyleClass().add("axis-minor-tick-mark");
 250         getChildren().add(minorTickPath);
 251     }
 252 
 253     /**
 254      * Create a non-auto-ranging ValueAxis with the given upper & lower bound
 255      *
 256      * @param lowerBound The lower bound for this axis, ie min plottable value
 257      * @param upperBound The upper bound for this axis, ie max plottable value
 258      */
 259     public ValueAxis(double lowerBound, double upperBound) {
 260         this();
 261         setAutoRanging(false);
 262         setLowerBound(lowerBound);
 263         setUpperBound(upperBound);
 264     }
 265 
 266     // -------------- PROTECTED METHODS --------------------------------------------------------------------------------
 267 
 268 
 269     /**
 270      * This calculates the upper and lower bound based on the data provided to invalidateRange() method. This must not
 271      * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be
 272      * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for
 273      * this axis.
 274      *
 275      * @param length The length of the axis in screen coordinates
 276      * @return Range information, this is implementation dependent
 277      */
 278     @Override protected final Object autoRange(double length) {
 279         // guess a sensible starting size for label size, that is approx 2 lines vertically or 2 charts horizontally
 280         if (isAutoRanging()) {
 281             // guess a sensible starting size for label size, that is approx 2 lines vertically or 2 charts horizontally
 282             double labelSize = getTickLabelFont().getSize() * 2;
 283             return autoRange(dataMinValue,dataMaxValue,length,labelSize);
 284         } else {
 285             return getRange();
 286         }
 287     }
 288 
 289     /**
 290      * Calculate a new scale for this axis. This should not effect any state(properties) of this axis.
 291      *
 292      * @param length The display length of the axis
 293      * @param lowerBound The lower bound value
 294      * @param upperBound The upper bound value
 295      * @return new scale to fit the range from lower bound to upper bound in the given display length
 296      */
 297     protected final double calculateNewScale(double length, double lowerBound, double upperBound) {
 298         double newScale = 1;
 299         final Side side = getEffectiveSide();
 300         if (side.isVertical()) {
 301             offset = length;
 302             newScale = ((upperBound-lowerBound) == 0) ? -length : -(length / (upperBound - lowerBound));
 303         } else { // HORIZONTAL
 304             offset = 0;
 305             newScale = ((upperBound-lowerBound) == 0) ? length : length / (upperBound - lowerBound);
 306         }
 307         return newScale;
 308     }
 309 
 310     /**
 311      * Called to set the upper and lower bound and anything else that needs to be auto-ranged. This must not effect
 312      * the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be returned
 313      * in the range object. This will we passed to setRange() if it has been decided to adopt this range for this axis.
 314      *
 315      * @param minValue The min data value that needs to be plotted on this axis
 316      * @param maxValue The max data value that needs to be plotted on this axis
 317      * @param length The length of the axis in display coordinates
 318      * @param labelSize The approximate average size a label takes along the axis
 319      * @return The calculated range
 320      */
 321     protected Object autoRange(double minValue, double maxValue, double length, double labelSize) {
 322         return null; // this method should have been abstract as there is no way for it to
 323         // return anything correct. so just return null.
 324 
 325     }
 326 
 327     /**
 328      * Calculate a list of the data values for every minor tick mark
 329      *
 330      * @return List of data values where to draw minor tick marks
 331      */
 332     protected abstract List<T> calculateMinorTickMarks();
 333 
 334     /**
 335      * Called during layout if the tickmarks have been updated, allowing subclasses to do anything they need to
 336      * in reaction.
 337      */
 338     @Override protected void tickMarksUpdated() {
 339         super.tickMarksUpdated();
 340         // recalculate minor tick marks
 341         minorTickMarkValues = calculateMinorTickMarks();
 342     }
 343 
 344     /**
 345      * Invoked during the layout pass to layout this axis and all its content.
 346      */
 347     @Override protected void layoutChildren() {
 348         final Side side = getEffectiveSide();
 349         final double length = side.isVertical() ? getHeight() :getWidth() ;
 350         // if we are not auto ranging we need to calculate the new scale
 351         if(!isAutoRanging()) {
 352             // calculate new scale
 353             setScale(calculateNewScale(length, getLowerBound(), getUpperBound()));
 354             // update current lower bound
 355             currentLowerBound.set(getLowerBound());
 356         }
 357         // we have done all auto calcs, let Axis position major tickmarks
 358         super.layoutChildren();
 359         int numMinorTicks = (getTickMarks().size() - 1)*(Math.max(1, getMinorTickCount()) - 1);
 360         double neededLength = (getTickMarks().size()+numMinorTicks)*2;
 361 
 362         // Update minor tickmarks
 363         minorTickPath.getElements().clear();
 364         // Don't draw minor tick marks if there isn't enough space for them!
 365 
 366         double minorTickLength = Math.max(0, getMinorTickLength());
 367         if (minorTickLength > 0 && length > neededLength) {
 368             if (Side.LEFT.equals(side)) {
 369                 // snap minorTickPath to pixels
 370                 minorTickPath.setLayoutX(-0.5);
 371                 minorTickPath.setLayoutY(0.5);
 372                 for (T value : minorTickMarkValues) {
 373                     double y = getDisplayPosition(value);
 374                     if (y >= 0 && y <= length) {
 375                         minorTickPath.getElements().addAll(
 376                                 new MoveTo(getWidth() - minorTickLength, y),
 377                                 new LineTo(getWidth() - 1, y));
 378                     }
 379                 }
 380             } else if (Side.RIGHT.equals(side)) {
 381                 // snap minorTickPath to pixels
 382                 minorTickPath.setLayoutX(0.5);
 383                 minorTickPath.setLayoutY(0.5);
 384                 for (T value : minorTickMarkValues) {
 385                     double y = getDisplayPosition(value);
 386                     if (y >= 0 && y <= length) {
 387                         minorTickPath.getElements().addAll(
 388                                 new MoveTo(1, y),
 389                                 new LineTo(minorTickLength, y));
 390                     }
 391                 }
 392             } else if (Side.TOP.equals(side)) {
 393                 // snap minorTickPath to pixels
 394                 minorTickPath.setLayoutX(0.5);
 395                 minorTickPath.setLayoutY(-0.5);
 396                 for (T value : minorTickMarkValues) {
 397                     double x = getDisplayPosition(value);
 398                     if (x >= 0 && x <= length) {
 399                         minorTickPath.getElements().addAll(
 400                                 new MoveTo(x, getHeight() - 1),
 401                                 new LineTo(x, getHeight() - minorTickLength));
 402                     }
 403                 }
 404             } else { // BOTTOM
 405                 // snap minorTickPath to pixels
 406                 minorTickPath.setLayoutX(0.5);
 407                 minorTickPath.setLayoutY(0.5);
 408                 for (T value : minorTickMarkValues) {
 409                     double x = getDisplayPosition(value);
 410                     if (x >= 0 && x <= length) {
 411                         minorTickPath.getElements().addAll(
 412                                 new MoveTo(x, 1.0F),
 413                                 new LineTo(x, minorTickLength));
 414                     }
 415                 }
 416             }
 417         }
 418     }
 419 
 420     // -------------- METHODS ------------------------------------------------------------------------------------------
 421 
 422     /**
 423      * Called when data has changed and the range may not be valid any more. This is only called by the chart if
 424      * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to
 425      * happen on next layout pass.
 426      *
 427      * @param data The current set of all data that needs to be plotted on this axis
 428      */
 429     @Override public void invalidateRange(List<T> data) {
 430         if (data.isEmpty()) {
 431             dataMaxValue = getUpperBound();
 432             dataMinValue = getLowerBound();
 433         } else {
 434             dataMinValue = Double.MAX_VALUE;
 435             // We need to init to the lowest negative double (which is NOT Double.MIN_VALUE)
 436             // in order to find the maximum (positive or negative)
 437             dataMaxValue = -Double.MAX_VALUE;
 438         }
 439         for(T dataValue: data) {
 440             dataMinValue = Math.min(dataMinValue, dataValue.doubleValue());
 441             dataMaxValue = Math.max(dataMaxValue, dataValue.doubleValue());
 442         }
 443         super.invalidateRange(data);
 444     }
 445 
 446     /**
 447      * Get the display position along this axis for a given value.
 448      * If the value is not in the current range, the returned value will be an extrapolation of the display
 449      * position.
 450      *
 451      * @param value The data value to work out display position for
 452      * @return display position
 453      */
 454     @Override public double getDisplayPosition(T value) {
 455         return offset + ((value.doubleValue() - currentLowerBound.get()) * getScale());
 456     }
 457 
 458     /**
 459      * Get the data value for the given display position on this axis. If the axis
 460      * is a CategoryAxis this will be the nearest value.
 461      *
 462      * @param  displayPosition A pixel position on this axis
 463      * @return the nearest data value to the given pixel position or
 464      *         null if not on axis;
 465      */
 466     @Override public T getValueForDisplay(double displayPosition) {
 467         return toRealValue(((displayPosition-offset) / getScale()) + currentLowerBound.get());
 468     }
 469 
 470     /**
 471      * Get the display position of the zero line along this axis.
 472      *
 473      * @return display position or Double.NaN if zero is not in current range;
 474      */
 475     @Override public double getZeroPosition() {
 476         if (0 < getLowerBound() || 0 > getUpperBound()) return Double.NaN;
 477         //noinspection unchecked
 478         return getDisplayPosition((T)Double.valueOf(0));
 479     }
 480 
 481     /**
 482      * Checks if the given value is plottable on this axis
 483      *
 484      * @param value The value to check if its on axis
 485      * @return true if the given value is plottable on this axis
 486      */
 487     @Override public boolean isValueOnAxis(T value) {
 488         final double num = value.doubleValue();
 489         return num >= getLowerBound() && num <= getUpperBound();
 490     }
 491 
 492     /**
 493      * All axis values must be representable by some numeric value. This gets the numeric value for a given data value.
 494      *
 495      * @param value The data value to convert
 496      * @return Numeric value for the given data value
 497      */
 498     @Override public double toNumericValue(T value) {
 499         return (value == null) ? Double.NaN : value.doubleValue();
 500     }
 501 
 502     /**
 503      * All axis values must be representable by some numeric value. This gets the data value for a given numeric value.
 504      *
 505      * @param value The numeric value to convert
 506      * @return Data value for given numeric value
 507      */
 508     @Override public T toRealValue(double value) {
 509         //noinspection unchecked
 510         return (T)new Double(value);
 511     }
 512 
 513     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 514 
 515     private static class StyleableProperties  {
 516         private  static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_LENGTH =
 517             new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-length",
 518                 SizeConverter.getInstance(), 5.0) {
 519 
 520             @Override
 521             public boolean isSettable(ValueAxis<? extends Number> n) {
 522                 return n.minorTickLength == null || !n.minorTickLength.isBound();
 523             }
 524 
 525             @Override
 526             public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) {
 527                 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickLengthProperty();
 528             }
 529         };
 530 
 531         private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_COUNT =
 532             new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-count",
 533                 SizeConverter.getInstance(), 5) {
 534 
 535             @Override
 536             public boolean isSettable(ValueAxis<? extends Number> n) {
 537                 return n.minorTickCount == null || !n.minorTickCount.isBound();
 538             }
 539 
 540             @Override
 541             public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) {
 542                 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickCountProperty();
 543             }
 544         };
 545 
 546          private static final CssMetaData<ValueAxis<? extends Number>,Boolean> MINOR_TICK_VISIBLE =
 547             new CssMetaData<ValueAxis<? extends Number>,Boolean>("-fx-minor-tick-visible",
 548                  BooleanConverter.getInstance(), Boolean.TRUE) {
 549 
 550             @Override
 551             public boolean isSettable(ValueAxis<? extends Number> n) {
 552                 return n.minorTickVisible == null || !n.minorTickVisible.isBound();
 553             }
 554 
 555             @Override
 556             public StyleableProperty<Boolean> getStyleableProperty(ValueAxis<? extends Number> n) {
 557                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.minorTickVisibleProperty();
 558             }
 559         };
 560 
 561         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 562          static {
 563             final List<CssMetaData<? extends Styleable, ?>> styleables =
 564                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData());
 565             styleables.add(MINOR_TICK_COUNT);
 566             styleables.add(MINOR_TICK_LENGTH);
 567             styleables.add(MINOR_TICK_COUNT);
 568             styleables.add(MINOR_TICK_VISIBLE);
 569             STYLEABLES = Collections.unmodifiableList(styleables);
 570          }
 571      }
 572 
 573     /**
 574      * @return The CssMetaData associated with this class, which may include the
 575      * CssMetaData of its super classes.
 576      * @since JavaFX 8.0
 577      */
 578     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 579         return StyleableProperties.STYLEABLES;
 580     }
 581 
 582     /**
 583      * {@inheritDoc}
 584      * @since JavaFX 8.0
 585      */
 586     @Override
 587     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 588         return getClassCssMetaData();
 589     }
 590 
 591 }