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 * StandardXYItemRenderer.java
029 * ---------------------------
030 * (C) Copyright 2001-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Mark Watson (www.markwatson.com);
034 *                   Jonathan Nash;
035 *                   Andreas Schneider;
036 *                   Norbert Kiesel (for TBD Networks);
037 *                   Christian W. Zuckschwerdt;
038 *                   Bill Kelemen;
039 *                   Nicolas Brodu (for Astrium and EADS Corporate Research
040 *                   Center);
041 *
042 */
043
044package org.jfree.chart.renderer.xy;
045
046import java.awt.Graphics2D;
047import java.awt.Image;
048import java.awt.Paint;
049import java.awt.Point;
050import java.awt.Shape;
051import java.awt.Stroke;
052import java.awt.geom.GeneralPath;
053import java.awt.geom.Line2D;
054import java.awt.geom.Rectangle2D;
055import java.io.IOException;
056import java.io.ObjectInputStream;
057import java.io.ObjectOutputStream;
058import java.io.Serializable;
059import java.util.HashMap;
060import java.util.Map;
061
062import org.jfree.chart.legend.LegendItem;
063import org.jfree.chart.axis.ValueAxis;
064import org.jfree.chart.entity.EntityCollection;
065import org.jfree.chart.event.RendererChangeEvent;
066import org.jfree.chart.labels.XYToolTipGenerator;
067import org.jfree.chart.plot.CrosshairState;
068import org.jfree.chart.plot.Plot;
069import org.jfree.chart.plot.PlotOrientation;
070import org.jfree.chart.plot.PlotRenderingInfo;
071import org.jfree.chart.plot.XYPlot;
072import org.jfree.chart.api.RectangleEdge;
073import org.jfree.chart.urls.XYURLGenerator;
074import org.jfree.chart.internal.Args;
075import org.jfree.chart.api.PublicCloneable;
076import org.jfree.chart.internal.SerialUtils;
077import org.jfree.chart.internal.ShapeUtils;
078import org.jfree.chart.api.UnitType;
079import org.jfree.chart.internal.CloneUtils;
080import org.jfree.data.xy.XYDataset;
081
082/**
083 * Standard item renderer for an {@link XYPlot}.  This class can draw (a)
084 * shapes at each point, or (b) lines between points, or (c) both shapes and
085 * lines.
086 * <P>
087 * This renderer has been retained for historical reasons and, in general, you
088 * should use the {@link XYLineAndShapeRenderer} class instead.
089 */
090public class StandardXYItemRenderer extends AbstractXYItemRenderer
091        implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
092
093    /** For serialization. */
094    private static final long serialVersionUID = -3271351259436865995L;
095
096    /** Constant for the type of rendering (shapes only). */
097    public static final int SHAPES = 1;
098
099    /** Constant for the type of rendering (lines only). */
100    public static final int LINES = 2;
101
102    /** Constant for the type of rendering (shapes and lines). */
103    public static final int SHAPES_AND_LINES = SHAPES | LINES;
104
105    /** Constant for the type of rendering (images only). */
106    public static final int IMAGES = 4;
107
108    /** Constant for the type of rendering (discontinuous lines). */
109    public static final int DISCONTINUOUS = 8;
110
111    /** Constant for the type of rendering (discontinuous lines). */
112    public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS;
113
114    /** A flag indicating whether or not shapes are drawn at each XY point. */
115    private boolean baseShapesVisible;
116
117    /** A flag indicating whether or not lines are drawn between XY points. */
118    private boolean plotLines;
119
120    /** A flag indicating whether or not images are drawn between XY points. */
121    private boolean plotImages;
122
123    /** A flag controlling whether or not discontinuous lines are used. */
124    private boolean plotDiscontinuous;
125
126    /** Specifies how the gap threshold value is interpreted. */
127    private UnitType gapThresholdType = UnitType.RELATIVE;
128
129    /** Threshold for deciding when to discontinue a line. */
130    private double gapThreshold = 1.0;
131
132    /**
133     * A table of flags that control (per series) whether or not shapes are
134     * filled.
135     */
136    private Map<Integer, Boolean> seriesShapesFilledMap;
137
138    /** The default value returned by the getShapeFilled() method. */
139    private boolean baseShapesFilled;
140
141    /**
142     * A flag that controls whether or not each series is drawn as a single
143     * path.
144     */
145    private boolean drawSeriesLineAsPath;
146
147    /**
148     * The shape that is used to represent a line in the legend.
149     * This should never be set to {@code null}.
150     */
151    private transient Shape legendLine;
152
153    /**
154     * Constructs a new renderer.
155     */
156    public StandardXYItemRenderer() {
157        this(LINES, null);
158    }
159
160    /**
161     * Constructs a new renderer.  To specify the type of renderer, use one of
162     * the constants: {@link #SHAPES}, {@link #LINES} or
163     * {@link #SHAPES_AND_LINES}.
164     *
165     * @param type  the type.
166     */
167    public StandardXYItemRenderer(int type) {
168        this(type, null);
169    }
170
171    /**
172     * Constructs a new renderer.  To specify the type of renderer, use one of
173     * the constants: {@link #SHAPES}, {@link #LINES} or
174     * {@link #SHAPES_AND_LINES}.
175     *
176     * @param type  the type of renderer.
177     * @param toolTipGenerator  the item label generator ({@code null}
178     *                          permitted).
179     */
180    public StandardXYItemRenderer(int type,
181                                  XYToolTipGenerator toolTipGenerator) {
182        this(type, toolTipGenerator, null);
183    }
184
185    /**
186     * Constructs a new renderer.  To specify the type of renderer, use one of
187     * the constants: {@link #SHAPES}, {@link #LINES} or
188     * {@link #SHAPES_AND_LINES}.
189     *
190     * @param type  the type of renderer.
191     * @param toolTipGenerator  the item label generator ({@code null}
192     *                          permitted).
193     * @param urlGenerator  the URL generator.
194     */
195    public StandardXYItemRenderer(int type, XYToolTipGenerator toolTipGenerator,
196           XYURLGenerator urlGenerator) {
197
198        super();
199        setDefaultToolTipGenerator(toolTipGenerator);
200        setURLGenerator(urlGenerator);
201        if ((type & SHAPES) != 0) {
202            this.baseShapesVisible = true;
203        }
204        if ((type & LINES) != 0) {
205            this.plotLines = true;
206        }
207        if ((type & IMAGES) != 0) {
208            this.plotImages = true;
209        }
210        if ((type & DISCONTINUOUS) != 0) {
211            this.plotDiscontinuous = true;
212        }
213
214        this.seriesShapesFilledMap = new HashMap<>();
215        this.baseShapesFilled = true;
216        this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
217        this.drawSeriesLineAsPath = false;
218    }
219
220    /**
221     * Returns true if shapes are being plotted by the renderer.
222     *
223     * @return {@code true} if shapes are being plotted by the renderer.
224     *
225     * @see #setBaseShapesVisible
226     */
227    public boolean getBaseShapesVisible() {
228        return this.baseShapesVisible;
229    }
230
231    /**
232     * Sets the flag that controls whether or not a shape is plotted at each
233     * data point.
234     *
235     * @param flag  the flag.
236     *
237     * @see #getBaseShapesVisible
238     */
239    public void setBaseShapesVisible(boolean flag) {
240        if (this.baseShapesVisible != flag) {
241            this.baseShapesVisible = flag;
242            fireChangeEvent();
243        }
244    }
245
246    // SHAPES FILLED
247
248    /**
249     * Returns the flag used to control whether or not the shape for an item is
250     * filled.
251     * <p>
252     * The default implementation passes control to the
253     * {@code getSeriesShapesFilled()} method.  You can override this method
254     * if you require different behaviour.
255     *
256     * @param series  the series index (zero-based).
257     * @param item  the item index (zero-based).
258     *
259     * @return A boolean.
260     *
261     * @see #getSeriesShapesFilled(int)
262     */
263    public boolean getItemShapeFilled(int series, int item) {
264
265        // otherwise look up the paint table
266        Boolean flag = this.seriesShapesFilledMap.get(series);
267        if (flag != null) {
268            return flag;
269        } else {
270            return this.baseShapesFilled;
271        }
272    }
273
274    /**
275     * Returns the flag used to control whether or not the shapes for a series
276     * are filled.
277     *
278     * @param series  the series index (zero-based).
279     *
280     * @return A boolean.
281     */
282    public Boolean getSeriesShapesFilled(int series) {
283        return this.seriesShapesFilledMap.get(series);
284    }
285
286    /**
287     * Sets the 'shapes filled' flag for a series and sends a
288     * {@link RendererChangeEvent} to all registered listeners.
289     *
290     * @param series  the series index (zero-based).
291     * @param flag  the flag.
292     *
293     * @see #getSeriesShapesFilled(int)
294     */
295    public void setSeriesShapesFilled(int series, Boolean flag) {
296        this.seriesShapesFilledMap.put(series, flag);
297        fireChangeEvent();
298    }
299
300    /**
301     * Returns the base 'shape filled' attribute.
302     *
303     * @return The base flag.
304     *
305     * @see #setBaseShapesFilled(boolean)
306     */
307    public boolean getBaseShapesFilled() {
308        return this.baseShapesFilled;
309    }
310
311    /**
312     * Sets the base 'shapes filled' flag and sends a
313     * {@link RendererChangeEvent} to all registered listeners.
314     *
315     * @param flag  the flag.
316     *
317     * @see #getBaseShapesFilled()
318     */
319    public void setBaseShapesFilled(boolean flag) {
320        this.baseShapesFilled = flag;
321    }
322
323    /**
324     * Returns true if lines are being plotted by the renderer.
325     *
326     * @return {@code true} if lines are being plotted by the renderer.
327     *
328     * @see #setPlotLines(boolean)
329     */
330    public boolean getPlotLines() {
331        return this.plotLines;
332    }
333
334    /**
335     * Sets the flag that controls whether or not a line is plotted between
336     * each data point and sends a {@link RendererChangeEvent} to all
337     * registered listeners.
338     *
339     * @param flag  the flag.
340     *
341     * @see #getPlotLines()
342     */
343    public void setPlotLines(boolean flag) {
344        if (this.plotLines != flag) {
345            this.plotLines = flag;
346            fireChangeEvent();
347        }
348    }
349
350    /**
351     * Returns the gap threshold type (relative or absolute).
352     *
353     * @return The type.
354     *
355     * @see #setGapThresholdType(UnitType)
356     */
357    public UnitType getGapThresholdType() {
358        return this.gapThresholdType;
359    }
360
361    /**
362     * Sets the gap threshold type and sends a {@link RendererChangeEvent} to
363     * all registered listeners.
364     *
365     * @param thresholdType  the type ({@code null} not permitted).
366     *
367     * @see #getGapThresholdType()
368     */
369    public void setGapThresholdType(UnitType thresholdType) {
370        Args.nullNotPermitted(thresholdType, "thresholdType");
371        this.gapThresholdType = thresholdType;
372        fireChangeEvent();
373    }
374
375    /**
376     * Returns the gap threshold for discontinuous lines.
377     *
378     * @return The gap threshold.
379     *
380     * @see #setGapThreshold(double)
381     */
382    public double getGapThreshold() {
383        return this.gapThreshold;
384    }
385
386    /**
387     * Sets the gap threshold for discontinuous lines and sends a
388     * {@link RendererChangeEvent} to all registered listeners.
389     *
390     * @param t  the threshold.
391     *
392     * @see #getGapThreshold()
393     */
394    public void setGapThreshold(double t) {
395        this.gapThreshold = t;
396        fireChangeEvent();
397    }
398
399    /**
400     * Returns true if images are being plotted by the renderer.
401     *
402     * @return {@code true} if images are being plotted by the renderer.
403     *
404     * @see #setPlotImages(boolean)
405     */
406    public boolean getPlotImages() {
407        return this.plotImages;
408    }
409
410    /**
411     * Sets the flag that controls whether or not an image is drawn at each
412     * data point and sends a {@link RendererChangeEvent} to all registered
413     * listeners.
414     *
415     * @param flag  the flag.
416     *
417     * @see #getPlotImages()
418     */
419    public void setPlotImages(boolean flag) {
420        if (this.plotImages != flag) {
421            this.plotImages = flag;
422            fireChangeEvent();
423        }
424    }
425
426    /**
427     * Returns a flag that controls whether or not the renderer shows
428     * discontinuous lines.
429     *
430     * @return {@code true} if lines should be discontinuous.
431     */
432    public boolean getPlotDiscontinuous() {
433        return this.plotDiscontinuous;
434    }
435
436    /**
437     * Sets the flag that controls whether or not the renderer shows
438     * discontinuous lines, and sends a {@link RendererChangeEvent} to all
439     * registered listeners.
440     *
441     * @param flag  the new flag value.
442     */
443    public void setPlotDiscontinuous(boolean flag) {
444        if (this.plotDiscontinuous != flag) {
445            this.plotDiscontinuous = flag;
446            fireChangeEvent();
447        }
448    }
449
450    /**
451     * Returns a flag that controls whether or not each series is drawn as a
452     * single path.
453     *
454     * @return A boolean.
455     *
456     * @see #setDrawSeriesLineAsPath(boolean)
457     */
458    public boolean getDrawSeriesLineAsPath() {
459        return this.drawSeriesLineAsPath;
460    }
461
462    /**
463     * Sets the flag that controls whether or not each series is drawn as a
464     * single path.
465     *
466     * @param flag  the flag.
467     *
468     * @see #getDrawSeriesLineAsPath()
469     */
470    public void setDrawSeriesLineAsPath(boolean flag) {
471        this.drawSeriesLineAsPath = flag;
472    }
473
474    /**
475     * Returns the shape used to represent a line in the legend.
476     *
477     * @return The legend line (never {@code null}).
478     *
479     * @see #setLegendLine(Shape)
480     */
481    public Shape getLegendLine() {
482        return this.legendLine;
483    }
484
485    /**
486     * Sets the shape used as a line in each legend item and sends a
487     * {@link RendererChangeEvent} to all registered listeners.
488     *
489     * @param line  the line ({@code null} not permitted).
490     *
491     * @see #getLegendLine()
492     */
493    public void setLegendLine(Shape line) {
494        Args.nullNotPermitted(line, "line");
495        this.legendLine = line;
496        fireChangeEvent();
497    }
498
499    /**
500     * Returns a legend item for a series.
501     *
502     * @param datasetIndex  the dataset index (zero-based).
503     * @param series  the series index (zero-based).
504     *
505     * @return A legend item for the series.
506     */
507    @Override
508    public LegendItem getLegendItem(int datasetIndex, int series) {
509        XYPlot plot = getPlot();
510        if (plot == null) {
511            return null;
512        }
513        LegendItem result = null;
514        XYDataset dataset = plot.getDataset(datasetIndex);
515        if (dataset != null) {
516            if (getItemVisible(series, 0)) {
517                String label = getLegendItemLabelGenerator().generateLabel(
518                        dataset, series);
519                String description = label;
520                String toolTipText = null;
521                if (getLegendItemToolTipGenerator() != null) {
522                    toolTipText = getLegendItemToolTipGenerator().generateLabel(
523                            dataset, series);
524                }
525                String urlText = null;
526                if (getLegendItemURLGenerator() != null) {
527                    urlText = getLegendItemURLGenerator().generateLabel(
528                            dataset, series);
529                }
530                Shape shape = lookupLegendShape(series);
531                boolean shapeFilled = getItemShapeFilled(series, 0);
532                Paint paint = lookupSeriesPaint(series);
533                Paint linePaint = paint;
534                Stroke lineStroke = lookupSeriesStroke(series);
535                result = new LegendItem(label, description, toolTipText,
536                        urlText, this.baseShapesVisible, shape, shapeFilled,
537                        paint, !shapeFilled, paint, lineStroke,
538                        this.plotLines, this.legendLine, lineStroke, linePaint);
539                result.setLabelFont(lookupLegendTextFont(series));
540                Paint labelPaint = lookupLegendTextPaint(series);
541                if (labelPaint != null) {
542                    result.setLabelPaint(labelPaint);
543                }
544                result.setDataset(dataset);
545                result.setDatasetIndex(datasetIndex);
546                result.setSeriesKey(dataset.getSeriesKey(series));
547                result.setSeriesIndex(series);
548            }
549        }
550        return result;
551    }
552
553    /**
554     * Records the state for the renderer.  This is used to preserve state
555     * information between calls to the drawItem() method for a single chart
556     * drawing.
557     */
558    public static class State extends XYItemRendererState {
559
560        /** The path for the current series. */
561        public GeneralPath seriesPath;
562
563        /** The series index. */
564        private int seriesIndex;
565
566        /**
567         * A flag that indicates if the last (x, y) point was 'good'
568         * (non-null).
569         */
570        private boolean lastPointGood;
571
572        /**
573         * Creates a new state instance.
574         *
575         * @param info  the plot rendering info.
576         */
577        public State(PlotRenderingInfo info) {
578            super(info);
579        }
580
581        /**
582         * Returns a flag that indicates if the last point drawn (in the
583         * current series) was 'good' (non-null).
584         *
585         * @return A boolean.
586         */
587        public boolean isLastPointGood() {
588            return this.lastPointGood;
589        }
590
591        /**
592         * Sets a flag that indicates if the last point drawn (in the current
593         * series) was 'good' (non-null).
594         *
595         * @param good  the flag.
596         */
597        public void setLastPointGood(boolean good) {
598            this.lastPointGood = good;
599        }
600
601        /**
602         * Returns the series index for the current path.
603         *
604         * @return The series index for the current path.
605         */
606        public int getSeriesIndex() {
607            return this.seriesIndex;
608        }
609
610        /**
611         * Sets the series index for the current path.
612         *
613         * @param index  the index.
614         */
615        public void setSeriesIndex(int index) {
616            this.seriesIndex = index;
617        }
618    }
619
620    /**
621     * Initialises the renderer.
622     * <P>
623     * This method will be called before the first item is rendered, giving the
624     * renderer an opportunity to initialise any state information it wants to
625     * maintain. The renderer can do nothing if it chooses.
626     *
627     * @param g2  the graphics device.
628     * @param dataArea  the area inside the axes.
629     * @param plot  the plot.
630     * @param data  the data.
631     * @param info  an optional info collection object to return data back to
632     *              the caller.
633     *
634     * @return The renderer state.
635     */
636    @Override
637    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
638            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
639
640        State state = new State(info);
641        state.seriesPath = new GeneralPath();
642        state.seriesIndex = -1;
643        return state;
644
645    }
646
647    /**
648     * Draws the visual representation of a single data item.
649     *
650     * @param g2  the graphics device.
651     * @param state  the renderer state.
652     * @param dataArea  the area within which the data is being drawn.
653     * @param info  collects information about the drawing.
654     * @param plot  the plot (can be used to obtain standard color information
655     *              etc).
656     * @param domainAxis  the domain axis.
657     * @param rangeAxis  the range axis.
658     * @param dataset  the dataset.
659     * @param series  the series index (zero-based).
660     * @param item  the item index (zero-based).
661     * @param crosshairState  crosshair information for the plot
662     *                        ({@code null} permitted).
663     * @param pass  the pass index.
664     */
665    @Override
666    public void drawItem(Graphics2D g2, XYItemRendererState state,
667            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
668            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
669            int series, int item, CrosshairState crosshairState, int pass) {
670
671        boolean itemVisible = getItemVisible(series, item);
672
673        // setup for collecting optional entity info...
674        Shape entityArea = null;
675        EntityCollection entities = null;
676        if (info != null) {
677            entities = info.getOwner().getEntityCollection();
678        }
679
680        PlotOrientation orientation = plot.getOrientation();
681        Paint paint = getItemPaint(series, item);
682        Stroke seriesStroke = getItemStroke(series, item);
683        g2.setPaint(paint);
684        g2.setStroke(seriesStroke);
685
686        // get the data point...
687        double x1 = dataset.getXValue(series, item);
688        double y1 = dataset.getYValue(series, item);
689        if (Double.isNaN(x1) || Double.isNaN(y1)) {
690            itemVisible = false;
691        }
692
693        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
694        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
695        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
696        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
697
698        if (getPlotLines()) {
699            if (this.drawSeriesLineAsPath) {
700                State s = (State) state;
701                if (s.getSeriesIndex() != series) {
702                    // we are starting a new series path
703                    s.seriesPath.reset();
704                    s.lastPointGood = false;
705                    s.setSeriesIndex(series);
706                }
707
708                // update path to reflect latest point
709                if (itemVisible && !Double.isNaN(transX1)
710                        && !Double.isNaN(transY1)) {
711                    float x = (float) transX1;
712                    float y = (float) transY1;
713                    if (orientation == PlotOrientation.HORIZONTAL) {
714                        x = (float) transY1;
715                        y = (float) transX1;
716                    }
717                    if (s.isLastPointGood()) {
718                        // TODO: check threshold
719                        s.seriesPath.lineTo(x, y);
720                    }
721                    else {
722                        s.seriesPath.moveTo(x, y);
723                    }
724                    s.setLastPointGood(true);
725                }
726                else {
727                    s.setLastPointGood(false);
728                }
729                if (item == dataset.getItemCount(series) - 1) {
730                    if (s.seriesIndex == series) {
731                        // draw path
732                        g2.setStroke(lookupSeriesStroke(series));
733                        g2.setPaint(lookupSeriesPaint(series));
734                        g2.draw(s.seriesPath);
735                    }
736                }
737            }
738
739            else if (item != 0 && itemVisible) {
740                // get the previous data point...
741                double x0 = dataset.getXValue(series, item - 1);
742                double y0 = dataset.getYValue(series, item - 1);
743                if (!Double.isNaN(x0) && !Double.isNaN(y0)) {
744                    boolean drawLine = true;
745                    if (getPlotDiscontinuous()) {
746                        // only draw a line if the gap between the current and
747                        // previous data point is within the threshold
748                        int numX = dataset.getItemCount(series);
749                        double minX = dataset.getXValue(series, 0);
750                        double maxX = dataset.getXValue(series, numX - 1);
751                        if (this.gapThresholdType == UnitType.ABSOLUTE) {
752                            drawLine = Math.abs(x1 - x0) <= this.gapThreshold;
753                        }
754                        else {
755                            drawLine = Math.abs(x1 - x0) <= ((maxX - minX)
756                                / numX * getGapThreshold());
757                        }
758                    }
759                    if (drawLine) {
760                        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
761                                xAxisLocation);
762                        double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
763                                yAxisLocation);
764
765                        // only draw if we have good values
766                        if (Double.isNaN(transX0) || Double.isNaN(transY0)
767                            || Double.isNaN(transX1) || Double.isNaN(transY1)) {
768                            return;
769                        }
770
771                        if (orientation == PlotOrientation.HORIZONTAL) {
772                            state.workingLine.setLine(transY0, transX0,
773                                    transY1, transX1);
774                        }
775                        else if (orientation == PlotOrientation.VERTICAL) {
776                            state.workingLine.setLine(transX0, transY0,
777                                    transX1, transY1);
778                        }
779
780                        if (state.workingLine.intersects(dataArea)) {
781                            g2.draw(state.workingLine);
782                        }
783                    }
784                }
785            }
786        }
787
788        // we needed to get this far even for invisible items, to ensure that
789        // seriesPath updates happened, but now there is nothing more we need
790        // to do for non-visible items...
791        if (!itemVisible) {
792            return;
793        }
794
795        if (getBaseShapesVisible()) {
796
797            Shape shape = getItemShape(series, item);
798            if (orientation == PlotOrientation.HORIZONTAL) {
799                shape = ShapeUtils.createTranslatedShape(shape, transY1,
800                        transX1);
801            }
802            else if (orientation == PlotOrientation.VERTICAL) {
803                shape = ShapeUtils.createTranslatedShape(shape, transX1,
804                        transY1);
805            }
806            if (shape.intersects(dataArea)) {
807                if (getItemShapeFilled(series, item)) {
808                    g2.fill(shape);
809                }
810                else {
811                    g2.draw(shape);
812                }
813            }
814            entityArea = shape;
815
816        }
817
818        if (getPlotImages()) {
819            Image image = getImage(plot, series, item, transX1, transY1);
820            if (image != null) {
821                Point hotspot = getImageHotspot(plot, series, item, transX1,
822                        transY1, image);
823                g2.drawImage(image, (int) (transX1 - hotspot.getX()),
824                        (int) (transY1 - hotspot.getY()), null);
825                entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(),
826                        transY1 - hotspot.getY(), image.getWidth(null),
827                        image.getHeight(null));
828            }
829
830        }
831
832        double xx = transX1;
833        double yy = transY1;
834        if (orientation == PlotOrientation.HORIZONTAL) {
835            xx = transY1;
836            yy = transX1;
837        }
838
839        // draw the item label if there is one...
840        if (isItemLabelVisible(series, item)) {
841            drawItemLabel(g2, orientation, dataset, series, item, xx, yy,
842                    (y1 < 0.0));
843        }
844
845        int datasetIndex = plot.indexOf(dataset);
846        updateCrosshairValues(crosshairState, x1, y1, datasetIndex,
847                transX1, transY1, orientation);
848
849        // add an entity for the item...
850        if (entities != null && ShapeUtils.isPointInRect(dataArea, xx, yy)) {
851            addEntity(entities, entityArea, dataset, series, item, xx, yy);
852        }
853
854    }
855
856    /**
857     * Tests this renderer for equality with another object.
858     *
859     * @param obj  the object ({@code null} permitted).
860     *
861     * @return A boolean.
862     */
863    @Override
864    public boolean equals(Object obj) {
865
866        if (obj == this) {
867            return true;
868        }
869        if (!(obj instanceof StandardXYItemRenderer)) {
870            return false;
871        }
872        StandardXYItemRenderer that = (StandardXYItemRenderer) obj;
873        if (this.baseShapesVisible != that.baseShapesVisible) {
874            return false;
875        }
876        if (this.plotLines != that.plotLines) {
877            return false;
878        }
879        if (this.plotImages != that.plotImages) {
880            return false;
881        }
882        if (this.plotDiscontinuous != that.plotDiscontinuous) {
883            return false;
884        }
885        if (this.gapThresholdType != that.gapThresholdType) {
886            return false;
887        }
888        if (this.gapThreshold != that.gapThreshold) {
889            return false;
890        }
891        if (!this.seriesShapesFilledMap.equals(that.seriesShapesFilledMap)) {
892            return false;
893        }
894        if (this.baseShapesFilled != that.baseShapesFilled) {
895            return false;
896        }
897        if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) {
898            return false;
899        }
900        if (!ShapeUtils.equal(this.legendLine, that.legendLine)) {
901            return false;
902        }
903        return super.equals(obj);
904
905    }
906
907    /**
908     * Returns a clone of the renderer.
909     *
910     * @return A clone.
911     *
912     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
913     */
914    @Override
915    public Object clone() throws CloneNotSupportedException {
916        StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone();
917        clone.seriesShapesFilledMap = new HashMap<>(this.seriesShapesFilledMap);
918        clone.legendLine = CloneUtils.clone(this.legendLine);
919        return clone;
920    }
921
922    ////////////////////////////////////////////////////////////////////////////
923    // PROTECTED METHODS
924    // These provide the opportunity to subclass the standard renderer and
925    // create custom effects.
926    ////////////////////////////////////////////////////////////////////////////
927
928    /**
929     * Returns the image used to draw a single data item.
930     *
931     * @param plot  the plot (can be used to obtain standard color information
932     *              etc).
933     * @param series  the series index.
934     * @param item  the item index.
935     * @param x  the x value of the item.
936     * @param y  the y value of the item.
937     *
938     * @return The image.
939     *
940     * @see #getPlotImages()
941     */
942    protected Image getImage(Plot plot, int series, int item,
943                             double x, double y) {
944        // this method must be overridden if you want to display images
945        return null;
946    }
947
948    /**
949     * Returns the hotspot of the image used to draw a single data item.
950     * The hotspot is the point relative to the top left of the image
951     * that should indicate the data item. The default is the center of the
952     * image.
953     *
954     * @param plot  the plot (can be used to obtain standard color information
955     *              etc).
956     * @param image  the image (can be used to get size information about the
957     *               image)
958     * @param series  the series index
959     * @param item  the item index
960     * @param x  the x value of the item
961     * @param y  the y value of the item
962     *
963     * @return The hotspot used to draw the data item.
964     */
965    protected Point getImageHotspot(Plot plot, int series, int item,
966                                    double x, double y, Image image) {
967
968        int height = image.getHeight(null);
969        int width = image.getWidth(null);
970        return new Point(width / 2, height / 2);
971
972    }
973
974    /**
975     * Provides serialization support.
976     *
977     * @param stream  the input stream.
978     *
979     * @throws IOException  if there is an I/O error.
980     * @throws ClassNotFoundException  if there is a classpath problem.
981     */
982    private void readObject(ObjectInputStream stream)
983            throws IOException, ClassNotFoundException {
984        stream.defaultReadObject();
985        this.legendLine = SerialUtils.readShape(stream);
986    }
987
988    /**
989     * Provides serialization support.
990     *
991     * @param stream  the output stream.
992     *
993     * @throws IOException  if there is an I/O error.
994     */
995    private void writeObject(ObjectOutputStream stream) throws IOException {
996        stream.defaultWriteObject();
997        SerialUtils.writeShape(this.legendLine, stream);
998    }
999
1000}