001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, 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-present, 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
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        g2.setPaint(this.thermometerPaint);
1145        g2.setFont(this.valueFont);
1146
1147        //  draw units indicator
1148        metrics = g2.getFontMetrics();
1149        int tickX1 = midX - getColumnRadius() - getGap() * 2
1150                     - metrics.stringWidth(UNITS[this.units]);
1151        if (tickX1 > area.getMinX()) {
1152            g2.drawString(UNITS[this.units], tickX1,
1153                    (int) (area.getMinY() + 20));
1154        }
1155
1156        // draw thermometer outline
1157        g2.setStroke(this.thermometerStroke);
1158        g2.draw(outerThermometer);
1159        g2.draw(innerThermometer);
1160
1161        drawOutline(g2, area);
1162    }
1163
1164    /**
1165     * A zoom method that does nothing.  Plots are required to support the
1166     * zoom operation.  In the case of a thermometer chart, it doesn't make
1167     * sense to zoom in or out, so the method is empty.
1168     *
1169     * @param percent  the zoom percentage.
1170     */
1171    @Override
1172    public void zoom(double percent) {
1173        // intentionally blank
1174   }
1175
1176    /**
1177     * Returns a short string describing the type of plot.
1178     *
1179     * @return A short string describing the type of plot.
1180     */
1181    @Override
1182    public String getPlotType() {
1183        return localizationResources.getString("Thermometer_Plot");
1184    }
1185
1186    /**
1187     * Checks to see if a new value means the axis range needs adjusting.
1188     *
1189     * @param event  the dataset change event.
1190     */
1191    @Override
1192    public void datasetChanged(DatasetChangeEvent event) {
1193        if (this.dataset != null) {
1194            Number vn = this.dataset.getValue();
1195            if (vn != null) {
1196                double value = vn.doubleValue();
1197                if (inSubrange(NORMAL, value)) {
1198                    this.subrange = NORMAL;
1199                }
1200                else if (inSubrange(WARNING, value)) {
1201                   this.subrange = WARNING;
1202                }
1203                else if (inSubrange(CRITICAL, value)) {
1204                    this.subrange = CRITICAL;
1205                }
1206                else {
1207                    this.subrange = -1;
1208                }
1209                setAxisRange();
1210            }
1211        }
1212        super.datasetChanged(event);
1213    }
1214
1215    /**
1216     * Returns the data range.
1217     *
1218     * @param axis  the axis.
1219     *
1220     * @return The range of data displayed.
1221     */
1222    @Override
1223    public Range getDataRange(ValueAxis axis) {
1224       return new Range(this.lowerBound, this.upperBound);
1225    }
1226
1227    /**
1228     * Sets the axis range to the current values in the rangeInfo array.
1229     */
1230    protected void setAxisRange() {
1231        if ((this.subrange >= 0) && (this.followDataInSubranges)) {
1232            this.rangeAxis.setRange(
1233                    new Range(this.subrangeInfo[this.subrange][DISPLAY_LOW],
1234                    this.subrangeInfo[this.subrange][DISPLAY_HIGH]));
1235        }
1236        else {
1237            this.rangeAxis.setRange(this.lowerBound, this.upperBound);
1238        }
1239    }
1240
1241    /**
1242     * Returns the legend items for the plot.
1243     *
1244     * @return {@code null}.
1245     */
1246    @Override
1247    public LegendItemCollection getLegendItems() {
1248        return null;
1249    }
1250
1251    /**
1252     * Returns the orientation of the plot.
1253     *
1254     * @return The orientation (always {@link PlotOrientation#VERTICAL}).
1255     */
1256    @Override
1257    public PlotOrientation getOrientation() {
1258        return PlotOrientation.VERTICAL;
1259    }
1260
1261    /**
1262     * Determine whether a number is valid and finite.
1263     *
1264     * @param d  the number to be tested.
1265     *
1266     * @return {@code true} if the number is valid and finite, and
1267     *         {@code false} otherwise.
1268     */
1269    protected static boolean isValidNumber(double d) {
1270        return (!(Double.isNaN(d) || Double.isInfinite(d)));
1271    }
1272
1273    /**
1274     * Returns true if the value is in the specified range, and false otherwise.
1275     *
1276     * @param subrange  the subrange.
1277     * @param value  the value to check.
1278     *
1279     * @return A boolean.
1280     */
1281    private boolean inSubrange(int subrange, double value) {
1282        return (value > this.subrangeInfo[subrange][RANGE_LOW]
1283            && value <= this.subrangeInfo[subrange][RANGE_HIGH]);
1284    }
1285
1286    /**
1287     * Returns the mercury paint corresponding to the current data value.
1288     * Called from the {@link #draw(Graphics2D, Rectangle2D, Point2D,
1289     * PlotState, PlotRenderingInfo)} method.
1290     *
1291     * @return The paint (never {@code null}).
1292     */
1293    private Paint getCurrentPaint() {
1294        Paint result = this.mercuryPaint;
1295        if (this.useSubrangePaint) {
1296            double value = this.dataset.getValue().doubleValue();
1297            if (inSubrange(NORMAL, value)) {
1298                result = this.subrangePaint[NORMAL];
1299            }
1300            else if (inSubrange(WARNING, value)) {
1301                result = this.subrangePaint[WARNING];
1302            }
1303            else if (inSubrange(CRITICAL, value)) {
1304                result = this.subrangePaint[CRITICAL];
1305            }
1306        }
1307        return result;
1308    }
1309
1310    /**
1311     * Tests this plot for equality with another object.  The plot's dataset
1312     * is not considered in the test.
1313     *
1314     * @param obj  the object ({@code null} permitted).
1315     *
1316     * @return {@code true} or {@code false}.
1317     */
1318    @Override
1319    public boolean equals(Object obj) {
1320        if (obj == this) {
1321            return true;
1322        }
1323        if (!(obj instanceof ThermometerPlot)) {
1324            return false;
1325        }
1326        ThermometerPlot that = (ThermometerPlot) obj;
1327        if (!super.equals(obj)) {
1328            return false;
1329        }
1330        if (!Objects.equals(this.rangeAxis, that.rangeAxis)) {
1331            return false;
1332        }
1333        if (this.axisLocation != that.axisLocation) {
1334            return false;
1335        }
1336        if (this.lowerBound != that.lowerBound) {
1337            return false;
1338        }
1339        if (this.upperBound != that.upperBound) {
1340            return false;
1341        }
1342        if (!Objects.equals(this.padding, that.padding)) {
1343            return false;
1344        }
1345        if (!Objects.equals(this.thermometerStroke,
1346                that.thermometerStroke)) {
1347            return false;
1348        }
1349        if (!PaintUtils.equal(this.thermometerPaint,
1350                that.thermometerPaint)) {
1351            return false;
1352        }
1353        if (this.units != that.units) {
1354            return false;
1355        }
1356        if (this.valueLocation != that.valueLocation) {
1357            return false;
1358        }
1359        if (!Objects.equals(this.valueFont, that.valueFont)) {
1360            return false;
1361        }
1362        if (!PaintUtils.equal(this.valuePaint, that.valuePaint)) {
1363            return false;
1364        }
1365        if (!Objects.equals(this.valueFormat, that.valueFormat)) {
1366            return false;
1367        }
1368        if (!PaintUtils.equal(this.mercuryPaint, that.mercuryPaint)) {
1369            return false;
1370        }
1371        if (this.showValueLines != that.showValueLines) {
1372            return false;
1373        }
1374        if (this.subrange != that.subrange) {
1375            return false;
1376        }
1377        if (this.followDataInSubranges != that.followDataInSubranges) {
1378            return false;
1379        }
1380        if (!equal(this.subrangeInfo, that.subrangeInfo)) {
1381            return false;
1382        }
1383        if (this.useSubrangePaint != that.useSubrangePaint) {
1384            return false;
1385        }
1386        if (this.bulbRadius != that.bulbRadius) {
1387            return false;
1388        }
1389        if (this.columnRadius != that.columnRadius) {
1390            return false;
1391        }
1392        if (this.gap != that.gap) {
1393            return false;
1394        }
1395        for (int i = 0; i < this.subrangePaint.length; i++) {
1396            if (!PaintUtils.equal(this.subrangePaint[i],
1397                    that.subrangePaint[i])) {
1398                return false;
1399            }
1400        }
1401        return true;
1402    }
1403
1404    /**
1405     * Tests two double[][] arrays for equality.
1406     *
1407     * @param array1  the first array ({@code null} permitted).
1408     * @param array2  the second arrray ({@code null} permitted).
1409     *
1410     * @return A boolean.
1411     */
1412    private static boolean equal(double[][] array1, double[][] array2) {
1413        if (array1 == null) {
1414            return (array2 == null);
1415        }
1416        if (array2 == null) {
1417            return false;
1418        }
1419        if (array1.length != array2.length) {
1420            return false;
1421        }
1422        for (int i = 0; i < array1.length; i++) {
1423            if (!Arrays.equals(array1[i], array2[i])) {
1424                return false;
1425            }
1426        }
1427        return true;
1428    }
1429
1430    /**
1431     * Returns a clone of the plot.
1432     *
1433     * @return A clone.
1434     *
1435     * @throws CloneNotSupportedException  if the plot cannot be cloned.
1436     */
1437    @Override
1438    public Object clone() throws CloneNotSupportedException {
1439
1440        ThermometerPlot clone = (ThermometerPlot) super.clone();
1441
1442        if (clone.dataset != null) {
1443            clone.dataset.addChangeListener(clone);
1444        }
1445        clone.rangeAxis = (ValueAxis) ObjectUtils.clone(this.rangeAxis);
1446        if (clone.rangeAxis != null) {
1447            clone.rangeAxis.setPlot(clone);
1448            clone.rangeAxis.addChangeListener(clone);
1449        }
1450        clone.valueFormat = (NumberFormat) this.valueFormat.clone();
1451        clone.subrangePaint = (Paint[]) this.subrangePaint.clone();
1452
1453        return clone;
1454
1455    }
1456
1457    /**
1458     * Provides serialization support.
1459     *
1460     * @param stream  the output stream.
1461     *
1462     * @throws IOException  if there is an I/O error.
1463     */
1464    private void writeObject(ObjectOutputStream stream) throws IOException {
1465        stream.defaultWriteObject();
1466        SerialUtils.writeStroke(this.thermometerStroke, stream);
1467        SerialUtils.writePaint(this.thermometerPaint, stream);
1468        SerialUtils.writePaint(this.valuePaint, stream);
1469        SerialUtils.writePaint(this.mercuryPaint, stream);
1470        SerialUtils.writeStroke(this.subrangeIndicatorStroke, stream);
1471        SerialUtils.writeStroke(this.rangeIndicatorStroke, stream);
1472        for (int i = 0; i < 3; i++) {
1473            SerialUtils.writePaint(this.subrangePaint[i], stream);
1474        }
1475    }
1476
1477    /**
1478     * Provides serialization support.
1479     *
1480     * @param stream  the input stream.
1481     *
1482     * @throws IOException  if there is an I/O error.
1483     * @throws ClassNotFoundException  if there is a classpath problem.
1484     */
1485    private void readObject(ObjectInputStream stream) throws IOException,
1486            ClassNotFoundException {
1487        stream.defaultReadObject();
1488        this.thermometerStroke = SerialUtils.readStroke(stream);
1489        this.thermometerPaint = SerialUtils.readPaint(stream);
1490        this.valuePaint = SerialUtils.readPaint(stream);
1491        this.mercuryPaint = SerialUtils.readPaint(stream);
1492        this.subrangeIndicatorStroke = SerialUtils.readStroke(stream);
1493        this.rangeIndicatorStroke = SerialUtils.readStroke(stream);
1494        this.subrangePaint = new Paint[3];
1495        for (int i = 0; i < 3; i++) {
1496            this.subrangePaint[i] = SerialUtils.readPaint(stream);
1497        }
1498        if (this.rangeAxis != null) {
1499            this.rangeAxis.addChangeListener(this);
1500        }
1501    }
1502
1503    /**
1504     * Multiplies the range on the domain axis/axes by the specified factor.
1505     *
1506     * @param factor  the zoom factor.
1507     * @param state  the plot state.
1508     * @param source  the source point.
1509     */
1510    @Override
1511    public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1512                               Point2D source) {
1513        // no domain axis to zoom
1514    }
1515
1516    /**
1517     * Multiplies the range on the domain axis/axes by the specified factor.
1518     *
1519     * @param factor  the zoom factor.
1520     * @param state  the plot state.
1521     * @param source  the source point.
1522     * @param useAnchor  a flag that controls whether or not the source point
1523     *         is used for the zoom anchor.
1524     */
1525    @Override
1526    public void zoomDomainAxes(double factor, PlotRenderingInfo state,
1527                               Point2D source, boolean useAnchor) {
1528        // no domain axis to zoom
1529    }
1530
1531    /**
1532     * Multiplies the range on the range axis/axes by the specified factor.
1533     *
1534     * @param factor  the zoom factor.
1535     * @param state  the plot state.
1536     * @param source  the source point.
1537     */
1538    @Override
1539    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1540                              Point2D source) {
1541        this.rangeAxis.resizeRange(factor);
1542    }
1543
1544    /**
1545     * Multiplies the range on the range axis/axes by the specified factor.
1546     *
1547     * @param factor  the zoom factor.
1548     * @param state  the plot state.
1549     * @param source  the source point.
1550     * @param useAnchor  a flag that controls whether or not the source point
1551     *         is used for the zoom anchor.
1552     */
1553    @Override
1554    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
1555                              Point2D source, boolean useAnchor) {
1556        double anchorY = this.getRangeAxis().java2DToValue(source.getY(),
1557                state.getDataArea(), RectangleEdge.LEFT);
1558        this.rangeAxis.resizeRange(factor, anchorY);
1559    }
1560
1561    /**
1562     * This method does nothing.
1563     *
1564     * @param lowerPercent  the lower percent.
1565     * @param upperPercent  the upper percent.
1566     * @param state  the plot state.
1567     * @param source  the source point.
1568     */
1569    @Override
1570    public void zoomDomainAxes(double lowerPercent, double upperPercent,
1571                               PlotRenderingInfo state, Point2D source) {
1572        // no domain axis to zoom
1573    }
1574
1575    /**
1576     * Zooms the range axes.
1577     *
1578     * @param lowerPercent  the lower percent.
1579     * @param upperPercent  the upper percent.
1580     * @param state  the plot state.
1581     * @param source  the source point.
1582     */
1583    @Override
1584    public void zoomRangeAxes(double lowerPercent, double upperPercent,
1585                              PlotRenderingInfo state, Point2D source) {
1586        this.rangeAxis.zoomRange(lowerPercent, upperPercent);
1587    }
1588
1589    /**
1590     * Returns {@code false}.
1591     *
1592     * @return A boolean.
1593     */
1594    @Override
1595    public boolean isDomainZoomable() {
1596        return false;
1597    }
1598
1599    /**
1600     * Returns {@code true}.
1601     *
1602     * @return A boolean.
1603     */
1604    @Override
1605    public boolean isRangeZoomable() {
1606        return true;
1607    }
1608
1609}