001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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, 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
042package org.jfree.chart.renderer.category;
043
044import java.awt.Graphics2D;
045import java.awt.Paint;
046import java.awt.Shape;
047import java.awt.Stroke;
048import java.awt.geom.Line2D;
049import java.awt.geom.Rectangle2D;
050import java.io.IOException;
051import java.io.ObjectInputStream;
052import java.io.ObjectOutputStream;
053import java.io.Serializable;
054import java.util.List;
055
056import org.jfree.chart.LegendItem;
057import org.jfree.chart.axis.CategoryAxis;
058import org.jfree.chart.axis.ValueAxis;
059import org.jfree.chart.event.RendererChangeEvent;
060import org.jfree.chart.plot.CategoryPlot;
061import org.jfree.chart.plot.PlotOrientation;
062import org.jfree.data.category.CategoryDataset;
063import org.jfree.data.statistics.MultiValueCategoryDataset;
064import org.jfree.util.BooleanList;
065import org.jfree.util.BooleanUtilities;
066import org.jfree.util.ObjectUtilities;
067import org.jfree.util.PublicCloneable;
068import 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 */
077public 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        notifyListeners(new RendererChangeEvent(this));
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        notifyListeners(new RendererChangeEvent(this));
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        notifyListeners(new RendererChangeEvent(this));
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.
236     *
237     * @param use the flag.
238     * 
239     * @see #getUseOutlinePaint()
240     */
241    public void setUseOutlinePaint(boolean use) {
242        this.useOutlinePaint = use;
243        notifyListeners(new RendererChangeEvent(this));
244    }
245
246    // SHAPES FILLED
247
248    /**
249     * Returns the flag used to control whether or not the shape for an item
250     * is filled. The default implementation passes control to the
251     * <code>getSeriesShapesFilled</code> method. You can override this method
252     * if you require different behaviour.
253     *
254     * @param series the series index (zero-based).
255     * @param item   the item index (zero-based).
256     * @return A boolean.
257     */
258    public boolean getItemShapeFilled(int series, int item) {
259        return getSeriesShapesFilled(series);
260    }
261
262    /**
263     * Returns the flag used to control whether or not the shapes for a series
264     * are filled.
265     *
266     * @param series the series index (zero-based).
267     * @return A boolean.
268     */
269    public boolean getSeriesShapesFilled(int series) {
270        Boolean flag = this.seriesShapesFilled.getBoolean(series);
271        if (flag != null) {
272            return flag.booleanValue();
273        } 
274        else {
275            return this.baseShapesFilled;
276        }
277
278    }
279
280    /**
281     * Sets the 'shapes filled' flag for a series.
282     *
283     * @param series the series index (zero-based).
284     * @param filled the flag.
285     */
286    public void setSeriesShapesFilled(int series, Boolean filled) {
287        this.seriesShapesFilled.setBoolean(series, filled);
288        notifyListeners(new RendererChangeEvent(this));
289    }
290
291    /**
292     * Sets the 'shapes filled' flag for a series.
293     *
294     * @param series the series index (zero-based).
295     * @param filled the flag.
296     */
297    public void setSeriesShapesFilled(int series, boolean filled) {
298        this.seriesShapesFilled.setBoolean(series, 
299                BooleanUtilities.valueOf(filled));
300        notifyListeners(new RendererChangeEvent(this));
301    }
302
303    /**
304     * Returns the base 'shape filled' attribute.
305     *
306     * @return The base flag.
307     */
308    public boolean getBaseShapesFilled() {
309        return this.baseShapesFilled;
310    }
311
312    /**
313     * Sets the base 'shapes filled' flag.
314     *
315     * @param flag the flag.
316     */
317    public void setBaseShapesFilled(boolean flag) {
318        this.baseShapesFilled = flag;
319        notifyListeners(new RendererChangeEvent(this));
320    }
321
322    /**
323     * Returns <code>true</code> if the renderer should use the fill paint
324     * setting to fill shapes, and <code>false</code> if it should just
325     * use the regular paint.
326     *
327     * @return A boolean.
328     */
329    public boolean getUseFillPaint() {
330        return this.useFillPaint;
331    }
332
333    /**
334     * Sets the flag that controls whether the fill paint is used to fill
335     * shapes, and sends a {@link RendererChangeEvent} to all
336     * registered listeners.
337     *
338     * @param flag the flag.
339     */
340    public void setUseFillPaint(boolean flag) {
341        this.useFillPaint = flag;
342        notifyListeners(new RendererChangeEvent(this));
343    }
344    
345    /**
346     * Draw a single data item.
347     *
348     * @param g2  the graphics device.
349     * @param state  the renderer state.
350     * @param dataArea  the area in which the data is drawn.
351     * @param plot  the plot.
352     * @param domainAxis  the domain axis.
353     * @param rangeAxis  the range axis.
354     * @param dataset  the dataset.
355     * @param row  the row index (zero-based).
356     * @param column  the column index (zero-based).
357     * @param pass  the pass index.
358     */
359    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
360            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
361            ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
362            int pass) {
363
364        // do nothing if item is not visible
365        if (!getItemVisible(row, column)) {
366            return;   
367        }
368
369        PlotOrientation orientation = plot.getOrientation();
370
371        MultiValueCategoryDataset d = (MultiValueCategoryDataset) dataset;
372        List values = d.getValues(row, column);
373        if (values == null) {
374            return;
375        }
376        int valueCount = values.size();
377        for (int i = 0; i < valueCount; i++) {
378            // current data point...
379            double x1;
380            if (this.useSeriesOffset) {
381                x1 = domainAxis.getCategorySeriesMiddle(dataset.getColumnKey(
382                        column), dataset.getRowKey(row), dataset, 
383                        this.itemMargin, dataArea, plot.getDomainAxisEdge());
384            }
385            else {
386                x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
387                        dataArea, plot.getDomainAxisEdge());
388            }
389            Number n = (Number) values.get(i);
390            double value = n.doubleValue();
391            double y1 = rangeAxis.valueToJava2D(value, dataArea, 
392                    plot.getRangeAxisEdge());
393
394            Shape shape = getItemShape(row, column);
395            if (orientation == PlotOrientation.HORIZONTAL) {
396                shape = ShapeUtilities.createTranslatedShape(shape, y1, x1);
397            }
398            else if (orientation == PlotOrientation.VERTICAL) {
399                shape = ShapeUtilities.createTranslatedShape(shape, x1, y1);
400            }
401            if (getItemShapeFilled(row, column)) {
402                if (this.useFillPaint) {
403                    g2.setPaint(getItemFillPaint(row, column));
404                }
405                else {
406                    g2.setPaint(getItemPaint(row, column));   
407                }
408                g2.fill(shape);
409            }
410            if (this.drawOutlines) {
411                if (this.useOutlinePaint) {
412                    g2.setPaint(getItemOutlinePaint(row, column));   
413                }
414                else {
415                    g2.setPaint(getItemPaint(row, column));
416                }
417                g2.setStroke(getItemOutlineStroke(row, column));
418                g2.draw(shape);
419            }
420        }
421
422    }
423    
424    /**
425     * Returns a legend item for a series.
426     *
427     * @param datasetIndex  the dataset index (zero-based).
428     * @param series  the series index (zero-based).
429     *
430     * @return The legend item.
431     */
432    public LegendItem getLegendItem(int datasetIndex, int series) {
433
434        CategoryPlot cp = getPlot();
435        if (cp == null) {
436            return null;
437        }
438
439        if (isSeriesVisible(series) && isSeriesVisibleInLegend(series)) {
440            CategoryDataset dataset = cp.getDataset(datasetIndex);
441            String label = getLegendItemLabelGenerator().generateLabel(
442                    dataset, series);
443            String description = label;
444            String toolTipText = null; 
445            if (getLegendItemToolTipGenerator() != null) {
446                toolTipText = getLegendItemToolTipGenerator().generateLabel(
447                        dataset, series);   
448            }
449            String urlText = null;
450            if (getLegendItemURLGenerator() != null) {
451                urlText = getLegendItemURLGenerator().generateLabel(
452                        dataset, series);   
453            }
454            Shape shape = lookupSeriesShape(series);
455            Paint paint = lookupSeriesPaint(series);
456            Paint fillPaint = (this.useFillPaint 
457                    ? getItemFillPaint(series, 0) : paint);
458            boolean shapeOutlineVisible = this.drawOutlines;
459            Paint outlinePaint = (this.useOutlinePaint 
460                    ? getItemOutlinePaint(series, 0) : paint);
461            Stroke outlineStroke = lookupSeriesOutlineStroke(series);
462            LegendItem result = new LegendItem(label, description, toolTipText, 
463                    urlText, true, shape, getItemShapeFilled(series, 0),
464                    fillPaint, shapeOutlineVisible, outlinePaint, outlineStroke,
465                    false, new Line2D.Double(-7.0, 0.0, 7.0, 0.0),
466                    getItemStroke(series, 0), getItemPaint(series, 0));
467            result.setDataset(dataset);
468            result.setDatasetIndex(datasetIndex);
469            result.setSeriesKey(dataset.getRowKey(series));
470            result.setSeriesIndex(series);
471            return result;
472        }
473        return null;
474
475    }
476
477    /**
478     * Tests this renderer for equality with an arbitrary object.
479     *
480     * @param obj the object (<code>null</code> permitted).
481     * @return A boolean.
482     */
483    public boolean equals(Object obj) {
484        if (obj == this) {
485            return true;
486        }
487        if (!(obj instanceof ScatterRenderer)) {
488            return false;
489        }
490        ScatterRenderer that = (ScatterRenderer) obj;
491        if (!ObjectUtilities.equal(this.seriesShapesFilled,
492                that.seriesShapesFilled)) {
493            return false;
494        }
495        if (this.baseShapesFilled != that.baseShapesFilled) {
496            return false;
497        }
498        if (this.useFillPaint != that.useFillPaint) {
499            return false;
500        }
501        if (this.drawOutlines != that.drawOutlines) {
502            return false;
503        }
504        if (this.useOutlinePaint != that.useOutlinePaint) {
505            return false;
506        }
507        if (this.useSeriesOffset != that.useSeriesOffset) {
508            return false;
509        }
510        if (this.itemMargin != that.itemMargin) {
511            return false;
512        }
513        return super.equals(obj);
514    }
515
516    /**
517     * Returns an independent copy of the renderer.
518     * 
519     * @return A clone.
520     * 
521     * @throws CloneNotSupportedException  should not happen.
522     */
523    public Object clone() throws CloneNotSupportedException {
524        ScatterRenderer clone = (ScatterRenderer) super.clone();
525        clone.seriesShapesFilled 
526                = (BooleanList) this.seriesShapesFilled.clone();
527        return clone;
528    }
529
530    /**
531     * Provides serialization support.
532     *
533     * @param stream the output stream.
534     * @throws java.io.IOException if there is an I/O error.
535     */
536    private void writeObject(ObjectOutputStream stream) throws IOException {
537        stream.defaultWriteObject();
538
539    }
540
541    /**
542     * Provides serialization support.
543     *
544     * @param stream the input stream.
545     * @throws java.io.IOException    if there is an I/O error.
546     * @throws ClassNotFoundException if there is a classpath problem.
547     */
548    private void readObject(ObjectInputStream stream)
549            throws IOException, ClassNotFoundException {
550        stream.defaultReadObject();
551
552    }
553
554}