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