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