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