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