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.Styleable;
  29 import javafx.css.CssMetaData;
  30 import javafx.css.PseudoClass;
  31 import javafx.css.StyleableBooleanProperty;
  32 import javafx.css.StyleableDoubleProperty;
  33 import javafx.css.StyleableObjectProperty;
  34 
  35 import javafx.css.converter.BooleanConverter;
  36 import javafx.css.converter.EnumConverter;
  37 import javafx.css.converter.PaintConverter;
  38 import javafx.css.converter.SizeConverter;
  39 
  40 import java.util.*;
  41 
  42 import javafx.animation.FadeTransition;
  43 import javafx.beans.binding.DoubleExpression;
  44 import javafx.beans.binding.ObjectExpression;
  45 import javafx.beans.binding.StringExpression;
  46 import javafx.beans.property.*;
  47 import javafx.beans.value.WritableValue;
  48 import javafx.collections.FXCollections;
  49 import javafx.collections.ObservableList;
  50 import javafx.css.FontCssMetaData;
  51 import javafx.css.StyleableProperty;
  52 import javafx.geometry.Bounds;
  53 import javafx.geometry.Dimension2D;
  54 import javafx.geometry.Orientation;
  55 import javafx.geometry.Pos;
  56 import javafx.geometry.Side;
  57 import javafx.scene.control.Label;
  58 import javafx.scene.layout.Region;
  59 import javafx.scene.paint.Color;
  60 import javafx.scene.paint.Paint;
  61 import javafx.scene.shape.LineTo;
  62 import javafx.scene.shape.MoveTo;
  63 import javafx.scene.shape.Path;
  64 import javafx.scene.text.Font;
  65 import javafx.scene.text.Text;
  66 import javafx.scene.transform.Rotate;
  67 import javafx.scene.transform.Translate;
  68 import javafx.util.Duration;
  69 
  70 
  71 /**
  72  * Base class for all axes in JavaFX that represents an axis drawn on a chart area.
  73  * It holds properties for axis auto ranging, ticks and labels along the axis.
  74  * <p>
  75  * Some examples of concrete subclasses include {@link NumberAxis} whose axis plots data
  76  * in numbers and {@link CategoryAxis} whose values / ticks represent string
  77  * categories along its axis.
  78  * @since JavaFX 2.0
  79  */
  80 public abstract class Axis<T> extends Region {
  81 
  82     // -------------- PRIVATE FIELDS -----------------------------------------------------------------------------------
  83 
  84     Text measure = new Text();
  85     private Orientation effectiveOrientation;
  86     private double effectiveTickLabelRotation = Double.NaN;
  87     private Label axisLabel = new Label();
  88     private final Path tickMarkPath = new Path();
  89     private double oldLength = 0;
  90     /** True when the current range invalid and all dependent calculations need to be updated */
  91     boolean rangeValid = false;
  92     boolean measureInvalid = false;
  93     boolean tickLabelsVisibleInvalid = false;
  94 
  95     private BitSet labelsToSkip = new BitSet();
  96 
  97     // -------------- PUBLIC PROPERTIES --------------------------------------------------------------------------------
  98 
  99     private final ObservableList<TickMark<T>> tickMarks = FXCollections.observableArrayList();
 100     private final ObservableList<TickMark<T>> unmodifiableTickMarks = FXCollections.unmodifiableObservableList(tickMarks);
 101     /**
 102      * Unmodifiable observable list of tickmarks, each TickMark directly representing a tickmark on this axis. This is updated
 103      * whenever the displayed tickmarks changes.
 104      *
 105      * @return Unmodifiable observable list of TickMarks on this axis
 106      */
 107     public ObservableList<TickMark<T>> getTickMarks() { return unmodifiableTickMarks; }
 108 
 109     /** The side of the plot which this axis is being drawn on */
 110     private ObjectProperty<Side> side = new StyleableObjectProperty<Side>(){
 111         @Override protected void invalidated() {
 112             // cause refreshTickMarks
 113             Side edge = get();
 114             pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, edge == Side.TOP);
 115             pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, edge == Side.RIGHT);
 116             pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, edge == Side.BOTTOM);
 117             pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, edge == Side.LEFT);
 118             requestAxisLayout();
 119         }
 120 
 121         @Override
 122         public CssMetaData<Axis<?>,Side> getCssMetaData() {
 123             return StyleableProperties.SIDE;
 124         }
 125 
 126         @Override
 127         public Object getBean() {
 128             return Axis.this;
 129         }
 130 
 131         @Override
 132         public String getName() {
 133             return "side";
 134         }
 135     };
 136     public final Side getSide() { return side.get(); }
 137     public final void setSide(Side value) { side.set(value); }
 138     public final ObjectProperty<Side> sideProperty() { return side; }
 139 
 140     final void setEffectiveOrientation(Orientation orientation) {
 141         effectiveOrientation = orientation;
 142     }
 143 
 144     final Side getEffectiveSide() {
 145         final Side side = getSide();
 146         if (side == null || (side.isVertical() && effectiveOrientation == Orientation.HORIZONTAL)
 147                 || side.isHorizontal() && effectiveOrientation == Orientation.VERTICAL) {
 148             // Means side == null && effectiveOrientation == null produces Side.BOTTOM
 149             return effectiveOrientation == Orientation.VERTICAL ? Side.LEFT : Side.BOTTOM;
 150         }
 151         return side;
 152     }
 153 
 154     /** The axis label */
 155     private ObjectProperty<String> label = new ObjectPropertyBase<String>() {
 156         @Override protected void invalidated() {
 157             axisLabel.setText(get());
 158             requestAxisLayout();
 159         }
 160 
 161         @Override
 162         public Object getBean() {
 163             return Axis.this;
 164         }
 165 
 166         @Override
 167         public String getName() {
 168             return "label";
 169         }
 170     };
 171     public final String getLabel() { return label.get(); }
 172     public final void setLabel(String value) { label.set(value); }
 173     public final ObjectProperty<String> labelProperty() { return label; }
 174 
 175     /** true if tick marks should be displayed */
 176     private BooleanProperty tickMarkVisible = new StyleableBooleanProperty(true) {
 177         @Override protected void invalidated() {
 178             tickMarkPath.setVisible(get());
 179             requestAxisLayout();
 180         }
 181 
 182         @Override
 183         public CssMetaData<Axis<?>,Boolean> getCssMetaData() {
 184             return StyleableProperties.TICK_MARK_VISIBLE;
 185         }
 186         @Override
 187         public Object getBean() {
 188             return Axis.this;
 189         }
 190 
 191         @Override
 192         public String getName() {
 193             return "tickMarkVisible";
 194         }
 195     };
 196     public final boolean isTickMarkVisible() { return tickMarkVisible.get(); }
 197     public final void setTickMarkVisible(boolean value) { tickMarkVisible.set(value); }
 198     public final BooleanProperty tickMarkVisibleProperty() { return tickMarkVisible; }
 199 
 200     /** true if tick mark labels should be displayed */
 201     private BooleanProperty tickLabelsVisible = new StyleableBooleanProperty(true) {
 202         @Override protected void invalidated() {
 203             // update textNode visibility for each tick
 204             for (TickMark<T> tick : tickMarks) {
 205                 tick.setTextVisible(get());
 206             }
 207             tickLabelsVisibleInvalid = true;
 208             requestAxisLayout();
 209         }
 210 
 211         @Override
 212         public CssMetaData<Axis<?>,Boolean> getCssMetaData() {
 213             return StyleableProperties.TICK_LABELS_VISIBLE;
 214         }
 215 
 216         @Override
 217         public Object getBean() {
 218             return Axis.this;
 219         }
 220 
 221         @Override
 222         public String getName() {
 223             return "tickLabelsVisible";
 224         }
 225     };
 226     public final boolean isTickLabelsVisible() { return tickLabelsVisible.get(); }
 227     public final void setTickLabelsVisible(boolean value) {
 228         tickLabelsVisible.set(value); }
 229     public final BooleanProperty tickLabelsVisibleProperty() { return tickLabelsVisible; }
 230 
 231     /** The length of tick mark lines */
 232     private DoubleProperty tickLength = new StyleableDoubleProperty(8) {
 233         @Override protected void invalidated() {
 234             if (tickLength.get() < 0 && !tickLength.isBound()) {
 235                 tickLength.set(0);
 236             }
 237             // this effects preferred size so request layout
 238             requestAxisLayout();
 239         }
 240 
 241         @Override
 242         public CssMetaData<Axis<?>,Number> getCssMetaData() {
 243             return StyleableProperties.TICK_LENGTH;
 244         }
 245         @Override
 246         public Object getBean() {
 247             return Axis.this;
 248         }
 249 
 250         @Override
 251         public String getName() {
 252             return "tickLength";
 253         }
 254     };
 255     public final double getTickLength() { return tickLength.get(); }
 256     public final void setTickLength(double value) { tickLength.set(value); }
 257     public final DoubleProperty tickLengthProperty() { return tickLength; }
 258 
 259     /** This is true when the axis determines its range from the data automatically */
 260     private BooleanProperty autoRanging = new BooleanPropertyBase(true) {
 261         @Override protected void invalidated() {
 262             if(get()) {
 263                 // auto range turned on, so need to auto range now
 264 //                autoRangeValid = false;
 265                 requestAxisLayout();
 266             }
 267         }
 268 
 269         @Override
 270         public Object getBean() {
 271             return Axis.this;
 272         }
 273 
 274         @Override
 275         public String getName() {
 276             return "autoRanging";
 277         }
 278     };
 279     public final boolean isAutoRanging() { return autoRanging.get(); }
 280     public final void setAutoRanging(boolean value) { autoRanging.set(value); }
 281     public final BooleanProperty autoRangingProperty() { return autoRanging; }
 282 
 283     /** The font for all tick labels */
 284     private ObjectProperty<Font> tickLabelFont = new StyleableObjectProperty<Font>(Font.font("System",8)) {
 285         @Override protected void invalidated() {
 286             Font f = get();
 287             measure.setFont(f);
 288             for(TickMark<T> tm : getTickMarks()) {
 289                 tm.textNode.setFont(f);
 290             }
 291             measureInvalid = true;
 292             requestAxisLayout();
 293         }
 294 
 295         @Override
 296         public CssMetaData<Axis<?>,Font> getCssMetaData() {
 297             return StyleableProperties.TICK_LABEL_FONT;
 298         }
 299 
 300         @Override
 301         public Object getBean() {
 302             return Axis.this;
 303         }
 304 
 305         @Override
 306         public String getName() {
 307             return "tickLabelFont";
 308         }
 309     };
 310     public final Font getTickLabelFont() { return tickLabelFont.get(); }
 311     public final void setTickLabelFont(Font value) { tickLabelFont.set(value); }
 312     public final ObjectProperty<Font> tickLabelFontProperty() { return tickLabelFont; }
 313 
 314     /** The fill for all tick labels */
 315     private ObjectProperty<Paint> tickLabelFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
 316         @Override protected void invalidated() {
 317             for (TickMark<T> tick : tickMarks) {
 318                 tick.textNode.setFill(getTickLabelFill());
 319             }
 320         }
 321 
 322         @Override
 323         public CssMetaData<Axis<?>,Paint> getCssMetaData() {
 324             return StyleableProperties.TICK_LABEL_FILL;
 325         }
 326 
 327         @Override
 328         public Object getBean() {
 329             return Axis.this;
 330         }
 331 
 332         @Override
 333         public String getName() {
 334             return "tickLabelFill";
 335         }
 336     };
 337     public final Paint getTickLabelFill() { return tickLabelFill.get(); }
 338     public final void setTickLabelFill(Paint value) { tickLabelFill.set(value); }
 339     public final ObjectProperty<Paint> tickLabelFillProperty() { return tickLabelFill; }
 340 
 341     /** The gap between tick labels and the tick mark lines */
 342     private DoubleProperty tickLabelGap = new StyleableDoubleProperty(3) {
 343         @Override protected void invalidated() {
 344            requestAxisLayout();
 345         }
 346 
 347         @Override
 348         public CssMetaData<Axis<?>,Number> getCssMetaData() {
 349             return StyleableProperties.TICK_LABEL_TICK_GAP;
 350         }
 351 
 352         @Override
 353         public Object getBean() {
 354             return Axis.this;
 355         }
 356 
 357         @Override
 358         public String getName() {
 359             return "tickLabelGap";
 360         }
 361     };
 362     public final double getTickLabelGap() { return tickLabelGap.get(); }
 363     public final void setTickLabelGap(double value) { tickLabelGap.set(value); }
 364     public final DoubleProperty tickLabelGapProperty() { return tickLabelGap; }
 365 
 366     /**
 367      * When true any changes to the axis and its range will be animated.
 368      */
 369     private BooleanProperty animated = new SimpleBooleanProperty(this, "animated", true);
 370 
 371     /**
 372      * Indicates whether the changes to axis range will be animated or not.
 373      *
 374      * @return true if axis range changes will be animated and false otherwise
 375      */
 376     public final boolean getAnimated() { return animated.get(); }
 377     public final void setAnimated(boolean value) { animated.set(value); }
 378     public final BooleanProperty animatedProperty() { return animated; }
 379 
 380     /**
 381      * Rotation in degrees of tick mark labels from their normal horizontal.
 382      */
 383     private DoubleProperty tickLabelRotation = new DoublePropertyBase(0) {
 384         @Override protected void invalidated() {
 385             if (isAutoRanging()) {
 386                 invalidateRange(); // NumberAxis and CategoryAxis use this property in autorange
 387             }
 388             requestAxisLayout();
 389         }
 390 
 391         @Override
 392         public Object getBean() {
 393             return Axis.this;
 394         }
 395 
 396         @Override
 397         public String getName() {
 398             return "tickLabelRotation";
 399         }
 400     };
 401     public final double getTickLabelRotation() { return tickLabelRotation.getValue(); }
 402     public final void setTickLabelRotation(double value) { tickLabelRotation.setValue(value); }
 403     public final DoubleProperty tickLabelRotationProperty() { return tickLabelRotation; }
 404 
 405     // -------------- CONSTRUCTOR --------------------------------------------------------------------------------------
 406 
 407     /**
 408      * Creates and initializes a new instance of the Axis class.
 409      */
 410     public Axis() {
 411         getStyleClass().setAll("axis");
 412         axisLabel.getStyleClass().add("axis-label");
 413         axisLabel.setAlignment(Pos.CENTER);
 414         tickMarkPath.getStyleClass().add("axis-tick-mark");
 415         getChildren().addAll(axisLabel, tickMarkPath);
 416     }
 417 
 418     // -------------- METHODS ------------------------------------------------------------------------------------------
 419 
 420     /**
 421      * See if the current range is valid, if it is not then any range dependent calulcations need to redone on the next layout pass
 422      *
 423      * @return true if current range calculations are valid
 424      */
 425     protected final boolean isRangeValid() { return rangeValid; }
 426 
 427     /**
 428      * Mark the current range invalid, this will cause anything that depends on the range to be recalculated on the
 429      * next layout.
 430      */
 431     protected final void invalidateRange() { rangeValid = false; }
 432 
 433     /**
 434      * This is used to check if any given animation should run. It returns true if animation is enabled and the node
 435      * is visible and in a scene.
 436      *
 437      * @return true if animations should happen
 438      */
 439     protected final boolean shouldAnimate(){
 440         return getAnimated() && impl_isTreeVisible() && getScene() != null;
 441     }
 442 
 443     /**
 444      * We suppress requestLayout() calls here by doing nothing as we don't want changes to our children to cause
 445      * layout. If you really need to request layout then call requestAxisLayout().
 446      */
 447     @Override public void requestLayout() {}
 448 
 449     /**
 450      * Request that the axis is laid out in the next layout pass. This replaces requestLayout() as it has been
 451      * overridden to do nothing so that changes to children's bounds etc do not cause a layout. This was done as a
 452      * optimization as the Axis knows the exact minimal set of changes that really need layout to be updated. So we
 453      * only want to request layout then, not on any child change.
 454      */
 455     public void requestAxisLayout() {
 456         super.requestLayout();
 457     }
 458 
 459     /**
 460      * Called when data has changed and the range may not be valid any more. This is only called by the chart if
 461      * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to
 462      * happen on next layout pass.
 463      *
 464      * @param data The current set of all data that needs to be plotted on this axis
 465      */
 466     public void invalidateRange(List<T> data) {
 467         invalidateRange();
 468         requestAxisLayout();
 469     }
 470 
 471     /**
 472      * This calculates the upper and lower bound based on the data provided to invalidateRange() method. This must not
 473      * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be
 474      * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for
 475      * this axis.
 476      *
 477      * @param length The length of the axis in screen coordinates
 478      * @return Range information, this is implementation dependent
 479      */
 480     protected abstract Object autoRange(double length);
 481 
 482     /**
 483      * Called to set the current axis range to the given range. If isAnimating() is true then this method should
 484      * animate the range to the new range.
 485      *
 486      * @param range A range object returned from autoRange()
 487      * @param animate If true animate the change in range
 488      */
 489     protected abstract void setRange(Object range, boolean animate);
 490 
 491     /**
 492      * Called to get the current axis range.
 493      *
 494      * @return A range object that can be passed to setRange() and calculateTickValues()
 495      */
 496     protected abstract Object getRange();
 497 
 498     /**
 499      * Get the display position of the zero line along this axis.
 500      *
 501      * @return display position or Double.NaN if zero is not in current range;
 502      */
 503     public abstract double getZeroPosition();
 504 
 505     /**
 506      * Get the display position along this axis for a given value.
 507      * If the value is not in the current range, the returned value will be an extrapolation of the display
 508      * position.
 509      *
 510      * If the value is not valid for this Axis and the axis cannot display such value in any range,
 511      * Double.NaN is returned
 512      *
 513      * @param value The data value to work out display position for
 514      * @return display position or Double.NaN if value not valid
 515      */
 516     public abstract double getDisplayPosition(T value);
 517 
 518     /**
 519      * Get the data value for the given display position on this axis. If the axis
 520      * is a CategoryAxis this will be the nearest value.
 521      *
 522      * @param  displayPosition A pixel position on this axis
 523      * @return the nearest data value to the given pixel position or
 524      *         null if not on axis;
 525      */
 526     public abstract T getValueForDisplay(double displayPosition);
 527 
 528     /**
 529      * Checks if the given value is plottable on this axis
 530      *
 531      * @param value The value to check if its on axis
 532      * @return true if the given value is plottable on this axis
 533      */
 534     public abstract boolean isValueOnAxis(T value);
 535 
 536     /**
 537      * All axis values must be representable by some numeric value. This gets the numeric value for a given data value.
 538      *
 539      * @param value The data value to convert
 540      * @return Numeric value for the given data value
 541      */
 542     public abstract double toNumericValue(T value);
 543 
 544     /**
 545      * All axis values must be representable by some numeric value. This gets the data value for a given numeric value.
 546      *
 547      * @param value The numeric value to convert
 548      * @return Data value for given numeric value
 549      */
 550     public abstract T toRealValue(double value);
 551 
 552     /**
 553      * Calculate a list of all the data values for each tick mark in range
 554      *
 555      * @param length The length of the axis in display units
 556      * @param range A range object returned from autoRange()
 557      * @return A list of tick marks that fit along the axis if it was the given length
 558      */
 559     protected abstract List<T> calculateTickValues(double length, Object range);
 560 
 561     /**
 562      * Computes the preferred height of this axis for the given width. If axis orientation
 563      * is horizontal, it takes into account the tick mark length, tick label gap and
 564      * label height.
 565      *
 566      * @return the computed preferred width for this axis
 567      */
 568     @Override protected double computePrefHeight(double width) {
 569         final Side side = getEffectiveSide();
 570         if (side.isVertical()) {
 571             // TODO for now we have no hard and fast answer here, I guess it should work
 572             // TODO out the minimum size needed to display min, max and zero tick mark labels.
 573             return 100;
 574         } else { // HORIZONTAL
 575             // we need to first auto range as this may/will effect tick marks
 576             Object range = autoRange(width);
 577             // calculate max tick label height
 578             double maxLabelHeight = 0;
 579             // calculate the new tick marks
 580             if (isTickLabelsVisible()) {
 581                 final List<T> newTickValues = calculateTickValues(width, range);
 582                 for (T value: newTickValues) {
 583                     maxLabelHeight = Math.max(maxLabelHeight,measureTickMarkSize(value, range).getHeight());
 584                 }
 585             }
 586             // calculate tick mark length
 587             final double tickMarkLength = isTickMarkVisible() ? (getTickLength() > 0) ? getTickLength() : 0 : 0;
 588             // calculate label height
 589             final double labelHeight =
 590                     axisLabel.getText() == null || axisLabel.getText().length() == 0 ?
 591                     0 : axisLabel.prefHeight(-1);
 592             return maxLabelHeight + getTickLabelGap() + tickMarkLength + labelHeight;
 593         }
 594     }
 595 
 596     /**
 597      * Computes the preferred width of this axis for the given height. If axis orientation
 598      * is vertical, it takes into account the tick mark length, tick label gap and
 599      * label height.
 600      *
 601      * @return the computed preferred width for this axis
 602      */
 603     @Override protected double computePrefWidth(double height) {
 604         final Side side = getEffectiveSide();
 605         if (side.isVertical()) {
 606             // we need to first auto range as this may/will effect tick marks
 607             Object range = autoRange(height);
 608             // calculate max tick label width
 609             double maxLabelWidth = 0;
 610             // calculate the new tick marks
 611             if (isTickLabelsVisible()) {
 612                 final List<T> newTickValues = calculateTickValues(height,range);
 613                 for (T value: newTickValues) {
 614                     maxLabelWidth = Math.max(maxLabelWidth, measureTickMarkSize(value, range).getWidth());
 615                 }
 616             }
 617             // calculate tick mark length
 618             final double tickMarkLength = isTickMarkVisible() ? (getTickLength() > 0) ? getTickLength() : 0 : 0;
 619             // calculate label height
 620             final double labelHeight =
 621                     axisLabel.getText() == null || axisLabel.getText().length() == 0 ?
 622                     0 : axisLabel.prefHeight(-1);
 623             return maxLabelWidth + getTickLabelGap() + tickMarkLength + labelHeight;
 624         } else  { // HORIZONTAL
 625             // TODO for now we have no hard and fast answer here, I guess it should work
 626             // TODO out the minimum size needed to display min, max and zero tick mark labels.
 627             return 100;
 628         }
 629     }
 630 
 631     /**
 632      * Called during layout if the tickmarks have been updated, allowing subclasses to do anything they need to
 633      * in reaction.
 634      */
 635     protected void tickMarksUpdated(){}
 636 
 637     /**
 638      * Invoked during the layout pass to layout this axis and all its content.
 639      */
 640     @Override protected void layoutChildren() {
 641         final double width = getWidth();
 642         final double height = getHeight();
 643         final double tickMarkLength = (isTickMarkVisible() && getTickLength() > 0) ? getTickLength() : 0;
 644         final boolean isFirstPass = oldLength == 0;
 645         // auto range if it is not valid
 646         final Side side = getEffectiveSide();
 647         final double length = (side.isVertical()) ? height : width;
 648         boolean rangeInvalid = !isRangeValid();
 649         boolean lengthDiffers = oldLength != length;
 650         if (lengthDiffers || rangeInvalid) {
 651             // get range
 652             Object range;
 653             if(isAutoRanging()) {
 654                 // auto range
 655                 range = autoRange(length);
 656                 // set current range to new range
 657                 setRange(range, getAnimated() && !isFirstPass && impl_isTreeVisible() && rangeInvalid);
 658             } else {
 659                 range = getRange();
 660             }
 661             // calculate new tick marks
 662             List<T> newTickValues = calculateTickValues(length, range);
 663 
 664             // remove everything
 665             Iterator<TickMark<T>> tickMarkIterator = tickMarks.iterator();
 666             while (tickMarkIterator.hasNext()) {
 667                 TickMark<T> tick = tickMarkIterator.next();
 668                 final TickMark<T> tm = tick;
 669                 if (shouldAnimate()) {
 670                     FadeTransition ft = new FadeTransition(Duration.millis(250),tick.textNode);
 671                     ft.setToValue(0);
 672                     ft.setOnFinished(actionEvent -> {
 673                         getChildren().remove(tm.textNode);
 674                     });
 675                     ft.play();
 676                 } else {
 677                     getChildren().remove(tm.textNode);
 678                 }
 679                 // we have to remove the tick mark immediately so we don't draw tick line for it or grid lines and fills
 680                 tickMarkIterator.remove();
 681             }
 682 
 683             // add new tick marks for new values
 684             for(T newValue: newTickValues) {
 685                 final TickMark<T> tick = new TickMark<T>();
 686                 tick.setValue(newValue);
 687                 tick.textNode.setText(getTickMarkLabel(newValue));
 688                 tick.textNode.setFont(getTickLabelFont());
 689                 tick.textNode.setFill(getTickLabelFill());
 690                 tick.setTextVisible(isTickLabelsVisible());
 691                 if (shouldAnimate()) tick.textNode.setOpacity(0);
 692                 getChildren().add(tick.textNode);
 693                 tickMarks.add(tick);
 694                 if (shouldAnimate()) {
 695                     FadeTransition ft = new FadeTransition(Duration.millis(750),tick.textNode);
 696                     ft.setFromValue(0);
 697                     ft.setToValue(1);
 698                     ft.play();
 699                 }
 700             }
 701 
 702             // call tick marks updated to inform subclasses that we have updated tick marks
 703             tickMarksUpdated();
 704             // mark all done
 705             oldLength = length;
 706             rangeValid = true;
 707         }
 708 
 709         if (lengthDiffers || rangeInvalid || measureInvalid || tickLabelsVisibleInvalid) {
 710             measureInvalid = false;
 711             tickLabelsVisibleInvalid = false;
 712             // RT-12272 : tick labels overlapping
 713             labelsToSkip.clear();
 714             double prevEnd = -Double.MAX_VALUE;
 715             double lastStart = Double.MAX_VALUE;
 716             switch (side) {
 717                 case LEFT:
 718                 case RIGHT:
 719                     int stop = 0;
 720                     for (; stop < tickMarks.size(); ++stop) {
 721                         TickMark<T> m = tickMarks.get(stop);
 722                         double tickPosition = updateAndGetDisplayPosition(m);
 723                         if (m.isTextVisible()) {
 724                             double tickHeight = measureTickMarkSize(m.getValue(), getRange()).getHeight();
 725                             lastStart = tickPosition - tickHeight / 2;
 726                             break;
 727                         } else {
 728                             labelsToSkip.set(stop);
 729                         }
 730                     }
 731 
 732                     for (int i = tickMarks.size() - 1; i > stop; i--) {
 733                         TickMark<T> m = tickMarks.get(i);
 734                         double tickPosition = updateAndGetDisplayPosition(m);
 735                         if (!m.isTextVisible()) {
 736                             labelsToSkip.set(i);
 737                             continue;
 738                         }
 739                         double tickHeight = measureTickMarkSize(m.getValue(), getRange()).getHeight();
 740                         double tickStart = tickPosition - tickHeight / 2;
 741                         if (tickStart <= prevEnd || tickStart + tickHeight > lastStart) {
 742                             labelsToSkip.set(i);
 743                         } else {
 744                             prevEnd = tickStart + tickHeight;
 745                         }
 746                     }
 747                     break;
 748                 case BOTTOM:
 749                 case TOP:
 750                     stop = tickMarks.size() - 1;
 751                     for (; stop >= 0; --stop) {
 752                         TickMark<T> m = tickMarks.get(stop);
 753                         double tickPosition = updateAndGetDisplayPosition(m);
 754                         if (m.isTextVisible()) {
 755                             double tickWidth = measureTickMarkSize(m.getValue(), getRange()).getWidth();
 756                             lastStart = tickPosition - tickWidth / 2;
 757                             break;
 758                         } else {
 759                             labelsToSkip.set(stop);
 760                         }
 761                     }
 762 
 763                     for (int i = 0; i < stop; ++i) {
 764                         TickMark<T> m = tickMarks.get(i);
 765                         double tickPosition = updateAndGetDisplayPosition(m);
 766                         if (!m.isTextVisible()) {
 767                             labelsToSkip.set(i);
 768                             continue;
 769                         }
 770                         double tickWidth = measureTickMarkSize(m.getValue(), getRange()).getWidth();
 771                         double tickStart = tickPosition - tickWidth / 2;
 772                         if (tickStart <= prevEnd || tickStart + tickWidth > lastStart) {
 773                             labelsToSkip.set(i);
 774                         } else {
 775                             prevEnd = tickStart + tickWidth;
 776                         }
 777                     }
 778                     break;
 779             }
 780         }
 781 
 782         // clear tick mark path elements as we will recreate
 783         tickMarkPath.getElements().clear();
 784         // do layout of axis label, tick mark lines and text
 785         double effectiveLabelRotation = getEffectiveTickLabelRotation();
 786         if (Side.LEFT.equals(side)) {
 787             // offset path to make strokes snap to pixel
 788             tickMarkPath.setLayoutX(-0.5);
 789             tickMarkPath.setLayoutY(0.5);
 790             if (getLabel() != null) {
 791                 axisLabel.getTransforms().setAll(new Translate(0, height), new Rotate(-90, 0, 0));
 792                 axisLabel.setLayoutX(0);
 793                 axisLabel.setLayoutY(0);
 794                 //noinspection SuspiciousNameCombination
 795                 axisLabel.resize(height, Math.ceil(axisLabel.prefHeight(width)));
 796             }
 797             for (int i = 0; i < tickMarks.size(); i++) {
 798                 TickMark<T> tick = tickMarks.get(i);
 799                 positionTextNode(tick.textNode, width - getTickLabelGap() - tickMarkLength,
 800                         tick.getPosition(), effectiveLabelRotation, side);
 801                 updateTickMark(tick, i, length,
 802                         width - tickMarkLength, tick.getPosition(),
 803                         width, tick.getPosition());
 804             }
 805         } else if (Side.RIGHT.equals(side)) {
 806             // offset path to make strokes snap to pixel
 807             tickMarkPath.setLayoutX(0.5);
 808             tickMarkPath.setLayoutY(0.5);
 809             if (getLabel() != null) {
 810                 final double axisLabelWidth = Math.ceil(axisLabel.prefHeight(width));
 811                 axisLabel.getTransforms().setAll(new Translate(0, height), new Rotate(-90, 0, 0));
 812                 axisLabel.setLayoutX(width-axisLabelWidth);
 813                 axisLabel.setLayoutY(0);
 814                 //noinspection SuspiciousNameCombination
 815                 axisLabel.resize(height, axisLabelWidth);
 816             }
 817             for (int i = 0; i < tickMarks.size(); i++) {
 818                 TickMark<T> tick = tickMarks.get(i);
 819                 positionTextNode(tick.textNode, getTickLabelGap() + tickMarkLength,
 820                         tick.getPosition(), effectiveLabelRotation, side);
 821                 updateTickMark(tick, i, length,
 822                         0, tick.getPosition(),
 823                         tickMarkLength, tick.getPosition());
 824             }
 825         } else if (Side.TOP.equals(side)) {
 826             // offset path to make strokes snap to pixel
 827             tickMarkPath.setLayoutX(0.5);
 828             tickMarkPath.setLayoutY(-0.5);
 829             if (getLabel() != null) {
 830                 axisLabel.getTransforms().clear();
 831                 axisLabel.setLayoutX(0);
 832                 axisLabel.setLayoutY(0);
 833                 axisLabel.resize(width, Math.ceil(axisLabel.prefHeight(width)));
 834             }
 835             for (int i = 0; i < tickMarks.size(); i++) {
 836                 TickMark<T> tick = tickMarks.get(i);
 837                 positionTextNode(tick.textNode, tick.getPosition(), height - tickMarkLength - getTickLabelGap(),
 838                         effectiveLabelRotation, side);
 839                 updateTickMark(tick, i, length,
 840                         tick.getPosition(), height,
 841                         tick.getPosition(), height - tickMarkLength);
 842             }
 843         } else {
 844             // BOTTOM
 845             // offset path to make strokes snap to pixel
 846             tickMarkPath.setLayoutX(0.5);
 847             tickMarkPath.setLayoutY(0.5);
 848             if (getLabel() != null) {
 849                 axisLabel.getTransforms().clear();
 850                 final double labelHeight = Math.ceil(axisLabel.prefHeight(width));
 851                 axisLabel.setLayoutX(0);
 852                 axisLabel.setLayoutY(height - labelHeight);
 853                 axisLabel.resize(width, labelHeight);
 854             }
 855             for (int i = 0; i < tickMarks.size(); i++) {
 856                 TickMark<T> tick = tickMarks.get(i);
 857                 positionTextNode(tick.textNode, tick.getPosition(), tickMarkLength + getTickLabelGap(),
 858                         effectiveLabelRotation, side);
 859                 updateTickMark(tick, i, length,
 860                         tick.getPosition(), 0,
 861                         tick.getPosition(), tickMarkLength);
 862             }
 863         }
 864     }
 865 
 866     private double updateAndGetDisplayPosition(TickMark<T> m) {
 867         double displayPosition = getDisplayPosition(m.getValue());
 868         m.setPosition(displayPosition);
 869         return displayPosition;
 870     }
 871 
 872     /**
 873      * Positions a text node to one side of the given point, it X height is vertically centered on point if LEFT or
 874      * RIGHT and its centered horizontally if TOP ot BOTTOM.
 875      *
 876      * @param node The text node to position
 877      * @param posX The x position, to place text next to
 878      * @param posY The y position, to place text next to
 879      * @param angle The text rotation
 880      * @param side The side to place text next to position x,y at
 881      */
 882     private void positionTextNode(Text node, double posX, double posY, double angle, Side side) {
 883         node.setLayoutX(0);
 884         node.setLayoutY(0);
 885         node.setRotate(angle);
 886         final Bounds bounds = node.getBoundsInParent();
 887         if (Side.LEFT.equals(side)) {
 888             node.setLayoutX(posX-bounds.getWidth()-bounds.getMinX());
 889             node.setLayoutY(posY - (bounds.getHeight() / 2d) - bounds.getMinY());
 890         } else if (Side.RIGHT.equals(side)) {
 891             node.setLayoutX(posX-bounds.getMinX());
 892             node.setLayoutY(posY-(bounds.getHeight()/2d)-bounds.getMinY());
 893         } else if (Side.TOP.equals(side)) {
 894             node.setLayoutX(posX-(bounds.getWidth()/2d)-bounds.getMinX());
 895             node.setLayoutY(posY-bounds.getHeight()-bounds.getMinY());
 896         } else {
 897             node.setLayoutX(posX-(bounds.getWidth()/2d)-bounds.getMinX());
 898             node.setLayoutY(posY-bounds.getMinY());
 899         }
 900     }
 901 
 902     /**
 903      * Updates visibility of the text node and adds the tick mark to the path
 904      */
 905     private void updateTickMark(TickMark<T> tick, int index, double length,
 906             double startX, double startY, double endX, double endY)
 907     {
 908         // check if position is inside bounds
 909         if (tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
 910             if (isTickLabelsVisible()) {
 911                 tick.textNode.setVisible(!labelsToSkip.get(index));
 912             }
 913             // add tick mark line
 914             tickMarkPath.getElements().addAll(
 915                     new MoveTo(startX, startY),
 916                     new LineTo(endX, endY)
 917             );
 918         } else {
 919             tick.textNode.setVisible(false);
 920         }
 921     }
 922     /**
 923      * Get the string label name for a tick mark with the given value
 924      *
 925      * @param value The value to format into a tick label string
 926      * @return A formatted string for the given value
 927      */
 928     protected abstract String getTickMarkLabel(T value);
 929 
 930     /**
 931      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 932      *
 933      *
 934      * @param labelText     tick mark label text
 935      * @param rotation  The text rotation
 936      * @return size of tick mark label for given value
 937      */
 938     protected final Dimension2D measureTickMarkLabelSize(String labelText, double rotation) {
 939         measure.setRotate(rotation);
 940         measure.setText(labelText);
 941         Bounds bounds = measure.getBoundsInParent();
 942         return new Dimension2D(bounds.getWidth(), bounds.getHeight());
 943     }
 944 
 945     /**
 946      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 947      *
 948      * @param value     tick mark value
 949      * @param rotation  The text rotation
 950      * @return size of tick mark label for given value
 951      */
 952     protected final Dimension2D measureTickMarkSize(T value, double rotation) {
 953         return measureTickMarkLabelSize(getTickMarkLabel(value), rotation);
 954     }
 955 
 956     /**
 957      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 958      *
 959      * @param value tick mark value
 960      * @param range range to use during calculations
 961      * @return size of tick mark label for given value
 962      */
 963     protected Dimension2D measureTickMarkSize(T value, Object range) {
 964         return measureTickMarkSize(value, getEffectiveTickLabelRotation());
 965     }
 966 
 967     final double getEffectiveTickLabelRotation() {
 968         return !isAutoRanging() || Double.isNaN(effectiveTickLabelRotation) ? getTickLabelRotation() : effectiveTickLabelRotation;
 969     }
 970 
 971     /**
 972      *
 973      * @param rotation NaN for using the tickLabelRotationProperty()
 974      */
 975     final void setEffectiveTickLabelRotation(double rotation) {
 976         effectiveTickLabelRotation = rotation;
 977     }
 978 
 979     // -------------- TICKMARK INNER CLASS -----------------------------------------------------------------------------
 980 
 981     /**
 982      * TickMark represents the label text, its associated properties for each tick
 983      * along the Axis.
 984      * @since JavaFX 2.0
 985      */
 986     public static final class TickMark<T> {
 987         /**
 988          * The display text for tick mark
 989          */
 990         private StringProperty label = new StringPropertyBase() {
 991             @Override protected void invalidated() {
 992                 textNode.setText(getValue());
 993             }
 994 
 995             @Override
 996             public Object getBean() {
 997                 return TickMark.this;
 998             }
 999 
1000             @Override
1001             public String getName() {
1002                 return "label";
1003             }
1004         };
1005         public final String getLabel() { return label.get(); }
1006         public final void setLabel(String value) { label.set(value); }
1007         public final StringExpression labelProperty() { return label; }
1008 
1009         /**
1010          * The value for this tick mark in data units
1011          */
1012         private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value");
1013         public final T getValue() { return value.get(); }
1014         public final void setValue(T v) { value.set(v); }
1015         public final ObjectExpression<T> valueProperty() { return value; }
1016 
1017         /**
1018          * The display position along the axis from axis origin in display units
1019          */
1020         private DoubleProperty position = new SimpleDoubleProperty(this, "position");
1021         public final double getPosition() { return position.get(); }
1022         public final void setPosition(double value) { position.set(value); }
1023         public final DoubleExpression positionProperty() { return position; }
1024 
1025         Text textNode = new Text();
1026 
1027         /** true if tick mark labels should be displayed */
1028         private BooleanProperty textVisible = new BooleanPropertyBase(true) {
1029             @Override protected void invalidated() {
1030                 if(!get()) {
1031                     textNode.setVisible(false);
1032                 }
1033             }
1034 
1035             @Override
1036             public Object getBean() {
1037                 return TickMark.this;
1038             }
1039 
1040             @Override
1041             public String getName() {
1042                 return "textVisible";
1043             }
1044         };
1045 
1046         /**
1047          * Indicates whether this tick mark label text is displayed or not.
1048          * @return true if tick mark label text is visible and false otherwise
1049          */
1050         public final boolean isTextVisible() { return textVisible.get(); }
1051 
1052         /**
1053          * Specifies whether this tick mark label text is displayed or not.
1054          * @param value true if tick mark label text is visible and false otherwise
1055          */
1056         public final void setTextVisible(boolean value) { textVisible.set(value); }
1057 
1058         /**
1059          * Creates and initializes an instance of TickMark.
1060          */
1061         public TickMark() {
1062         }
1063 
1064         /**
1065          * Returns a string representation of this {@code TickMark} object.
1066          * @return a string representation of this {@code TickMark} object.
1067          */
1068         @Override public String toString() {
1069             return value.get().toString();
1070         }
1071     }
1072 
1073     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
1074 
1075     private static class StyleableProperties {
1076         private static final CssMetaData<Axis<?>,Side> SIDE =
1077             new CssMetaData<Axis<?>,Side>("-fx-side",
1078                 new EnumConverter<Side>(Side.class)) {
1079 
1080             @Override
1081             public boolean isSettable(Axis<?> n) {
1082                 return n.side == null || !n.side.isBound();
1083             }
1084 
1085             @SuppressWarnings("unchecked") // sideProperty() is StyleableProperty<Side>
1086             @Override
1087             public StyleableProperty<Side> getStyleableProperty(Axis<?> n) {
1088                 return (StyleableProperty<Side>)n.sideProperty();
1089             }
1090         };
1091 
1092         private static final CssMetaData<Axis<?>,Number> TICK_LENGTH =
1093             new CssMetaData<Axis<?>,Number>("-fx-tick-length",
1094                 SizeConverter.getInstance(), 8.0) {
1095 
1096             @Override
1097             public boolean isSettable(Axis<?> n) {
1098                 return n.tickLength == null || !n.tickLength.isBound();
1099             }
1100 
1101             @Override
1102             public StyleableProperty<Number> getStyleableProperty(Axis<?> n) {
1103                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tickLengthProperty();
1104             }
1105         };
1106 
1107         private static final CssMetaData<Axis<?>,Font> TICK_LABEL_FONT =
1108             new FontCssMetaData<Axis<?>>("-fx-tick-label-font",
1109                 Font.font("system", 8.0)) {
1110 
1111             @Override
1112             public boolean isSettable(Axis<?> n) {
1113                 return n.tickLabelFont == null || !n.tickLabelFont.isBound();
1114             }
1115 
1116             @SuppressWarnings("unchecked") // tickLabelFontProperty() is StyleableProperty<Font>
1117             @Override
1118             public StyleableProperty<Font> getStyleableProperty(Axis<?> n) {
1119                 return (StyleableProperty<Font>)n.tickLabelFontProperty();
1120             }
1121         };
1122 
1123         private static final CssMetaData<Axis<?>,Paint> TICK_LABEL_FILL =
1124             new CssMetaData<Axis<?>,Paint>("-fx-tick-label-fill",
1125                 PaintConverter.getInstance(), Color.BLACK) {
1126 
1127             @Override
1128             public boolean isSettable(Axis<?> n) {
1129                 return n.tickLabelFill == null | !n.tickLabelFill.isBound();
1130             }
1131 
1132             @SuppressWarnings("unchecked") // tickLabelFillProperty() is StyleableProperty<Paint>
1133             @Override
1134             public StyleableProperty<Paint> getStyleableProperty(Axis<?> n) {
1135                 return (StyleableProperty<Paint>)n.tickLabelFillProperty();
1136             }
1137         };
1138 
1139         private static final CssMetaData<Axis<?>,Number> TICK_LABEL_TICK_GAP =
1140             new CssMetaData<Axis<?>,Number>("-fx-tick-label-gap",
1141                 SizeConverter.getInstance(), 3.0) {
1142 
1143             @Override
1144             public boolean isSettable(Axis<?> n) {
1145                 return n.tickLabelGap == null || !n.tickLabelGap.isBound();
1146             }
1147 
1148             @Override
1149             public StyleableProperty<Number> getStyleableProperty(Axis<?> n) {
1150                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tickLabelGapProperty();
1151             }
1152         };
1153 
1154         private static final CssMetaData<Axis<?>,Boolean> TICK_MARK_VISIBLE =
1155             new CssMetaData<Axis<?>,Boolean>("-fx-tick-mark-visible",
1156                 BooleanConverter.getInstance(), Boolean.TRUE) {
1157 
1158             @Override
1159             public boolean isSettable(Axis<?> n) {
1160                 return n.tickMarkVisible == null || !n.tickMarkVisible.isBound();
1161             }
1162 
1163             @Override
1164             public StyleableProperty<Boolean> getStyleableProperty(Axis<?> n) {
1165                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.tickMarkVisibleProperty();
1166             }
1167         };
1168 
1169         private static final CssMetaData<Axis<?>,Boolean> TICK_LABELS_VISIBLE =
1170             new CssMetaData<Axis<?>,Boolean>("-fx-tick-labels-visible",
1171                 BooleanConverter.getInstance(), Boolean.TRUE) {
1172 
1173             @Override
1174             public boolean isSettable(Axis<?> n) {
1175                 return n.tickLabelsVisible == null || !n.tickLabelsVisible.isBound();
1176             }
1177 
1178             @Override
1179             public StyleableProperty<Boolean> getStyleableProperty(Axis<?> n) {
1180                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.tickLabelsVisibleProperty();
1181             }
1182         };
1183 
1184         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1185         static {
1186         final List<CssMetaData<? extends Styleable, ?>> styleables =
1187             new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
1188             styleables.add(SIDE);
1189             styleables.add(TICK_LENGTH);
1190             styleables.add(TICK_LABEL_FONT);
1191             styleables.add(TICK_LABEL_FILL);
1192             styleables.add(TICK_LABEL_TICK_GAP);
1193             styleables.add(TICK_MARK_VISIBLE);
1194             styleables.add(TICK_LABELS_VISIBLE);
1195             STYLEABLES = Collections.unmodifiableList(styleables);
1196         }
1197     }
1198 
1199     /**
1200      * @return The CssMetaData associated with this class, which may include the
1201      * CssMetaData of its super classes.
1202      * @since JavaFX 8.0
1203      */
1204     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1205         return StyleableProperties.STYLEABLES;
1206     }
1207 
1208     /**
1209      * {@inheritDoc}
1210      * @since JavaFX 8.0
1211      */
1212     @Override
1213     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1214         return getClassCssMetaData();
1215     }
1216 
1217     /** pseudo-class indicating this is a vertical Top side Axis. */
1218     private static final PseudoClass TOP_PSEUDOCLASS_STATE =
1219             PseudoClass.getPseudoClass("top");
1220     /** pseudo-class indicating this is a vertical Bottom side Axis. */
1221     private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
1222             PseudoClass.getPseudoClass("bottom");
1223     /** pseudo-class indicating this is a vertical Left side Axis. */
1224     private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
1225             PseudoClass.getPseudoClass("left");
1226     /** pseudo-class indicating this is a vertical Right side Axis. */
1227     private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
1228             PseudoClass.getPseudoClass("right");
1229 
1230 }