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 
 360         // Update minor tickmarks
 361         minorTickPath.getElements().clear();
 362 
 363         double minorTickLength = Math.max(0, getMinorTickLength());
 364         // The length must be greater then the space required for tick marks, otherwise, there's no reason to create
 365         // minor tick marks
 366         if (minorTickLength > 0 && length > 2 * getTickMarks().size()) {
 367             // Strip factor is >= 1. When == 1, all minor ticks will fit.
 368             // It's computed as number of minor tick marks divided by available length
 369             int stripFactor = (int)Math.ceil(2 * minorTickMarkValues.size() / (length - 2 * getTickMarks().size()));
 370             if (Side.LEFT.equals(side)) {
 371                 // snap minorTickPath to pixels
 372                 minorTickPath.setLayoutX(-0.5);
 373                 minorTickPath.setLayoutY(0.5);
 374                 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) {
 375                     T value = minorTickMarkValues.get(i);
 376                     double y = getDisplayPosition(value);
 377                     if (y >= 0 && y <= length) {
 378                         minorTickPath.getElements().addAll(
 379                                 new MoveTo(getWidth() - minorTickLength, y),
 380                                 new LineTo(getWidth() - 1, y));
 381                     }
 382                 }
 383             } else if (Side.RIGHT.equals(side)) {
 384                 // snap minorTickPath to pixels
 385                 minorTickPath.setLayoutX(0.5);
 386                 minorTickPath.setLayoutY(0.5);
 387                 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) {
 388                     T value = minorTickMarkValues.get(i);
 389                     double y = getDisplayPosition(value);
 390                     if (y >= 0 && y <= length) {
 391                         minorTickPath.getElements().addAll(
 392                                 new MoveTo(1, y),
 393                                 new LineTo(minorTickLength, y));
 394                     }
 395                 }
 396             } else if (Side.TOP.equals(side)) {
 397                 // snap minorTickPath to pixels
 398                 minorTickPath.setLayoutX(0.5);
 399                 minorTickPath.setLayoutY(-0.5);
 400                 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) {
 401                     T value = minorTickMarkValues.get(i);
 402                     double x = getDisplayPosition(value);
 403                     if (x >= 0 && x <= length) {
 404                         minorTickPath.getElements().addAll(
 405                                 new MoveTo(x, getHeight() - 1),
 406                                 new LineTo(x, getHeight() - minorTickLength));
 407                     }
 408                 }
 409             } else { // BOTTOM
 410                 // snap minorTickPath to pixels
 411                 minorTickPath.setLayoutX(0.5);
 412                 minorTickPath.setLayoutY(0.5);
 413                 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) {
 414                     T value = minorTickMarkValues.get(i);
 415                     double x = getDisplayPosition(value);
 416                     if (x >= 0 && x <= length) {
 417                         minorTickPath.getElements().addAll(
 418                                 new MoveTo(x, 1.0F),
 419                                 new LineTo(x, minorTickLength));
 420                     }
 421                 }
 422             }
 423         }
 424     }
 425 
 426     // -------------- METHODS ------------------------------------------------------------------------------------------
 427 
 428     /**
 429      * Called when data has changed and the range may not be valid any more. This is only called by the chart if
 430      * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to
 431      * happen on next layout pass.
 432      *
 433      * @param data The current set of all data that needs to be plotted on this axis
 434      */
 435     @Override public void invalidateRange(List<T> data) {
 436         if (data.isEmpty()) {
 437             dataMaxValue = getUpperBound();
 438             dataMinValue = getLowerBound();
 439         } else {
 440             dataMinValue = Double.MAX_VALUE;
 441             // We need to init to the lowest negative double (which is NOT Double.MIN_VALUE)
 442             // in order to find the maximum (positive or negative)
 443             dataMaxValue = -Double.MAX_VALUE;
 444         }
 445         for(T dataValue: data) {
 446             dataMinValue = Math.min(dataMinValue, dataValue.doubleValue());
 447             dataMaxValue = Math.max(dataMaxValue, dataValue.doubleValue());
 448         }
 449         super.invalidateRange(data);
 450     }
 451 
 452     /**
 453      * Get the display position along this axis for a given value.
 454      * If the value is not in the current range, the returned value will be an extrapolation of the display
 455      * position.
 456      *
 457      * @param value The data value to work out display position for
 458      * @return display position
 459      */
 460     @Override public double getDisplayPosition(T value) {
 461         return offset + ((value.doubleValue() - currentLowerBound.get()) * getScale());
 462     }
 463 
 464     /**
 465      * Get the data value for the given display position on this axis. If the axis
 466      * is a CategoryAxis this will be the nearest value.
 467      *
 468      * @param  displayPosition A pixel position on this axis
 469      * @return the nearest data value to the given pixel position or
 470      *         null if not on axis;
 471      */
 472     @Override public T getValueForDisplay(double displayPosition) {
 473         return toRealValue(((displayPosition-offset) / getScale()) + currentLowerBound.get());
 474     }
 475 
 476     /**
 477      * Get the display position of the zero line along this axis.
 478      *
 479      * @return display position or Double.NaN if zero is not in current range;
 480      */
 481     @Override public double getZeroPosition() {
 482         if (0 < getLowerBound() || 0 > getUpperBound()) return Double.NaN;
 483         //noinspection unchecked
 484         return getDisplayPosition((T)Double.valueOf(0));
 485     }
 486 
 487     /**
 488      * Checks if the given value is plottable on this axis
 489      *
 490      * @param value The value to check if its on axis
 491      * @return true if the given value is plottable on this axis
 492      */
 493     @Override public boolean isValueOnAxis(T value) {
 494         final double num = value.doubleValue();
 495         return num >= getLowerBound() && num <= getUpperBound();
 496     }
 497 
 498     /**
 499      * All axis values must be representable by some numeric value. This gets the numeric value for a given data value.
 500      *
 501      * @param value The data value to convert
 502      * @return Numeric value for the given data value
 503      */
 504     @Override public double toNumericValue(T value) {
 505         return (value == null) ? Double.NaN : value.doubleValue();
 506     }
 507 
 508     /**
 509      * All axis values must be representable by some numeric value. This gets the data value for a given numeric value.
 510      *
 511      * @param value The numeric value to convert
 512      * @return Data value for given numeric value
 513      */
 514     @Override public T toRealValue(double value) {
 515         //noinspection unchecked
 516         return (T)new Double(value);
 517     }
 518 
 519     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 520 
 521     private static class StyleableProperties  {
 522         private  static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_LENGTH =
 523             new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-length",
 524                 SizeConverter.getInstance(), 5.0) {
 525 
 526             @Override
 527             public boolean isSettable(ValueAxis<? extends Number> n) {
 528                 return n.minorTickLength == null || !n.minorTickLength.isBound();
 529             }
 530 
 531             @Override
 532             public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) {
 533                 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickLengthProperty();
 534             }
 535         };
 536 
 537         private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_COUNT =
 538             new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-count",
 539                 SizeConverter.getInstance(), 5) {
 540 
 541             @Override
 542             public boolean isSettable(ValueAxis<? extends Number> n) {
 543                 return n.minorTickCount == null || !n.minorTickCount.isBound();
 544             }
 545 
 546             @Override
 547             public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) {
 548                 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickCountProperty();
 549             }
 550         };
 551 
 552          private static final CssMetaData<ValueAxis<? extends Number>,Boolean> MINOR_TICK_VISIBLE =
 553             new CssMetaData<ValueAxis<? extends Number>,Boolean>("-fx-minor-tick-visible",
 554                  BooleanConverter.getInstance(), Boolean.TRUE) {
 555 
 556             @Override
 557             public boolean isSettable(ValueAxis<? extends Number> n) {
 558                 return n.minorTickVisible == null || !n.minorTickVisible.isBound();
 559             }
 560 
 561             @Override
 562             public StyleableProperty<Boolean> getStyleableProperty(ValueAxis<? extends Number> n) {
 563                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.minorTickVisibleProperty();
 564             }
 565         };
 566 
 567         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 568          static {
 569             final List<CssMetaData<? extends Styleable, ?>> styleables =
 570                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData());
 571             styleables.add(MINOR_TICK_COUNT);
 572             styleables.add(MINOR_TICK_LENGTH);
 573             styleables.add(MINOR_TICK_COUNT);
 574             styleables.add(MINOR_TICK_VISIBLE);
 575             STYLEABLES = Collections.unmodifiableList(styleables);
 576          }
 577      }
 578 
 579     /**
 580      * @return The CssMetaData associated with this class, which may include the
 581      * CssMetaData of its super classes.
 582      * @since JavaFX 8.0
 583      */
 584     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 585         return StyleableProperties.STYLEABLES;
 586     }
 587 
 588     /**
 589      * {@inheritDoc}
 590      * @since JavaFX 8.0
 591      */
 592     @Override
 593     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 594         return getClassCssMetaData();
 595     }
 596 
 597 }