1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.chart;
  27 
  28 import 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         int tickIndex = 0;
 649         boolean rangeInvalid = !isRangeValid();
 650         boolean lengthDiffers = oldLength != length;
 651         if (lengthDiffers || rangeInvalid) {
 652             // get range
 653             Object range;
 654             if(isAutoRanging()) {
 655                 // auto range
 656                 range = autoRange(length);
 657                 // set current range to new range
 658                 setRange(range, getAnimated() && !isFirstPass && impl_isTreeVisible() && rangeInvalid);
 659             } else {
 660                 range = getRange();
 661             }
 662             // calculate new tick marks
 663             List<T> newTickValues = calculateTickValues(length, range);
 664 
 665             // remove everything
 666             Iterator<TickMark<T>> tickMarkIterator = tickMarks.iterator();
 667             while (tickMarkIterator.hasNext()) {
 668                 TickMark<T> tick = tickMarkIterator.next();
 669                 final TickMark<T> tm = tick;
 670                 if (shouldAnimate()) {
 671                     FadeTransition ft = new FadeTransition(Duration.millis(250),tick.textNode);
 672                     ft.setToValue(0);
 673                     ft.setOnFinished(actionEvent -> {
 674                         getChildren().remove(tm.textNode);
 675                     });
 676                     ft.play();
 677                 } else {
 678                     getChildren().remove(tm.textNode);
 679                 }
 680                 // we have to remove the tick mark immediately so we don't draw tick line for it or grid lines and fills
 681                 tickMarkIterator.remove();
 682             }
 683 
 684             // add new tick marks for new values
 685             for(T newValue: newTickValues) {
 686                 final TickMark<T> tick = new TickMark<T>();
 687                 tick.setValue(newValue);
 688                 tick.textNode.setText(getTickMarkLabel(newValue));
 689                 tick.textNode.setFont(getTickLabelFont());
 690                 tick.textNode.setFill(getTickLabelFill());
 691                 tick.setTextVisible(isTickLabelsVisible());
 692                 if (shouldAnimate()) tick.textNode.setOpacity(0);
 693                 getChildren().add(tick.textNode);
 694                 tickMarks.add(tick);
 695                 if (shouldAnimate()) {
 696                     FadeTransition ft = new FadeTransition(Duration.millis(750),tick.textNode);
 697                     ft.setFromValue(0);
 698                     ft.setToValue(1);
 699                     ft.play();
 700                 }
 701             }
 702 
 703             // call tick marks updated to inform subclasses that we have updated tick marks
 704             tickMarksUpdated();
 705             // mark all done
 706             oldLength = length;
 707             rangeValid = true;
 708         }
 709 
 710         if (lengthDiffers || rangeInvalid || measureInvalid || tickLabelsVisibleInvalid) {
 711             measureInvalid = false;
 712             tickLabelsVisibleInvalid = false;
 713             // RT-12272 : tick labels overlapping
 714             labelsToSkip.clear();
 715             double prevEnd = -Double.MAX_VALUE;
 716             double lastStart = Double.MAX_VALUE;
 717             switch (side) {
 718                 case LEFT:
 719                 case RIGHT:
 720                     int stop = 0;
 721                     for (; stop < tickMarks.size(); ++stop) {
 722                         TickMark<T> m = tickMarks.get(stop);
 723                         if (m.isTextVisible()) {
 724                             double tickHeight = measureTickMarkSize(m.getValue(), getRange()).getHeight();
 725                             lastStart = updateAndGetDisplayPosition(m) - 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                         if (!m.isTextVisible()) {
 735                             labelsToSkip.set(i);
 736                             continue;
 737                         }
 738                         double tickHeight = measureTickMarkSize(m.getValue(), getRange()).getHeight();
 739                         double tickStart = updateAndGetDisplayPosition(m) - tickHeight / 2;
 740                         if (tickStart <= prevEnd || tickStart + tickHeight > lastStart) {
 741                             labelsToSkip.set(i);
 742                         } else {
 743                             prevEnd = tickStart + tickHeight;
 744                         }
 745                     }
 746                     break;
 747                 case BOTTOM:
 748                 case TOP:
 749                     stop = tickMarks.size() - 1;
 750                     for (; stop >= 0; --stop) {
 751                         TickMark<T> m = tickMarks.get(stop);
 752                         if (m.isTextVisible()) {
 753                             double tickWidth = measureTickMarkSize(m.getValue(), getRange()).getWidth();
 754                             lastStart = updateAndGetDisplayPosition(m) - tickWidth / 2;
 755                             break;
 756                         } else {
 757                             labelsToSkip.set(stop);
 758                         }
 759                     }
 760 
 761                     for (int i = 0; i < stop; ++i) {
 762                         TickMark<T> m = tickMarks.get(i);
 763                         if (!m.isTextVisible()) {
 764                             labelsToSkip.set(i);
 765                             continue;
 766                         }
 767                         double tickWidth = measureTickMarkSize(m.getValue(), getRange()).getWidth();
 768                         double tickStart = updateAndGetDisplayPosition(m) - tickWidth / 2;
 769                         if (tickStart <= prevEnd || tickStart + tickWidth > lastStart) {
 770                             labelsToSkip.set(i);
 771                         } else {
 772                             prevEnd = tickStart + tickWidth;
 773                         }
 774                     }
 775                     break;
 776             }
 777         }
 778 
 779         // clear tick mark path elements as we will recreate
 780         tickMarkPath.getElements().clear();
 781         // do layout of axis label, tick mark lines and text
 782         double effectiveLabelRotation = getEffectiveTickLabelRotation();
 783         if (Side.LEFT.equals(side)) {
 784             // offset path to make strokes snap to pixel
 785             tickMarkPath.setLayoutX(-0.5);
 786             tickMarkPath.setLayoutY(0.5);
 787             if (getLabel() != null) {
 788                 axisLabel.getTransforms().setAll(new Translate(0, height), new Rotate(-90, 0, 0));
 789                 axisLabel.setLayoutX(0);
 790                 axisLabel.setLayoutY(0);
 791                 //noinspection SuspiciousNameCombination
 792                 axisLabel.resize(height, Math.ceil(axisLabel.prefHeight(width)));
 793             }
 794             tickIndex = 0;
 795             for (int i = 0; i < tickMarks.size(); i++) {
 796                 TickMark<T> tick = tickMarks.get(i);
 797                 positionTextNode(tick.textNode, width - getTickLabelGap() - tickMarkLength,
 798                         tick.getPosition(), effectiveLabelRotation, side);
 799 
 800                 // check if position is inside bounds
 801                 if (tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
 802                     if (isTickLabelsVisible()) {
 803                         tick.textNode.setVisible(!labelsToSkip.get(i));
 804                         tickIndex++;
 805                     }
 806                     // add tick mark line
 807                     tickMarkPath.getElements().addAll(
 808                             new MoveTo(width - tickMarkLength, tick.getPosition()),
 809                             new LineTo(width, tick.getPosition())
 810                     );
 811                 } else {
 812                     tick.textNode.setVisible(false);
 813                 }
 814             }
 815         } else if (Side.RIGHT.equals(side)) {
 816             // offset path to make strokes snap to pixel
 817             tickMarkPath.setLayoutX(0.5);
 818             tickMarkPath.setLayoutY(0.5);
 819             tickIndex = 0;
 820             for (int i = 0; i < tickMarks.size(); i++) {
 821                 TickMark<T> tick = tickMarks.get(i);
 822                 positionTextNode(tick.textNode, getTickLabelGap() + tickMarkLength,
 823                         tick.getPosition(), effectiveLabelRotation, side);
 824                 // check if position is inside bounds
 825                 if (tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
 826                     if (isTickLabelsVisible()) {
 827                         tick.textNode.setVisible(!labelsToSkip.get(i));
 828                         tickIndex++;
 829                     }
 830                     // add tick mark line
 831                     tickMarkPath.getElements().addAll(
 832                             new MoveTo(0, tick.getPosition()),
 833                             new LineTo(tickMarkLength, tick.getPosition())
 834                     );
 835                 } else {
 836                     tick.textNode.setVisible(false);
 837                 }
 838             }
 839             if (getLabel() != null) {
 840                 final double axisLabelWidth = Math.ceil(axisLabel.prefHeight(width));
 841                 axisLabel.getTransforms().setAll(new Translate(0, height), new Rotate(-90, 0, 0));
 842                 axisLabel.setLayoutX(width-axisLabelWidth);
 843                 axisLabel.setLayoutY(0);
 844                 //noinspection SuspiciousNameCombination
 845                 axisLabel.resize(height, axisLabelWidth);
 846             }
 847         } else if (Side.TOP.equals(side)) {
 848             // offset path to make strokes snap to pixel
 849             tickMarkPath.setLayoutX(0.5);
 850             tickMarkPath.setLayoutY(-0.5);
 851             if (getLabel() != null) {
 852                 axisLabel.getTransforms().clear();
 853                 axisLabel.setLayoutX(0);
 854                 axisLabel.setLayoutY(0);
 855                 axisLabel.resize(width, Math.ceil(axisLabel.prefHeight(width)));
 856             }
 857             tickIndex = 0;
 858             for (int i = 0; i < tickMarks.size(); i++) {
 859                 TickMark<T> tick = tickMarks.get(i);
 860                 positionTextNode(tick.textNode, tick.getPosition(), height - tickMarkLength - getTickLabelGap(),
 861                         effectiveLabelRotation, side);
 862                 // check if position is inside bounds
 863                 if (tick.getPosition() >= 0 && tick.getPosition() <= Math.ceil(length)) {
 864                     if (isTickLabelsVisible()) {
 865                         tick.textNode.setVisible(!labelsToSkip.get(i));
 866                         tickIndex++;
 867                     }
 868                     // add tick mark line
 869                     tickMarkPath.getElements().addAll(
 870                             new MoveTo(tick.getPosition(), height),
 871                             new LineTo(tick.getPosition(), height - tickMarkLength)
 872                     );
 873                 } else {
 874                     tick.textNode.setVisible(false);
 875                 }
 876             }
 877         } else {
 878             // BOTTOM
 879             // offset path to make strokes snap to pixel
 880             tickMarkPath.setLayoutX(0.5);
 881             tickMarkPath.setLayoutY(0.5);
 882             tickIndex = 0;
 883             for (int i = 0; i < tickMarks.size(); i++) {
 884                 TickMark<T> tick = tickMarks.get(i);
 885                 final double xPos = Math.round(getDisplayPosition(tick.getValue()));
 886 //                System.out.println("tick pos at : "+tickIndex+" = "+xPos);
 887                 positionTextNode(tick.textNode, xPos, tickMarkLength + getTickLabelGap(),
 888                         effectiveLabelRotation, side);
 889                 // check if position is inside bounds
 890                 if (xPos >= 0 && xPos <= Math.ceil(length)) {
 891                     if (isTickLabelsVisible()) {
 892                         tick.textNode.setVisible(!labelsToSkip.get(i));
 893                         tickIndex++;
 894                     }
 895                     // add tick mark line
 896                     tickMarkPath.getElements().addAll(
 897                             new MoveTo(xPos, 0),
 898                             new LineTo(xPos, tickMarkLength)
 899                     );
 900                 } else {
 901                     tick.textNode.setVisible(false);
 902                 }
 903             }
 904             if (getLabel() != null) {
 905                 axisLabel.getTransforms().clear();
 906                 final double labelHeight = Math.ceil(axisLabel.prefHeight(width));
 907                 axisLabel.setLayoutX(0);
 908                 axisLabel.setLayoutY(height-labelHeight);
 909                 axisLabel.resize(width, labelHeight);
 910             }
 911         }
 912     }
 913 
 914     private double updateAndGetDisplayPosition(TickMark<T> m) {
 915         double displayPosition = getDisplayPosition(m.getValue());
 916         m.setPosition(displayPosition);
 917         return displayPosition;
 918     }
 919 
 920     /**
 921      * Positions a text node to one side of the given point, it X height is vertically centered on point if LEFT or
 922      * RIGHT and its centered horizontally if TOP ot BOTTOM.
 923      *
 924      * @param node The text node to position
 925      * @param posX The x position, to place text next to
 926      * @param posY The y position, to place text next to
 927      * @param angle The text rotation
 928      * @param side The side to place text next to position x,y at
 929      */
 930     private void positionTextNode(Text node, double posX, double posY, double angle, Side side) {
 931         node.setLayoutX(0);
 932         node.setLayoutY(0);
 933         node.setRotate(angle);
 934         final Bounds bounds = node.getBoundsInParent();
 935         if (Side.LEFT.equals(side)) {
 936             node.setLayoutX(posX-bounds.getWidth()-bounds.getMinX());
 937             node.setLayoutY(posY - (bounds.getHeight() / 2d) - bounds.getMinY());
 938         } else if (Side.RIGHT.equals(side)) {
 939             node.setLayoutX(posX-bounds.getMinX());
 940             node.setLayoutY(posY-(bounds.getHeight()/2d)-bounds.getMinY());
 941         } else if (Side.TOP.equals(side)) {
 942             node.setLayoutX(posX-(bounds.getWidth()/2d)-bounds.getMinX());
 943             node.setLayoutY(posY-bounds.getHeight()-bounds.getMinY());
 944         } else {
 945             node.setLayoutX(posX-(bounds.getWidth()/2d)-bounds.getMinX());
 946             node.setLayoutY(posY-bounds.getMinY());
 947         }
 948     }
 949 
 950     /**
 951      * Get the string label name for a tick mark with the given value
 952      *
 953      * @param value The value to format into a tick label string
 954      * @return A formatted string for the given value
 955      */
 956     protected abstract String getTickMarkLabel(T value);
 957 
 958     /**
 959      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 960      *
 961      *
 962      * @param labelText     tick mark label text
 963      * @param rotation  The text rotation
 964      * @return size of tick mark label for given value
 965      */
 966     protected final Dimension2D measureTickMarkLabelSize(String labelText, double rotation) {
 967         measure.setRotate(rotation);
 968         measure.setText(labelText);
 969         Bounds bounds = measure.getBoundsInParent();
 970         return new Dimension2D(bounds.getWidth(), bounds.getHeight());
 971     }
 972 
 973     /**
 974      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 975      *
 976      * @param value     tick mark value
 977      * @param rotation  The text rotation
 978      * @return size of tick mark label for given value
 979      */
 980     protected final Dimension2D measureTickMarkSize(T value, double rotation) {
 981         return measureTickMarkLabelSize(getTickMarkLabel(value), rotation);
 982     }
 983 
 984     /**
 985      * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks
 986      *
 987      * @param value tick mark value
 988      * @param range range to use during calculations
 989      * @return size of tick mark label for given value
 990      */
 991     protected Dimension2D measureTickMarkSize(T value, Object range) {
 992         return measureTickMarkSize(value, getEffectiveTickLabelRotation());
 993     }
 994 
 995     final double getEffectiveTickLabelRotation() {
 996         return !isAutoRanging() || Double.isNaN(effectiveTickLabelRotation) ? getTickLabelRotation() : effectiveTickLabelRotation;
 997     }
 998 
 999     /**
1000      *
1001      * @param rotation NaN for using the tickLabelRotationProperty()
1002      */
1003     final void setEffectiveTickLabelRotation(double rotation) {
1004         effectiveTickLabelRotation = rotation;
1005     }
1006 
1007     // -------------- TICKMARK INNER CLASS -----------------------------------------------------------------------------
1008 
1009     /**
1010      * TickMark represents the label text, its associated properties for each tick
1011      * along the Axis.
1012      * @since JavaFX 2.0
1013      */
1014     public static final class TickMark<T> {
1015         /**
1016          * The display text for tick mark
1017          */
1018         private StringProperty label = new StringPropertyBase() {
1019             @Override protected void invalidated() {
1020                 textNode.setText(getValue());
1021             }
1022 
1023             @Override
1024             public Object getBean() {
1025                 return TickMark.this;
1026             }
1027 
1028             @Override
1029             public String getName() {
1030                 return "label";
1031             }
1032         };
1033         public final String getLabel() { return label.get(); }
1034         public final void setLabel(String value) { label.set(value); }
1035         public final StringExpression labelProperty() { return label; }
1036 
1037         /**
1038          * The value for this tick mark in data units
1039          */
1040         private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value");
1041         public final T getValue() { return value.get(); }
1042         public final void setValue(T v) { value.set(v); }
1043         public final ObjectExpression<T> valueProperty() { return value; }
1044 
1045         /**
1046          * The display position along the axis from axis origin in display units
1047          */
1048         private DoubleProperty position = new SimpleDoubleProperty(this, "position");
1049         public final double getPosition() { return position.get(); }
1050         public final void setPosition(double value) { position.set(value); }
1051         public final DoubleExpression positionProperty() { return position; }
1052 
1053         Text textNode = new Text();
1054 
1055         /** true if tick mark labels should be displayed */
1056         private BooleanProperty textVisible = new BooleanPropertyBase(true) {
1057             @Override protected void invalidated() {
1058                 if(!get()) {
1059                     textNode.setVisible(false);
1060                 }
1061             }
1062 
1063             @Override
1064             public Object getBean() {
1065                 return TickMark.this;
1066             }
1067 
1068             @Override
1069             public String getName() {
1070                 return "textVisible";
1071             }
1072         };
1073 
1074         /**
1075          * Indicates whether this tick mark label text is displayed or not.
1076          * @return true if tick mark label text is visible and false otherwise
1077          */
1078         public final boolean isTextVisible() { return textVisible.get(); }
1079 
1080         /**
1081          * Specifies whether this tick mark label text is displayed or not.
1082          * @param value true if tick mark label text is visible and false otherwise
1083          */
1084         public final void setTextVisible(boolean value) { textVisible.set(value); }
1085 
1086         /**
1087          * Creates and initializes an instance of TickMark.
1088          */
1089         public TickMark() {
1090         }
1091 
1092         /**
1093          * Returns a string representation of this {@code TickMark} object.
1094          * @return a string representation of this {@code TickMark} object.
1095          */
1096         @Override public String toString() {
1097             return value.get().toString();
1098         }
1099     }
1100 
1101     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
1102 
1103     /** @treatAsPrivate implementation detail */
1104     private static class StyleableProperties {
1105         private static final CssMetaData<Axis<?>,Side> SIDE =
1106             new CssMetaData<Axis<?>,Side>("-fx-side",
1107                 new EnumConverter<Side>(Side.class)) {
1108 
1109             @Override
1110             public boolean isSettable(Axis<?> n) {
1111                 return n.side == null || !n.side.isBound();
1112             }
1113 
1114             @SuppressWarnings("unchecked") // sideProperty() is StyleableProperty<Side>
1115             @Override
1116             public StyleableProperty<Side> getStyleableProperty(Axis<?> n) {
1117                 return (StyleableProperty<Side>)n.sideProperty();
1118             }
1119         };
1120 
1121         private static final CssMetaData<Axis<?>,Number> TICK_LENGTH =
1122             new CssMetaData<Axis<?>,Number>("-fx-tick-length",
1123                 SizeConverter.getInstance(), 8.0) {
1124 
1125             @Override
1126             public boolean isSettable(Axis<?> n) {
1127                 return n.tickLength == null || !n.tickLength.isBound();
1128             }
1129 
1130             @Override
1131             public StyleableProperty<Number> getStyleableProperty(Axis<?> n) {
1132                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tickLengthProperty();
1133             }
1134         };
1135 
1136         private static final CssMetaData<Axis<?>,Font> TICK_LABEL_FONT =
1137             new FontCssMetaData<Axis<?>>("-fx-tick-label-font",
1138                 Font.font("system", 8.0)) {
1139 
1140             @Override
1141             public boolean isSettable(Axis<?> n) {
1142                 return n.tickLabelFont == null || !n.tickLabelFont.isBound();
1143             }
1144 
1145             @SuppressWarnings("unchecked") // tickLabelFontProperty() is StyleableProperty<Font>
1146             @Override
1147             public StyleableProperty<Font> getStyleableProperty(Axis<?> n) {
1148                 return (StyleableProperty<Font>)n.tickLabelFontProperty();
1149             }
1150         };
1151 
1152         private static final CssMetaData<Axis<?>,Paint> TICK_LABEL_FILL =
1153             new CssMetaData<Axis<?>,Paint>("-fx-tick-label-fill",
1154                 PaintConverter.getInstance(), Color.BLACK) {
1155 
1156             @Override
1157             public boolean isSettable(Axis<?> n) {
1158                 return n.tickLabelFill == null | !n.tickLabelFill.isBound();
1159             }
1160 
1161             @SuppressWarnings("unchecked") // tickLabelFillProperty() is StyleableProperty<Paint>
1162             @Override
1163             public StyleableProperty<Paint> getStyleableProperty(Axis<?> n) {
1164                 return (StyleableProperty<Paint>)n.tickLabelFillProperty();
1165             }
1166         };
1167 
1168         private static final CssMetaData<Axis<?>,Number> TICK_LABEL_TICK_GAP =
1169             new CssMetaData<Axis<?>,Number>("-fx-tick-label-gap",
1170                 SizeConverter.getInstance(), 3.0) {
1171 
1172             @Override
1173             public boolean isSettable(Axis<?> n) {
1174                 return n.tickLabelGap == null || !n.tickLabelGap.isBound();
1175             }
1176 
1177             @Override
1178             public StyleableProperty<Number> getStyleableProperty(Axis<?> n) {
1179                 return (StyleableProperty<Number>)(WritableValue<Number>)n.tickLabelGapProperty();
1180             }
1181         };
1182 
1183         private static final CssMetaData<Axis<?>,Boolean> TICK_MARK_VISIBLE =
1184             new CssMetaData<Axis<?>,Boolean>("-fx-tick-mark-visible",
1185                 BooleanConverter.getInstance(), Boolean.TRUE) {
1186 
1187             @Override
1188             public boolean isSettable(Axis<?> n) {
1189                 return n.tickMarkVisible == null || !n.tickMarkVisible.isBound();
1190             }
1191 
1192             @Override
1193             public StyleableProperty<Boolean> getStyleableProperty(Axis<?> n) {
1194                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.tickMarkVisibleProperty();
1195             }
1196         };
1197 
1198         private static final CssMetaData<Axis<?>,Boolean> TICK_LABELS_VISIBLE =
1199             new CssMetaData<Axis<?>,Boolean>("-fx-tick-labels-visible",
1200                 BooleanConverter.getInstance(), Boolean.TRUE) {
1201 
1202             @Override
1203             public boolean isSettable(Axis<?> n) {
1204                 return n.tickLabelsVisible == null || !n.tickLabelsVisible.isBound();
1205             }
1206 
1207             @Override
1208             public StyleableProperty<Boolean> getStyleableProperty(Axis<?> n) {
1209                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.tickLabelsVisibleProperty();
1210             }
1211         };
1212 
1213         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1214         static {
1215         final List<CssMetaData<? extends Styleable, ?>> styleables =
1216             new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
1217             styleables.add(SIDE);
1218             styleables.add(TICK_LENGTH);
1219             styleables.add(TICK_LABEL_FONT);
1220             styleables.add(TICK_LABEL_FILL);
1221             styleables.add(TICK_LABEL_TICK_GAP);
1222             styleables.add(TICK_MARK_VISIBLE);
1223             styleables.add(TICK_LABELS_VISIBLE);
1224             STYLEABLES = Collections.unmodifiableList(styleables);
1225         }
1226     }
1227 
1228     /**
1229      * @return The CssMetaData associated with this class, which may include the
1230      * CssMetaData of its super classes.
1231      * @since JavaFX 8.0
1232      */
1233     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1234         return StyleableProperties.STYLEABLES;
1235     }
1236 
1237     /**
1238      * {@inheritDoc}
1239      * @since JavaFX 8.0
1240      */
1241     @Override
1242     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1243         return getClassCssMetaData();
1244     }
1245 
1246     /** pseudo-class indicating this is a vertical Top side Axis. */
1247     private static final PseudoClass TOP_PSEUDOCLASS_STATE =
1248             PseudoClass.getPseudoClass("top");
1249     /** pseudo-class indicating this is a vertical Bottom side Axis. */
1250     private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
1251             PseudoClass.getPseudoClass("bottom");
1252     /** pseudo-class indicating this is a vertical Left side Axis. */
1253     private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
1254             PseudoClass.getPseudoClass("left");
1255     /** pseudo-class indicating this is a vertical Right side Axis. */
1256     private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
1257             PseudoClass.getPseudoClass("right");
1258 
1259 }