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 360 // Update minor tickmarks 361 minorTickPath.getElements().clear(); 362 363 double minorTickLength = Math.max(0, getMinorTickLength()); 364 // The length must be greater then the space required for tick marks, otherwise, there's no reason to create 365 // minor tick marks 366 if (minorTickLength > 0 && length > 2 * getTickMarks().size()) { 367 // Strip factor is >= 1. When == 1, all minor ticks will fit. 368 // It's computed as number of minor tick marks divided by available length 369 int stripFactor = (int)Math.ceil(2 * minorTickMarkValues.size() / (length - 2 * getTickMarks().size())); 370 if (Side.LEFT.equals(side)) { 371 // snap minorTickPath to pixels 372 minorTickPath.setLayoutX(-0.5); 373 minorTickPath.setLayoutY(0.5); 374 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) { 375 T value = minorTickMarkValues.get(i); 376 double y = getDisplayPosition(value); 377 if (y >= 0 && y <= length) { 378 minorTickPath.getElements().addAll( 379 new MoveTo(getWidth() - minorTickLength, y), 380 new LineTo(getWidth() - 1, y)); 381 } 382 } 383 } else if (Side.RIGHT.equals(side)) { 384 // snap minorTickPath to pixels 385 minorTickPath.setLayoutX(0.5); 386 minorTickPath.setLayoutY(0.5); 387 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) { 388 T value = minorTickMarkValues.get(i); 389 double y = getDisplayPosition(value); 390 if (y >= 0 && y <= length) { 391 minorTickPath.getElements().addAll( 392 new MoveTo(1, y), 393 new LineTo(minorTickLength, y)); 394 } 395 } 396 } else if (Side.TOP.equals(side)) { 397 // snap minorTickPath to pixels 398 minorTickPath.setLayoutX(0.5); 399 minorTickPath.setLayoutY(-0.5); 400 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) { 401 T value = minorTickMarkValues.get(i); 402 double x = getDisplayPosition(value); 403 if (x >= 0 && x <= length) { 404 minorTickPath.getElements().addAll( 405 new MoveTo(x, getHeight() - 1), 406 new LineTo(x, getHeight() - minorTickLength)); 407 } 408 } 409 } else { // BOTTOM 410 // snap minorTickPath to pixels 411 minorTickPath.setLayoutX(0.5); 412 minorTickPath.setLayoutY(0.5); 413 for (int i = 0; i < minorTickMarkValues.size(); i += stripFactor) { 414 T value = minorTickMarkValues.get(i); 415 double x = getDisplayPosition(value); 416 if (x >= 0 && x <= length) { 417 minorTickPath.getElements().addAll( 418 new MoveTo(x, 1.0F), 419 new LineTo(x, minorTickLength)); 420 } 421 } 422 } 423 } 424 } 425 426 // -------------- METHODS ------------------------------------------------------------------------------------------ 427 428 /** 429 * Called when data has changed and the range may not be valid any more. This is only called by the chart if 430 * isAutoRanging() returns true. If we are auto ranging it will cause layout to be requested and auto ranging to 431 * happen on next layout pass. 432 * 433 * @param data The current set of all data that needs to be plotted on this axis 434 */ 435 @Override public void invalidateRange(List<T> data) { 436 if (data.isEmpty()) { 437 dataMaxValue = getUpperBound(); 438 dataMinValue = getLowerBound(); 439 } else { 440 dataMinValue = Double.MAX_VALUE; 441 // We need to init to the lowest negative double (which is NOT Double.MIN_VALUE) 442 // in order to find the maximum (positive or negative) 443 dataMaxValue = -Double.MAX_VALUE; 444 } 445 for(T dataValue: data) { 446 dataMinValue = Math.min(dataMinValue, dataValue.doubleValue()); 447 dataMaxValue = Math.max(dataMaxValue, dataValue.doubleValue()); 448 } 449 super.invalidateRange(data); 450 } 451 452 /** 453 * Get the display position along this axis for a given value. 454 * If the value is not in the current range, the returned value will be an extrapolation of the display 455 * position. 456 * 457 * @param value The data value to work out display position for 458 * @return display position 459 */ 460 @Override public double getDisplayPosition(T value) { 461 return offset + ((value.doubleValue() - currentLowerBound.get()) * getScale()); 462 } 463 464 /** 465 * Get the data value for the given display position on this axis. If the axis 466 * is a CategoryAxis this will be the nearest value. 467 * 468 * @param displayPosition A pixel position on this axis 469 * @return the nearest data value to the given pixel position or 470 * null if not on axis; 471 */ 472 @Override public T getValueForDisplay(double displayPosition) { 473 return toRealValue(((displayPosition-offset) / getScale()) + currentLowerBound.get()); 474 } 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 @Override public double getZeroPosition() { 482 if (0 < getLowerBound() || 0 > getUpperBound()) return Double.NaN; 483 //noinspection unchecked 484 return getDisplayPosition((T)Double.valueOf(0)); 485 } 486 487 /** 488 * Checks if the given value is plottable on this axis 489 * 490 * @param value The value to check if its on axis 491 * @return true if the given value is plottable on this axis 492 */ 493 @Override public boolean isValueOnAxis(T value) { 494 final double num = value.doubleValue(); 495 return num >= getLowerBound() && num <= getUpperBound(); 496 } 497 498 /** 499 * All axis values must be representable by some numeric value. This gets the numeric value for a given data value. 500 * 501 * @param value The data value to convert 502 * @return Numeric value for the given data value 503 */ 504 @Override public double toNumericValue(T value) { 505 return (value == null) ? Double.NaN : value.doubleValue(); 506 } 507 508 /** 509 * All axis values must be representable by some numeric value. This gets the data value for a given numeric value. 510 * 511 * @param value The numeric value to convert 512 * @return Data value for given numeric value 513 */ 514 @Override public T toRealValue(double value) { 515 //noinspection unchecked 516 return (T)new Double(value); 517 } 518 519 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 520 521 private static class StyleableProperties { 522 private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_LENGTH = 523 new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-length", 524 SizeConverter.getInstance(), 5.0) { 525 526 @Override 527 public boolean isSettable(ValueAxis<? extends Number> n) { 528 return n.minorTickLength == null || !n.minorTickLength.isBound(); 529 } 530 531 @Override 532 public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) { 533 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickLengthProperty(); 534 } 535 }; 536 537 private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_COUNT = 538 new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-count", 539 SizeConverter.getInstance(), 5) { 540 541 @Override 542 public boolean isSettable(ValueAxis<? extends Number> n) { 543 return n.minorTickCount == null || !n.minorTickCount.isBound(); 544 } 545 546 @Override 547 public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) { 548 return (StyleableProperty<Number>)(WritableValue<Number>)n.minorTickCountProperty(); 549 } 550 }; 551 552 private static final CssMetaData<ValueAxis<? extends Number>,Boolean> MINOR_TICK_VISIBLE = 553 new CssMetaData<ValueAxis<? extends Number>,Boolean>("-fx-minor-tick-visible", 554 BooleanConverter.getInstance(), Boolean.TRUE) { 555 556 @Override 557 public boolean isSettable(ValueAxis<? extends Number> n) { 558 return n.minorTickVisible == null || !n.minorTickVisible.isBound(); 559 } 560 561 @Override 562 public StyleableProperty<Boolean> getStyleableProperty(ValueAxis<? extends Number> n) { 563 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)n.minorTickVisibleProperty(); 564 } 565 }; 566 567 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 568 static { 569 final List<CssMetaData<? extends Styleable, ?>> styleables = 570 new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData()); 571 styleables.add(MINOR_TICK_COUNT); 572 styleables.add(MINOR_TICK_LENGTH); 573 styleables.add(MINOR_TICK_COUNT); 574 styleables.add(MINOR_TICK_VISIBLE); 575 STYLEABLES = Collections.unmodifiableList(styleables); 576 } 577 } 578 579 /** 580 * @return The CssMetaData associated with this class, which may include the 581 * CssMetaData of its super classes. 582 * @since JavaFX 8.0 583 */ 584 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 585 return StyleableProperties.STYLEABLES; 586 } 587 588 /** 589 * {@inheritDoc} 590 * @since JavaFX 8.0 591 */ 592 @Override 593 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 594 return getClassCssMetaData(); 595 } 596 597 }