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 }