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