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