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