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