1. /* ===========================================================
  2. * JFreeChart : a free chart library for the Java(tm) platform
  3. * ===========================================================
  4. *
  5. * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
  6. *
  7. * Project Info: http://www.jfree.org/jfreechart/index.html
  8. *
  9. * This library is free software; you can redistribute it and/or modify it
  10. * under the terms of the GNU Lesser General Public License as published by
  11. * the Free Software Foundation; either version 2.1 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This library is distributed in the hope that it will be useful, but
  15. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  16. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
  17. * License for more details.
  18. *
  19. * You should have received a copy of the GNU Lesser General Public License
  20. * along with this library; if not, write to the Free Software Foundation,
  21. * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
  22. *
  23. * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
  24. * in the United States and other countries.]
  25. *
  26. * --------------------------
  27. * BoxAndWhiskerRenderer.java
  28. * --------------------------
  29. * (C) Copyright 2003-2005, by David Browning and Contributors.
  30. *
  31. * Original Author: David Browning (for the Australian Institute of Marine
  32. * Science);
  33. * Contributor(s): David Gilbert (for Object Refinery Limited);
  34. * Tim Bardzil;
  35. *
  36. * $Id: BoxAndWhiskerRenderer.java,v 1.5 2005/03/09 11:45:38 mungady Exp $
  37. *
  38. * Changes
  39. * -------
  40. * 21-Aug-2003 : Version 1, contributed by David Browning (for the Australian
  41. * Institute of Marine Science);
  42. * 01-Sep-2003 : Incorporated outlier and farout symbols for low values
  43. * also (DG);
  44. * 08-Sep-2003 : Changed ValueAxis API (DG);
  45. * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
  46. * 07-Oct-2003 : Added renderer state (DG);
  47. * 12-Nov-2003 : Fixed casting bug reported by Tim Bardzil (DG);
  48. * 13-Nov-2003 : Added drawHorizontalItem() method contributed by Tim
  49. * Bardzil (DG);
  50. * 25-Apr-2004 : Added fillBox attribute, equals() method and added
  51. * serialization code (DG);
  52. * 29-Apr-2004 : Changed drawing of upper and lower shadows - see bug report
  53. * 944011 (DG);
  54. * 05-Nov-2004 : Modified drawItem() signature (DG);
  55. * 09-Mar-2005 : Override getLegendItem() method so that legend item shapes
  56. * are shown as blocks (DG);
  57. *
  58. */
  59. package org.jfree.chart.renderer.category;
  60. import java.awt.Color;
  61. import java.awt.Graphics2D;
  62. import java.awt.Paint;
  63. import java.awt.Shape;
  64. import java.awt.Stroke;
  65. import java.awt.geom.Ellipse2D;
  66. import java.awt.geom.Line2D;
  67. import java.awt.geom.Point2D;
  68. import java.awt.geom.Rectangle2D;
  69. import java.io.IOException;
  70. import java.io.ObjectInputStream;
  71. import java.io.ObjectOutputStream;
  72. import java.io.Serializable;
  73. import java.util.ArrayList;
  74. import java.util.Collections;
  75. import java.util.Iterator;
  76. import java.util.List;
  77. import org.jfree.chart.LegendItem;
  78. import org.jfree.chart.axis.CategoryAxis;
  79. import org.jfree.chart.axis.ValueAxis;
  80. import org.jfree.chart.entity.CategoryItemEntity;
  81. import org.jfree.chart.entity.EntityCollection;
  82. import org.jfree.chart.event.RendererChangeEvent;
  83. import org.jfree.chart.labels.CategoryToolTipGenerator;
  84. import org.jfree.chart.plot.CategoryPlot;
  85. import org.jfree.chart.plot.PlotOrientation;
  86. import org.jfree.chart.plot.PlotRenderingInfo;
  87. import org.jfree.chart.renderer.Outlier;
  88. import org.jfree.chart.renderer.OutlierList;
  89. import org.jfree.chart.renderer.OutlierListCollection;
  90. import org.jfree.data.category.CategoryDataset;
  91. import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset;
  92. import org.jfree.io.SerialUtilities;
  93. import org.jfree.ui.RectangleEdge;
  94. import org.jfree.util.ObjectUtilities;
  95. import org.jfree.util.PublicCloneable;
  96. /**
  97. * A box-and-whisker renderer.
  98. */
  99. public class BoxAndWhiskerRenderer extends AbstractCategoryItemRenderer
  100. implements Cloneable, PublicCloneable,
  101. Serializable {
  102. /** The color used to paint the median line and average marker. */
  103. private transient Paint artifactPaint;
  104. /** A flag that controls whether or not the box is filled. */
  105. private boolean fillBox;
  106. /** The margin between items (boxes) within a category. */
  107. private double itemMargin;
  108. /**
  109. * Default constructor.
  110. */
  111. public BoxAndWhiskerRenderer() {
  112. this.artifactPaint = Color.black;
  113. this.fillBox = true;
  114. this.itemMargin = 0.20;
  115. }
  116. /**
  117. * Returns the paint used to color the median and average markers.
  118. *
  119. * @return A paint.
  120. */
  121. public Paint getArtifactPaint() {
  122. return this.artifactPaint;
  123. }
  124. /**
  125. * Sets the paint used to color the median and average markers.
  126. *
  127. * @param paint the paint.
  128. */
  129. public void setArtifactPaint(Paint paint) {
  130. this.artifactPaint = paint;
  131. }
  132. /**
  133. * Returns the flag that controls whether or not the box is filled.
  134. *
  135. * @return A boolean.
  136. */
  137. public boolean getFillBox() {
  138. return this.fillBox;
  139. }
  140. /**
  141. * Sets the flag that controls whether or not the box is filled and sends a
  142. * {@link RendererChangeEvent} to all registered listeners.
  143. *
  144. * @param flag the flag.
  145. */
  146. public void setFillBox(boolean flag) {
  147. this.fillBox = flag;
  148. notifyListeners(new RendererChangeEvent(this));
  149. }
  150. /**
  151. * Returns the item margin. This is a percentage of the available space
  152. * that is allocated to the space between items in the chart.
  153. *
  154. * @return The margin.
  155. */
  156. public double getItemMargin() {
  157. return this.itemMargin;
  158. }
  159. /**
  160. * Sets the item margin.
  161. *
  162. * @param margin the margin.
  163. */
  164. public void setItemMargin(double margin) {
  165. this.itemMargin = margin;
  166. }
  167. /**
  168. * Returns a legend item for a series.
  169. *
  170. * @param datasetIndex the dataset index (zero-based).
  171. * @param series the series index (zero-based).
  172. *
  173. * @return The legend item.
  174. */
  175. public LegendItem getLegendItem(int datasetIndex, int series) {
  176. CategoryPlot cp = getPlot();
  177. if (cp == null) {
  178. return null;
  179. }
  180. CategoryDataset dataset;
  181. dataset = cp.getDataset(datasetIndex);
  182. String label = dataset.getRowKey(series).toString();
  183. String description = label;
  184. Shape shape = new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0);
  185. Paint paint = getSeriesPaint(series);
  186. Paint outlinePaint = getSeriesOutlinePaint(series);
  187. Stroke outlineStroke = getSeriesOutlineStroke(series);
  188. return new LegendItem(
  189. label, description, shape, paint, outlineStroke, outlinePaint
  190. );
  191. }
  192. /**
  193. * Initialises the renderer. This method gets called once at the start of
  194. * the process of drawing a chart.
  195. *
  196. * @param g2 the graphics device.
  197. * @param dataArea the area in which the data is to be plotted.
  198. * @param plot the plot.
  199. * @param rendererIndex the renderer index.
  200. * @param info collects chart rendering information for return to caller.
  201. *
  202. * @return The renderer state.
  203. */
  204. public CategoryItemRendererState initialise(Graphics2D g2,
  205. Rectangle2D dataArea,
  206. CategoryPlot plot,
  207. int rendererIndex,
  208. PlotRenderingInfo info) {
  209. CategoryItemRendererState state = super.initialise(
  210. g2, dataArea, plot, rendererIndex, info
  211. );
  212. // calculate the box width
  213. CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex);
  214. CategoryDataset dataset = plot.getDataset(rendererIndex);
  215. if (dataset != null) {
  216. int columns = dataset.getColumnCount();
  217. int rows = dataset.getRowCount();
  218. double space = 0.0;
  219. PlotOrientation orientation = plot.getOrientation();
  220. if (orientation == PlotOrientation.HORIZONTAL) {
  221. space = dataArea.getHeight();
  222. }
  223. else if (orientation == PlotOrientation.VERTICAL) {
  224. space = dataArea.getWidth();
  225. }
  226. double categoryMargin = 0.0;
  227. double currentItemMargin = 0.0;
  228. if (columns > 1) {
  229. categoryMargin = domainAxis.getCategoryMargin();
  230. }
  231. if (rows > 1) {
  232. currentItemMargin = getItemMargin();
  233. }
  234. double used = space * (1 - domainAxis.getLowerMargin()
  235. - domainAxis.getUpperMargin()
  236. - categoryMargin - currentItemMargin);
  237. if ((rows * columns) > 0) {
  238. state.setBarWidth(
  239. used / (dataset.getColumnCount() * dataset.getRowCount())
  240. );
  241. }
  242. else {
  243. state.setBarWidth(used);
  244. }
  245. }
  246. return state;
  247. }
  248. /**
  249. * Draw a single data item.
  250. *
  251. * @param g2 the graphics device.
  252. * @param state the renderer state.
  253. * @param dataArea the area in which the data is drawn.
  254. * @param plot the plot.
  255. * @param domainAxis the domain axis.
  256. * @param rangeAxis the range axis.
  257. * @param dataset the data.
  258. * @param row the row index (zero-based).
  259. * @param column the column index (zero-based).
  260. * @param pass the pass index.
  261. */
  262. public void drawItem(Graphics2D g2,
  263. CategoryItemRendererState state,
  264. Rectangle2D dataArea,
  265. CategoryPlot plot,
  266. CategoryAxis domainAxis,
  267. ValueAxis rangeAxis,
  268. CategoryDataset dataset,
  269. int row,
  270. int column,
  271. int pass) {
  272. if (!(dataset instanceof BoxAndWhiskerCategoryDataset)) {
  273. throw new IllegalArgumentException(
  274. "BoxAndWhiskerRenderer.drawItem() : the data should be of type "
  275. + "BoxAndWhiskerCategoryDataset only."
  276. );
  277. }
  278. PlotOrientation orientation = plot.getOrientation();
  279. if (orientation == PlotOrientation.HORIZONTAL) {
  280. drawHorizontalItem(
  281. g2, state, dataArea, plot, domainAxis, rangeAxis,
  282. dataset, row, column
  283. );
  284. }
  285. else if (orientation == PlotOrientation.VERTICAL) {
  286. drawVerticalItem(
  287. g2, state, dataArea, plot, domainAxis, rangeAxis,
  288. dataset, row, column
  289. );
  290. }
  291. }
  292. /**
  293. * Draws the visual representation of a single data item when the plot has
  294. * a horizontal orientation.
  295. *
  296. * @param g2 the graphics device.
  297. * @param state the renderer state.
  298. * @param dataArea the area within which the plot is being drawn.
  299. * @param plot the plot (can be used to obtain standard color
  300. * information etc).
  301. * @param domainAxis the domain axis.
  302. * @param rangeAxis the range axis.
  303. * @param dataset the dataset.
  304. * @param row the row index (zero-based).
  305. * @param column the column index (zero-based).
  306. */
  307. public void drawHorizontalItem(Graphics2D g2,
  308. CategoryItemRendererState state,
  309. Rectangle2D dataArea,
  310. CategoryPlot plot,
  311. CategoryAxis domainAxis,
  312. ValueAxis rangeAxis,
  313. CategoryDataset dataset,
  314. int row,
  315. int column) {
  316. BoxAndWhiskerCategoryDataset bawDataset
  317. = (BoxAndWhiskerCategoryDataset) dataset;
  318. double categoryEnd = domainAxis.getCategoryEnd(
  319. column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
  320. );
  321. double categoryStart = domainAxis.getCategoryStart(
  322. column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
  323. );
  324. double categoryWidth = Math.abs(categoryEnd - categoryStart);
  325. double yy = categoryStart;
  326. int seriesCount = getRowCount();
  327. int categoryCount = getColumnCount();
  328. if (seriesCount > 1) {
  329. double seriesGap = dataArea.getWidth() * getItemMargin()
  330. / (categoryCount * (seriesCount - 1));
  331. double usedWidth = (state.getBarWidth() * seriesCount)
  332. + (seriesGap * (seriesCount - 1));
  333. // offset the start of the boxes if the total width used is smaller
  334. // than the category width
  335. double offset = (categoryWidth - usedWidth) / 2;
  336. yy = yy + offset + (row * (state.getBarWidth() + seriesGap));
  337. }
  338. else {
  339. // offset the start of the box if the box width is smaller than
  340. // the category width
  341. double offset = (categoryWidth - state.getBarWidth()) / 2;
  342. yy = yy + offset;
  343. }
  344. Paint p = this.getItemPaint(row, column);
  345. if (p != null) {
  346. g2.setPaint(p);
  347. }
  348. Stroke s = getItemStroke(row, column);
  349. g2.setStroke(s);
  350. RectangleEdge location = plot.getRangeAxisEdge();
  351. Number xQ1 = bawDataset.getQ1Value(row, column);
  352. Number xQ3 = bawDataset.getQ3Value(row, column);
  353. Number xMax = bawDataset.getMaxRegularValue(row, column);
  354. Number xMin = bawDataset.getMinRegularValue(row, column);
  355. Shape box = null;
  356. if (xQ1 != null && xQ3 != null && xMax != null && xMin != null) {
  357. double xxQ1 = rangeAxis.valueToJava2D(
  358. xQ1.doubleValue(), dataArea, location
  359. );
  360. double xxQ3 = rangeAxis.valueToJava2D(
  361. xQ3.doubleValue(), dataArea, location
  362. );
  363. double xxMax = rangeAxis.valueToJava2D(
  364. xMax.doubleValue(), dataArea, location
  365. );
  366. double xxMin = rangeAxis.valueToJava2D(
  367. xMin.doubleValue(), dataArea, location
  368. );
  369. double yymid = yy + state.getBarWidth() / 2.0;
  370. // draw the upper shadow...
  371. g2.draw(new Line2D.Double(xxMax, yymid, xxQ3, yymid));
  372. g2.draw(
  373. new Line2D.Double(xxMax, yy, xxMax, yy + state.getBarWidth())
  374. );
  375. // draw the lower shadow...
  376. g2.draw(new Line2D.Double(xxMin, yymid, xxQ1, yymid));
  377. g2.draw(
  378. new Line2D.Double(xxMin, yy, xxMin, yy + state.getBarWidth())
  379. );
  380. // draw the box...
  381. box = new Rectangle2D.Double(
  382. Math.min(xxQ1, xxQ3), yy, Math.abs(xxQ1 - xxQ3),
  383. state.getBarWidth()
  384. );
  385. if (this.fillBox) {
  386. g2.fill(box);
  387. }
  388. g2.draw(box);
  389. }
  390. g2.setPaint(this.artifactPaint);
  391. double aRadius = 0; // average radius
  392. // draw mean - SPECIAL AIMS REQUIREMENT...
  393. Number xMean = bawDataset.getMeanValue(row, column);
  394. if (xMean != null) {
  395. double xxMean = rangeAxis.valueToJava2D(
  396. xMean.doubleValue(), dataArea, location
  397. );
  398. aRadius = state.getBarWidth() / 4;
  399. Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
  400. xxMean - aRadius, yy + aRadius, aRadius * 2, aRadius * 2
  401. );
  402. g2.fill(avgEllipse);
  403. g2.draw(avgEllipse);
  404. }
  405. // draw median...
  406. Number xMedian = bawDataset.getMedianValue(row, column);
  407. if (xMedian != null) {
  408. double xxMedian = rangeAxis.valueToJava2D(
  409. xMedian.doubleValue(), dataArea, location
  410. );
  411. g2.draw(
  412. new Line2D.Double(
  413. xxMedian, yy, xxMedian, yy + state.getBarWidth()
  414. )
  415. );
  416. }
  417. // collect entity and tool tip information...
  418. if (state.getInfo() != null) {
  419. EntityCollection entities
  420. = state.getInfo().getOwner().getEntityCollection();
  421. if (entities != null) {
  422. String tip = null;
  423. CategoryToolTipGenerator tipster
  424. = getToolTipGenerator(row, column);
  425. if (tipster != null) {
  426. tip = tipster.generateToolTip(dataset, row, column);
  427. }
  428. String url = null;
  429. if (getItemURLGenerator(row, column) != null) {
  430. url = getItemURLGenerator(row, column).generateURL(
  431. dataset, row, column
  432. );
  433. }
  434. CategoryItemEntity entity = new CategoryItemEntity(
  435. box, tip, url, dataset, row, dataset.getColumnKey(column),
  436. column
  437. );
  438. entities.add(entity);
  439. }
  440. }
  441. }
  442. /**
  443. * Draws the visual representation of a single data item when the plot has
  444. * a vertical orientation.
  445. *
  446. * @param g2 the graphics device.
  447. * @param state the renderer state.
  448. * @param dataArea the area within which the plot is being drawn.
  449. * @param plot the plot (can be used to obtain standard color information
  450. * etc).
  451. * @param domainAxis the domain axis.
  452. * @param rangeAxis the range axis.
  453. * @param dataset the dataset.
  454. * @param row the row index (zero-based).
  455. * @param column the column index (zero-based).
  456. */
  457. public void drawVerticalItem(Graphics2D g2,
  458. CategoryItemRendererState state,
  459. Rectangle2D dataArea,
  460. CategoryPlot plot,
  461. CategoryAxis domainAxis,
  462. ValueAxis rangeAxis,
  463. CategoryDataset dataset,
  464. int row,
  465. int column) {
  466. BoxAndWhiskerCategoryDataset bawDataset
  467. = (BoxAndWhiskerCategoryDataset) dataset;
  468. double categoryEnd = domainAxis.getCategoryEnd(
  469. column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
  470. );
  471. double categoryStart = domainAxis.getCategoryStart(
  472. column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
  473. );
  474. double categoryWidth = categoryEnd - categoryStart;
  475. double xx = categoryStart;
  476. int seriesCount = getRowCount();
  477. int categoryCount = getColumnCount();
  478. if (seriesCount > 1) {
  479. double seriesGap = dataArea.getWidth() * getItemMargin()
  480. / (categoryCount * (seriesCount - 1));
  481. double usedWidth = (state.getBarWidth() * seriesCount)
  482. + (seriesGap * (seriesCount - 1));
  483. // offset the start of the boxes if the total width used is smaller
  484. // than the category width
  485. double offset = (categoryWidth - usedWidth) / 2;
  486. xx = xx + offset + (row * (state.getBarWidth() + seriesGap));
  487. }
  488. else {
  489. // offset the start of the box if the box width is smaller than the
  490. // category width
  491. double offset = (categoryWidth - state.getBarWidth()) / 2;
  492. xx = xx + offset;
  493. }
  494. double yyAverage = 0.0;
  495. double yyOutlier;
  496. Paint p = this.getItemPaint(row, column);
  497. if (p != null) {
  498. g2.setPaint(p);
  499. }
  500. Stroke s = getItemStroke(row, column);
  501. g2.setStroke(s);
  502. double aRadius = 0; // average radius
  503. RectangleEdge location = plot.getRangeAxisEdge();
  504. Number yQ1 = bawDataset.getQ1Value(row, column);
  505. Number yQ3 = bawDataset.getQ3Value(row, column);
  506. Number yMax = bawDataset.getMaxRegularValue(row, column);
  507. Number yMin = bawDataset.getMinRegularValue(row, column);
  508. Shape box = null;
  509. if (yQ1 != null && yQ3 != null && yMax != null && yMin != null) {
  510. double yyQ1 = rangeAxis.valueToJava2D(
  511. yQ1.doubleValue(), dataArea, location
  512. );
  513. double yyQ3 = rangeAxis.valueToJava2D(
  514. yQ3.doubleValue(), dataArea, location
  515. );
  516. double yyMax = rangeAxis.valueToJava2D(
  517. yMax.doubleValue(), dataArea, location
  518. );
  519. double yyMin = rangeAxis.valueToJava2D(
  520. yMin.doubleValue(), dataArea, location
  521. );
  522. double xxmid = xx + state.getBarWidth() / 2.0;
  523. // draw the upper shadow...
  524. g2.draw(new Line2D.Double(xxmid, yyMax, xxmid, yyQ3));
  525. g2.draw(
  526. new Line2D.Double(xx, yyMax, xx + state.getBarWidth(), yyMax)
  527. );
  528. // draw the lower shadow...
  529. g2.draw(new Line2D.Double(xxmid, yyMin, xxmid, yyQ1));
  530. g2.draw(
  531. new Line2D.Double(xx, yyMin, xx + state.getBarWidth(), yyMin)
  532. );
  533. // draw the body...
  534. box = new Rectangle2D.Double(
  535. xx, Math.min(yyQ1, yyQ3), state.getBarWidth(),
  536. Math.abs(yyQ1 - yyQ3)
  537. );
  538. if (this.fillBox) {
  539. g2.fill(box);
  540. }
  541. g2.draw(box);
  542. }
  543. g2.setPaint(this.artifactPaint);
  544. // draw mean - SPECIAL AIMS REQUIREMENT...
  545. Number yMean = bawDataset.getMeanValue(row, column);
  546. if (yMean != null) {
  547. yyAverage = rangeAxis.valueToJava2D(
  548. yMean.doubleValue(), dataArea, location
  549. );
  550. aRadius = state.getBarWidth() / 4;
  551. Ellipse2D.Double avgEllipse = new Ellipse2D.Double(
  552. xx + aRadius, yyAverage - aRadius, aRadius * 2, aRadius * 2
  553. );
  554. g2.fill(avgEllipse);
  555. g2.draw(avgEllipse);
  556. }
  557. // draw median...
  558. Number yMedian = bawDataset.getMedianValue(row, column);
  559. if (yMedian != null) {
  560. double yyMedian = rangeAxis.valueToJava2D(
  561. yMedian.doubleValue(), dataArea, location
  562. );
  563. g2.draw(
  564. new Line2D.Double(
  565. xx, yyMedian, xx + state.getBarWidth(), yyMedian
  566. )
  567. );
  568. }
  569. // draw yOutliers...
  570. double maxAxisValue = rangeAxis.valueToJava2D(
  571. rangeAxis.getUpperBound(), dataArea, location
  572. ) + aRadius;
  573. double minAxisValue = rangeAxis.valueToJava2D(
  574. rangeAxis.getLowerBound(), dataArea, location
  575. ) - aRadius;
  576. g2.setPaint(p);
  577. // draw outliers
  578. double oRadius = state.getBarWidth() / 3; // outlier radius
  579. List outliers = new ArrayList();
  580. OutlierListCollection outlierListCollection
  581. = new OutlierListCollection();
  582. // From outlier array sort out which are outliers and put these into a
  583. // list If there are any farouts, set the flag on the
  584. // OutlierListCollection
  585. List yOutliers = bawDataset.getOutliers(row, column);
  586. if (yOutliers != null) {
  587. for (int i = 0; i < yOutliers.size(); i++) {
  588. double outlier = ((Number) yOutliers.get(i)).doubleValue();
  589. Number minOutlier = bawDataset.getMinOutlier(row, column);
  590. Number maxOutlier = bawDataset.getMaxOutlier(row, column);
  591. Number minRegular = bawDataset.getMinRegularValue(row, column);
  592. Number maxRegular = bawDataset.getMaxRegularValue(row, column);
  593. if (outlier > maxOutlier.doubleValue()) {
  594. outlierListCollection.setHighFarOut(true);
  595. }
  596. else if (outlier < minOutlier.doubleValue()) {
  597. outlierListCollection.setLowFarOut(true);
  598. }
  599. else if (outlier > maxRegular.doubleValue()) {
  600. yyOutlier = rangeAxis.valueToJava2D(
  601. outlier, dataArea, location
  602. );
  603. outliers.add(
  604. new Outlier(
  605. xx + state.getBarWidth() / 2.0, yyOutlier, oRadius
  606. )
  607. );
  608. }
  609. else if (outlier < minRegular.doubleValue()) {
  610. yyOutlier = rangeAxis.valueToJava2D(
  611. outlier, dataArea, location
  612. );
  613. outliers.add(
  614. new Outlier(
  615. xx + state.getBarWidth() / 2.0, yyOutlier, oRadius
  616. )
  617. );
  618. }
  619. Collections.sort(outliers);
  620. }
  621. // Process outliers. Each outlier is either added to the
  622. // appropriate outlier list or a new outlier list is made
  623. for (Iterator iterator = outliers.iterator(); iterator.hasNext();) {
  624. Outlier outlier = (Outlier) iterator.next();
  625. outlierListCollection.add(outlier);
  626. }
  627. for (Iterator iterator = outlierListCollection.iterator();
  628. iterator.hasNext();) {
  629. OutlierList list = (OutlierList) iterator.next();
  630. Outlier outlier = list.getAveragedOutlier();
  631. Point2D point = outlier.getPoint();
  632. if (list.isMultiple()) {
  633. drawMultipleEllipse(
  634. point, state.getBarWidth(), oRadius, g2
  635. );
  636. }
  637. else {
  638. drawEllipse(point, oRadius, g2);
  639. }
  640. }
  641. // draw farout indicators
  642. if (outlierListCollection.isHighFarOut()) {
  643. drawHighFarOut(
  644. aRadius / 2.0, g2, xx + state.getBarWidth() / 2.0,
  645. maxAxisValue
  646. );
  647. }
  648. if (outlierListCollection.isLowFarOut()) {
  649. drawLowFarOut(
  650. aRadius / 2.0, g2, xx + state.getBarWidth() / 2.0,
  651. minAxisValue
  652. );
  653. }
  654. }
  655. // collect entity and tool tip information...
  656. if (state.getInfo() != null) {
  657. EntityCollection entities
  658. = state.getInfo().getOwner().getEntityCollection();
  659. if (entities != null) {
  660. String tip = null;
  661. CategoryToolTipGenerator tipster
  662. = getToolTipGenerator(row, column);
  663. if (tipster != null) {
  664. tip = tipster.generateToolTip(dataset, row, column);
  665. }
  666. String url = null;
  667. if (getItemURLGenerator(row, column) != null) {
  668. url = getItemURLGenerator(row, column).generateURL(
  669. dataset, row, column
  670. );
  671. }
  672. CategoryItemEntity entity = new CategoryItemEntity(
  673. box, tip, url, dataset, row, dataset.getColumnKey(column),
  674. column
  675. );
  676. entities.add(entity);
  677. }
  678. }
  679. }
  680. /**
  681. * Draws a dot to represent an outlier.
  682. *
  683. * @param point the location.
  684. * @param oRadius the radius.
  685. * @param g2 the graphics device.
  686. */
  687. private void drawEllipse(Point2D point, double oRadius, Graphics2D g2) {
  688. Ellipse2D dot = new Ellipse2D.Double(
  689. point.getX() + oRadius / 2, point.getY(), oRadius, oRadius
  690. );
  691. g2.draw(dot);
  692. }
  693. /**
  694. * Draws two dots to represent the average value of more than one outlier.
  695. *
  696. * @param point the location
  697. * @param boxWidth the box width.
  698. * @param oRadius the radius.
  699. * @param g2 the graphics device.
  700. */
  701. private void drawMultipleEllipse(Point2D point, double boxWidth,
  702. double oRadius, Graphics2D g2) {
  703. Ellipse2D dot1 = new Ellipse2D.Double(
  704. point.getX() - (boxWidth / 2) + oRadius, point.getY(),
  705. oRadius, oRadius
  706. );
  707. Ellipse2D dot2 = new Ellipse2D.Double(
  708. point.getX() + (boxWidth / 2), point.getY(), oRadius, oRadius
  709. );
  710. g2.draw(dot1);
  711. g2.draw(dot2);
  712. }
  713. /**
  714. * Draws a triangle to indicate the presence of far-out values.
  715. *
  716. * @param aRadius the radius.
  717. * @param g2 the graphics device.
  718. * @param xx the x coordinate.
  719. * @param m the y coordinate.
  720. */
  721. private void drawHighFarOut(double aRadius, Graphics2D g2, double xx,
  722. double m) {
  723. double side = aRadius * 2;
  724. g2.draw(new Line2D.Double(xx - side, m + side, xx + side, m + side));
  725. g2.draw(new Line2D.Double(xx - side, m + side, xx, m));
  726. g2.draw(new Line2D.Double(xx + side, m + side, xx, m));
  727. }
  728. /**
  729. * Draws a triangle to indicate the presence of far-out values.
  730. *
  731. * @param aRadius the radius.
  732. * @param g2 the graphics device.
  733. * @param xx the x coordinate.
  734. * @param m the y coordinate.
  735. */
  736. private void drawLowFarOut(double aRadius, Graphics2D g2, double xx,
  737. double m) {
  738. double side = aRadius * 2;
  739. g2.draw(new Line2D.Double(xx - side, m - side, xx + side, m - side));
  740. g2.draw(new Line2D.Double(xx - side, m - side, xx, m));
  741. g2.draw(new Line2D.Double(xx + side, m - side, xx, m));
  742. }
  743. /**
  744. * Tests this renderer for equality with an arbitrary object.
  745. *
  746. * @param obj the object (<code>null</code> permitted).
  747. *
  748. * @return <code>true</code> or <code>false</code>.
  749. */
  750. public boolean equals(Object obj) {
  751. if (obj == this) {
  752. return true;
  753. }
  754. if (obj instanceof BoxAndWhiskerRenderer && super.equals(obj)) {
  755. BoxAndWhiskerRenderer r = (BoxAndWhiskerRenderer) obj;
  756. if (!ObjectUtilities.equal(r.artifactPaint, this.artifactPaint)) {
  757. return false;
  758. }
  759. if (!(r.fillBox == this.fillBox)) {
  760. return false;
  761. }
  762. if (!(r.itemMargin == this.itemMargin)) {
  763. return false;
  764. }
  765. return true;
  766. }
  767. return false;
  768. }
  769. /**
  770. * Provides serialization support.
  771. *
  772. * @param stream the output stream.
  773. *
  774. * @throws IOException if there is an I/O error.
  775. */
  776. private void writeObject(ObjectOutputStream stream) throws IOException {
  777. stream.defaultWriteObject();
  778. SerialUtilities.writePaint(this.artifactPaint, stream);
  779. }
  780. /**
  781. * Provides serialization support.
  782. *
  783. * @param stream the input stream.
  784. *
  785. * @throws IOException if there is an I/O error.
  786. * @throws ClassNotFoundException if there is a classpath problem.
  787. */
  788. private void readObject(ObjectInputStream stream)
  789. throws IOException, ClassNotFoundException {
  790. stream.defaultReadObject();
  791. this.artifactPaint = SerialUtilities.readPaint(stream);
  792. }
  793. }