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 * XYAreaRenderer2.java
029 * --------------------
030 * (C) Copyright 2004-2021, by Hari and Contributors.
031 *
032 * Original Author:  Hari (ourhari@hotmail.com);
033 * Contributor(s):   David Gilbert;
034 *                   Richard Atkinson;
035 *                   Christian W. Zuckschwerdt;
036 *                   Martin Krauskopf;
037 *                   Ulrich Voigt (patch #312);
038 */
039
040package org.jfree.chart.renderer.xy;
041
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.Shape;
045import java.awt.Stroke;
046import java.awt.geom.Area;
047import java.awt.geom.GeneralPath;
048import java.awt.geom.Rectangle2D;
049import java.io.IOException;
050import java.io.ObjectInputStream;
051import java.io.ObjectOutputStream;
052
053import org.jfree.chart.legend.LegendItem;
054import org.jfree.chart.axis.ValueAxis;
055import org.jfree.chart.entity.EntityCollection;
056import org.jfree.chart.entity.XYItemEntity;
057import org.jfree.chart.event.RendererChangeEvent;
058import org.jfree.chart.labels.XYSeriesLabelGenerator;
059import org.jfree.chart.labels.XYToolTipGenerator;
060import org.jfree.chart.plot.CrosshairState;
061import org.jfree.chart.plot.PlotOrientation;
062import org.jfree.chart.plot.PlotRenderingInfo;
063import org.jfree.chart.plot.XYPlot;
064import org.jfree.chart.urls.XYURLGenerator;
065import org.jfree.chart.internal.Args;
066import org.jfree.chart.api.PublicCloneable;
067import org.jfree.chart.internal.CloneUtils;
068import org.jfree.chart.internal.SerialUtils;
069import org.jfree.chart.internal.ShapeUtils;
070import org.jfree.data.xy.XYDataset;
071
072/**
073 * Area item renderer for an {@link XYPlot}. The example shown here is
074 * generated by the {@code XYAreaRenderer2Demo1.java} program included in
075 * the JFreeChart demo collection:
076 * <br><br>
077 * <img src="doc-files/XYAreaRenderer2Sample.png"
078 * alt="XYAreaRenderer2Sample.png">
079 */
080public class XYAreaRenderer2 extends AbstractXYItemRenderer
081        implements XYItemRenderer, PublicCloneable {
082
083    /** For serialization. */
084    private static final long serialVersionUID = -7378069681579984133L;
085
086    /** A flag that controls whether or not the outline is shown. */
087    private boolean showOutline;
088
089    /**
090     * The shape used to represent an area in each legend item (this should
091     * never be {@code null}).
092     */
093    private transient Shape legendArea;
094
095    /**
096     * Constructs a new renderer.
097     */
098    public XYAreaRenderer2() {
099        this(null, null);
100    }
101
102    /**
103     * Constructs a new renderer.
104     *
105     * @param labelGenerator  the tool tip generator to use ({@code null} 
106     *     permitted).
107     * @param urlGenerator  the URL generator ({@code null} permitted).
108     */
109    public XYAreaRenderer2(XYToolTipGenerator labelGenerator,
110                           XYURLGenerator urlGenerator) {
111        super();
112        this.showOutline = false;
113        setDefaultToolTipGenerator(labelGenerator);
114        setURLGenerator(urlGenerator);
115        GeneralPath area = new GeneralPath();
116        area.moveTo(0.0f, -4.0f);
117        area.lineTo(3.0f, -2.0f);
118        area.lineTo(4.0f, 4.0f);
119        area.lineTo(-4.0f, 4.0f);
120        area.lineTo(-3.0f, -2.0f);
121        area.closePath();
122        this.legendArea = area;
123    }
124
125    /**
126     * Returns a flag that controls whether or not outlines of the areas are
127     * drawn.
128     *
129     * @return The flag.
130     *
131     * @see #setOutline(boolean)
132     */
133    public boolean isOutline() {
134        return this.showOutline;
135    }
136
137    /**
138     * Sets a flag that controls whether or not outlines of the areas are
139     * drawn, and sends a {@link RendererChangeEvent} to all registered
140     * listeners.
141     *
142     * @param show  the flag.
143     *
144     * @see #isOutline()
145     */
146    public void setOutline(boolean show) {
147        this.showOutline = show;
148        fireChangeEvent();
149    }
150
151    /**
152     * Returns the shape used to represent an area in the legend.
153     *
154     * @return The legend area (never {@code null}).
155     *
156     * @see #setLegendArea(Shape)
157     */
158    public Shape getLegendArea() {
159        return this.legendArea;
160    }
161
162    /**
163     * Sets the shape used as an area in each legend item and sends a
164     * {@link RendererChangeEvent} to all registered listeners.
165     *
166     * @param area  the area ({@code null} not permitted).
167     *
168     * @see #getLegendArea()
169     */
170    public void setLegendArea(Shape area) {
171        Args.nullNotPermitted(area, "area");
172        this.legendArea = area;
173        fireChangeEvent();
174    }
175
176    /**
177     * Returns a default legend item for the specified series.  Subclasses
178     * should override this method to generate customised items.
179     *
180     * @param datasetIndex  the dataset index (zero-based).
181     * @param series  the series index (zero-based).
182     *
183     * @return A legend item for the series.
184     */
185    @Override
186    public LegendItem getLegendItem(int datasetIndex, int series) {
187        LegendItem result = null;
188        XYPlot xyplot = getPlot();
189        if (xyplot != null) {
190            XYDataset dataset = xyplot.getDataset(datasetIndex);
191            if (dataset != null) {
192                XYSeriesLabelGenerator lg = getLegendItemLabelGenerator();
193                String label = lg.generateLabel(dataset, series);
194                String description = label;
195                String toolTipText = null;
196                if (getLegendItemToolTipGenerator() != null) {
197                    toolTipText = getLegendItemToolTipGenerator().generateLabel(
198                            dataset, series);
199                }
200                String urlText = null;
201                if (getLegendItemURLGenerator() != null) {
202                    urlText = getLegendItemURLGenerator().generateLabel(
203                            dataset, series);
204                }
205                Paint paint = lookupSeriesPaint(series);
206                result = new LegendItem(label, description, toolTipText,
207                        urlText, this.legendArea, paint);
208                result.setLabelFont(lookupLegendTextFont(series));
209                Paint labelPaint = lookupLegendTextPaint(series);
210                if (labelPaint != null) {
211                    result.setLabelPaint(labelPaint);
212                }
213                result.setDataset(dataset);
214                result.setDatasetIndex(datasetIndex);
215                result.setSeriesKey(dataset.getSeriesKey(series));
216                result.setSeriesIndex(series);
217            }
218        }
219        return result;
220    }
221
222    /**
223     * Draws the visual representation of a single data item.
224     *
225     * @param g2  the graphics device.
226     * @param state  the renderer state.
227     * @param dataArea  the area within which the data is being drawn.
228     * @param info  collects information about the drawing.
229     * @param plot  the plot (can be used to obtain standard color
230     *              information etc).
231     * @param domainAxis  the domain axis.
232     * @param rangeAxis  the range axis.
233     * @param dataset  the dataset.
234     * @param series  the series index (zero-based).
235     * @param item  the item index (zero-based).
236     * @param crosshairState  crosshair information for the plot
237     *                        ({@code null} permitted).
238     * @param pass  the pass index.
239     */
240    @Override
241    public void drawItem(Graphics2D g2, XYItemRendererState state,
242         Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
243         ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
244         int series, int item, CrosshairState crosshairState, int pass) {
245
246        if (!getItemVisible(series, item)) {
247            return;
248        }
249        // get the data point...
250        double x1 = dataset.getXValue(series, item);
251        double y1 = dataset.getYValue(series, item);
252        if (Double.isNaN(y1)) {
253            y1 = 0.0;
254        }
255
256        double transX1 = domainAxis.valueToJava2D(x1, dataArea,
257                plot.getDomainAxisEdge());
258        double transY1 = rangeAxis.valueToJava2D(y1, dataArea,
259                plot.getRangeAxisEdge());
260
261        // get the previous point and the next point so we can calculate a
262        // "hot spot" for the area (used by the chart entity)...
263        double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
264        double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
265        if (Double.isNaN(y0)) {
266            y0 = 0.0;
267        }
268        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
269                plot.getDomainAxisEdge());
270        double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
271                plot.getRangeAxisEdge());
272
273        int itemCount = dataset.getItemCount(series);
274        double x2 = dataset.getXValue(series, Math.min(item + 1,
275                itemCount - 1));
276        double y2 = dataset.getYValue(series, Math.min(item + 1,
277                itemCount - 1));
278        if (Double.isNaN(y2)) {
279            y2 = 0.0;
280        }
281        double transX2 = domainAxis.valueToJava2D(x2, dataArea,
282                plot.getDomainAxisEdge());
283        double transY2 = rangeAxis.valueToJava2D(y2, dataArea,
284                plot.getRangeAxisEdge());
285
286        double transZero = rangeAxis.valueToJava2D(0.0, dataArea,
287                plot.getRangeAxisEdge());
288        GeneralPath hotspot = new GeneralPath();
289        if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
290            moveTo(hotspot, transZero, ((transX0 + transX1) / 2.0));
291            lineTo(hotspot, ((transY0 + transY1) / 2.0),
292                            ((transX0 + transX1) / 2.0));
293            lineTo(hotspot, transY1, transX1);
294            lineTo(hotspot, ((transY1 + transY2) / 2.0),
295                            ((transX1 + transX2) / 2.0));
296            lineTo(hotspot, transZero, ((transX1 + transX2) / 2.0));
297        }
298        else {  // vertical orientation
299            moveTo(hotspot, ((transX0 + transX1) / 2.0), transZero);
300            lineTo(hotspot, ((transX0 + transX1) / 2.0),
301                            ((transY0 + transY1) / 2.0));
302            lineTo(hotspot, transX1, transY1);
303            lineTo(hotspot, ((transX1 + transX2) / 2.0),
304                            ((transY1 + transY2) / 2.0));
305            lineTo(hotspot, ((transX1 + transX2) / 2.0), transZero);
306        }
307        hotspot.closePath();
308
309        PlotOrientation orientation = plot.getOrientation();
310        Paint paint = getItemPaint(series, item);
311        Stroke stroke = getItemStroke(series, item);
312        g2.setPaint(paint);
313        g2.setStroke(stroke);
314
315        // Check if the item is the last item for the series.
316        // and number of items > 0.  We can't draw an area for a single point.
317        g2.fill(hotspot);
318
319        // draw an outline around the Area.
320        if (isOutline()) {
321            g2.setStroke(lookupSeriesOutlineStroke(series));
322            g2.setPaint(lookupSeriesOutlinePaint(series));
323            g2.draw(hotspot);
324        }
325        int datasetIndex = plot.indexOf(dataset);
326        updateCrosshairValues(crosshairState, x1, y1, datasetIndex,
327                transX1, transY1, orientation);
328
329        // collect entity and tool tip information...
330        if (state.getInfo() != null) {
331            EntityCollection entities = state.getEntityCollection();
332            if (entities != null) {
333                // limit the entity hotspot area to the data area
334                Area dataAreaHotspot = new Area(hotspot);
335                dataAreaHotspot.intersect(new Area(dataArea));
336                if (!dataAreaHotspot.isEmpty()) {
337                    String tip = null;
338                    XYToolTipGenerator generator = getToolTipGenerator(series,
339                         item);
340                    if (generator != null) {
341                        tip = generator.generateToolTip(dataset, series, item);
342                    }
343                    String url = null;
344                    if (getURLGenerator() != null) {
345                        url = getURLGenerator().generateURL(dataset, series, 
346                                item);
347                    }
348                    XYItemEntity entity = new XYItemEntity(dataAreaHotspot, 
349                            dataset, series, item, tip, url);
350                    entities.add(entity);
351                }
352            }
353        }
354
355    }
356
357    /**
358     * Tests this renderer for equality with an arbitrary object.
359     *
360     * @param obj  the object ({@code null} not permitted).
361     *
362     * @return A boolean.
363     */
364    @Override
365    public boolean equals(Object obj) {
366        if (obj == this) {
367            return true;
368        }
369        if (!(obj instanceof XYAreaRenderer2)) {
370            return false;
371        }
372        XYAreaRenderer2 that = (XYAreaRenderer2) obj;
373        if (this.showOutline != that.showOutline) {
374            return false;
375        }
376        if (!ShapeUtils.equal(this.legendArea, that.legendArea)) {
377            return false;
378        }
379        return super.equals(obj);
380    }
381
382    /**
383     * Returns a clone of the renderer.
384     *
385     * @return A clone.
386     *
387     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
388     */
389    @Override
390    public Object clone() throws CloneNotSupportedException {
391        XYAreaRenderer2 clone = (XYAreaRenderer2) super.clone();
392        clone.legendArea = CloneUtils.clone(this.legendArea);
393        return clone;
394    }
395
396    /**
397     * Provides serialization support.
398     *
399     * @param stream  the input stream.
400     *
401     * @throws IOException  if there is an I/O error.
402     * @throws ClassNotFoundException  if there is a classpath problem.
403     */
404    private void readObject(ObjectInputStream stream)
405            throws IOException, ClassNotFoundException {
406        stream.defaultReadObject();
407        this.legendArea = SerialUtils.readShape(stream);
408    }
409
410    /**
411     * Provides serialization support.
412     *
413     * @param stream  the output stream.
414     *
415     * @throws IOException  if there is an I/O error.
416     */
417    private void writeObject(ObjectOutputStream stream) throws IOException {
418        stream.defaultWriteObject();
419        SerialUtils.writeShape(this.legendArea, stream);
420    }
421
422}
423