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