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.CssMetaData;
029import javafx.css.StyleableBooleanProperty;
030import javafx.css.StyleableDoubleProperty;
031import javafx.css.StyleableIntegerProperty;
032import com.sun.javafx.css.converters.BooleanConverter;
033import com.sun.javafx.css.converters.SizeConverter;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.List;
037
038import javafx.beans.property.*;
039import javafx.css.Styleable;
040import javafx.css.StyleableProperty;
041import javafx.geometry.Side;
042import javafx.scene.shape.LineTo;
043import javafx.scene.shape.MoveTo;
044import javafx.scene.shape.Path;
045import javafx.util.StringConverter;
046
047
048/**
049 * A axis who's data is defined as Numbers. It can also draw minor
050 * tick-marks between the major ones.
051 */
052public abstract class ValueAxis<T extends Number> extends Axis<T> {
053
054    // -------------- PRIVATE FIELDS -----------------------------------------------------------------------------------
055
056    private final Path minorTickPath  = new Path();
057
058    private boolean saveMinorTickVisible = false;
059    private boolean restoreMinorTickVisiblity = false;
060    
061    private double offset;
062    /** This is the minimum current data value and it is used while auto ranging. */
063    private double dataMinValue;
064    /** This is the maximum current data value and it is used while auto ranging. */
065    private double dataMaxValue;
066    /** List of the values at which there are minor ticks */
067    private List<T> minorTickMarkValues = null;
068    // -------------- PRIVATE PROPERTIES -------------------------------------------------------------------------------
069
070    /**
071     * The current value for the lowerBound of this axis, ie min value.
072     * This may be the same as lowerBound or different. It is used by NumberAxis to animate the
073     * lowerBound from the old value to the new value. 
074     */
075    protected final DoubleProperty currentLowerBound = new SimpleDoubleProperty(this, "currentLowerBound");
076
077    // -------------- PUBLIC PROPERTIES --------------------------------------------------------------------------------
078
079    /** true if minor tick marks should be displayed */
080    private BooleanProperty minorTickVisible = new StyleableBooleanProperty(true) {
081        @Override protected void invalidated() {
082            minorTickPath.setVisible(get());
083            requestAxisLayout();
084        }
085
086        @Override
087        public Object getBean() {
088            return ValueAxis.this;
089        }
090
091        @Override
092        public String getName() {
093            return "minorTickVisible";
094        }
095
096        @Override
097        public CssMetaData<ValueAxis<? extends Number>,Boolean> getCssMetaData() {
098            return StyleableProperties.MINOR_TICK_VISIBLE;
099        }
100    };
101    public final boolean isMinorTickVisible() { return minorTickVisible.get(); }
102    public final void setMinorTickVisible(boolean value) { minorTickVisible.set(value); }
103    public final BooleanProperty minorTickVisibleProperty() { return minorTickVisible; }
104
105    
106    /** The scale factor from data units to visual units */
107    private ReadOnlyDoubleWrapper scale = new ReadOnlyDoubleWrapper(this, "scale", 0);
108    public final double getScale() { return scale.get(); }
109    protected final void setScale(double scale) { this.scale.set(scale); }
110    public final ReadOnlyDoubleProperty scaleProperty() { return scale.getReadOnlyProperty(); }
111    ReadOnlyDoubleWrapper scalePropertyImpl() { return scale; }
112
113    /** The value for the upper bound of this axis, ie max value. This is automatically set if auto ranging is on. */
114    private DoubleProperty upperBound = new DoublePropertyBase(100) {
115        @Override protected void invalidated() {
116            if(!isAutoRanging()) {
117                invalidateRange();
118                requestAxisLayout();
119            }
120        }
121
122        @Override
123        public Object getBean() {
124            return ValueAxis.this;
125        }
126
127        @Override
128        public String getName() {
129            return "upperBound";
130        }
131    };
132    public final double getUpperBound() { return upperBound.get(); }
133    public final void setUpperBound(double value) { upperBound.set(value); }
134    public final DoubleProperty upperBoundProperty() { return upperBound; }
135
136    /** The value for the lower bound of this axis, ie min value. This is automatically set if auto ranging is on. */
137    private DoubleProperty lowerBound = new DoublePropertyBase(0) {
138        @Override protected void invalidated() {
139            if(!isAutoRanging()) {
140                invalidateRange();
141                requestAxisLayout();
142            }
143        }
144
145        @Override
146        public Object getBean() {
147            return ValueAxis.this;
148        }
149
150        @Override
151        public String getName() {
152            return "lowerBound";
153        }
154    };
155    public final double getLowerBound() { return lowerBound.get(); }
156    public final void setLowerBound(double value) { lowerBound.set(value); }
157    public final DoubleProperty lowerBoundProperty() { return lowerBound; }
158
159    /** StringConverter used to format tick mark labels. If null a default will be used */
160    private final ObjectProperty<StringConverter<T>> tickLabelFormatter = new ObjectPropertyBase<StringConverter<T>>(null){
161        @Override protected void invalidated() {
162            invalidateRange();
163            formatterValid = true;
164            requestAxisLayout();
165        }
166
167        @Override
168        public Object getBean() {
169            return ValueAxis.this;
170        }
171
172        @Override
173        public String getName() {
174            return "tickLabelFormatter";
175        }
176    };
177    public final StringConverter<T> getTickLabelFormatter() { return tickLabelFormatter.getValue(); }
178    public final void setTickLabelFormatter(StringConverter<T> value) { tickLabelFormatter.setValue(value); }
179    public final ObjectProperty<StringConverter<T>> tickLabelFormatterProperty() { return tickLabelFormatter; }
180
181    /** The length of minor tick mark lines. Set to 0 to not display minor tick marks. */
182    private DoubleProperty minorTickLength = new StyleableDoubleProperty(5) {
183        @Override protected void invalidated() {
184            // RT-16747 if tick length is negative - set it to 0
185            if (minorTickLength.get() < 0 && !minorTickLength.isBound()) {
186                minorTickLength.set(0);
187            }
188            requestAxisLayout();
189        }
190
191        @Override
192        public Object getBean() {
193            return ValueAxis.this;
194        }
195
196        @Override
197        public String getName() {
198            return "minorTickLength";
199        }
200
201        @Override
202        public CssMetaData<ValueAxis<? extends Number>,Number> getCssMetaData() {
203            return StyleableProperties.MINOR_TICK_LENGTH;
204        }
205    };
206    public final double getMinorTickLength() { return minorTickLength.get(); }
207    public final void setMinorTickLength(double value) { minorTickLength.set(value); }
208    public final DoubleProperty minorTickLengthProperty() { return minorTickLength; }
209
210    /**
211     * The number of minor tick divisions to be displayed between each major tick mark.
212     * The number of actual minor tick marks will be one less than this.
213     */
214    private IntegerProperty minorTickCount = new StyleableIntegerProperty(5) {
215        @Override protected void invalidated() {
216            // RT-16747 if the tick count is negative, set it to 0
217            if ((minorTickCount.get() - 1) < 0 && !minorTickCount.isBound()) {
218                minorTickCount.set(0);
219            }
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 = getSide();
300        if(side != null) {
301            if (side.equals(Side.TOP) || side.equals(Side.BOTTOM)) { // HORIZONTAL
302                offset = 0;
303                newScale = ((upperBound-lowerBound) == 0) ? length : length / (upperBound - lowerBound);
304            } else { // VERTICAL
305                offset = length;
306                newScale = ((upperBound-lowerBound) == 0) ? -length : -(length / (upperBound - lowerBound));
307            }
308        }
309        return newScale;
310    }
311
312    /**
313     * Called to set the upper and lower bound and anything else that needs to be auto-ranged. This must not effect
314     * the state of the axis, changing any properties of the axis. Any results of the auto-ranging should be returned
315     * in the range object. This will we passed to setRange() if it has been decided to adopt this range for this axis.
316     *
317     * @param minValue The min data value that needs to be plotted on this axis
318     * @param maxValue The max data value that needs to be plotted on this axis
319     * @param length The length of the axis in display coordinates
320     * @param labelSize The approximate average size a label takes along the axis
321     * @return The calculated range
322     */
323    protected Object autoRange(double minValue, double maxValue, double length, double labelSize) {
324        return null; // this method should have been abstract as there is no way for it to 
325        // return anything correct. so just return null.
326        
327    }
328
329    /**
330     * Calculate a list of the data values for every minor tick mark
331     *
332     * @return List of data values where to draw minor tick marks
333     */
334    protected abstract List<T> calculateMinorTickMarks();
335
336    /**
337     * Called during layout if the tickmarks have been updated, allowing subclasses to do anything they need to
338     * in reaction.
339     */
340    @Override protected void tickMarksUpdated() {
341        super.tickMarksUpdated();
342        // recalculate minor tick marks
343        minorTickMarkValues = calculateMinorTickMarks();
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 = getSide();
351        final double length = (Side.TOP.equals(side) || Side.BOTTOM.equals(side)) ? getWidth() : getHeight();
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        int numMinorTicks = (getTickMarks().size() - 1)*(getMinorTickCount() - 1);
362        double neededLength = (getTickMarks().size()+numMinorTicks)*2;
363        if (length < neededLength) {
364            if (!restoreMinorTickVisiblity) {
365                restoreMinorTickVisiblity = true;
366                saveMinorTickVisible = isMinorTickVisible();
367                setMinorTickVisible(false);
368            }
369        } else {
370            if (restoreMinorTickVisiblity) {
371                setMinorTickVisible(saveMinorTickVisible);
372                restoreMinorTickVisiblity = false;
373            }
374        }
375        // Update minor tickmarks
376        minorTickPath.getElements().clear();
377        if (getMinorTickLength() > 0) {
378            if (side.equals(Side.LEFT)) {
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() - getMinorTickLength(), y),
387                                new LineTo(getWidth()-1, y));
388                    }
389                }
390            } else if (side.equals(Side.RIGHT)) {
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(getMinorTickLength(), y));
400                    }
401                }
402            } else if (side.equals(Side.TOP)) {
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() - getMinorTickLength()));
412                    }
413                }
414            } else if (side.equals(Side.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, getMinorTickLength()));
424                    }
425                }
426            }
427        }
428    }
429
430    // -------------- METHODS ------------------------------------------------------------------------------------------
431
432    /**
433     * Called when data has changed and the range may not be valid any more. 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            dataMaxValue = Double.MIN_VALUE;
446        }
447        for(T dataValue: data) {
448            dataMinValue = Math.min(dataMinValue, dataValue.doubleValue());
449            dataMaxValue = Math.max(dataMaxValue, dataValue.doubleValue());
450        }
451        super.invalidateRange(data);
452    }
453
454    /**
455     * Get the display position along this axis for a given value
456     *
457     * @param value The data value to work out display position for
458     * @return display position or Double.NaN if zero is not in current range;
459     */
460    @Override public double getDisplayPosition(T value) {
461        return Math.ceil(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     /** @treatAsPrivate implementation detail */
522    private static class StyleableProperties  {
523        private  static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_LENGTH =
524            new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-length",
525                SizeConverter.getInstance(), 5.0) {
526
527            @Override
528            public boolean isSettable(ValueAxis<? extends Number> n) {
529                return n.minorTickLength == null || !n.minorTickLength.isBound();
530            }
531
532            @Override
533            public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) {
534                return (StyleableProperty<Number>)n.minorTickLengthProperty();
535            }
536        };
537        
538        private static final CssMetaData<ValueAxis<? extends Number>,Number> MINOR_TICK_COUNT =
539            new CssMetaData<ValueAxis<? extends Number>,Number>("-fx-minor-tick-count",
540                SizeConverter.getInstance(), 5) {
541
542            @Override
543            public boolean isSettable(ValueAxis<? extends Number> n) {
544                return n.minorTickCount == null || !n.minorTickCount.isBound();
545            }
546
547            @Override
548            public StyleableProperty<Number> getStyleableProperty(ValueAxis<? extends Number> n) {
549                return (StyleableProperty<Number>)n.minorTickCountProperty();
550            }
551        };
552        
553         private static final CssMetaData<ValueAxis<? extends Number>,Boolean> MINOR_TICK_VISIBLE =
554            new CssMetaData<ValueAxis<? extends Number>,Boolean>("-fx-minor-tick-visible",
555                 BooleanConverter.getInstance(), Boolean.TRUE) {
556
557            @Override
558            public boolean isSettable(ValueAxis<? extends Number> n) {
559                return n.minorTickVisible == null || !n.minorTickVisible.isBound();
560            }
561
562            @Override
563            public StyleableProperty<Boolean> getStyleableProperty(ValueAxis<? extends Number> n) {
564                return (StyleableProperty<Boolean>)n.minorTickVisibleProperty();
565            }
566        };
567
568        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
569         static {
570            final List<CssMetaData<? extends Styleable, ?>> styleables =
571                new ArrayList<CssMetaData<? extends Styleable, ?>>(Axis.getClassCssMetaData());
572            styleables.add(MINOR_TICK_COUNT);
573            styleables.add(MINOR_TICK_LENGTH);
574            styleables.add(MINOR_TICK_COUNT);
575            styleables.add(MINOR_TICK_VISIBLE);
576            STYLEABLES = Collections.unmodifiableList(styleables);
577         }
578     }
579
580    /**
581     * @return The CssMetaData associated with this class, which may include the
582     * CssMetaData of its super classes.
583     */
584    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
585        return StyleableProperties.STYLEABLES;
586    }
587
588    /**
589     * {@inheritDoc}
590     */
591    @Override
592    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
593        return getClassCssMetaData();
594    }
595
596}