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