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 * BoxAndWhiskerRenderer.java
029 * --------------------------
030 * (C) Copyright 2003-2008, by David Browning and Contributors.
031 *
032 * Original Author: David Browning (for the Australian Institute of Marine
033 * Science);
034 * Contributor(s): David Gilbert (for Object Refinery Limited);
035 * Tim Bardzil;
036 * Rob Van der Sanden (patches 1866446 and 1888422);
037 *
038 * Changes
039 * -------
040 * 21-Aug-2003 : Version 1, contributed by David Browning (for the Australian
041 * Institute of Marine Science);
042 * 01-Sep-2003 : Incorporated outlier and farout symbols for low values
043 * also (DG);
044 * 08-Sep-2003 : Changed ValueAxis API (DG);
045 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
046 * 07-Oct-2003 : Added renderer state (DG);
047 * 12-Nov-2003 : Fixed casting bug reported by Tim Bardzil (DG);
048 * 13-Nov-2003 : Added drawHorizontalItem() method contributed by Tim
049 * Bardzil (DG);
050 * 25-Apr-2004 : Added fillBox attribute, equals() method and added
051 * serialization code (DG);
052 * 29-Apr-2004 : Changed drawing of upper and lower shadows - see bug report
053 * 944011 (DG);
054 * 05-Nov-2004 : Modified drawItem() signature (DG);
055 * 09-Mar-2005 : Override getLegendItem() method so that legend item shapes
056 * are shown as blocks (DG);
057 * 20-Apr-2005 : Generate legend labels, tooltips and URLs (DG);
058 * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
059 * ------------- JFREECHART 1.0.x ---------------------------------------------
060 * 12-Oct-2006 : Source reformatting and API doc updates (DG);
061 * 12-Oct-2006 : Fixed bug 1572478, potential NullPointerException (DG);
062 * 05-Feb-2006 : Added event notifications to a couple of methods (DG);
063 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
064 * 11-May-2007 : Added check for visibility in getLegendItem() (DG);
065 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG);
066 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
067 * 03-Jan-2008 : Check visibility of average marker before drawing it (DG);
068 * 15-Jan-2008 : Add getMaximumBarWidth() and setMaximumBarWidth()
069 * methods (RVdS);
070 * 14-Feb-2008 : Fix bar position for horizontal chart, see patch
071 * 1888422 (RVdS);
072 * 27-Mar-2008 : Boxes should use outlinePaint/Stroke settings (DG);
073 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG);
074 *
075 */
076
077 package org.jfree.chart.renderer.category;
078
079 import java.awt.Color;
080 import java.awt.Graphics2D;
081 import java.awt.Paint;
082 import java.awt.Shape;
083 import java.awt.Stroke;
084 import java.awt.geom.Ellipse2D;
085 import java.awt.geom.Line2D;
086 import java.awt.geom.Point2D;
087 import java.awt.geom.Rectangle2D;
088 import java.io.IOException;
089 import java.io.ObjectInputStream;
090 import java.io.ObjectOutputStream;
091 import java.io.Serializable;
092 import java.util.ArrayList;
093 import java.util.Collections;
094 import java.util.Iterator;
095 import java.util.List;
096
097 import org.jfree.chart.LegendItem;
098 import org.jfree.chart.axis.CategoryAxis;
099 import org.jfree.chart.axis.ValueAxis;
100 import org.jfree.chart.entity.EntityCollection;
101 import org.jfree.chart.event.RendererChangeEvent;
102 import org.jfree.chart.plot.CategoryPlot;
103 import org.jfree.chart.plot.PlotOrientation;
104 import org.jfree.chart.plot.PlotRenderingInfo;
105 import org.jfree.chart.renderer.Outlier;
106 import org.jfree.chart.renderer.OutlierList;
107 import org.jfree.chart.renderer.OutlierListCollection;
108 import org.jfree.data.category.CategoryDataset;
109 import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
110 import org.jfree.io.SerialUtilities;
111 import org.jfree.ui.RectangleEdge;
112 import org.jfree.util.PaintUtilities;
113 import org.jfree.util.PublicCloneable;
114
115 /**
116 * A box-and-whisker renderer. This renderer requires a
117 * {@link BoxAndWhiskerCategoryDataset} and is for use with the
118 * {@link CategoryPlot} class.
119 */
120 public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer
121 implements Cloneable, PublicCloneable, Serializable {
122
123 /** For serialization. */
124 private static final long serialVersionUID = 632027470694481177L;
125
126 /** The color used to paint the median line and average marker. */
127 private transient Paint artifactPaint;
128
129 /** A flag that controls whether or not the box is filled. */
130 private boolean fillBox;
131
132 /** The margin between items (boxes) within a category. */
133 private double itemMargin;
134
135 /**
136 * The maximum bar width as percentage of the available space in the plot,
137 * where 0.05 is five percent.
138 */
139 private double maximumBarWidth;
140
141 /**
142 * Default constructor.
143 */
144 public BoxAndWhiskerRenderer() {
145 this.artifactPaint = Color.black;
146 this.fillBox = true;
147 this.itemMargin = 0.20;
148 this.maximumBarWidth = 1.0;
149 setBaseLegendShape(new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0));
150 }
151
152 /**
153 * Returns the paint used to color the median and average markers.
154 *
155 * @return The paint used to draw the median and average markers (never
156 * <code>null</code>).
157 *
158 * @see #setArtifactPaint(Paint)
159 */
160 public Paint getArtifactPaint() {
161 return this.artifactPaint;
162 }
163
164 /**
165 * Sets the paint used to color the median and average markers and sends
166 * a {@link RendererChangeEvent} to all registered listeners.
167 *
168 * @param paint the paint (<code>null</code> not permitted).
169 *
170 * @see #getArtifactPaint()
171 */
172 public void setArtifactPaint(Paint paint) {
173 if (paint == null) {
174 throw new IllegalArgumentException("Null 'paint' argument.");
175 }
176 this.artifactPaint = paint;
177 fireChangeEvent();
178 }
179
180 /**
181 * Returns the flag that controls whether or not the box is filled.
182 *
183 * @return A boolean.
184 *
185 * @see #setFillBox(boolean)
186 */
187 public boolean getFillBox() {
188 return this.fillBox;
189 }
190
191 /**
192 * Sets the flag that controls whether or not the box is filled and sends a
193 * {@link RendererChangeEvent} to all registered listeners.
194 *
195 * @param flag the flag.
196 *
197 * @see #getFillBox()
198 */
199 public void setFillBox(boolean flag) {
200 this.fillBox = flag;
201 fireChangeEvent();
202 }
203
204 /**
205 * Returns the item margin. This is a percentage of the available space
206 * that is allocated to the space between items in the chart.
207 *
208 * @return The margin.
209 *
210 * @see #setItemMargin(double)
211 */
212 public double getItemMargin() {
213 return this.itemMargin;
214 }
215
216 /**
217 * Sets the item margin and sends a {@link RendererChangeEvent} to all
218 * registered listeners.
219 *
220 * @param margin the margin (a percentage).
221 *
222 * @see #getItemMargin()
223 */
224 public void setItemMargin(double margin) {
225 this.itemMargin = margin;
226 fireChangeEvent();
227 }
228
229 /**
230 * Returns the maximum bar width as a percentage of the available drawing
231 * space.
232 *
233 * @return The maximum bar width.
234 *
235 * @see #setMaximumBarWidth(double)
236 *
237 * @since 1.0.10
238 */
239 public double getMaximumBarWidth() {
240 return this.maximumBarWidth;
241 }
242
243 /**
244 * Sets the maximum bar width, which is specified as a percentage of the
245 * available space for all bars, and sends a {@link RendererChangeEvent}
246 * to all registered listeners.
247 *
248 * @param percent the maximum Bar Width (a percentage).
249 *
250 * @see #getMaximumBarWidth()
251 *
252 * @since 1.0.10
253 */
254 public void setMaximumBarWidth(double percent) {
255 this.maximumBarWidth = percent;
256 fireChangeEvent();
257 }
258
259 /**
260 * Returns a legend item for a series.
261 *
262 * @param datasetIndex the dataset index (zero-based).
263 * @param series the series index (zero-based).
264 *
265 * @return The legend item (possibly <code>null</code>).
266 */
267 public LegendItem getLegendItem(int datasetIndex, int series) {
268
269 CategoryPlot cp = getPlot();
270 if (cp == null) {
271 return null;
272 }
273
274 // check that a legend item needs to be displayed...
275 if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) {
276 return null;
277 }
278
279 CategoryDataset dataset = cp.getDataset(datasetIndex);
280 String label = getLegendItemLabelGenerator().generateLabel(dataset,
281 series);
282 String description = label;
283 String toolTipText = null;
284 if (getLegendItemToolTipGenerator() != null) {
285 toolTipText = getLegendItemToolTipGenerator().generateLabel(
286 dataset, series);
287 }
288 String urlText = null;
289 if (getLegendItemURLGenerator() != null) {
290 urlText = getLegendItemURLGenerator().generateLabel(dataset,
291 series);
292 }
293 Shape shape = lookupLegendShape(series);
294 Paint paint = lookupSeriesPaint(series);
295 Paint outlinePaint = lookupSeriesOutlinePaint(series);
296 Stroke outlineStroke = lookupSeriesOutlineStroke(series);
297 LegendItem result = new LegendItem(label, description, toolTipText,
298 urlText, shape, paint, outlineStroke, outlinePaint);
299 result.setLabelFont(lookupLegendTextFont(series));
300 Paint labelPaint = lookupLegendTextPaint(series);
301 if (labelPaint != null) {
302 result.setLabelPaint(labelPaint);
303 }
304 result.setDataset(dataset);
305 result.setDatasetIndex(datasetIndex);
306 result.setSeriesKey(dataset.getRowKey(series));
307 result.setSeriesIndex(series);
308 return result;
309
310 }
311
312 /**
313 * Initialises the renderer. This method gets called once at the start of
314 * the process of drawing a chart.
315 *
316 * @param g2 the graphics device.
317 * @param dataArea the area in which the data is to be plotted.
318 * @param plot the plot.
319 * @param rendererIndex the renderer index.
320 * @param info collects chart rendering information for return to caller.
321 *
322 * @return The renderer state.
323 */
324 public CategoryItemRendererState initialise(Graphics2D g2,
325 Rectangle2D dataArea,
326 CategoryPlot plot,
327 int rendererIndex,
328 PlotRenderingInfo info) {
329
330 CategoryItemRendererState state = super.initialise(g2, dataArea, plot,
331 rendererIndex, info);
332
333 // calculate the box width
334 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex);
335 CategoryDataset dataset = plot.getDataset(rendererIndex);
336 if (dataset != null) {
337 int columns = dataset.getColumnCount();
338 int rows = dataset.getRowCount();
339 double space = 0.0;
340 PlotOrientation orientation = plot.getOrientation();
341 if (orientation == PlotOrientation.HORIZONTAL) {
342 space = dataArea.getHeight();
343 }
344 else if (orientation == PlotOrientation.VERTICAL) {
345 space = dataArea.getWidth();
346 }
347 double maxWidth = space * getMaximumBarWidth();
348 double categoryMargin = 0.0;
349 double currentItemMargin = 0.0;
350 if (columns > 1) {
351 categoryMargin = domainAxis.getCategoryMargin();
352 }
353 if (rows > 1) {
354 currentItemMargin = getItemMargin();
355 }
356 double used = space * (1 - domainAxis.getLowerMargin()
357 - domainAxis.getUpperMargin()
358 - categoryMargin - currentItemMargin);
359 if ((rows * columns) > 0) {
360 state.setBarWidth(Math.min(used / (dataset.getColumnCount()
361 * dataset.getRowCount()), maxWidth));
362 }
363 else {
364 state.setBarWidth(Math.min(used, maxWidth));
365 }
366 }
367
368 return state;
369
370 }
371
372 /**
373 * Draw a single data item.
374 *
375 * @param g2 the graphics device.
376 * @param state the renderer state.
377 * @param dataArea the area in which the data is drawn.
378 * @param plot the plot.
379 * @param domainAxis the domain axis.
380 * @param rangeAxis the range axis.
381 * @param dataset the data (must be an instance of
382 * {@link BoxAndWhiskerCategoryDataset}).
383 * @param row the row index (zero-based).
384 * @param column the column index (zero-based).
385 * @param pass the pass index.
386 */
387 public void drawItem(Graphics2D g2,
388 CategoryItemRendererState state,
389 Rectangle2D dataArea,
390 CategoryPlot plot,
391 CategoryAxis domainAxis,
392 ValueAxis rangeAxis,
393 CategoryDataset dataset,
394 int row,
395 int column,
396 int pass) {
397
398 if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) {
399 throw new IllegalArgumentException(
400 "BoxAndWhiskerRenderer.drawItem() : the data should be "
401 + "of type BoxAndWhiskerCategoryDataset only.");
402 }
403
404 PlotOrientation orientation = plot.getOrientation();
405
406 if (orientation == PlotOrientation.HORIZONTAL) {
407 drawHorizontalItem(g2, state, dataArea, plot, domainAxis,
408 rangeAxis, dataset, row, column);
409 }
410 else if (orientation == PlotOrientation.VERTICAL) {
411 drawVerticalItem(g2, state, dataArea, plot, domainAxis,
412 rangeAxis, dataset, row, column);
413 }
414
415 }
416
417 /**
418 * Draws the visual representation of a single data item when the plot has
419 * a horizontal orientation.
420 *
421 * @param g2 the graphics device.
422 * @param state the renderer state.
423 * @param dataArea the area within which the plot is being drawn.
424 * @param plot the plot (can be used to obtain standard color
425 * information etc).
426 * @param domainAxis the domain axis.
427 * @param rangeAxis the range axis.
428 * @param dataset the dataset (must be an instance of
429 * {@link BoxAndWhiskerCategoryDataset}).
430 * @param row the row index (zero-based).
431 * @param column the column index (zero-based).
432 */
433 public void drawHorizontalItem(Graphics2D g2,
434 CategoryItemRendererState state,
435 Rectangle2D dataArea,
436 CategoryPlot plot,
437 CategoryAxis domainAxis,
438 ValueAxis rangeAxis,
439 CategoryDataset dataset,
440 int row,
441 int column) {
442
443 BoxAndWhiskerCategoryDataset bawDataset
444 = (BoxAndWhiskerCategoryDataset) dataset;
445
446 double categoryEnd = domainAxis.getCategoryEnd(column,
447 getColumnCount(), dataArea, plot.getDomainAxisEdge());
448 double categoryStart = domainAxis.getCategoryStart(column,
449 getColumnCount(), dataArea, plot.getDomainAxisEdge());
450 double categoryWidth = Math.abs(categoryEnd - categoryStart);
451
452 double yy = categoryStart;
453 int seriesCount = getRowCount();
454 int categoryCount = getColumnCount();
455
456 if (seriesCount > 1) {
457 double seriesGap = dataArea.getHeight() * getItemMargin()
458 / (categoryCount * (seriesCount - 1));
459 double usedWidth = (state.getBarWidth() * seriesCount)
460 + (seriesGap * (seriesCount - 1));
461 // offset the start of the boxes if the total width used is smaller
462 // than the category width
463 double offset = (categoryWidth - usedWidth) / 2;
464 yy = yy + offset + (row * (state.getBarWidth() + seriesGap));
465 }
466 else {
467 // offset the start of the box if the box width is smaller than
468 // the category width
469 double offset = (categoryWidth - state.getBarWidth()) / 2;
470 yy = yy + offset;
471 }
472
473 g2.setPaint(getItemPaint(row, column));
474 Stroke s = getItemStroke(row, column);
475 g2.setStroke(s);
476
477 RectangleEdge location = plot.getRangeAxisEdge();
478
479 Number xQ1 = bawDataset.getQ1Value(row, column);
480 Number xQ3 = bawDataset.getQ3Value(row, column);
481 Number xMax = bawDataset.getMaxRegularValue(row, column);
482 Number xMin = bawDataset.getMinRegularValue(row, column);
483
484 Shape box = null;
485 if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) {
486
487 double xxQ1 = rangeAxis.valueToJava2D(xQ1.doubleValue(), dataArea,
488 location);
489 double xxQ3 = rangeAxis.valueToJava2D(xQ3.doubleValue(), dataArea,
490 location);
491 double xxMax = rangeAxis.valueToJava2D(xMax.doubleValue(), dataArea,
492 location);
493 double xxMin = rangeAxis.valueToJava2D(xMin.doubleValue(), dataArea,
494 location);
495 double yymid = yy + state.getBarWidth() / 2.0;
496
497 // draw the upper shadow...
498 g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid));
499 g2.draw(new Line2D.Double(xxMax, yy, xxMax,
500 yy + state.getBarWidth()));
501
502 // draw the lower shadow...
503 g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid));
504 g2.draw(new Line2D.Double(xxMin, yy, xxMin,
505 yy + state.getBarWidth()));
506
507 // draw the box...
508 box = new Rectangle2D.Double(Math.min(xxQ1, xxQ3), yy,
509 Math.abs(xxQ1 - xxQ3), state.getBarWidth());
510 if (this.fillBox) {
511 g2.fill(box);
512 }
513 g2.setStroke(getItemOutlineStroke(row, column));
514 g2.setPaint(getItemOutlinePaint(row, column));
515 g2.draw(box);
516 }
517
518 g2.setPaint(this.artifactPaint);
519 double aRadius = 0; // average radius
520
521 // draw mean - SPECIAL AIMS REQUIREMENT...
522 Number xMean = bawDataset.getMeanValue(row, column);
523 if (xMean != null) {
524 double xxMean = rangeAxis.valueToJava2D(xMean.doubleValue(),
525 dataArea, location);
526 aRadius = state.getBarWidth() / 4;
527 // here we check that the average marker will in fact be visible
528 // before drawing it...
529 if ((xxMean > (dataArea.getMinX() - aRadius))
530 && (xxMean < (dataArea.getMaxX() + aRadius))) {
531 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xxMean
532 - aRadius, yy + aRadius, aRadius * 2, aRadius * 2);
533 g2.fill(avgEllipse);
534 g2.draw(avgEllipse);
535 }
536 }
537
538 // draw median...
539 Number xMedian = bawDataset.getMedianValue(row, column);
540 if (xMedian != null) {
541 double xxMedian = rangeAxis.valueToJava2D(xMedian.doubleValue(),
542 dataArea, location);
543 g2.draw(new Line2D.Double(xxMedian, yy, xxMedian,
544 yy + state.getBarWidth()));
545 }
546
547 // collect entity and tool tip information...
548 if (state.getInfo() != null && box != null) {
549 EntityCollection entities = state.getEntityCollection();
550 if (entities != null) {
551 addItemEntity(entities, dataset, row, column, box);
552 }
553 }
554
555 }
556
557 /**
558 * Draws the visual representation of a single data item when the plot has
559 * a vertical orientation.
560 *
561 * @param g2 the graphics device.
562 * @param state the renderer state.
563 * @param dataArea the area within which the plot is being drawn.
564 * @param plot the plot (can be used to obtain standard color information
565 * etc).
566 * @param domainAxis the domain axis.
567 * @param rangeAxis the range axis.
568 * @param dataset the dataset (must be an instance of
569 * {@link BoxAndWhiskerCategoryDataset}).
570 * @param row the row index (zero-based).
571 * @param column the column index (zero-based).
572 */
573 public void drawVerticalItem(Graphics2D g2,
574 CategoryItemRendererState state,
575 Rectangle2D dataArea,
576 CategoryPlot plot,
577 CategoryAxis domainAxis,
578 ValueAxis rangeAxis,
579 CategoryDataset dataset,
580 int row,
581 int column) {
582
583 BoxAndWhiskerCategoryDataset bawDataset
584 = (BoxAndWhiskerCategoryDataset) dataset;
585
586 double categoryEnd = domainAxis.getCategoryEnd(column,
587 getColumnCount(), dataArea, plot.getDomainAxisEdge());
588 double categoryStart = domainAxis.getCategoryStart(column,
589 getColumnCount(), dataArea, plot.getDomainAxisEdge());
590 double categoryWidth = categoryEnd - categoryStart;
591
592 double xx = categoryStart;
593 int seriesCount = getRowCount();
594 int categoryCount = getColumnCount();
595
596 if (seriesCount > 1) {
597 double seriesGap = dataArea.getWidth() * getItemMargin()
598 / (categoryCount * (seriesCount - 1));
599 double usedWidth = (state.getBarWidth() * seriesCount)
600 + (seriesGap * (seriesCount - 1));
601 // offset the start of the boxes if the total width used is smaller
602 // than the category width
603 double offset = (categoryWidth - usedWidth) / 2;
604 xx = xx + offset + (row * (state.getBarWidth() + seriesGap));
605 }
606 else {
607 // offset the start of the box if the box width is smaller than the
608 // category width
609 double offset = (categoryWidth - state.getBarWidth()) / 2;
610 xx = xx + offset;
611 }
612
613 double yyAverage = 0.0;
614 double yyOutlier;
615
616 Paint itemPaint = getItemPaint(row, column);
617 g2.setPaint(itemPaint);
618 Stroke s = getItemStroke(row, column);
619 g2.setStroke(s);
620
621 double aRadius = 0; // average radius
622
623 RectangleEdge location = plot.getRangeAxisEdge();
624
625 Number yQ1 = bawDataset.getQ1Value(row, column);
626 Number yQ3 = bawDataset.getQ3Value(row, column);
627 Number yMax = bawDataset.getMaxRegularValue(row, column);
628 Number yMin = bawDataset.getMinRegularValue(row, column);
629 Shape box = null;
630 if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) {
631
632 double yyQ1 = rangeAxis.valueToJava2D(yQ1.doubleValue(), dataArea,
633 location);
634 double yyQ3 = rangeAxis.valueToJava2D(yQ3.doubleValue(), dataArea,
635 location);
636 double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(),
637 dataArea, location);
638 double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(),
639 dataArea, location);
640 double xxmid = xx + state.getBarWidth() / 2.0;
641
642 // draw the upper shadow...
643 g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3));
644 g2.draw(new Line2D.Double(xx, yyMax, xx + state.getBarWidth(),
645 yyMax));
646
647 // draw the lower shadow...
648 g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1));
649 g2.draw(new Line2D.Double(xx, yyMin, xx + state.getBarWidth(),
650 yyMin));
651
652 // draw the body...
653 box = new Rectangle2D.Double(xx, Math.min(yyQ1, yyQ3),
654 state.getBarWidth(), Math.abs(yyQ1 - yyQ3));
655 if (this.fillBox) {
656 g2.fill(box);
657 }
658 g2.setStroke(getItemOutlineStroke(row, column));
659 g2.setPaint(getItemOutlinePaint(row, column));
660 g2.draw(box);
661 }
662
663 g2.setPaint(this.artifactPaint);
664
665 // draw mean - SPECIAL AIMS REQUIREMENT...
666 Number yMean = bawDataset.getMeanValue(row, column);
667 if (yMean != null) {
668 yyAverage = rangeAxis.valueToJava2D(yMean.doubleValue(),
669 dataArea, location);
670 aRadius = state.getBarWidth() / 4;
671 // here we check that the average marker will in fact be visible
672 // before drawing it...
673 if ((yyAverage > (dataArea.getMinY() - aRadius))
674 && (yyAverage < (dataArea.getMaxY() + aRadius))) {
675 Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx + aRadius,
676 yyAverage - aRadius, aRadius * 2, aRadius * 2);
677 g2.fill(avgEllipse);
678 g2.draw(avgEllipse);
679 }
680 }
681
682 // draw median...
683 Number yMedian = bawDataset.getMedianValue(row, column);
684 if (yMedian != null) {
685 double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
686 dataArea, location);
687 g2.draw(new Line2D.Double(xx, yyMedian, xx + state.getBarWidth(),
688 yyMedian));
689 }
690
691 // draw yOutliers...
692 double maxAxisValue = rangeAxis.valueToJava2D(
693 rangeAxis.getUpperBound(), dataArea, location) + aRadius;
694 double minAxisValue = rangeAxis.valueToJava2D(
695 rangeAxis.getLowerBound(), dataArea, location) - aRadius;
696
697 g2.setPaint(itemPaint);
698
699 // draw outliers
700 double oRadius = state.getBarWidth() / 3; // outlier radius
701 List outliers = new ArrayList();
702 OutlierListCollection outlierListCollection
703 = new OutlierListCollection();
704
705 // From outlier array sort out which are outliers and put these into a
706 // list If there are any farouts, set the flag on the
707 // OutlierListCollection
708 List yOutliers = bawDataset.getOutliers(row, column);
709 if (yOutliers != null) {
710 for (int i = 0; i < yOutliers.size(); i++) {
711 double outlier = ((Number) yOutliers.get(i)).doubleValue();
712 Number minOutlier = bawDataset.getMinOutlier(row, column);
713 Number maxOutlier = bawDataset.getMaxOutlier(row, column);
714 Number minRegular = bawDataset.getMinRegularValue(row, column);
715 Number maxRegular = bawDataset.getMaxRegularValue(row, column);
716 if (outlier > maxOutlier.doubleValue()) {
717 outlierListCollection.setHighFarOut(true);
718 }
719 else if (outlier < minOutlier.doubleValue()) {
720 outlierListCollection.setLowFarOut(true);
721 }
722 else if (outlier > maxRegular.doubleValue()) {
723 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
724 location);
725 outliers.add(new Outlier(xx + state.getBarWidth() / 2.0,
726 yyOutlier, oRadius));
727 }
728 else if (outlier < minRegular.doubleValue()) {
729 yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
730 location);
731 outliers.add(new Outlier(xx + state.getBarWidth() / 2.0,
732 yyOutlier, oRadius));
733 }
734 Collections.sort(outliers);
735 }
736
737 // Process outliers. Each outlier is either added to the
738 // appropriate outlier list or a new outlier list is made
739 for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
740 Outlier outlier = (Outlier) iterator.next();
741 outlierListCollection.add(outlier);
742 }
743
744 for (Iterator iterator = outlierListCollection.iterator();
745 iterator.hasNext();) {
746 OutlierList list = (OutlierList) iterator.next();
747 Outlier outlier = list.getAveragedOutlier();
748 Point2D point = outlier.getPoint();
749
750 if (list.isMultiple()) {
751 drawMultipleEllipse(point, state.getBarWidth(), oRadius,
752 g2);
753 }
754 else {
755 drawEllipse(point, oRadius, g2);
756 }
757 }
758
759 // draw farout indicators
760 if (outlierListCollection.isHighFarOut()) {
761 drawHighFarOut(aRadius / 2.0, g2,
762 xx + state.getBarWidth() / 2.0, maxAxisValue);
763 }
764
765 if (outlierListCollection.isLowFarOut()) {
766 drawLowFarOut(aRadius / 2.0, g2,
767 xx + state.getBarWidth() / 2.0, minAxisValue);
768 }
769 }
770 // collect entity and tool tip information...
771 if (state.getInfo() != null && box != null) {
772 EntityCollection entities = state.getEntityCollection();
773 if (entities != null) {
774 addItemEntity(entities, dataset, row, column, box);
775 }
776 }
777
778 }
779
780 /**
781 * Draws a dot to represent an outlier.
782 *
783 * @param point the location.
784 * @param oRadius the radius.
785 * @param g2 the graphics device.
786 */
787 private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
788 Ellipse2D dot = new Ellipse2D.Double(point.getX() + oRadius / 2,
789 point.getY(), oRadius, oRadius);
790 g2.draw(dot);
791 }
792
793 /**
794 * Draws two dots to represent the average value of more than one outlier.
795 *
796 * @param point the location
797 * @param boxWidth the box width.
798 * @param oRadius the radius.
799 * @param g2 the graphics device.
800 */
801 private void drawMultipleEllipse(Point2D point, double boxWidth,
802 double oRadius, Graphics2D g2) {
803
804 Ellipse2D dot1 = new Ellipse2D.Double(point.getX() - (boxWidth / 2)
805 + oRadius, point.getY(), oRadius, oRadius);
806 Ellipse2D dot2 = new Ellipse2D.Double(point.getX() + (boxWidth / 2),
807 point.getY(), oRadius, oRadius);
808 g2.draw(dot1);
809 g2.draw(dot2);
810 }
811
812 /**
813 * Draws a triangle to indicate the presence of far-out values.
814 *
815 * @param aRadius the radius.
816 * @param g2 the graphics device.
817 * @param xx the x coordinate.
818 * @param m the y coordinate.
819 */
820 private void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
821 double m) {
822 double side = aRadius * 2;
823 g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
824 g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
825 g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
826 }
827
828 /**
829 * Draws a triangle to indicate the presence of far-out values.
830 *
831 * @param aRadius the radius.
832 * @param g2 the graphics device.
833 * @param xx the x coordinate.
834 * @param m the y coordinate.
835 */
836 private void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
837 double m) {
838 double side = aRadius * 2;
839 g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
840 g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
841 g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
842 }
843
844 /**
845 * Tests this renderer for equality with an arbitrary object.
846 *
847 * @param obj the object (<code>null</code> permitted).
848 *
849 * @return <code>true</code> or <code>false</code>.
850 */
851 public boolean equals(Object obj) {
852 if (obj == this) {
853 return true;
854 }
855 if (!(obj instanceof BoxAndWhiskerRenderer)) {
856 return false;
857 }
858 if (!super.equals(obj)) {
859 return false;
860 }
861 BoxAndWhiskerRenderer that = (BoxAndWhiskerRenderer) obj;
862 if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
863 return false;
864 }
865 if (this.fillBox != that.fillBox) {
866 return false;
867 }
868 if (this.itemMargin != that.itemMargin) {
869 return false;
870 }
871 if (this.maximumBarWidth != that.maximumBarWidth) {
872 return false;
873 }
874 return true;
875 }
876
877 /**
878 * Provides serialization support.
879 *
880 * @param stream the output stream.
881 *
882 * @throws IOException if there is an I/O error.
883 */
884 private void writeObject(ObjectOutputStream stream) throws IOException {
885 stream.defaultWriteObject();
886 SerialUtilities.writePaint(this.artifactPaint, stream);
887 }
888
889 /**
890 * Provides serialization support.
891 *
892 * @param stream the input stream.
893 *
894 * @throws IOException if there is an I/O error.
895 * @throws ClassNotFoundException if there is a classpath problem.
896 */
897 private void readObject(ObjectInputStream stream)
898 throws IOException, ClassNotFoundException {
899 stream.defaultReadObject();
900 this.artifactPaint = SerialUtilities.readPaint(stream);
901 }
902
903 }