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.CssMetaData; 29 import javafx.css.StyleableBooleanProperty; 30 import javafx.css.StyleableDoubleProperty; 31 import javafx.css.StyleableIntegerProperty; 32 33 import javafx.css.converter.BooleanConverter; 34 import javafx.css.converter.SizeConverter; 35 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.List; 39 40 import javafx.beans.property.*; 41 import javafx.beans.value.WritableValue; 42 import javafx.css.Styleable; 43 import javafx.css.StyleableProperty; 44 import javafx.geometry.Side; 45 import javafx.scene.shape.LineTo; 46 import javafx.scene.shape.MoveTo; 47 import javafx.scene.shape.Path; 48 import javafx.util.StringConverter; 49 50 51 /** 52 * A axis who's data is defined as Numbers. It can also draw minor 53 * tick-marks between the major ones. 54 * @since JavaFX 2.0 55 */ 56 public abstract class ValueAxis<T extends Number> extends Axis<T> { 57 58 // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- 59 60 private final Path minorTickPath = new Path(); 61 62 private double offset; 63 /** This is the minimum current data value and it is used while auto ranging. 64 * Package private solely for test purposes */ 65 double dataMinValue; 66 /** This is the maximum current data value and it is used while auto ranging. 67 * Package private solely for test purposes */ 68 double dataMaxValue; 69 /** List of the values at which there are minor ticks */ 70 private List<T> minorTickMarkValues = null; 71 // -------------- PRIVATE PROPERTIES ------------------------------------------------------------------------------- 72 73 /** 74 * The current value for the lowerBound of this axis, ie min value. 75 * This may be the same as lowerBound or different. It is used by NumberAxis to animate the 76 * lowerBound from the old value to the new value. 77 */ 78 protected final DoubleProperty currentLowerBound = new SimpleDoubleProperty(this, "currentLowerBound"); 79 80 // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- 81 82 /** true if minor tick marks should be displayed */ 83 private BooleanProperty minorTickVisible = new StyleableBooleanProperty(true) { 84 @Override protected void invalidated() { 85 minorTickPath.setVisible(get()); 86 requestAxisLayout(); 87 } 88 89 @Override 90 public Object getBean() { 91 return ValueAxis.this; 92 } 93 94 @Override 95 public String getName() { 96 return "minorTickVisible"; 97 } 98 99 @Override 100 public CssMetaData<ValueAxis<? extends Number>,Boolean> getCssMetaData() { 101 return StyleableProperties.MINOR_TICK_VISIBLE; 102 } 103 }; 104 public final boolean isMinorTickVisible() { return minorTickVisible.get(); } 105 public final void setMinorTickVisible(boolean value) { minorTickVisible.set(value); } 106 public final BooleanProperty minorTickVisibleProperty() { return minorTickVisible; } 107 108 109 /** The scale factor from data units to visual units */ 110 private ReadOnlyDoubleWrapper scale = new ReadOnlyDoubleWrapper(this, "scale", 0) { 111 @Override 112 protected void invalidated() { 113 requestAxisLayout(); 114 measureInvalid = true; 115 } 116 }; 117 public final double getScale() { return scale.get(); } 118 protected final void setScale(double scale) { this.scale.set(scale); } 119 public final ReadOnlyDoubleProperty scaleProperty() { return scale.getReadOnlyProperty(); } 120 ReadOnlyDoubleWrapper scalePropertyImpl() { return scale; } 121 122 /** The value for the upper bound of this axis, ie max value. This is automatically set if auto ranging is on. */ 123 private DoubleProperty upperBound = new DoublePropertyBase(100) { 124 @Override protected void invalidated() { 125 if(!isAutoRanging()) { 126 invalidateRange(); 127 requestAxisLayout(); 128 } 129 } 130 131 @Override 132 public Object getBean() { 133 return ValueAxis.this; 134 } 135 136 @Override 137 public String getName() { 138 return "upperBound"; 139 } 140 }; 141 public final double getUpperBound() { return upperBound.get(); } 142 public final void setUpperBound(double value) { upperBound.set(value); } 143 public final DoubleProperty upperBoundProperty() { return upperBound; } 144 145 /** The value for the lower bound of this axis, ie min value. This is automatically set if auto ranging is on. */ 146 private DoubleProperty lowerBound = new DoublePropertyBase(0) { 147 @Override protected void invalidated() { 148 if(!isAutoRanging()) { 149 invalidateRange(); 150 requestAxisLayout(); 151 } 152 } 153 154 @Override 155 public Object getBean() { 156 return ValueAxis.this; 157 } 158 159 @Override 160 public String getName() { 161 return "lowerBound"; 162 } 163 }; 164 public final double getLowerBound() { return lowerBound.get(); } 165 public final void setLowerBound(double value) { lowerBound.set(value); } 166 public final DoubleProperty lowerBoundProperty() { return lowerBound; } 167 168 /** StringConverter used to format tick mark labels. If null a default will be used */ 169 private final ObjectProperty<StringConverter<T>> tickLabelFormatter = new ObjectPropertyBase<StringConverter<T>>(null){ 170 @Override protected void invalidated() { 171 invalidateRange(); 172 requestAxisLayout(); 173 } 174 175 @Override 176 public Object getBean() { 177 return ValueAxis.this; 178 } 179 180 @Override 181 public String getName() { 182 return "tickLabelFormatter"; 183 } 184 }; 185 public final StringConverter<T> getTickLabelFormatter() { return tickLabelFormatter.getValue(); } 186 public final void setTickLabelFormatter(StringConverter<T> value) { tickLabelFormatter.setValue(value); } 187 public final ObjectProperty<StringConverter<T>> tickLabelFormatterProperty() { return tickLabelFormatter; } 188 189 /** The length of minor tick mark lines. Set to 0 to not display minor tick marks. */ 190 private DoubleProperty minorTickLength = new StyleableDoubleProperty(5) { 191 @Override protected void invalidated() { 192 requestAxisLayout(); 193 } 194 195 @Override 196 public Object getBean() { 197 return ValueAxis.this; 198 } 199 200 @Override 201 public String getName() { 202 return "minorTickLength"; 203 } 204 205 @Override 206 public CssMetaData<ValueAxis<? extends Number>,Number> getCssMetaData() { 207 return StyleableProperties.MINOR_TICK_LENGTH; 208 } 209 }; 210 public final double getMinorTickLength() { return minorTickLength.get(); } 211 public final void setMinorTickLength(double value) { minorTickLength.set(value); } 212 public final DoubleProperty minorTickLengthProperty() { return minorTickLength; } 213 214 /** 215 * The number of minor tick divisions to be displayed between each major tick mark. 216 * The number of actual minor tick marks will be one less than this. 217 */ 218 private IntegerProperty minorTickCount = new StyleableIntegerProperty(5) { 219 @Override protected void invalidated() { 220 invalidateRange(); 221 requestAxisLayout(); 222 } 223 224 @Override 225 public Object getBean() { 226 return ValueAxis.this; 227 } 228 229 @Override 230 public String getName() { 231 return "minorTickCount"; 232 } 233 234 @Override 235 public CssMetaData<ValueAxis<? extends Number>,Number> getCssMetaData() { 236 return StyleableProperties.MINOR_TICK_COUNT; 237 } 238 }; 239 public final int getMinorTickCount() { return minorTickCount.get(); } 240 public final void setMinorTickCount(int value) { minorTickCount.set(value); } 241 public final IntegerProperty minorTickCountProperty() { return minorTickCount; } 242 243 // -------------- CONSTRUCTORS ------------------------------------------------------------------------------------- 244 245 /** 246 * Create a auto-ranging ValueAxis 247 */ 248 public ValueAxis() { 249 minorTickPath.getStyleClass().add("axis-minor-tick-mark"); 250 getChildren().add(minorTickPath); 251 } 252 253 /** 254 * Create a non-auto-ranging ValueAxis with the given upper & lower bound 255 * 256 * @param lowerBound The lower bound for this axis, ie min plottable value 257 * @param upperBound The upper bound for this axis, ie max plottable value 258 */ 259 public ValueAxis(double lowerBound, double upperBound) { 260 this(); 261 setAutoRanging(false); 262 setLowerBound(lowerBound); 263 setUpperBound(upperBound); 264 } 265 266 // -------------- PROTECTED METHODS -------------------------------------------------------------------------------- 267 268 269 /** 270 * This calculates the upper and lower bound based on the data provided to invalidateRange() method. This must not 271 * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be 272 * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for 273 * this axis. 274 * 275 * @param length The length of the axis in screen coordinates 276 * @return Range information, this is implementation dependent 277 */ 278 @Override protected final Object autoRange(double length) { 279 // guess a sensible starting size for label size, that is approx 2 lines vertically or 2 charts horizontally 280 if (isAutoRanging()) { 281 // guess a sensible starting size for label size, that is approx 2 lines vertically or 2 charts horizontally 282 double labelSize = getTickLabelFont().getSize() * 2; 283 return autoRange(dataMinValue,dataMaxValue,length,labelSize); 284 } else { 285 return getRange(); 286 } 287 } 288 289 /** 290 * Calculate a new scale for this axis. This should not effect any state(properties) of this axis. 291 * 292 * @param length The display length of the axis 293 * @param lowerBound The lower bound value 294 * @param upperBound The upper bound value 295 * @return new scale to fit the range from lower bound to upper bound in the given display length 296 */ 297 protected final double calculateNewScale(double length, double lowerBound, double upperBound) { 298 double newScale = 1; 299 final Side side = getEffectiveSide(); 300 if (side.isVertical()) { 301 offset = length; 302 newScale = ((upperBound-lowerBound) == 0) ? -length : -(length / (upperBound - lowerBound)); 303 } else { // HORIZONTAL 304 offset = 0; 305 newScale = ((upperBound-lowerBound) == 0) ? length : length / (upperBound - lowerBound); 306 } 307 return newScale; 308 } 309 310 /** 311 * Called to set the upper and lower bound and anything else that needs to be auto-ranged. This must not effect 312 * the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be returned 313 * in the range object. This will we passed to setRange() if it has been decided to adopt this range for this axis. 314 * 315 * @param minValue The min data value that needs to be plotted on this axis 316 * @param maxValue The max data value that needs to be plotted on this axis 317 * @param length The length of the axis in display coordinates 318 * @param labelSize The approximate average size a label takes along the axis 319 * @return The calculated range 320 */ 321 protected Object autoRange(double minValue, double maxValue, double length, double labelSize) { 322 return null; // this method should have been abstract as there is no way for it to 323 // return anything correct. so just return null. 324 325 } 326 327 /** 328 * Calculate a list of the data values for every minor tick mark 329 * 330 * @return List of data values where to draw minor tick marks 331 */ 332 protected abstract List<T> calculateMinorTickMarks(); 333 334 /** 335 * Called during layout if the tickmarks have been updated, allowing subclasses to do anything they need to 336 * in reaction. 337 */ 338 @Override protected void tickMarksUpdated() { 339 super.tickMarksUpdated(); 340 // recalculate minor tick marks 341 minorTickMarkValues = calculateMinorTickMarks(); 342 } 343 344 /** 345 * Invoked during the layout pass to layout this axis and all its content. 346 */ 347 @Override protected void layoutChildren() { 348 final Side side = getEffectiveSide(); 349 final double length = side.isVertical() ? getHeight() :getWidth() ; 350 // if we are not auto ranging we need to calculate the new scale 351 if(!isAutoRanging()) { 352 // calculate new scale 353 setScale(calculateNewScale(length, getLowerBound(), getUpperBound())); 354 // update current lower bound 355 currentLowerBound.set(getLowerBound()); 356 } 357 // we have done all auto calcs, let Axis position major tickmarks 358 super.layoutChildren(); 359 int numMinorTicks = (getTickMarks().size() - 1)*(Math.max(1, getMinorTickCount()) - 1); 360 double neededLength = (getTickMarks().size()+numMinorTicks)*2; 361 362 // Update minor tickmarks 363 minorTickPath.getElements().clear(); 364 // Don't draw minor tick marks if there isn't enough space for them! 365 366 double minorTickLength = Math.max(0, getMinorTickLength()); 367 if (minorTickLength > 0 && length > neededLength) { 368 if (Side.LEFT.equals(side)) { 369 // snap minorTickPath to pixels 370 minorTickPath.setLayoutX(-0.5); 371 minorTickPath.setLayoutY(0.5); 372 for (T value : minorTickMarkValues) { 373 double y = getDisplayPosition(value); 374 if (y >= 0 && y <= length) { 375 minorTickPath.getElements().addAll( 376 new MoveTo(getWidth() - minorTickLength, y), 377 new LineTo(getWidth() - 1, y)); 378 } 379 } 380 } else if (Side.RIGHT.equals(side)) { 381 // snap minorTickPath to pixels 382 minorTickPath.setLayoutX(0.5); 383 minorTickPath.setLayoutY(0.5); 384 for (T value : minorTickMarkValues) { 385 double y = getDisplayPosition(value); 386 if (y >= 0 && y <= length) { 387 minorTickPath.getElements().addAll( 388 new MoveTo(1, y), 389 new LineTo(minorTickLength, y)); 390 } 391 } 392 } else if (Side.TOP.equals(side)) { 393 // snap minorTickPath to pixels 394 minorTickPath.setLayoutX(0.5); 395 minorTickPath.setLayoutY(-0.5); 396 for (T value : minorTickMarkValues) { 397 double x = getDisplayPosition(value); 398 if (x >= 0 && x <= length) { 399 minorTickPath.getElements().addAll( 400 new MoveTo(x, getHeight() - 1), 401 new LineTo(x, getHeight() - minorTickLength)); 402 } 403 } 404 } else { // BOTTOM 405 // snap minorTickPath to pixels 406 minorTickPath.setLayoutX(0.5); 407 minorTickPath.setLayoutY(0.5); 408 for (T value : minorTickMarkValues) { 409 double x = getDisplayPosition(value); 410 if (x >= 0 && x <= length) { 411 minorTickPath.getElements().addAll( 412 new MoveTo(x, 1.0F), 413 new LineTo(x, minorTickLength)); 414 } 415 } 416 } 417 } 418 } 419 420 // -------------- METHODS ------------------------------------------------------------------------------------------ 421 422 /** 423 * Called when data has changed and the range may not be valid any more. This is only called by the chart if 424 * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to 425 * happen on next layout pass. 426 * 427 * @param data The current set of all data that needs to be plotted on this axis 428 */ 429 @Override public void invalidateRange(List<T> data) { 430 if (data.isEmpty()) { 431 dataMaxValue = getUpperBound(); 432 dataMinValue = getLowerBound(); 433 } else { 434 dataMinValue = Double.MAX_VALUE; 435 // We need to init to the lowest negative double (which is NOT Double.MIN_VALUE) 436 // in order to find the maximum (positive or negative) 437 dataMaxValue = -Double.MAX_VALUE; 438 } 439 for(T dataValue: data) { 440 dataMinValue = Math.min(dataMinValue, dataValue.doubleValue()); 441 dataMaxValue = Math.max(dataMaxValue, dataValue.doubleValue()); 442 } 443 super.invalidateRange(data); 444 } 445 446 /** 447 * Get the display position along this axis for a given value. 448 * If the value is not in the current range, the returned value will be an extrapolation of the display 449 * position. 450 * 451 * @param value The data value to work out display position for 452 * @return display position 453 */ 454 @Override public double getDisplayPosition(T value) { 455 return offset + ((value.doubleValue() - currentLowerBound.get()) * getScale()); 456 } 457 458 /** 459 * Get the data value for the given display position on this axis. If the axis 460 * is a CategoryAxis this will be the nearest value. 461 * 462 * @param displayPosition A pixel position on this axis 463 * @return the nearest data value to the given pixel position or 464 * null if not on axis; 465 */ 466 @Override public T getValueForDisplay(double displayPosition) { 467 return toRealValue(((displayPosition-offset) / getScale()) + currentLowerBound.get()); 468 } 469 470 /** 471 * Get the display position of the zero line along this axis. 472 * 473 * @return display position or Double.NaN if zero is not in current range; 474 */ 475 @Override public double getZeroPosition() { 476 if (0 < getLowerBound() || 0 > getUpperBound()) return Double.NaN; 477 //noinspection unchecked 478 return getDisplayPosition((T)Double.valueOf(0)); 479 } 480 481 /** 482 * Checks if the given value is plottable on this axis 483 * 484 * @param value The value to check if its on axis 485 * @return true if the given value is plottable on this axis 486 */ 487 @Override public boolean isValueOnAxis(T value) { 488 final double num = value.doubleValue(); 489 return num >= getLowerBound() && num <= getUpperBound(); 490 } 491 492 /** 493 * All axis values must be representable by some numeric value. This gets the numeric value for a given data value. 494 * 495 * @param value The data value to convert 496 * @return Numeric value for the given data value 497 */ 498 @Override public double toNumericValue(T value) { 499 return (value == null) ? Double.NaN : value.doubleValue(); 500 } 501 502 /** 503 * All axis values must be representable by some numeric value. This gets the data value for a given numeric value. 504 * 505 * @param value The numeric value to convert 506 * @return Data value for given numeric value 507 */ 508 @Override public T toRealValue(double value) { 509 //noinspection unchecked 510 return (T)new Double(value); 511 } 512 513 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 514 515 private static class StyleableProperties { 516 private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_LENGTH = 517 new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-length", 518 SizeConverter.getInstance(), 5.0) { 519 520 @Override 521 public boolean isSettable(ValueAxis<? extends Number> n) { 522 return n.minorTickLength == null || !n.minorTickLength.isBound(); 523 } 524 525 @Override 526 public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) { 527 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickLengthProperty(); 528 } 529 }; 530 531 private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_COUNT = 532 new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-count", 533 SizeConverter.getInstance(), 5) { 534 535 @Override 536 public boolean isSettable(ValueAxis<? extends Number> n) { 537 return n.minorTickCount == null || !n.minorTickCount.isBound(); 538 } 539 540 @Override 541 public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) { 542 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickCountProperty(); 543 } 544 }; 545 546 private static final CssMetaData<ValueAxis<? extends Number>,Boolean> MINOR_TICK_VISIBLE = 547 new CssMetaData<ValueAxis<? extends Number>,Boolean>("-fx-minor-tick-visible", 548 BooleanConverter.getInstance(), Boolean.TRUE) { 549 550 @Override 551 public boolean isSettable(ValueAxis<? extends Number> n) { 552 return n.minorTickVisible == null || !n.minorTickVisible.isBound(); 553 } 554 555 @Override 556 public StyleableProperty<Boolean> getStyleableProperty(ValueAxis<? extends Number> n) { 557 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.minorTickVisibleProperty(); 558 } 559 }; 560 561 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 562 static { 563 final List<CssMetaData<? extends Styleable, ?>> styleables = 564 new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData()); 565 styleables.add(MINOR_TICK_COUNT); 566 styleables.add(MINOR_TICK_LENGTH); 567 styleables.add(MINOR_TICK_COUNT); 568 styleables.add(MINOR_TICK_VISIBLE); 569 STYLEABLES = Collections.unmodifiableList(styleables); 570 } 571 } 572 573 /** 574 * @return The CssMetaData associated with this class, which may include the 575 * CssMetaData of its super classes. 576 * @since JavaFX 8.0 577 */ 578 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 579 return StyleableProperties.STYLEABLES; 580 } 581 582 /** 583 * {@inheritDoc} 584 * @since JavaFX 8.0 585 */ 586 @Override 587 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 588 return getClassCssMetaData(); 589 } 590 591 }