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 java.util.ArrayList; 029import java.util.List; 030 031import javafx.animation.KeyFrame; 032import javafx.animation.KeyValue; 033import javafx.beans.property.BooleanProperty; 034import javafx.beans.property.DoubleProperty; 035import javafx.beans.property.ObjectProperty; 036import javafx.beans.property.ObjectPropertyBase; 037import javafx.beans.property.ReadOnlyDoubleProperty; 038import javafx.beans.property.ReadOnlyDoubleWrapper; 039import javafx.beans.property.SimpleDoubleProperty; 040import javafx.collections.FXCollections; 041import javafx.collections.ListChangeListener; 042import javafx.collections.ObservableList; 043import javafx.geometry.Dimension2D; 044import javafx.geometry.Side; 045import javafx.util.Duration; 046 047import com.sun.javafx.charts.ChartLayoutAnimator; 048import javafx.css.StyleableBooleanProperty; 049import javafx.css.StyleableDoubleProperty; 050import javafx.css.CssMetaData; 051import com.sun.javafx.css.converters.BooleanConverter; 052import com.sun.javafx.css.converters.SizeConverter; 053import java.util.Collections; 054import javafx.css.Styleable; 055import javafx.css.StyleableProperty; 056 057/** 058 * A axis implementation that will works on string categories where each 059 * value as a unique category(tick mark) along the axis. 060 */ 061public final class CategoryAxis extends Axis<String> { 062 063 // -------------- PRIVATE FIELDS ------------------------------------------- 064 private List<String> allDataCategories = new ArrayList<String>(); 065 private boolean changeIsLocal = false; 066 /** This is the gap between one category and the next along this axis */ 067 private final DoubleProperty firstCategoryPos = new SimpleDoubleProperty(this, "firstCategoryPos", 0); 068 private Object currentAnimationID; 069 private final ChartLayoutAnimator animator = new ChartLayoutAnimator(this); 070 private ListChangeListener<String> itemsListener = new ListChangeListener<String>() { 071 @Override public void onChanged(Change<? extends String> c) { 072 while (c.next()) { 073 if(!c.getAddedSubList().isEmpty()) { 074 // remove duplicates else they will get rendered on the chart. 075 // Ideally we should be using a Set for categories. 076 for (String addedStr : c.getAddedSubList()) 077 checkAndRemoveDuplicates(addedStr); 078 } 079 if (!isAutoRanging()) { 080 allDataCategories.clear(); 081 allDataCategories.addAll(getCategories()); 082 rangeValid = false; 083 } 084 requestAxisLayout(); 085 } 086 } 087 }; 088 089 // -------------- PUBLIC PROPERTIES ---------------------------------------- 090 091 /** The margin between the axis start and the first tick-mark */ 092 private DoubleProperty startMargin = new StyleableDoubleProperty(5) { 093 @Override protected void invalidated() { 094 requestAxisLayout(); 095 } 096 097 @Override public CssMetaData<CategoryAxis,Number> getCssMetaData() { 098 return StyleableProperties.START_MARGIN; 099 } 100 101 @Override 102 public Object getBean() { 103 return CategoryAxis.this; 104 } 105 106 @Override 107 public String getName() { 108 return "startMargin"; 109 } 110 }; 111 public final double getStartMargin() { return startMargin.getValue(); } 112 public final void setStartMargin(double value) { startMargin.setValue(value); } 113 public final DoubleProperty startMarginProperty() { return startMargin; } 114 115 /** The margin between the last tick mark and the axis end */ 116 private DoubleProperty endMargin = new StyleableDoubleProperty(5) { 117 @Override protected void invalidated() { 118 requestAxisLayout(); 119 } 120 121 122 @Override public CssMetaData<CategoryAxis,Number> getCssMetaData() { 123 return StyleableProperties.END_MARGIN; 124 } 125 126 @Override 127 public Object getBean() { 128 return CategoryAxis.this; 129 } 130 131 @Override 132 public String getName() { 133 return "endMargin"; 134 } 135 }; 136 public final double getEndMargin() { return endMargin.getValue(); } 137 public final void setEndMargin(double value) { endMargin.setValue(value); } 138 public final DoubleProperty endMarginProperty() { return endMargin; } 139 140 /** If this is true then half the space between ticks is left at the start 141 * and end 142 */ 143 private BooleanProperty gapStartAndEnd = new StyleableBooleanProperty(true) { 144 @Override protected void invalidated() { 145 requestAxisLayout(); 146 } 147 148 149 @Override public CssMetaData<CategoryAxis,Boolean> getCssMetaData() { 150 return StyleableProperties.GAP_START_AND_END; 151 } 152 153 @Override 154 public Object getBean() { 155 return CategoryAxis.this; 156 } 157 158 @Override 159 public String getName() { 160 return "gapStartAndEnd"; 161 } 162 }; 163 public final boolean isGapStartAndEnd() { return gapStartAndEnd.getValue(); } 164 public final void setGapStartAndEnd(boolean value) { gapStartAndEnd.setValue(value); } 165 public final BooleanProperty gapStartAndEndProperty() { return gapStartAndEnd; } 166 167 /** 168 * The ordered list of categories plotted on this axis. This is set automatically 169 * based on the charts data if autoRanging is true. If the application sets the categories 170 * then auto ranging is turned off. If there is an attempt to add duplicate entry into this list, 171 * an {@link IllegalArgumentException} is thrown. 172 */ 173 private ObjectProperty<ObservableList<String>> categories = new ObjectPropertyBase<ObservableList<String>>() { 174 ObservableList<String> old; 175 @Override protected void invalidated() { 176 if (getDuplicate() != null) { 177 throw new IllegalArgumentException("Duplicate category added; "+getDuplicate()+" already present"); 178 } 179 final ObservableList<String> newItems = get(); 180 if (old != newItems) { 181 // Add and remove listeners 182 if (old != null) old.removeListener(itemsListener); 183 if (newItems != null) newItems.addListener(itemsListener); 184 old = newItems; 185 } 186 } 187 188 @Override 189 public Object getBean() { 190 return CategoryAxis.this; 191 } 192 193 @Override 194 public String getName() { 195 return "categories"; 196 } 197 }; 198 public final void setCategories(ObservableList<String> value) { 199 categories.set(value); 200 if (!changeIsLocal) { 201 setAutoRanging(false); 202 allDataCategories.clear(); 203 allDataCategories.addAll(getCategories()); 204 } 205 requestAxisLayout(); 206 } 207 208 private void checkAndRemoveDuplicates(String category) { 209 if (getDuplicate() != null) { 210 getCategories().remove(category); 211 throw new IllegalArgumentException("Duplicate category ; "+category+" already present"); 212 } 213 } 214 215 private String getDuplicate() { 216 if (getCategories() != null) { 217 for (int i = 0; i < getCategories().size(); i++) { 218 for (int j = 0; j < getCategories().size(); j++) { 219 if (getCategories().get(i).equals(getCategories().get(j)) && i != j) { 220 return getCategories().get(i); 221 } 222 } 223 } 224 } 225 return null; 226 } 227 /** 228 * Returns a {@link ObservableList} of categories plotted on this axis. 229 * 230 * @return ObservableList of categories for this axis. 231 * @see #categories 232 */ 233 public final ObservableList<String> getCategories() { 234 return categories.get(); 235 } 236 237 /** This is the gap between one category and the next along this axis */ 238 private final ReadOnlyDoubleWrapper categorySpacing = new ReadOnlyDoubleWrapper(this, "categorySpacing", 1); 239 public final double getCategorySpacing() { 240 return categorySpacing.get(); 241 } 242 public final ReadOnlyDoubleProperty categorySpacingProperty() { 243 return categorySpacing.getReadOnlyProperty(); 244 } 245 246 // -------------- CONSTRUCTORS ------------------------------------------------------------------------------------- 247 248 /** 249 * Create a auto-ranging category axis with an empty list of categories. 250 */ 251 public CategoryAxis() { 252 changeIsLocal = true; 253 setCategories(FXCollections.<String>observableArrayList()); 254 changeIsLocal = false; 255 } 256 257 /** 258 * Create a category axis with the given categories. This will not auto-range but be fixed with the given categories. 259 * 260 * @param categories List of the categories for this axis 261 */ 262 public CategoryAxis(ObservableList<String> categories) { 263 setCategories(categories); 264 } 265 266 // -------------- PRIVATE METHODS ---------------------------------------------------------------------------------- 267 268 private double calculateNewSpacing(double length, List<String> categories) { 269 final Side side = getSide(); 270 double newCategorySpacing = 1; 271 if(side != null && categories != null) { 272 double bVal = (isGapStartAndEnd() ? (categories.size()) : (categories.size() - 1)); 273 // RT-14092 flickering : check if bVal is 0 274 newCategorySpacing = (bVal == 0) ? 1 : (length-getStartMargin()-getEndMargin()) / bVal; 275 } 276 // if autoranging is off setRange is not called so we update categorySpacing 277 if (!isAutoRanging()) categorySpacing.set(newCategorySpacing); 278 return newCategorySpacing; 279 } 280 281 private double calculateNewFirstPos(double length, double catSpacing) { 282 final Side side = getSide(); 283 double newPos = 1; 284 if(side != null) { 285 double offset = ((isGapStartAndEnd()) ? (catSpacing / 2) : (0)); 286 if (side.equals(Side.TOP) || side.equals(Side.BOTTOM)) { // HORIZONTAL 287 newPos = 0 + getStartMargin() + offset; 288 } else { // VERTICAL 289 newPos = length - getStartMargin() - offset; 290 } 291 } 292 // if autoranging is off setRange is not called so we update first cateogory pos. 293 if (!isAutoRanging()) firstCategoryPos.set(newPos); 294 return newPos; 295 } 296 297 // -------------- PROTECTED METHODS -------------------------------------------------------------------------------- 298 299 /** 300 * Called to get the current axis range. 301 * 302 * @return A range object that can be passed to setRange() and calculateTickValues() 303 */ 304 @Override protected Object getRange() { 305 return new Object[]{ getCategories(), categorySpacing.get(), firstCategoryPos.get(), getTickLabelRotation() }; 306 } 307 308 /** 309 * Called to set the current axis range to the given range. If isAnimating() is true then this method should 310 * animate the range to the new range. 311 * 312 * @param range A range object returned from autoRange() 313 * @param animate If true animate the change in range 314 */ 315 @Override protected void setRange(Object range, boolean animate) { 316 Object[] rangeArray = (Object[]) range; 317 @SuppressWarnings({"unchecked"}) List<String> categories = (List<String>)rangeArray[0]; 318// if (categories.isEmpty()) new java.lang.Throwable().printStackTrace(); 319 double newCategorySpacing = (Double)rangeArray[1]; 320 double newFirstCategoryPos = (Double)rangeArray[2]; 321 double tickLabelRotation = (Double)rangeArray[3]; 322 setTickLabelRotation(tickLabelRotation); 323 changeIsLocal = true; 324 setCategories(FXCollections.<String>observableArrayList(categories)); 325 changeIsLocal = false; 326 if (animate) { 327 animator.stop(currentAnimationID); 328 currentAnimationID = animator.animate( 329 new KeyFrame(Duration.ZERO, 330 new KeyValue(firstCategoryPos, firstCategoryPos.get()), 331 new KeyValue(categorySpacing, categorySpacing.get()) 332 ), 333 new KeyFrame(Duration.millis(1000), 334 new KeyValue(firstCategoryPos,newFirstCategoryPos), 335 new KeyValue(categorySpacing,newCategorySpacing) 336 ) 337 ); 338 } else { 339 categorySpacing.set(newCategorySpacing); 340 firstCategoryPos.set(newFirstCategoryPos); 341 } 342 } 343 344 /** 345 * This calculates the categories based on the data provided to invalidateRange() method. This must not 346 * effect the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be 347 * returned in the range object. This will we passed to setRange() if it has been decided to adopt this range for 348 * this axis. 349 * 350 * @param length The length of the axis in screen coordinates 351 * @return Range information, this is implementation dependent 352 */ 353 @Override protected Object autoRange(double length) { 354 final Side side = getSide(); 355 final boolean vertical = Side.LEFT.equals(side) || Side.RIGHT.equals(side); 356 // TODO check if we can display all categories 357 final double newCategorySpacing = calculateNewSpacing(length,allDataCategories); 358 final double newFirstPos = calculateNewFirstPos(length, newCategorySpacing); 359 double tickLabelRotation = getTickLabelRotation(); 360 if (length >= 0) { 361 double requiredLengthToDisplay = calculateRequiredSize(vertical,tickLabelRotation); 362 if (requiredLengthToDisplay > length) { 363 // change text to vertical 364 tickLabelRotation = 90; 365 } 366 } 367 return new Object[]{allDataCategories, newCategorySpacing, newFirstPos, tickLabelRotation}; 368 } 369 370 private double calculateRequiredSize(boolean axisVertical, double tickLabelRotation) { 371 double requiredLengthToDisplay = Double.MAX_VALUE; 372 // Calculate the max space required between categories labels 373 double maxReqTickGap = 0; 374 double last = 0; 375 boolean first = true; 376 for (String category: allDataCategories) { 377 Dimension2D textSize = measureTickMarkSize(category, tickLabelRotation); 378 double size = (axisVertical || (tickLabelRotation != 0)) ? textSize.getHeight() : textSize.getWidth(); 379 // TODO better handle calculations for rotated text, overlapping text etc 380 if (first) { 381 first = false; 382 last = size/2; 383 } else { 384 maxReqTickGap = Math.max(maxReqTickGap, last + 6 + (size/2) ); 385 } 386 } 387 return getStartMargin() + maxReqTickGap*allDataCategories.size() + getEndMargin(); 388 } 389 390 /** 391 * Calculate a list of all the data values for each tick mark in range 392 * 393 * @param length The length of the axis in display units 394 * @return A list of tick marks that fit along the axis if it was the given length 395 */ 396 @Override protected List<String> calculateTickValues(double length, Object range) { 397 Object[] rangeArray = (Object[]) range; 398 //noinspection unchecked 399 return (List<String>)rangeArray[0]; 400 } 401 402 /** 403 * Get the string label name for a tick mark with the given value 404 * 405 * @param value The value to format into a tick label string 406 * @return A formatted string for the given value 407 */ 408 @Override protected String getTickMarkLabel(String value) { 409 // TODO use formatter 410 return value; 411 } 412 413 /** 414 * Measure the size of the label for given tick mark value. This uses the font that is set for the tick marks 415 * 416 * @param value tick mark value 417 * @param range range to use during calculations 418 * @return size of tick mark label for given value 419 */ 420 @Override protected Dimension2D measureTickMarkSize(String value, Object range) { 421 final Object[] rangeArray = (Object[]) range; 422 final double tickLabelRotation = (Double)rangeArray[3]; 423 return measureTickMarkSize(value,tickLabelRotation); 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<String> data) { 436 super.invalidateRange(data); 437 // Create unique set of category names 438 List<String> categoryNames = new ArrayList<String>(); 439 categoryNames.addAll(allDataCategories); 440 //RT-21141 allDataCategories needs to be updated based on data - 441 // and should maintain the order it originally had for the categories already present. 442 // and remove categories not present in data 443 for(String cat : allDataCategories) { 444 if (!data.contains(cat)) categoryNames.remove(cat); 445 } 446 // add any new category found in data 447// for(String cat : data) { 448 for (int i = 0; i < data.size(); i++) { 449 int len = categoryNames.size(); 450 if (!categoryNames.contains(data.get(i))) categoryNames.add((i > len) ? len : i, data.get(i)); 451 } 452 allDataCategories.clear(); 453 allDataCategories.addAll(categoryNames); 454 } 455 456 /** 457 * Get the display position along this axis for a given value 458 * 459 * @param value The data value to work out display position for 460 * @return display position or Double.NaN if zero is not in current range; 461 */ 462 @Override public double getDisplayPosition(String value) { 463 // find index of value 464 if (Side.TOP.equals(getSide()) || Side.BOTTOM.equals(getSide())) { // HORIZONTAL 465 return firstCategoryPos.get() + getCategories().indexOf("" + value) * categorySpacing.get(); 466 } else { 467 return firstCategoryPos.get() + getCategories().indexOf("" + value) * categorySpacing.get() * -1; 468 } 469 } 470 471 /** 472 * Get the data value for the given display position on this axis. If the axis 473 * is a CategoryAxis this will be the nearest value. 474 * 475 * @param displayPosition A pixel position on this axis 476 * @return the nearest data value to the given pixel position or 477 * null if not on axis; 478 */ 479 @Override public String getValueForDisplay(double displayPosition) { 480 if (getSide().equals(Side.TOP) || getSide().equals(Side.BOTTOM)) { // HORIZONTAL 481 if (displayPosition < 0 || displayPosition > getWidth()) return null; 482 double d = (displayPosition - firstCategoryPos.get()) / categorySpacing.get(); 483 return toRealValue(d); 484 } else { // VERTICAL 485 if (displayPosition < 0 || displayPosition > getHeight()) return null; 486 double d = (displayPosition - firstCategoryPos.get()) / (categorySpacing.get() * -1); 487 return toRealValue(d); 488 } 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(String value) { 498 return getCategories().indexOf("" + value) != -1; 499 } 500 501 /** 502 * All axis values must be representable by some numeric value. This gets the numeric value for a given data value. 503 * 504 * @param value The data value to convert 505 * @return Numeric value for the given data value 506 */ 507 @Override public double toNumericValue(String value) { 508 return getCategories().indexOf(value); 509 } 510 511 /** 512 * All axis values must be representable by some numeric value. This gets the data value for a given numeric value. 513 * 514 * @param value The numeric value to convert 515 * @return Data value for given numeric value 516 */ 517 @Override public String toRealValue(double value) { 518 int index = (int)Math.round(value); 519 List<String> categories = getCategories(); 520 if (index >= 0 && index < categories.size()) { 521 return getCategories().get(index); 522 } else { 523 return null; 524 } 525 } 526 527 /** 528 * Get the display position of the zero line along this axis. As there is no concept of zero on a CategoryAxis 529 * this is always Double.NaN. 530 * 531 * @return always Double.NaN for CategoryAxis 532 */ 533 @Override public double getZeroPosition() { 534 return Double.NaN; 535 } 536 537 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 538 539 /** @treatAsPrivate implementation detail */ 540 private static class StyleableProperties { 541 private static final CssMetaData<CategoryAxis,Number> START_MARGIN = 542 new CssMetaData<CategoryAxis,Number>("-fx-start-margin", 543 SizeConverter.getInstance(), 5.0) { 544 545 @Override 546 public boolean isSettable(CategoryAxis n) { 547 return n.startMargin == null || !n.startMargin.isBound(); 548 } 549 550 @Override 551 public StyleableProperty<Number> getStyleableProperty(CategoryAxis n) { 552 return (StyleableProperty<Number>)n.startMarginProperty(); 553 } 554 }; 555 556 private static final CssMetaData<CategoryAxis,Number> END_MARGIN = 557 new CssMetaData<CategoryAxis,Number>("-fx-end-margin", 558 SizeConverter.getInstance(), 5.0) { 559 560 @Override 561 public boolean isSettable(CategoryAxis n) { 562 return n.endMargin == null || !n.endMargin.isBound(); 563 } 564 565 @Override 566 public StyleableProperty<Number> getStyleableProperty(CategoryAxis n) { 567 return (StyleableProperty<Number>)n.endMarginProperty(); 568 } 569 }; 570 571 private static final CssMetaData<CategoryAxis,Boolean> GAP_START_AND_END = 572 new CssMetaData<CategoryAxis,Boolean>("-fx-gap-start-and-end", 573 BooleanConverter.getInstance(), Boolean.TRUE) { 574 575 @Override 576 public boolean isSettable(CategoryAxis n) { 577 return n.gapStartAndEnd == null || !n.gapStartAndEnd.isBound(); 578 } 579 580 @Override 581 public StyleableProperty<Boolean> getStyleableProperty(CategoryAxis n) { 582 return (StyleableProperty<Boolean>)n.gapStartAndEndProperty(); 583 } 584 }; 585 586 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 587 static { 588 final List<CssMetaData<? extends Styleable, ?>> styleables = 589 new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData()); 590 styleables.add(START_MARGIN); 591 styleables.add(END_MARGIN); 592 styleables.add(GAP_START_AND_END); 593 STYLEABLES = Collections.unmodifiableList(styleables); 594 } 595 } 596 597 /** 598 * @return The CssMetaData associated with this class, which may include the 599 * CssMetaData of its super classes. 600 */ 601 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 602 return StyleableProperties.STYLEABLES; 603 } 604 605 /** 606 * {@inheritDoc} 607 */ 608 @Override 609 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 610 return getClassCssMetaData(); 611 } 612 613} 614