1:
68:
69: package ;
70:
71: import ;
72: import ;
73: import ;
74: import ;
75: import ;
76: import ;
77: import ;
78: import ;
79: import ;
80: import ;
81: import ;
82: import ;
83: import ;
84: import ;
85: import ;
86: import ;
87: import ;
88:
89: import ;
90: import ;
91: import ;
92: import ;
93: import ;
94: import ;
95: import ;
96: import ;
97: import ;
98: import ;
99: import ;
100: import ;
101: import ;
102: import ;
103: import ;
104: import ;
105: import ;
106: import ;
107: import ;
108:
109:
114: public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer
115: implements Cloneable, PublicCloneable,
116: Serializable {
117:
118:
119: private static final long serialVersionUID = 632027470694481177L;
120:
121:
122: private transient Paint artifactPaint;
123:
124:
125: private boolean fillBox;
126:
127:
128: private double itemMargin;
129:
130:
133: public BoxAndWhiskerRenderer() {
134: this.artifactPaint = Color.black;
135: this.fillBox = true;
136: this.itemMargin = 0.20;
137: }
138:
139:
147: public Paint getArtifactPaint() {
148: return this.artifactPaint;
149: }
150:
151:
159: public void setArtifactPaint(Paint paint) {
160: if (paint == null) {
161: throw new IllegalArgumentException("Null 'paint' argument.");
162: }
163: this.artifactPaint = paint;
164: notifyListeners(new RendererChangeEvent(this));
165: }
166:
167:
174: public boolean getFillBox() {
175: return this.fillBox;
176: }
177:
178:
186: public void setFillBox(boolean flag) {
187: this.fillBox = flag;
188: notifyListeners(new RendererChangeEvent(this));
189: }
190:
191:
199: public double getItemMargin() {
200: return this.itemMargin;
201: }
202:
203:
211: public void setItemMargin(double margin) {
212: this.itemMargin = margin;
213: notifyListeners(new RendererChangeEvent(this));
214: }
215:
216:
224: public LegendItem getLegendItem(int datasetIndex, int series) {
225:
226: CategoryPlot cp = getPlot();
227: if (cp == null) {
228: return null;
229: }
230:
231:
232: if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) {
233: return null;
234: }
235:
236: CategoryDataset dataset = cp.getDataset(datasetIndex);
237: String label = getLegendItemLabelGenerator().generateLabel(dataset,
238: series);
239: String description = label;
240: String toolTipText = null;
241: if (getLegendItemToolTipGenerator() != null) {
242: toolTipText = getLegendItemToolTipGenerator().generateLabel(
243: dataset, series);
244: }
245: String urlText = null;
246: if (getLegendItemURLGenerator() != null) {
247: urlText = getLegendItemURLGenerator().generateLabel(dataset,
248: series);
249: }
250: Shape shape = new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0);
251: Paint paint = lookupSeriesPaint(series);
252: Paint outlinePaint = lookupSeriesOutlinePaint(series);
253: Stroke outlineStroke = lookupSeriesOutlineStroke(series);
254: LegendItem result = new LegendItem(label, description, toolTipText,
255: urlText, shape, paint, outlineStroke, outlinePaint);
256: result.setDataset(dataset);
257: result.setDatasetIndex(datasetIndex);
258: result.setSeriesKey(dataset.getRowKey(series));
259: result.setSeriesIndex(series);
260: return result;
261:
262: }
263:
264:
276: public CategoryItemRendererState initialise(Graphics2D g2,
277: Rectangle2D dataArea,
278: CategoryPlot plot,
279: int rendererIndex,
280: PlotRenderingInfo info) {
281:
282: CategoryItemRendererState state = super.initialise(g2, dataArea, plot,
283: rendererIndex, info);
284:
285:
286: CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex);
287: CategoryDataset dataset = plot.getDataset(rendererIndex);
288: if (dataset != null) {
289: int columns = dataset.getColumnCount();
290: int rows = dataset.getRowCount();
291: double space = 0.0;
292: PlotOrientation orientation = plot.getOrientation();
293: if (orientation == PlotOrientation.HORIZONTAL) {
294: space = dataArea.getHeight();
295: }
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 * (1 - domainAxis.getLowerMargin()
308: - domainAxis.getUpperMargin()
309: - categoryMargin - currentItemMargin);
310: if ((rows * columns) > 0) {
311: state.setBarWidth(used / (dataset.getColumnCount()
312: * dataset.getRowCount()));
313: }
314: else {
315: state.setBarWidth(used);
316: }
317: }
318:
319: return state;
320:
321: }
322:
323:
337: public void drawItem(Graphics2D g2,
338: CategoryItemRendererState state,
339: Rectangle2D dataArea,
340: CategoryPlot plot,
341: CategoryAxis domainAxis,
342: ValueAxis rangeAxis,
343: CategoryDataset dataset,
344: int row,
345: int column,
346: int pass) {
347:
348: if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) {
349: throw new IllegalArgumentException(
350: "BoxAndWhiskerRenderer.drawItem() : the data should be "
351: + "of type BoxAndWhiskerCategoryDataset only.");
352: }
353:
354: PlotOrientation orientation = plot.getOrientation();
355:
356: if (orientation == PlotOrientation.HORIZONTAL) {
357: drawHorizontalItem(g2, state, dataArea, plot, domainAxis,
358: rangeAxis, dataset, row, column);
359: }
360: else if (orientation == PlotOrientation.VERTICAL) {
361: drawVerticalItem(g2, state, dataArea, plot, domainAxis,
362: rangeAxis, dataset, row, column);
363: }
364:
365: }
366:
367:
382: public void drawHorizontalItem(Graphics2D g2,
383: CategoryItemRendererState state,
384: Rectangle2D dataArea,
385: CategoryPlot plot,
386: CategoryAxis domainAxis,
387: ValueAxis rangeAxis,
388: CategoryDataset dataset,
389: int row,
390: int column) {
391:
392: BoxAndWhiskerCategoryDataset bawDataset
393: = (BoxAndWhiskerCategoryDataset) dataset;
394:
395: double categoryEnd = domainAxis.getCategoryEnd(column,
396: getColumnCount(), dataArea, plot.getDomainAxisEdge());
397: double categoryStart = domainAxis.getCategoryStart(column,
398: getColumnCount(), dataArea, plot.getDomainAxisEdge());
399: double categoryWidth = Math.abs(categoryEnd - categoryStart);
400:
401: double yy = categoryStart;
402: int seriesCount = getRowCount();
403: int categoryCount = getColumnCount();
404:
405: if (seriesCount > 1) {
406: double seriesGap = dataArea.getWidth() * getItemMargin()
407: / (categoryCount * (seriesCount - 1));
408: double usedWidth = (state.getBarWidth() * seriesCount)
409: + (seriesGap * (seriesCount - 1));
410:
411:
412: double offset = (categoryWidth - usedWidth) / 2;
413: yy = yy + offset + (row * (state.getBarWidth() + seriesGap));
414: }
415: else {
416:
417:
418: double offset = (categoryWidth - state.getBarWidth()) / 2;
419: yy = yy + offset;
420: }
421:
422: Paint p = getItemPaint(row, column);
423: if (p != null) {
424: g2.setPaint(p);
425: }
426: Stroke s = getItemStroke(row, column);
427: g2.setStroke(s);
428:
429: RectangleEdge location = plot.getRangeAxisEdge();
430:
431: Number xQ1 = bawDataset.getQ1Value(row, column);
432: Number xQ3 = bawDataset.getQ3Value(row, column);
433: Number xMax = bawDataset.getMaxRegularValue(row, column);
434: Number xMin = bawDataset.getMinRegularValue(row, column);
435:
436: Shape box = null;
437: if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) {
438:
439: double xxQ1 = rangeAxis.valueToJava2D(xQ1.doubleValue(), dataArea,
440: location);
441: double xxQ3 = rangeAxis.valueToJava2D(xQ3.doubleValue(), dataArea,
442: location);
443: double xxMax = rangeAxis.valueToJava2D(xMax.doubleValue(), dataArea,
444: location);
445: double xxMin = rangeAxis.valueToJava2D(xMin.doubleValue(), dataArea,
446: location);
447: double yymid = yy + state.getBarWidth() / 2.0;
448:
449:
450: g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid));
451: g2.draw(new Line2D.Double(xxMax, yy, xxMax,
452: yy + state.getBarWidth()));
453:
454:
455: g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid));
456: g2.draw(new Line2D.Double(xxMin, yy, xxMin,
457: yy + state.getBarWidth()));
458:
459:
460: box = new Rectangle2D.Double(Math.min(xxQ1, xxQ3), yy,
461: Math.abs(xxQ1 - xxQ3), state.getBarWidth());
462: if (this.fillBox) {
463: g2.fill(box);
464: }
465: g2.draw(box);
466:
467: }
468:
469: g2.setPaint(this.artifactPaint);
470: double aRadius = 0;
471:
472:
473: Number xMean = bawDataset.getMeanValue(row, column);
474: if (xMean != null) {
475: double xxMean = rangeAxis.valueToJava2D(xMean.doubleValue(),
476: dataArea, location);
477: aRadius = state.getBarWidth() / 4;
478: Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xxMean
479: - aRadius, yy + aRadius, aRadius * 2, aRadius * 2);
480: g2.fill(avgEllipse);
481: g2.draw(avgEllipse);
482: }
483:
484:
485: Number xMedian = bawDataset.getMedianValue(row, column);
486: if (xMedian != null) {
487: double xxMedian = rangeAxis.valueToJava2D(xMedian.doubleValue(),
488: dataArea, location);
489: g2.draw(new Line2D.Double(xxMedian, yy, xxMedian,
490: yy + state.getBarWidth()));
491: }
492:
493:
494: if (state.getInfo() != null && box != null) {
495: EntityCollection entities = state.getEntityCollection();
496: if (entities != null) {
497: String tip = null;
498: CategoryToolTipGenerator tipster
499: = getToolTipGenerator(row, column);
500: if (tipster != null) {
501: tip = tipster.generateToolTip(dataset, row, column);
502: }
503: String url = null;
504: if (getItemURLGenerator(row, column) != null) {
505: url = getItemURLGenerator(row, column).generateURL(
506: dataset, row, column);
507: }
508: CategoryItemEntity entity = new CategoryItemEntity(box, tip,
509: url, dataset, dataset.getRowKey(row),
510: dataset.getColumnKey(column));
511: entities.add(entity);
512: }
513: }
514:
515: }
516:
517:
532: public void drawVerticalItem(Graphics2D g2,
533: CategoryItemRendererState state,
534: Rectangle2D dataArea,
535: CategoryPlot plot,
536: CategoryAxis domainAxis,
537: ValueAxis rangeAxis,
538: CategoryDataset dataset,
539: int row,
540: int column) {
541:
542: BoxAndWhiskerCategoryDataset bawDataset
543: = (BoxAndWhiskerCategoryDataset) dataset;
544:
545: double categoryEnd = domainAxis.getCategoryEnd(column,
546: getColumnCount(), dataArea, plot.getDomainAxisEdge());
547: double categoryStart = domainAxis.getCategoryStart(column,
548: getColumnCount(), dataArea, plot.getDomainAxisEdge());
549: double categoryWidth = categoryEnd - categoryStart;
550:
551: double xx = categoryStart;
552: int seriesCount = getRowCount();
553: int categoryCount = getColumnCount();
554:
555: if (seriesCount > 1) {
556: double seriesGap = dataArea.getWidth() * getItemMargin()
557: / (categoryCount * (seriesCount - 1));
558: double usedWidth = (state.getBarWidth() * seriesCount)
559: + (seriesGap * (seriesCount - 1));
560:
561:
562: double offset = (categoryWidth - usedWidth) / 2;
563: xx = xx + offset + (row * (state.getBarWidth() + seriesGap));
564: }
565: else {
566:
567:
568: double offset = (categoryWidth - state.getBarWidth()) / 2;
569: xx = xx + offset;
570: }
571:
572: double yyAverage = 0.0;
573: double yyOutlier;
574:
575: Paint p = getItemPaint(row, column);
576: if (p != null) {
577: g2.setPaint(p);
578: }
579: Stroke s = getItemStroke(row, column);
580: g2.setStroke(s);
581:
582: double aRadius = 0;
583:
584: RectangleEdge location = plot.getRangeAxisEdge();
585:
586: Number yQ1 = bawDataset.getQ1Value(row, column);
587: Number yQ3 = bawDataset.getQ3Value(row, column);
588: Number yMax = bawDataset.getMaxRegularValue(row, column);
589: Number yMin = bawDataset.getMinRegularValue(row, column);
590: Shape box = null;
591: if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) {
592:
593: double yyQ1 = rangeAxis.valueToJava2D(yQ1.doubleValue(), dataArea,
594: location);
595: double yyQ3 = rangeAxis.valueToJava2D(yQ3.doubleValue(), dataArea,
596: location);
597: double yyMax = rangeAxis.valueToJava2D(yMax.doubleValue(),
598: dataArea, location);
599: double yyMin = rangeAxis.valueToJava2D(yMin.doubleValue(),
600: dataArea, location);
601: double xxmid = xx + state.getBarWidth() / 2.0;
602:
603:
604: g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3));
605: g2.draw(new Line2D.Double(xx, yyMax, xx + state.getBarWidth(),
606: yyMax));
607:
608:
609: g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1));
610: g2.draw(new Line2D.Double(xx, yyMin, xx + state.getBarWidth(),
611: yyMin));
612:
613:
614: box = new Rectangle2D.Double(xx, Math.min(yyQ1, yyQ3),
615: state.getBarWidth(), Math.abs(yyQ1 - yyQ3));
616: if (this.fillBox) {
617: g2.fill(box);
618: }
619: g2.draw(box);
620:
621: }
622:
623: g2.setPaint(this.artifactPaint);
624:
625:
626: Number yMean = bawDataset.getMeanValue(row, column);
627: if (yMean != null) {
628: yyAverage = rangeAxis.valueToJava2D(yMean.doubleValue(),
629: dataArea, location);
630: aRadius = state.getBarWidth() / 4;
631: Ellipse2D.Double avgEllipse = new Ellipse2D.Double(xx + aRadius,
632: yyAverage - aRadius, aRadius * 2, aRadius * 2);
633: g2.fill(avgEllipse);
634: g2.draw(avgEllipse);
635: }
636:
637:
638: Number yMedian = bawDataset.getMedianValue(row, column);
639: if (yMedian != null) {
640: double yyMedian = rangeAxis.valueToJava2D(yMedian.doubleValue(),
641: dataArea, location);
642: g2.draw(new Line2D.Double(xx, yyMedian, xx + state.getBarWidth(),
643: yyMedian));
644: }
645:
646:
647: double maxAxisValue = rangeAxis.valueToJava2D(
648: rangeAxis.getUpperBound(), dataArea, location) + aRadius;
649: double minAxisValue = rangeAxis.valueToJava2D(
650: rangeAxis.getLowerBound(), dataArea, location) - aRadius;
651:
652: g2.setPaint(p);
653:
654:
655: double oRadius = state.getBarWidth() / 3;
656: List outliers = new ArrayList();
657: OutlierListCollection outlierListCollection
658: = new OutlierListCollection();
659:
660:
661:
662:
663: List yOutliers = bawDataset.getOutliers(row, column);
664: if (yOutliers != null) {
665: for (int i = 0; i < yOutliers.size(); i++) {
666: double outlier = ((Number) yOutliers.get(i)).doubleValue();
667: Number minOutlier = bawDataset.getMinOutlier(row, column);
668: Number maxOutlier = bawDataset.getMaxOutlier(row, column);
669: Number minRegular = bawDataset.getMinRegularValue(row, column);
670: Number maxRegular = bawDataset.getMaxRegularValue(row, column);
671: if (outlier > maxOutlier.doubleValue()) {
672: outlierListCollection.setHighFarOut(true);
673: }
674: else if (outlier < minOutlier.doubleValue()) {
675: outlierListCollection.setLowFarOut(true);
676: }
677: else if (outlier > maxRegular.doubleValue()) {
678: yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
679: location);
680: outliers.add(new Outlier(xx + state.getBarWidth() / 2.0,
681: yyOutlier, oRadius));
682: }
683: else if (outlier < minRegular.doubleValue()) {
684: yyOutlier = rangeAxis.valueToJava2D(outlier, dataArea,
685: location);
686: outliers.add(new Outlier(xx + state.getBarWidth() / 2.0,
687: yyOutlier, oRadius));
688: }
689: Collections.sort(outliers);
690: }
691:
692:
693:
694: for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
695: Outlier outlier = (Outlier) iterator.next();
696: outlierListCollection.add(outlier);
697: }
698:
699: for (Iterator iterator = outlierListCollection.iterator();
700: iterator.hasNext();) {
701: OutlierList list = (OutlierList) iterator.next();
702: Outlier outlier = list.getAveragedOutlier();
703: Point2D point = outlier.getPoint();
704:
705: if (list.isMultiple()) {
706: drawMultipleEllipse(point, state.getBarWidth(), oRadius,
707: g2);
708: }
709: else {
710: drawEllipse(point, oRadius, g2);
711: }
712: }
713:
714:
715: if (outlierListCollection.isHighFarOut()) {
716: drawHighFarOut(aRadius / 2.0, g2,
717: xx + state.getBarWidth() / 2.0, maxAxisValue);
718: }
719:
720: if (outlierListCollection.isLowFarOut()) {
721: drawLowFarOut(aRadius / 2.0, g2,
722: xx + state.getBarWidth() / 2.0, minAxisValue);
723: }
724: }
725:
726: if (state.getInfo() != null && box != null) {
727: EntityCollection entities = state.getEntityCollection();
728: if (entities != null) {
729: String tip = null;
730: CategoryToolTipGenerator tipster
731: = getToolTipGenerator(row, column);
732: if (tipster != null) {
733: tip = tipster.generateToolTip(dataset, row, column);
734: }
735: String url = null;
736: if (getItemURLGenerator(row, column) != null) {
737: url = getItemURLGenerator(row, column).generateURL(dataset,
738: row, column);
739: }
740: CategoryItemEntity entity = new CategoryItemEntity(box, tip,
741: url, dataset, dataset.getRowKey(row),
742: dataset.getColumnKey(column));
743: entities.add(entity);
744: }
745: }
746:
747: }
748:
749:
756: private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
757: Ellipse2D dot = new Ellipse2D.Double(point.getX() + oRadius / 2,
758: point.getY(), oRadius, oRadius);
759: g2.draw(dot);
760: }
761:
762:
770: private void drawMultipleEllipse(Point2D point, double boxWidth,
771: double oRadius, Graphics2D g2) {
772:
773: Ellipse2D dot1 = new Ellipse2D.Double(point.getX() - (boxWidth / 2)
774: + oRadius, point.getY(), oRadius, oRadius);
775: Ellipse2D dot2 = new Ellipse2D.Double(point.getX() + (boxWidth / 2),
776: point.getY(), oRadius, oRadius);
777: g2.draw(dot1);
778: g2.draw(dot2);
779: }
780:
781:
789: private void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
790: double m) {
791: double side = aRadius * 2;
792: g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
793: g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
794: g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
795: }
796:
797:
805: private void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
806: double m) {
807: double side = aRadius * 2;
808: g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
809: g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
810: g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
811: }
812:
813:
820: public boolean equals(Object obj) {
821: if (obj == this) {
822: return true;
823: }
824: if (!(obj instanceof BoxAndWhiskerRenderer)) {
825: return false;
826: }
827: if (!super.equals(obj)) {
828: return false;
829: }
830: BoxAndWhiskerRenderer that = (BoxAndWhiskerRenderer) obj;
831: if (!PaintUtilities.equal(this.artifactPaint, that.artifactPaint)) {
832: return false;
833: }
834: if (!(this.fillBox == that.fillBox)) {
835: return false;
836: }
837: if (!(this.itemMargin == that.itemMargin)) {
838: return false;
839: }
840: return true;
841: }
842:
843:
850: private void writeObject(ObjectOutputStream stream) throws IOException {
851: stream.defaultWriteObject();
852: SerialUtilities.writePaint(this.artifactPaint, stream);
853: }
854:
855:
863: private void readObject(ObjectInputStream stream)
864: throws IOException, ClassNotFoundException {
865: stream.defaultReadObject();
866: this.artifactPaint = SerialUtilities.readPaint(stream);
867: }
868:
869: }