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