001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2021, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------------
028 * ThermometerPlot.java
029 * --------------------
030 *
031 * (C) Copyright 2000-2021, by Bryan Scott and Contributors.
032 *
033 * Original Author:  Bryan Scott (based on MeterPlot by Hari).
034 * Contributor(s):   David Gilbert (for Object Refinery Limited).
035 *                   Arnaud Lelievre;
036 *                   Julien Henry (see patch 1769088) (DG);
037 */
038
039package org.jfree.chart.plot;
040
041import java.awt.BasicStroke;
042import java.awt.Color;
043import java.awt.Font;
044import java.awt.FontMetrics;
045import java.awt.Graphics2D;
046import java.awt.Paint;
047import java.awt.Stroke;
048import java.awt.geom.Area;
049import java.awt.geom.Ellipse2D;
050import java.awt.geom.Line2D;
051import java.awt.geom.Point2D;
052import java.awt.geom.Rectangle2D;
053import java.awt.geom.RoundRectangle2D;
054import java.io.IOException;
055import java.io.ObjectInputStream;
056import java.io.ObjectOutputStream;
057import java.io.Serializable;
058import java.text.DecimalFormat;
059import java.text.NumberFormat;
060import java.util.Arrays;
061import java.util.Objects;
062import java.util.ResourceBundle;
063
064import org.jfree.chart.LegendItemCollection;
065import org.jfree.chart.axis.NumberAxis;
066import org.jfree.chart.axis.ValueAxis;
067import org.jfree.chart.event.PlotChangeEvent;
068import org.jfree.chart.ui.RectangleEdge;
069import org.jfree.chart.ui.RectangleInsets;
070import org.jfree.chart.util.ObjectUtils;
071import org.jfree.chart.util.PaintUtils;
072import org.jfree.chart.util.Args;
073import org.jfree.chart.util.ResourceBundleWrapper;
074import org.jfree.chart.util.SerialUtils;
075import org.jfree.chart.util.UnitType;
076import org.jfree.data.Range;
077import org.jfree.data.general.DatasetChangeEvent;
078import org.jfree.data.general.DefaultValueDataset;
079import org.jfree.data.general.ValueDataset;
080
081/**
082 * A plot that displays a single value (from a {@link ValueDataset}) in a
083 * thermometer type display.
084 * <p>
085 * This plot supports a number of options:
086 * <ol>
087 * <li>three sub-ranges which could be viewed as 'Normal', 'Warning'
088 *   and 'Critical' ranges.</li>
089 * <li>the thermometer can be run in two modes:
090 *      <ul>
091 *      <li>fixed range, or</li>
092 *      <li>range adjusts to current sub-range.</li>
093 *      </ul>
094 * </li>
095 * <li>settable units to be displayed.</li>
096 * <li>settable display location for the value text.</li>
097 * </ol>
098 */
099public class ThermometerPlot extends Plot implements ValueAxisPlot,
100        Zoomable, Cloneable, Serializable {
101
102    /** For serialization. */
103    private static final long serialVersionUID = 4087093313147984390L;
104
105    /** A constant for unit type 'None'. */
106    public static final int UNITS_NONE = 0;
107
108    /** A constant for unit type 'Fahrenheit'. */
109    public static final int UNITS_FAHRENHEIT = 1;
110
111    /** A constant for unit type 'Celcius'. */
112    public static final int UNITS_CELCIUS = 2;
113
114    /** A constant for unit type 'Kelvin'. */
115    public static final int UNITS_KELVIN = 3;
116
117    /** A constant for the value label position (no label). */
118    public static final int NONE = 0;
119
120    /** A constant for the value label position (right of the thermometer). */
121    public static final int RIGHT = 1;
122
123    /** A constant for the value label position (left of the thermometer). */
124    public static final int LEFT = 2;
125
126    /** A constant for the value label position (in the thermometer bulb). */
127    public static final int BULB = 3;
128
129    /** A constant for the 'normal' range. */
130    public static final int NORMAL = 0;
131
132    /** A constant for the 'warning' range. */
133    public static final int WARNING = 1;
134
135    /** A constant for the 'critical' range. */
136    public static final int CRITICAL = 2;
137
138    /** The axis gap. */
139    protected static final int AXIS_GAP = 10;
140
141    /** The unit strings. */
142    protected static final String[] UNITS = {"", "\u00B0F", "\u00B0C",
143            "\u00B0K"};
144
145    /** Index for low value in subrangeInfo matrix. */
146    protected static final int RANGE_LOW = 0;
147
148    /** Index for high value in subrangeInfo matrix. */
149    protected static final int RANGE_HIGH = 1;
150
151    /** Index for display low value in subrangeInfo matrix. */
152    protected static final int DISPLAY_LOW = 2;
153
154    /** Index for display high value in subrangeInfo matrix. */
155    protected static final int DISPLAY_HIGH = 3;
156
157    /** The default lower bound. */
158    protected static final double DEFAULT_LOWER_BOUND = 0.0;
159
160    /** The default upper bound. */
161    protected static final double DEFAULT_UPPER_BOUND = 100.0;
162
163    /**
164     * The default bulb radius.
165     */
166    protected static final int DEFAULT_BULB_RADIUS = 40;
167
168    /**
169     * The default column radius.
170     */
171    protected static final int DEFAULT_COLUMN_RADIUS = 20;
172
173    /**
174     * The default gap between the outlines representing the thermometer.
175     */
176    protected static final int DEFAULT_GAP = 5;
177
178    /** The dataset for the plot. */
179    private ValueDataset dataset;
180
181    /** The range axis. */
182    private ValueAxis rangeAxis;
183
184    /** The lower bound for the thermometer. */
185    private double lowerBound = DEFAULT_LOWER_BOUND;
186
187    /** The upper bound for the thermometer. */
188    private double upperBound = DEFAULT_UPPER_BOUND;
189
190    /**
191     * The value label position.
192     */
193    private int bulbRadius = DEFAULT_BULB_RADIUS;
194
195    /**
196     * The column radius.
197     */
198    private int columnRadius = DEFAULT_COLUMN_RADIUS;
199
200    /**
201     * The gap between the two outlines the represent the thermometer.
202     */
203    private int gap = DEFAULT_GAP;
204
205    /**
206     * Blank space inside the plot area around the outside of the thermometer.
207     */
208    private RectangleInsets padding;
209
210    /** Stroke for drawing the thermometer */
211    private transient Stroke thermometerStroke = new BasicStroke(1.0f);
212
213    /** Paint for drawing the thermometer */
214    private transient Paint thermometerPaint = Color.BLACK;
215
216    /** The display units */
217    private int units = UNITS_CELCIUS;
218
219    /** The value label position. */
220    private int valueLocation = BULB;
221
222    /** The position of the axis **/
223    private int axisLocation = LEFT;
224
225    /** The font to write the value in */
226    private Font valueFont = new Font("SansSerif", Font.BOLD, 16);
227
228    /** Colour that the value is written in */
229    private transient Paint valuePaint = Color.WHITE;
230
231    /** Number format for the value */
232    private NumberFormat valueFormat = new DecimalFormat();
233
234    /** The default paint for the mercury in the thermometer. */
235    private transient Paint mercuryPaint = Color.LIGHT_GRAY;
236
237    /** A flag that controls whether value lines are drawn. */
238    private boolean showValueLines = false;
239
240    /** The display sub-range. */
241    private int subrange = -1;
242
243    /** The start and end values for the subranges. */
244    private double[][] subrangeInfo = {
245        {0.0, 50.0, 0.0, 50.0},
246        {50.0, 75.0, 50.0, 75.0},
247        {75.0, 100.0, 75.0, 100.0}
248    };
249
250    /**
251     * A flag that controls whether or not the axis range adjusts to the
252     * sub-ranges.
253     */
254    private boolean followDataInSubranges = false;
255
256    /**
257     * A flag that controls whether or not the mercury paint changes with
258     * the subranges.
259     */
260    private boolean useSubrangePaint = true;
261
262    /** Paint for each range */
263    private transient Paint[] subrangePaint = {Color.GREEN, Color.ORANGE,
264            Color.RED};
265
266    /** A flag that controls whether the sub-range indicators are visible. */
267    private boolean subrangeIndicatorsVisible = true;
268
269    /** The stroke for the sub-range indicators. */
270    private transient Stroke subrangeIndicatorStroke = new BasicStroke(2.0f);
271
272    /** The range indicator stroke. */
273    private transient Stroke rangeIndicatorStroke = new BasicStroke(3.0f);
274
275    /** The resourceBundle for the localization. */
276    protected static ResourceBundle localizationResources
277            = ResourceBundleWrapper.getBundle(
278                    "org.jfree.chart.plot.LocalizationBundle");
279
280    /**
281     * Creates a new thermometer plot.
282     */
283    public ThermometerPlot() {
284        this(new DefaultValueDataset());
285    }
286
287    /**
288     * Creates a new thermometer plot, using default attributes where necessary.
289     *
290     * @param dataset  the data set.
291     */
292    public ThermometerPlot(ValueDataset dataset) {
293
294        super();
295
296        this.padding = new RectangleInsets(UnitType.RELATIVE, 0.05, 0.05, 0.05,
297                0.05);
298        this.dataset = dataset;
299        if (dataset != null) {
300            dataset.addChangeListener(this);
301        }
302        NumberAxis axis = new NumberAxis(null);
303        axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
304        axis.setAxisLineVisible(false);
305        axis.setPlot(this);
306        axis.addChangeListener(this);
307        this.rangeAxis = axis;
308        setAxisRange();
309    }
310
311    /**
312     * Returns the dataset for the plot.
313     *
314     * @return The dataset (possibly {@code null}).
315     *
316     * @see #setDataset(ValueDataset)
317     */
318    public ValueDataset getDataset() {
319        return this.dataset;
320    }
321
322    /**
323     * Sets the dataset for the plot, replacing the existing dataset if there
324     * is one, and sends a {@link PlotChangeEvent} to all registered listeners.
325     *
326     * @param dataset  the dataset ({@code null} permitted).
327     *
328     * @see #getDataset()
329     */
330    public void setDataset(ValueDataset dataset) {
331
332        // if there is an existing dataset, remove the plot from the list
333        // of change listeners...
334        ValueDataset existing = this.dataset;
335        if (existing != null) {
336            existing.removeChangeListener(this);
337        }
338
339        // set the new dataset, and register the chart as a change listener...
340        this.dataset = dataset;
341        if (dataset != null) {
342            setDatasetGroup(dataset.getGroup());
343            dataset.addChangeListener(this);
344        }
345
346        // send a dataset change event to self...
347        DatasetChangeEvent event = new DatasetChangeEvent(this, dataset);
348        datasetChanged(event);
349
350    }
351
352    /**
353     * Returns the range axis.
354     *
355     * @return The range axis (never {@code null}).
356     *
357     * @see #setRangeAxis(ValueAxis)
358     */
359    public ValueAxis getRangeAxis() {
360        return this.rangeAxis;
361    }
362
363    /**
364     * Sets the range axis for the plot and sends a {@link PlotChangeEvent} to
365     * all registered listeners.
366     *
367     * @param axis  the new axis ({@code null} not permitted).
368     *
369     * @see #getRangeAxis()
370     */
371    public void setRangeAxis(ValueAxis axis) {
372        Args.nullNotPermitted(axis, "axis");
373        // plot is registered as a listener with the existing axis...
374        this.rangeAxis.removeChangeListener(this);
375
376        axis.setPlot(this);
377        axis.addChangeListener(this);
378        this.rangeAxis = axis;
379        fireChangeEvent();
380    }
381
382    /**
383     * Returns the lower bound for the thermometer.  The data value can be set
384     * lower than this, but it will not be shown in the thermometer.
385     *
386     * @return The lower bound.
387     *
388     * @see #setLowerBound(double)
389     */
390    public double getLowerBound() {
391        return this.lowerBound;
392    }
393
394    /**
395     * Sets the lower bound for the thermometer.
396     *
397     * @param lower the lower bound.
398     *
399     * @see #getLowerBound()
400     */
401    public void setLowerBound(double lower) {
402        this.lowerBound = lower;
403        setAxisRange();
404    }
405
406    /**
407     * Returns the upper bound for the thermometer.  The data value can be set
408     * higher than this, but it will not be shown in the thermometer.
409     *
410     * @return The upper bound.
411     *
412     * @see #setUpperBound(double)
413     */
414    public double getUpperBound() {
415        return this.upperBound;
416    }
417
418    /**
419     * Sets the upper bound for the thermometer.
420     *
421     * @param upper the upper bound.
422     *
423     * @see #getUpperBound()
424     */
425    public void setUpperBound(double upper) {
426        this.upperBound = upper;
427        setAxisRange();
428    }
429
430    /**
431     * Sets the lower and upper bounds for the thermometer.
432     *
433     * @param lower  the lower bound.
434     * @param upper  the upper bound.
435     */
436    public void setRange(double lower, double upper) {
437        this.lowerBound = lower;
438        this.upperBound = upper;
439        setAxisRange();
440    }
441
442    /**
443     * Returns the padding for the thermometer.  This is the space inside the
444     * plot area.
445     *
446     * @return The padding (never {@code null}).
447     *
448     * @see #setPadding(RectangleInsets)
449     */
450    public RectangleInsets getPadding() {
451        return this.padding;
452    }
453
454    /**
455     * Sets the padding for the thermometer and sends a {@link PlotChangeEvent}
456     * to all registered listeners.
457     *
458     * @param padding  the padding ({@code null} not permitted).
459     *
460     * @see #getPadding()
461     */
462    public void setPadding(RectangleInsets padding) {
463        Args.nullNotPermitted(padding, "padding");
464        this.padding = padding;
465        fireChangeEvent();
466    }
467
468    /**
469     * Returns the stroke used to draw the thermometer outline.
470     *
471     * @return The stroke (never {@code null}).
472     *
473     * @see #setThermometerStroke(Stroke)
474     * @see #getThermometerPaint()
475     */
476    public Stroke getThermometerStroke() {
477        return this.thermometerStroke;
478    }
479
480    /**
481     * Sets the stroke used to draw the thermometer outline and sends a
482     * {@link PlotChangeEvent} to all registered listeners.
483     *
484     * @param s  the new stroke ({@code null} ignored).
485     *
486     * @see #getThermometerStroke()
487     */
488    public void setThermometerStroke(Stroke s) {
489        if (s != null) {
490            this.thermometerStroke = s;
491            fireChangeEvent();
492        }
493    }
494
495    /**
496     * Returns the paint used to draw the thermometer outline.
497     *
498     * @return The paint (never {@code null}).
499     *
500     * @see #setThermometerPaint(Paint)
501     * @see #getThermometerStroke()
502     */
503    public Paint getThermometerPaint() {
504        return this.thermometerPaint;
505    }
506
507    /**
508     * Sets the paint used to draw the thermometer outline and sends a
509     * {@link PlotChangeEvent} to all registered listeners.
510     *
511     * @param paint  the new paint ({@code null} ignored).
512     *
513     * @see #getThermometerPaint()
514     */
515    public void setThermometerPaint(Paint paint) {
516        if (paint != null) {
517            this.thermometerPaint = paint;
518            fireChangeEvent();
519        }
520    }
521
522    /**
523     * Returns a code indicating the unit display type.  This is one of
524     * {@link #UNITS_NONE}, {@link #UNITS_FAHRENHEIT}, {@link #UNITS_CELCIUS}
525     * and {@link #UNITS_KELVIN}.
526     *
527     * @return The units type.
528     *
529     * @see #setUnits(int)
530     */
531    public int getUnits() {
532        return this.units;
533    }
534
535    /**
536     * Sets the units to be displayed in the thermometer. Use one of the
537     * following constants:
538     *
539     * <ul>
540     * <li>UNITS_NONE : no units displayed.</li>
541     * <li>UNITS_FAHRENHEIT : units displayed in Fahrenheit.</li>
542     * <li>UNITS_CELCIUS : units displayed in Celcius.</li>
543     * <li>UNITS_KELVIN : units displayed in Kelvin.</li>
544     * </ul>
545     *
546     * @param u  the new unit type.
547     *
548     * @see #getUnits()
549     */
550    public void setUnits(int u) {
551        if ((u >= 0) && (u < UNITS.length)) {
552            if (this.units != u) {
553                this.units = u;
554                fireChangeEvent();
555            }
556        }
557    }
558
559    /**
560     * Returns a code indicating the location at which the value label is
561     * displayed.
562     *
563     * @return The location (one of {@link #NONE}, {@link #RIGHT},
564     *         {@link #LEFT} and {@link #BULB}.).
565     */
566    public int getValueLocation() {
567        return this.valueLocation;
568    }
569
570    /**
571     * Sets the location at which the current value is displayed and sends a
572     * {@link PlotChangeEvent} to all registered listeners.
573     * <P>
574     * The location can be one of the constants: {@code NONE}, {@code RIGHT},
575     * {@code LEFT} and {@code BULB}.
576     *
577     * @param location  the location.
578     */
579    public void setValueLocation(int location) {
580        if ((location >= 0) && (location < 4)) {
581            this.valueLocation = location;
582            fireChangeEvent();
583        }
584        else {
585            throw new IllegalArgumentException("Location not recognised.");
586        }
587    }
588
589    /**
590     * Returns the axis location.
591     *
592     * @return The location (one of {@link #NONE}, {@link #LEFT} and
593     *         {@link #RIGHT}).
594     *
595     * @see #setAxisLocation(int)
596     */
597    public int getAxisLocation() {
598        return this.axisLocation;
599    }
600
601    /**
602     * Sets the location at which the axis is displayed relative to the
603     * thermometer, and sends a {@link PlotChangeEvent} to all registered
604     * listeners.
605     *
606     * @param location  the location (one of {@link #NONE}, {@link #LEFT} and
607     *         {@link #RIGHT}).
608     *
609     * @see #getAxisLocation()
610     */
611    public void setAxisLocation(int location) {
612        if ((location >= 0) && (location < 3)) {
613            this.axisLocation = location;
614            fireChangeEvent();
615        }
616        else {
617            throw new IllegalArgumentException("Location not recognised.");
618        }
619    }
620
621    /**
622     * Gets the font used to display the current value.
623     *
624     * @return The font.
625     *
626     * @see #setValueFont(Font)
627     */
628    public Font getValueFont() {
629        return this.valueFont;
630    }
631
632    /**
633     * Sets the font used to display the current value.
634     *
635     * @param f  the new font ({@code null} not permitted).
636     *
637     * @see #getValueFont()
638     */
639    public void setValueFont(Font f) {
640        Args.nullNotPermitted(f, "f");
641        if (!this.valueFont.equals(f)) {
642            this.valueFont = f;
643            fireChangeEvent();
644        }
645    }
646
647    /**
648     * Gets the paint used to display the current value.
649    *
650     * @return The paint.
651     *
652     * @see #setValuePaint(Paint)
653     */
654    public Paint getValuePaint() {
655        return this.valuePaint;
656    }
657
658    /**
659     * Sets the paint used to display the current value and sends a
660     * {@link PlotChangeEvent} to all registered listeners.
661     *
662     * @param paint  the new paint ({@code null} not permitted).
663     *
664     * @see #getValuePaint()
665     */
666    public void setValuePaint(Paint paint) {
667        Args.nullNotPermitted(paint, "paint");
668        if (!this.valuePaint.equals(paint)) {
669            this.valuePaint = paint;
670            fireChangeEvent();
671        }
672    }
673
674    // FIXME: No getValueFormat() method?
675
676    /**
677     * Sets the formatter for the value label and sends a
678     * {@link PlotChangeEvent} to all registered listeners.
679     *
680     * @param formatter  the new formatter ({@code null} not permitted).
681     */
682    public void setValueFormat(NumberFormat formatter) {
683        Args.nullNotPermitted(formatter, "formatter");
684        this.valueFormat = formatter;
685        fireChangeEvent();
686    }
687
688    /**
689     * Returns the default mercury paint.
690     *
691     * @return The paint (never {@code null}).
692     *
693     * @see #setMercuryPaint(Paint)
694     */
695    public Paint getMercuryPaint() {
696        return this.mercuryPaint;
697    }
698
699    /**
700     * Sets the default mercury paint and sends a {@link PlotChangeEvent} to
701     * all registered listeners.
702     *
703     * @param paint  the new paint ({@code null} not permitted).
704     *
705     * @see #getMercuryPaint()
706     */
707    public void setMercuryPaint(Paint paint) {
708        Args.nullNotPermitted(paint, "paint");
709        this.mercuryPaint = paint;
710        fireChangeEvent();
711    }
712
713    /**
714     * Sets information for a particular range.
715     *
716     * @param range  the range to specify information about.
717     * @param low  the low value for the range
718     * @param hi  the high value for the range
719     */
720    public void setSubrangeInfo(int range, double low, double hi) {
721        setSubrangeInfo(range, low, hi, low, hi);
722    }
723
724    /**
725     * Sets the subrangeInfo attribute of the ThermometerPlot object
726     *
727     * @param range  the new rangeInfo value.
728     * @param rangeLow  the new rangeInfo value
729     * @param rangeHigh  the new rangeInfo value
730     * @param displayLow  the new rangeInfo value
731     * @param displayHigh  the new rangeInfo value
732     */
733    public void setSubrangeInfo(int range,
734                                double rangeLow, double rangeHigh,
735                                double displayLow, double displayHigh) {
736
737        if ((range >= 0) && (range < 3)) {
738            setSubrange(range, rangeLow, rangeHigh);
739            setDisplayRange(range, displayLow, displayHigh);
740            setAxisRange();
741            fireChangeEvent();
742        }
743
744    }
745
746    /**
747     * Sets the bounds for a subrange.
748     *
749     * @param range  the range type.
750     * @param low  the low value.
751     * @param high  the high value.
752     */
753    public void setSubrange(int range, double low, double high) {
754        if ((range >= 0) && (range < 3)) {
755            this.subrangeInfo[range][RANGE_HIGH] = high;
756            this.subrangeInfo[range][RANGE_LOW] = low;
757        }
758    }
759
760    /**
761     * Sets the displayed bounds for a sub range.
762     *
763     * @param range  the range type.
764     * @param low  the low value.
765     * @param high  the high value.
766     */
767    public void setDisplayRange(int range, double low, double high) {
768
769        if ((range >= 0) && (range < this.subrangeInfo.length)
770            && isValidNumber(high) && isValidNumber(low)) {
771
772            if (high > low) {
773                this.subrangeInfo[range][DISPLAY_HIGH] = high;
774                this.subrangeInfo[range][DISPLAY_LOW] = low;
775            }
776            else {
777                this.subrangeInfo[range][DISPLAY_HIGH] = low;
778                this.subrangeInfo[range][DISPLAY_LOW] = high;
779            }
780
781        }
782
783    }
784
785    /**
786     * Gets the paint used for a particular subrange.
787     *
788     * @param range  the range (.
789     *
790     * @return The paint.
791     *
792     * @see #setSubrangePaint(int, Paint)
793     */
794    public Paint getSubrangePaint(int range) {
795        if ((range >= 0) && (range < this.subrangePaint.length)) {
796            return this.subrangePaint[range];
797        }
798        else {
799            return this.mercuryPaint;
800        }
801    }
802
803    /**
804     * Sets the paint to be used for a subrange and sends a
805     * {@link PlotChangeEvent} to all registered listeners.
806     *
807     * @param range  the range (0, 1 or 2).
808     * @param paint  the paint to be applied ({@code null} not permitted).
809     *
810     * @see #getSubrangePaint(int)
811     */
812    public void setSubrangePaint(int range, Paint paint) {
813        if ((range >= 0)
814                && (range < this.subrangePaint.length) && (paint != null)) {
815            this.subrangePaint[range] = paint;
816            fireChangeEvent();
817        }
818    }
819
820    /**
821     * Returns a flag that controls whether or not the thermometer axis zooms
822     * to display the subrange within which the data value falls.
823     *
824     * @return The flag.
825     */
826    public boolean getFollowDataInSubranges() {
827        return this.followDataInSubranges;
828    }
829
830    /**
831     * Sets the flag that controls whether or not the thermometer axis zooms
832     * to display the subrange within which the data value falls.
833     *
834     * @param flag  the flag.
835     */
836    public void setFollowDataInSubranges(boolean flag) {
837        this.followDataInSubranges = flag;
838        fireChangeEvent();
839    }
840
841    /**
842     * Returns a flag that controls whether or not the mercury color changes
843     * for each subrange.
844     *
845     * @return The flag.
846     *
847     * @see #setUseSubrangePaint(boolean)
848     */
849    public boolean getUseSubrangePaint() {
850        return this.useSubrangePaint;
851    }
852
853    /**
854     * Sets the range colour change option.
855     *
856     * @param flag the new range colour change option
857     *
858     * @see #getUseSubrangePaint()
859     */
860    public void setUseSubrangePaint(boolean flag) {
861        this.useSubrangePaint = flag;
862        fireChangeEvent();
863    }
864
865    /**
866     * Returns the bulb radius, in Java2D units.
867
868     * @return The bulb radius.
869     */
870    public int getBulbRadius() {
871        return this.bulbRadius;
872    }
873
874    /**
875     * Sets the bulb radius (in Java2D units) and sends a
876     * {@link PlotChangeEvent} to all registered listeners.
877     *
878     * @param r  the new radius (in Java2D units).
879     *
880     * @see #getBulbRadius()
881     */
882    public void setBulbRadius(int r) {
883        this.bulbRadius = r;
884        fireChangeEvent();
885    }
886
887    /**
888     * Returns the bulb diameter, which is always twice the value returned
889     * by {@link #getBulbRadius()}.
890     *
891     * @return The bulb diameter.
892     */
893    public int getBulbDiameter() {
894        return getBulbRadius() * 2;
895    }
896
897    /**
898     * Returns the column radius, in Java2D units.
899     *
900     * @return The column radius.
901     *
902     * @see #setColumnRadius(int)
903     */
904    public int getColumnRadius() {
905        return this.columnRadius;
906    }
907
908    /**
909     * Sets the column radius (in Java2D units) and sends a
910     * {@link PlotChangeEvent} to all registered listeners.
911     *
912     * @param r  the new radius.
913     *
914     * @see #getColumnRadius()
915     */
916    public void setColumnRadius(int r) {
917        this.columnRadius = r;
918        fireChangeEvent();
919    }
920
921    /**
922     * Returns the column diameter, which is always twice the value returned
923     * by {@link #getColumnRadius()}.
924     *
925     * @return The column diameter.
926     */
927    public int getColumnDiameter() {
928        return getColumnRadius() * 2;
929    }
930
931    /**
932     * Returns the gap, in Java2D units, between the two outlines that
933     * represent the thermometer.
934     *
935     * @return The gap.
936     *
937     * @see #setGap(int)
938     */
939    public int getGap() {
940        return this.gap;
941    }
942
943    /**
944     * Sets the gap (in Java2D units) between the two outlines that represent
945     * the thermometer, and sends a {@link PlotChangeEvent} to all registered
946     * listeners.
947     *
948     * @param gap  the new gap.
949     *
950     * @see #getGap()
951     */
952    public void setGap(int gap) {
953        this.gap = gap;
954        fireChangeEvent();
955    }
956
957    /**
958     * Draws the plot on a Java 2D graphics device (such as the screen or a
959     * printer).
960     *
961     * @param g2  the graphics device.
962     * @param area  the area within which the plot should be drawn.
963     * @param anchor  the anchor point ({@code null} permitted).
964     * @param parentState  the state from the parent plot, if there is one.
965     * @param info  collects info about the drawing.
966     */
967    @Override
968    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
969                     PlotState parentState,
970                     PlotRenderingInfo info) {
971
972        RoundRectangle2D outerStem = new RoundRectangle2D.Double();
973        RoundRectangle2D innerStem = new RoundRectangle2D.Double();
974        RoundRectangle2D mercuryStem = new RoundRectangle2D.Double();
975        Ellipse2D outerBulb = new Ellipse2D.Double();
976        Ellipse2D innerBulb = new Ellipse2D.Double();
977        String temp;
978        FontMetrics metrics;
979        if (info != null) {
980            info.setPlotArea(area);
981        }
982
983        // adjust for insets...
984        RectangleInsets insets = getInsets();
985        insets.trim(area);
986        drawBackground(g2, area);
987
988        // adjust for padding...
989        Rectangle2D interior = (Rectangle2D) area.clone();
990        this.padding.trim(interior);
991        int midX = (int) (interior.getX() + (interior.getWidth() / 2));
992        int midY = (int) (interior.getY() + (interior.getHeight() / 2));
993        int stemTop = (int) (interior.getMinY() + getBulbRadius());
994        int stemBottom = (int) (interior.getMaxY() - getBulbDiameter());
995        Rectangle2D dataArea = new Rectangle2D.Double(midX - getColumnRadius(),
996                stemTop, getColumnRadius(), stemBottom - stemTop);
997
998        outerBulb.setFrame(midX - getBulbRadius(), stemBottom,
999                getBulbDiameter(), getBulbDiameter());
1000
1001        outerStem.setRoundRect(midX - getColumnRadius(), interior.getMinY(),
1002                getColumnDiameter(), stemBottom + getBulbDiameter() - stemTop,
1003                getColumnDiameter(), getColumnDiameter());
1004
1005        Area outerThermometer = new Area(outerBulb);
1006        Area tempArea = new Area(outerStem);
1007        outerThermometer.add(tempArea);
1008
1009        innerBulb.setFrame(midX - getBulbRadius() + getGap(), stemBottom
1010                + getGap(), getBulbDiameter() - getGap() * 2, getBulbDiameter()
1011                - getGap() * 2);
1012
1013        innerStem.setRoundRect(midX - getColumnRadius() + getGap(),
1014                interior.getMinY() + getGap(), getColumnDiameter()
1015                - getGap() * 2, stemBottom + getBulbDiameter() - getGap() * 2
1016                - stemTop, getColumnDiameter() - getGap() * 2,
1017                getColumnDiameter() - getGap() * 2);
1018
1019        Area innerThermometer = new Area(innerBulb);
1020        tempArea = new Area(innerStem);
1021        innerThermometer.add(tempArea);
1022
1023        if ((this.dataset != null) && (this.dataset.getValue() != null)) {
1024            double current = this.dataset.getValue().doubleValue();
1025            double ds = this.rangeAxis.valueToJava2D(current, dataArea,
1026                    RectangleEdge.LEFT);
1027
1028            int i = getColumnDiameter() - getGap() * 2; // already calculated
1029            int j = getColumnRadius() - getGap(); // already calculated
1030            int l = (i / 2);
1031            int k = (int) Math.round(ds);
1032            if (k < (getGap() + interior.getMinY())) {
1033                k = (int) (getGap() + interior.getMinY());
1034                l = getBulbRadius();
1035            }
1036
1037            Area mercury = new Area(innerBulb);
1038
1039            if (k < (stemBottom + getBulbRadius())) {
1040                mercuryStem.setRoundRect(midX - j, k, i,
1041                        (stemBottom + getBulbRadius()) - k, l, l);
1042                tempArea = new Area(mercuryStem);
1043                mercury.add(tempArea);
1044            }
1045
1046            g2.setPaint(getCurrentPaint());
1047            g2.fill(mercury);
1048
1049            // draw range indicators...
1050            if (this.subrangeIndicatorsVisible) {
1051                g2.setStroke(this.subrangeIndicatorStroke);
1052                Range range = this.rangeAxis.getRange();
1053
1054                // draw start of normal range
1055                double value = this.subrangeInfo[NORMAL][RANGE_LOW];
1056                if (range.contains(value)) {
1057                    double x = midX + getColumnRadius() + 2;
1058                    double y = this.rangeAxis.valueToJava2D(value, dataArea,
1059                            RectangleEdge.LEFT);
1060                    Line2D line = new Line2D.Double(x, y, x + 10, y);
1061                    g2.setPaint(this.subrangePaint[NORMAL]);
1062                    g2.draw(line);
1063                }
1064
1065                // draw start of warning range
1066                value = this.subrangeInfo[WARNING][RANGE_LOW];
1067                if (range.contains(value)) {
1068                    double x = midX + getColumnRadius() + 2;
1069                    double y = this.rangeAxis.valueToJava2D(value, dataArea,
1070                            RectangleEdge.LEFT);
1071                    Line2D line = new Line2D.Double(x, y, x + 10, y);
1072                    g2.setPaint(this.subrangePaint[WARNING]);
1073                    g2.draw(line);
1074                }
1075
1076                // draw start of critical range
1077                value = this.subrangeInfo[CRITICAL][RANGE_LOW];
1078                if (range.contains(value)) {
1079                    double x = midX + getColumnRadius() + 2;
1080                    double y = this.rangeAxis.valueToJava2D(value, dataArea,
1081                            RectangleEdge.LEFT);
1082                    Line2D line = new Line2D.Double(x, y, x + 10, y);
1083                    g2.setPaint(this.subrangePaint[CRITICAL]);
1084                    g2.draw(line);
1085                }
1086            }
1087
1088            // draw the axis...
1089            if ((this.rangeAxis != null) && (this.axisLocation != NONE)) {
1090                int drawWidth = AXIS_GAP;
1091                if (this.showValueLines) {
1092                    drawWidth += getColumnDiameter();
1093                }
1094                Rectangle2D drawArea;
1095                double cursor;
1096
1097                switch (this.axisLocation) {
1098                    case RIGHT:
1099                        cursor = midX + getColumnRadius();
1100                        drawArea = new Rectangle2D.Double(cursor,
1101                                stemTop, drawWidth, (stemBottom - stemTop + 1));
1102                        this.rangeAxis.draw(g2, cursor, area, drawArea,
1103                                RectangleEdge.RIGHT, null);
1104                        break;
1105
1106                    case LEFT:
1107                    default:
1108                        //cursor = midX - COLUMN_RADIUS - AXIS_GAP;
1109                        cursor = midX - getColumnRadius();
1110                        drawArea = new Rectangle2D.Double(cursor, stemTop,
1111                                drawWidth, (stemBottom - stemTop + 1));
1112                        this.rangeAxis.draw(g2, cursor, area, drawArea,
1113                                RectangleEdge.LEFT, null);
1114                        break;
1115                }
1116
1117            }
1118
1119            // draw text value on screen
1120            g2.setFont(this.valueFont);
1121            g2.setPaint(this.valuePaint);
1122            metrics = g2.getFontMetrics();
1123            switch (this.valueLocation) {
1124                case RIGHT:
1125                    g2.drawString(this.valueFormat.format(current),
1126                            midX + getColumnRadius() + getGap(), midY);
1127                    break;
1128                case LEFT:
1129                    String valueString = this.valueFormat.format(current);
1130                    int stringWidth = metrics.stringWidth(valueString);
1131                    g2.drawString(valueString, midX - getColumnRadius()
1132                            - getGap() - stringWidth, midY);
1133                    break;
1134                case BULB:
1135                    temp = this.valueFormat.format(current);
1136                    i = metrics.stringWidth(temp) / 2;
1137                    g2.drawString(temp, midX - i,
1138                            stemBottom + getBulbRadius() + getGap());
1139                    break;
1140                default:
1141            }
1142            /***/
1143        }
1144
1145        g2.setPaint(this.thermometerPaint);
1146        g2.setFont(this.valueFont);
1147
1148        //  draw units indicator
1149        metrics = g2.getFontMetrics();
1150        int tickX1 = midX - getColumnRadius() - getGap() * 2
1151                     - metrics.stringWidth(UNITS[this.units]);
1152        if (tickX1 > area.getMinX()) {
1153            g2.drawString(UNITS[this.units], tickX1,
1154                    (int) (area.getMinY() + 20));
1155        }
1156
1157        // draw thermometer outline
1158        g2.setStroke(this.thermometerStroke);
1159        g2.draw(outerThermometer);
1160        g2.draw(innerThermometer);
1161
1162        drawOutline(g2, area);
1163    }
1164
1165    /**
1166     * A zoom method that does nothing.  Plots are required to support the
1167     * zoom operation.  In the case of a thermometer chart, it doesn't make
1168     * sense to zoom in or out, so the method is empty.
1169     *
1170     * @param percent  the zoom percentage.
1171     */
1172    @Override
1173    public void zoom(double percent) {
1174        // intentionally blank
1175   }
1176
1177    /**
1178     * Returns a short string describing the type of plot.
1179     *
1180     * @return A short string describing the type of plot.
1181     */
1182    @Override
1183    public String getPlotType() {
1184        return localizationResources.getString("Thermometer_Plot");
1185    }
1186
1187    /**
1188     * Checks to see if a new value means the axis range needs adjusting.
1189     *
1190     * @param event  the dataset change event.
1191     */
1192    @Override
1193    public void datasetChanged(DatasetChangeEvent event) {
1194        if (this.dataset != null) {
1195            Number vn = this.dataset.getValue();
1196            if (vn != null) {
1197                double value = vn.doubleValue();
1198                if (inSubrange(NORMAL, value)) {
1199                    this.subrange = NORMAL;
1200                }
1201                else if (inSubrange(WARNING, value)) {
1202                   this.subrange = WARNING;
1203                }
1204                else if (inSubrange(CRITICAL, value)) {
1205                    this.subrange = CRITICAL;
1206                }
1207                else {
1208                    this.subrange = -1;
1209                }
1210                setAxisRange();
1211            }
1212        }
1213        super.datasetChanged(event);
1214    }
1215
1216    /**
1217     * Returns the data range.
1218     *
1219     * @param axis  the axis.
1220     *
1221     * @return The range of data displayed.
1222     */
1223    @Override
1224    public Range getDataRange(ValueAxis axis) {
1225       return new Range(this.lowerBound, this.upperBound);
1226    }
1227
1228    /**
1229     * Sets the axis range to the current values in the rangeInfo array.
1230     */
1231    protected void setAxisRange() {
1232        if ((this.subrange >= 0) && (this.followDataInSubranges)) {
1233            this.rangeAxis.setRange(
1234                    new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW],
1235                    this.subrangeInfo[this.subrange][DISPLAY_HIGH]));
1236        }
1237        else {
1238            this.rangeAxis.setRange(this.lowerBound, this.upperBound);
1239        }
1240    }
1241
1242    /**
1243     * Returns the legend items for the plot.
1244     *
1245     * @return {@code null}.
1246     */
1247    @Override
1248    public LegendItemCollection getLegendItems() {
1249        return null;
1250    }
1251
1252    /**
1253     * Returns the orientation of the plot.
1254     *
1255     * @return The orientation (always {@link PlotOrientation#VERTICAL}).
1256     */
1257    @Override
1258    public PlotOrientation getOrientation() {
1259        return PlotOrientation.VERTICAL;
1260    }
1261
1262    /**
1263     * Determine whether a number is valid and finite.
1264     *
1265     * @param d  the number to be tested.
1266     *
1267     * @return {@code true} if the number is valid and finite, and
1268     *         {@code false} otherwise.
1269     */
1270    protected static boolean isValidNumber(double d) {
1271        return (!(Double.isNaN(d) || Double.isInfinite(d)));
1272    }
1273
1274    /**
1275     * Returns true if the value is in the specified range, and false otherwise.
1276     *
1277     * @param subrange  the subrange.
1278     * @param value  the value to check.
1279     *
1280     * @return A boolean.
1281     */
1282    private boolean inSubrange(int subrange, double value) {
1283        return (value > this.subrangeInfo[subrange][RANGE_LOW]
1284            && value <= this.subrangeInfo[subrange][RANGE_HIGH]);
1285    }
1286
1287    /**
1288     * Returns the mercury paint corresponding to the current data value.
1289     * Called from the {@link #draw(Graphics2D, Rectangle2D, Point2D,
1290     * PlotState, PlotRenderingInfo)} method.
1291     *
1292     * @return The paint (never {@code null}).
1293     */
1294    private Paint getCurrentPaint() {
1295        Paint result = this.mercuryPaint;
1296        if (this.useSubrangePaint) {
1297            double value = this.dataset.getValue().doubleValue();
1298            if (inSubrange(NORMAL, value)) {
1299                result = this.subrangePaint[NORMAL];
1300            }
1301            else if (inSubrange(WARNING, value)) {
1302                result = this.subrangePaint[WARNING];
1303            }
1304            else if (inSubrange(CRITICAL, value)) {
1305                result = this.subrangePaint[CRITICAL];
1306            }
1307        }
1308        return result;
1309    }
1310
1311    /**
1312     * Tests this plot for equality with another object.  The plot's dataset
1313     * is not considered in the test.
1314     *
1315     * @param obj  the object ({@code null} permitted).
1316     *
1317     * @return {@code true} or {@code false}.
1318     */
1319    @Override
1320    public boolean equals(Object obj) {
1321        if (obj == this) {
1322            return true;
1323        }
1324        if (!(obj instanceof ThermometerPlot)) {
1325            return false;
1326        }
1327        ThermometerPlot that = (ThermometerPlot) obj;
1328        if (!super.equals(obj)) {
1329            return false;
1330        }
1331        if (!Objects.equals(this.rangeAxis, that.rangeAxis)) {
1332            return false;
1333        }
1334        if (this.axisLocation != that.axisLocation) {
1335            return false;
1336        }
1337        if (this.lowerBound != that.lowerBound) {
1338            return false;
1339        }
1340        if (this.upperBound != that.upperBound) {
1341            return false;
1342        }
1343        if (!Objects.equals(this.padding, that.padding)) {
1344            return false;
1345        }
1346        if (!Objects.equals(this.thermometerStroke,
1347                that.thermometerStroke)) {
1348            return false;
1349        }
1350        if (!PaintUtils.equal(this.thermometerPaint,
1351                that.thermometerPaint)) {
1352            return false;
1353        }
1354        if (this.units != that.units) {
1355            return false;
1356        }
1357        if (this.valueLocation != that.valueLocation) {
1358            return false;
1359        }
1360        if (!Objects.equals(this.valueFont, that.valueFont)) {
1361            return false;
1362        }
1363        if (!PaintUtils.equal(this.valuePaint, that.valuePaint)) {
1364            return false;
1365        }
1366        if (!Objects.equals(this.valueFormat, that.valueFormat)) {
1367            return false;
1368        }
1369        if (!PaintUtils.equal(this.mercuryPaint, that.mercuryPaint)) {
1370            return false;
1371        }
1372        if (this.showValueLines != that.showValueLines) {
1373            return false;
1374        }
1375        if (this.subrange != that.subrange) {
1376            return false;
1377        }
1378        if (this.followDataInSubranges != that.followDataInSubranges) {
1379            return false;
1380        }
1381        if (!equal(this.subrangeInfo, that.subrangeInfo)) {
1382            return false;
1383        }
1384        if (this.useSubrangePaint != that.useSubrangePaint) {
1385            return false;
1386        }
1387        if (this.bulbRadius != that.bulbRadius) {
1388            return false;
1389        }
1390        if (this.columnRadius != that.columnRadius) {
1391            return false;
1392        }
1393        if (this.gap != that.gap) {
1394            return false;
1395        }
1396        for (int i = 0; i < this.subrangePaint.length; i++) {
1397            if (!PaintUtils.equal(this.subrangePaint[i],
1398                    that.subrangePaint[i])) {
1399                return false;
1400            }
1401        }
1402        return true;
1403    }
1404
1405    /**
1406     * Tests two double[][] arrays for equality.
1407     *
1408     * @param array1  the first array ({@code null} permitted).
1409     * @param array2  the second arrray ({@code null} permitted).
1410     *
1411     * @return A boolean.
1412     */
1413    private static boolean equal(double[][] array1, double[][] array2) {
1414        if (array1 == null) {
1415            return (array2 == null);
1416        }
1417        if (array2 == null) {
1418            return false;
1419        }
1420        if (array1.length != array2.length) {
1421            return false;
1422        }
1423        for (int i = 0; i < array1.length; i++) {
1424            if (!Arrays.equals(array1[i], array2[i])) {
1425                return false;
1426            }
1427        }
1428        return true;
1429    }
1430
1431    /**
1432     * Returns a clone of the plot.
1433     *
1434     * @return A clone.
1435     *
1436     * @throws CloneNotSupportedException  if the plot cannot be cloned.
1437     */
1438    @Override
1439    public Object clone() throws CloneNotSupportedException {
1440
1441        ThermometerPlot clone = (ThermometerPlot) super.clone();
1442
1443        if (clone.dataset != null) {
1444            clone.dataset.addChangeListener(clone);
1445        }
1446        clone.rangeAxis = (ValueAxis) ObjectUtils.clone(this.rangeAxis);
1447        if (clone.rangeAxis != null) {
1448            clone.rangeAxis.setPlot(clone);
1449            clone.rangeAxis.addChangeListener(clone);
1450        }
1451        clone.valueFormat = (NumberFormat) this.valueFormat.clone();
1452        clone.subrangePaint = (Paint[]) this.subrangePaint.clone();
1453
1454        return clone;
1455
1456    }
1457
1458    /**
1459     * Provides serialization support.
1460     *
1461     * @param stream  the output stream.
1462     *
1463     * @throws IOException  if there is an I/O error.
1464     */
1465    private void writeObject(ObjectOutputStream stream) throws IOException {
1466        stream.defaultWriteObject();
1467        SerialUtils.writeStroke(this.thermometerStroke, stream);
1468        SerialUtils.writePaint(this.thermometerPaint, stream);
1469        SerialUtils.writePaint(this.valuePaint, stream);
1470        SerialUtils.writePaint(this.mercuryPaint, stream);
1471        SerialUtils.writeStroke(this.subrangeIndicatorStroke, stream);
1472        SerialUtils.writeStroke(this.rangeIndicatorStroke, stream);
1473        for (int i = 0; i < 3; i++) {
1474            SerialUtils.writePaint(this.subrangePaint[i], stream);
1475        }
1476    }
1477
1478    /**
1479     * Provides serialization support.
1480     *
1481     * @param stream  the input stream.
1482     *
1483     * @throws IOException  if there is an I/O error.
1484     * @throws ClassNotFoundException  if there is a classpath problem.
1485     */
1486    private void readObject(ObjectInputStream stream) throws IOException,
1487            ClassNotFoundException {
1488        stream.defaultReadObject();
1489        this.thermometerStroke = SerialUtils.readStroke(stream);
1490        this.thermometerPaint = SerialUtils.readPaint(stream);
1491        this.valuePaint = SerialUtils.readPaint(stream);
1492        this.mercuryPaint = SerialUtils.readPaint(stream);
1493        this.subrangeIndicatorStroke = SerialUtils.readStroke(stream);
1494        this.rangeIndicatorStroke = SerialUtils.readStroke(stream);
1495        this.subrangePaint = new Paint[3];
1496        for (int i = 0; i < 3; i++) {
1497            this.subrangePaint[i] = SerialUtils.readPaint(stream);
1498        }
1499        if (this.rangeAxis != null) {
1500            this.rangeAxis.addChangeListener(this);
1501        }
1502    }
1503
1504    /**
1505     * Multiplies the range on the domain axis/axes by the specified factor.
1506     *
1507     * @param factor  the zoom factor.
1508     * @param state  the plot state.
1509     * @param source  the source point.
1510     */
1511    @Override
1512    public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1513                               Point2D source) {
1514        // no domain axis to zoom
1515    }
1516
1517    /**
1518     * Multiplies the range on the domain axis/axes by the specified factor.
1519     *
1520     * @param factor  the zoom factor.
1521     * @param state  the plot state.
1522     * @param source  the source point.
1523     * @param useAnchor  a flag that controls whether or not the source point
1524     *         is used for the zoom anchor.
1525     */
1526    @Override
1527    public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1528                               Point2D source, boolean useAnchor) {
1529        // no domain axis to zoom
1530    }
1531
1532    /**
1533     * Multiplies the range on the range axis/axes by the specified factor.
1534     *
1535     * @param factor  the zoom factor.
1536     * @param state  the plot state.
1537     * @param source  the source point.
1538     */
1539    @Override
1540    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1541                              Point2D source) {
1542        this.rangeAxis.resizeRange(factor);
1543    }
1544
1545    /**
1546     * Multiplies the range on the range axis/axes by the specified factor.
1547     *
1548     * @param factor  the zoom factor.
1549     * @param state  the plot state.
1550     * @param source  the source point.
1551     * @param useAnchor  a flag that controls whether or not the source point
1552     *         is used for the zoom anchor.
1553     */
1554    @Override
1555    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1556                              Point2D source, boolean useAnchor) {
1557        double anchorY = this.getRangeAxis().java2DToValue(source.getY(),
1558                state.getDataArea(), RectangleEdge.LEFT);
1559        this.rangeAxis.resizeRange(factor, anchorY);
1560    }
1561
1562    /**
1563     * This method does nothing.
1564     *
1565     * @param lowerPercent  the lower percent.
1566     * @param upperPercent  the upper percent.
1567     * @param state  the plot state.
1568     * @param source  the source point.
1569     */
1570    @Override
1571    public void zoomDomainAxes(double lowerPercent, double upperPercent,
1572                               PlotRenderingInfo state, Point2D source) {
1573        // no domain axis to zoom
1574    }
1575
1576    /**
1577     * Zooms the range axes.
1578     *
1579     * @param lowerPercent  the lower percent.
1580     * @param upperPercent  the upper percent.
1581     * @param state  the plot state.
1582     * @param source  the source point.
1583     */
1584    @Override
1585    public void zoomRangeAxes(double lowerPercent, double upperPercent,
1586                              PlotRenderingInfo state, Point2D source) {
1587        this.rangeAxis.zoomRange(lowerPercent, upperPercent);
1588    }
1589
1590    /**
1591     * Returns {@code false}.
1592     *
1593     * @return A boolean.
1594     */
1595    @Override
1596    public boolean isDomainZoomable() {
1597        return false;
1598    }
1599
1600    /**
1601     * Returns {@code true}.
1602     *
1603     * @return A boolean.
1604     */
1605    @Override
1606    public boolean isRangeZoomable() {
1607        return true;
1608    }
1609
1610}