001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it
010     * under the terms of the GNU Lesser General Public License as published by
011     * the Free Software Foundation; either version 2.1 of the License, or
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022     * USA.
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * --------------------
028     * ScatterRenderer.java
029     * --------------------
030     * (C) Copyright 2007, 2008, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   David Forslund;
034     *
035     * Changes
036     * -------
037     * 08-Oct-2007 : Version 1, based on patch 1780779 by David Forslund (DG);
038     * 11-Oct-2007 : Renamed ScatterRenderer (DG);
039     * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG);
040     *
041     */
042    
043    package org.jfree.chart.renderer.category;
044    
045    import java.awt.Graphics2D;
046    import java.awt.Paint;
047    import java.awt.Shape;
048    import java.awt.Stroke;
049    import java.awt.geom.Line2D;
050    import java.awt.geom.Rectangle2D;
051    import java.io.IOException;
052    import java.io.ObjectInputStream;
053    import java.io.ObjectOutputStream;
054    import java.io.Serializable;
055    import java.util.List;
056    
057    import org.jfree.chart.LegendItem;
058    import org.jfree.chart.axis.CategoryAxis;
059    import org.jfree.chart.axis.ValueAxis;
060    import org.jfree.chart.event.RendererChangeEvent;
061    import org.jfree.chart.plot.CategoryPlot;
062    import org.jfree.chart.plot.PlotOrientation;
063    import org.jfree.data.category.CategoryDataset;
064    import org.jfree.data.statistics.MultiValueCategoryDataset;
065    import org.jfree.util.BooleanList;
066    import org.jfree.util.BooleanUtilities;
067    import org.jfree.util.ObjectUtilities;
068    import org.jfree.util.PublicCloneable;
069    import org.jfree.util.ShapeUtilities;
070    
071    /**
072     * A renderer that handles the multiple values from a
073     * {@link MultiValueCategoryDataset} by plotting a shape for each value for
074     * each given item in the dataset.
075     *
076     * @since 1.0.7
077     */
078    public class ScatterRenderer extends AbstractCategoryItemRenderer
079            implements Cloneable, PublicCloneable, Serializable {
080    
081        /**
082         * A table of flags that control (per series) whether or not shapes are
083         * filled.
084         */
085        private BooleanList seriesShapesFilled;
086    
087        /**
088         * The default value returned by the getShapeFilled() method.
089         */
090        private boolean baseShapesFilled;
091    
092        /**
093         * A flag that controls whether the fill paint is used for filling
094         * shapes.
095         */
096        private boolean useFillPaint;
097    
098        /**
099         * A flag that controls whether outlines are drawn for shapes.
100         */
101        private boolean drawOutlines;
102    
103        /**
104         * A flag that controls whether the outline paint is used for drawing shape
105         * outlines - if not, the regular series paint is used.
106         */
107        private boolean useOutlinePaint;
108    
109        /**
110         * A flag that controls whether or not the x-position for each item is
111         * offset within the category according to the series.
112         */
113        private boolean useSeriesOffset;
114    
115        /**
116         * The item margin used for series offsetting - this allows the positioning
117         * to match the bar positions of the {@link BarRenderer} class.
118         */
119        private double itemMargin;
120    
121        /**
122         * Constructs a new renderer.
123         */
124        public ScatterRenderer() {
125            this.seriesShapesFilled = new BooleanList();
126            this.baseShapesFilled = true;
127            this.useFillPaint = false;
128            this.drawOutlines = false;
129            this.useOutlinePaint = false;
130            this.useSeriesOffset = true;
131            this.itemMargin = 0.20;
132        }
133    
134        /**
135         * Returns the flag that controls whether or not the x-position for each
136         * data item is offset within the category according to the series.
137         *
138         * @return A boolean.
139         *
140         * @see #setUseSeriesOffset(boolean)
141         */
142        public boolean getUseSeriesOffset() {
143            return this.useSeriesOffset;
144        }
145    
146        /**
147         * Sets the flag that controls whether or not the x-position for each
148         * data item is offset within its category according to the series, and
149         * sends a {@link RendererChangeEvent} to all registered listeners.
150         *
151         * @param offset  the offset.
152         *
153         * @see #getUseSeriesOffset()
154         */
155        public void setUseSeriesOffset(boolean offset) {
156            this.useSeriesOffset = offset;
157            fireChangeEvent();
158        }
159    
160        /**
161         * Returns the item margin, which is the gap between items within a
162         * category (expressed as a percentage of the overall category width).
163         * This can be used to match the offset alignment with the bars drawn by
164         * a {@link BarRenderer}).
165         *
166         * @return The item margin.
167         *
168         * @see #setItemMargin(double)
169         * @see #getUseSeriesOffset()
170         */
171        public double getItemMargin() {
172            return this.itemMargin;
173        }
174    
175        /**
176         * Sets the item margin, which is the gap between items within a category
177         * (expressed as a percentage of the overall category width), and sends
178         * a {@link RendererChangeEvent} to all registered listeners.
179         *
180         * @param margin  the margin (0.0 <= margin < 1.0).
181         *
182         * @see #getItemMargin()
183         * @see #getUseSeriesOffset()
184         */
185        public void setItemMargin(double margin) {
186            if (margin < 0.0 || margin >= 1.0) {
187                throw new IllegalArgumentException("Requires 0.0 <= margin < 1.0.");
188            }
189            this.itemMargin = margin;
190            fireChangeEvent();
191        }
192    
193        /**
194         * Returns <code>true</code> if outlines should be drawn for shapes, and
195         * <code>false</code> otherwise.
196         *
197         * @return A boolean.
198         *
199         * @see #setDrawOutlines(boolean)
200         */
201        public boolean getDrawOutlines() {
202            return this.drawOutlines;
203        }
204    
205        /**
206         * Sets the flag that controls whether outlines are drawn for
207         * shapes, and sends a {@link RendererChangeEvent} to all registered
208         * listeners.
209         * <p/>
210         * In some cases, shapes look better if they do NOT have an outline, but
211         * this flag allows you to set your own preference.
212         *
213         * @param flag the flag.
214         *
215         * @see #getDrawOutlines()
216         */
217        public void setDrawOutlines(boolean flag) {
218            this.drawOutlines = flag;
219            fireChangeEvent();
220        }
221    
222        /**
223         * Returns the flag that controls whether the outline paint is used for
224         * shape outlines.  If not, the regular series paint is used.
225         *
226         * @return A boolean.
227         *
228         * @see #setUseOutlinePaint(boolean)
229         */
230        public boolean getUseOutlinePaint() {
231            return this.useOutlinePaint;
232        }
233    
234        /**
235         * Sets the flag that controls whether the outline paint is used for shape
236         * outlines, and sends a {@link RendererChangeEvent} to all registered
237         * listeners.
238         *
239         * @param use the flag.
240         *
241         * @see #getUseOutlinePaint()
242         */
243        public void setUseOutlinePaint(boolean use) {
244            this.useOutlinePaint = use;
245            fireChangeEvent();
246        }
247    
248        // SHAPES FILLED
249    
250        /**
251         * Returns the flag used to control whether or not the shape for an item
252         * is filled. The default implementation passes control to the
253         * <code>getSeriesShapesFilled</code> 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         * @return A boolean.
259         */
260        public boolean getItemShapeFilled(int series, int item) {
261            return getSeriesShapesFilled(series);
262        }
263    
264        /**
265         * Returns the flag used to control whether or not the shapes for a series
266         * are filled.
267         *
268         * @param series the series index (zero-based).
269         * @return A boolean.
270         */
271        public boolean getSeriesShapesFilled(int series) {
272            Boolean flag = this.seriesShapesFilled.getBoolean(series);
273            if (flag != null) {
274                return flag.booleanValue();
275            }
276            else {
277                return this.baseShapesFilled;
278            }
279    
280        }
281    
282        /**
283         * Sets the 'shapes filled' flag for a series and sends a
284         * {@link RendererChangeEvent} to all registered listeners.
285         *
286         * @param series the series index (zero-based).
287         * @param filled the flag.
288         */
289        public void setSeriesShapesFilled(int series, Boolean filled) {
290            this.seriesShapesFilled.setBoolean(series, filled);
291            fireChangeEvent();
292        }
293    
294        /**
295         * Sets the 'shapes filled' flag for a series and sends a
296         * {@link RendererChangeEvent} to all registered listeners.
297         *
298         * @param series the series index (zero-based).
299         * @param filled the flag.
300         */
301        public void setSeriesShapesFilled(int series, boolean filled) {
302            this.seriesShapesFilled.setBoolean(series,
303                    BooleanUtilities.valueOf(filled));
304            fireChangeEvent();
305        }
306    
307        /**
308         * Returns the base 'shape filled' attribute.
309         *
310         * @return The base flag.
311         */
312        public boolean getBaseShapesFilled() {
313            return this.baseShapesFilled;
314        }
315    
316        /**
317         * Sets the base 'shapes filled' flag and sends a
318         * {@link RendererChangeEvent} to all registered listeners.
319         *
320         * @param flag the flag.
321         */
322        public void setBaseShapesFilled(boolean flag) {
323            this.baseShapesFilled = flag;
324            fireChangeEvent();
325        }
326    
327        /**
328         * Returns <code>true</code> if the renderer should use the fill paint
329         * setting to fill shapes, and <code>false</code> if it should just
330         * use the regular paint.
331         *
332         * @return A boolean.
333         */
334        public boolean getUseFillPaint() {
335            return this.useFillPaint;
336        }
337    
338        /**
339         * Sets the flag that controls whether the fill paint is used to fill
340         * shapes, and sends a {@link RendererChangeEvent} to all
341         * registered listeners.
342         *
343         * @param flag the flag.
344         */
345        public void setUseFillPaint(boolean flag) {
346            this.useFillPaint = flag;
347            fireChangeEvent();
348        }
349    
350        /**
351         * Draw a single data item.
352         *
353         * @param g2  the graphics device.
354         * @param state  the renderer state.
355         * @param dataArea  the area in which the data is drawn.
356         * @param plot  the plot.
357         * @param domainAxis  the domain axis.
358         * @param rangeAxis  the range axis.
359         * @param dataset  the dataset.
360         * @param row  the row index (zero-based).
361         * @param column  the column index (zero-based).
362         * @param pass  the pass index.
363         */
364        public void drawItem(Graphics2D g2, CategoryItemRendererState state,
365                Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
366                ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
367                int pass) {
368    
369            // do nothing if item is not visible
370            if (!getItemVisible(row, column)) {
371                return;
372            }
373    
374            PlotOrientation orientation = plot.getOrientation();
375    
376            MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset;
377            List values = d.getValues(row, column);
378            if (values == null) {
379                return;
380            }
381            int valueCount = values.size();
382            for (int i = 0; i < valueCount; i++) {
383                // current data point...
384                double x1;
385                if (this.useSeriesOffset) {
386                    x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey(
387                            column), dataset.getRowKey(row), dataset,
388                            this.itemMargin, dataArea, plot.getDomainAxisEdge());
389                }
390                else {
391                    x1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
392                            dataArea, plot.getDomainAxisEdge());
393                }
394                Number n = (Number) values.get(i);
395                double value = n.doubleValue();
396                double y1 = rangeAxis.valueToJava2D(value, dataArea,
397                        plot.getRangeAxisEdge());
398    
399                Shape shape = getItemShape(row, column);
400                if (orientation == PlotOrientation.HORIZONTAL) {
401                    shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
402                }
403                else if (orientation == PlotOrientation.VERTICAL) {
404                    shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
405                }
406                if (getItemShapeFilled(row, column)) {
407                    if (this.useFillPaint) {
408                        g2.setPaint(getItemFillPaint(row, column));
409                    }
410                    else {
411                        g2.setPaint(getItemPaint(row, column));
412                    }
413                    g2.fill(shape);
414                }
415                if (this.drawOutlines) {
416                    if (this.useOutlinePaint) {
417                        g2.setPaint(getItemOutlinePaint(row, column));
418                    }
419                    else {
420                        g2.setPaint(getItemPaint(row, column));
421                    }
422                    g2.setStroke(getItemOutlineStroke(row, column));
423                    g2.draw(shape);
424                }
425            }
426    
427        }
428    
429        /**
430         * Returns a legend item for a series.
431         *
432         * @param datasetIndex  the dataset index (zero-based).
433         * @param series  the series index (zero-based).
434         *
435         * @return The legend item.
436         */
437        public LegendItem getLegendItem(int datasetIndex, int series) {
438    
439            CategoryPlot cp = getPlot();
440            if (cp == null) {
441                return null;
442            }
443    
444            if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) {
445                CategoryDataset dataset = cp.getDataset(datasetIndex);
446                String label = getLegendItemLabelGenerator().generateLabel(
447                        dataset, series);
448                String description = label;
449                String toolTipText = null;
450                if (getLegendItemToolTipGenerator() != null) {
451                    toolTipText = getLegendItemToolTipGenerator().generateLabel(
452                            dataset, series);
453                }
454                String urlText = null;
455                if (getLegendItemURLGenerator() != null) {
456                    urlText = getLegendItemURLGenerator().generateLabel(
457                            dataset, series);
458                }
459                Shape shape = lookupLegendShape(series);
460                Paint paint = lookupSeriesPaint(series);
461                Paint fillPaint = (this.useFillPaint
462                        ? getItemFillPaint(series, 0) : paint);
463                boolean shapeOutlineVisible = this.drawOutlines;
464                Paint outlinePaint = (this.useOutlinePaint
465                        ? getItemOutlinePaint(series, 0) : paint);
466                Stroke outlineStroke = lookupSeriesOutlineStroke(series);
467                LegendItem result = new LegendItem(label, description, toolTipText,
468                        urlText, true, shape, getItemShapeFilled(series, 0),
469                        fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke,
470                        false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0),
471                        getItemStroke(series, 0), getItemPaint(series, 0));
472                result.setLabelFont(lookupLegendTextFont(series));
473                Paint labelPaint = lookupLegendTextPaint(series);
474                if (labelPaint != null) {
475                    result.setLabelPaint(labelPaint);
476                }
477                result.setDataset(dataset);
478                result.setDatasetIndex(datasetIndex);
479                result.setSeriesKey(dataset.getRowKey(series));
480                result.setSeriesIndex(series);
481                return result;
482            }
483            return null;
484    
485        }
486    
487        /**
488         * Tests this renderer for equality with an arbitrary object.
489         *
490         * @param obj the object (<code>null</code> permitted).
491         * @return A boolean.
492         */
493        public boolean equals(Object obj) {
494            if (obj == this) {
495                return true;
496            }
497            if (!(obj instanceof ScatterRenderer)) {
498                return false;
499            }
500            ScatterRenderer that = (ScatterRenderer) obj;
501            if (!ObjectUtilities.equal(this.seriesShapesFilled,
502                    that.seriesShapesFilled)) {
503                return false;
504            }
505            if (this.baseShapesFilled != that.baseShapesFilled) {
506                return false;
507            }
508            if (this.useFillPaint != that.useFillPaint) {
509                return false;
510            }
511            if (this.drawOutlines != that.drawOutlines) {
512                return false;
513            }
514            if (this.useOutlinePaint != that.useOutlinePaint) {
515                return false;
516            }
517            if (this.useSeriesOffset != that.useSeriesOffset) {
518                return false;
519            }
520            if (this.itemMargin != that.itemMargin) {
521                return false;
522            }
523            return super.equals(obj);
524        }
525    
526        /**
527         * Returns an independent copy of the renderer.
528         *
529         * @return A clone.
530         *
531         * @throws CloneNotSupportedException  should not happen.
532         */
533        public Object clone() throws CloneNotSupportedException {
534            ScatterRenderer clone = (ScatterRenderer) super.clone();
535            clone.seriesShapesFilled
536                    = (BooleanList) this.seriesShapesFilled.clone();
537            return clone;
538        }
539    
540        /**
541         * Provides serialization support.
542         *
543         * @param stream the output stream.
544         * @throws java.io.IOException if there is an I/O error.
545         */
546        private void writeObject(ObjectOutputStream stream) throws IOException {
547            stream.defaultWriteObject();
548    
549        }
550    
551        /**
552         * Provides serialization support.
553         *
554         * @param stream the input stream.
555         * @throws java.io.IOException    if there is an I/O error.
556         * @throws ClassNotFoundException if there is a classpath problem.
557         */
558        private void readObject(ObjectInputStream stream)
559                throws IOException, ClassNotFoundException {
560            stream.defaultReadObject();
561    
562        }
563    
564    }