001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ---------------- 028 * BarRenderer.java 029 * ---------------- 030 * (C) Copyright 2002-2008, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Christian W. Zuckschwerdt; 034 * 035 * Changes 036 * ------- 037 * 14-Mar-2002 : Version 1 (DG); 038 * 23-May-2002 : Added tooltip generator to renderer (DG); 039 * 29-May-2002 : Moved tooltip generator to abstract super-class (DG); 040 * 25-Jun-2002 : Changed constructor to protected and removed redundant 041 * code (DG); 042 * 26-Jun-2002 : Added axis to initialise method, and record upper and lower 043 * clip values (DG); 044 * 24-Sep-2002 : Added getLegendItem() method (DG); 045 * 09-Oct-2002 : Modified constructor to include URL generator (DG); 046 * 05-Nov-2002 : Base dataset is now TableDataset not CategoryDataset (DG); 047 * 10-Jan-2003 : Moved get/setItemMargin() method up from subclasses (DG); 048 * 17-Jan-2003 : Moved plot classes into a separate package (DG); 049 * 25-Mar-2003 : Implemented Serializable (DG); 050 * 01-May-2003 : Modified clipping to allow for dual axes and datasets (DG); 051 * 12-May-2003 : Merged horizontal and vertical bar renderers (DG); 052 * 12-Jun-2003 : Updates for item labels (DG); 053 * 30-Jul-2003 : Modified entity constructor (CZ); 054 * 02-Sep-2003 : Changed initialise method to fix bug 790407 (DG); 055 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 056 * 07-Oct-2003 : Added renderer state (DG); 057 * 27-Oct-2003 : Merged drawHorizontalItem() and drawVerticalItem() 058 * methods (DG); 059 * 28-Oct-2003 : Added support for gradient paint on bars (DG); 060 * 14-Nov-2003 : Added 'maxBarWidth' attribute (DG); 061 * 10-Feb-2004 : Small changes inside drawItem() method to ease cut-and-paste 062 * overriding (DG); 063 * 19-Mar-2004 : Fixed bug introduced with separation of tool tip and item 064 * label generators. Fixed equals() method (DG); 065 * 11-May-2004 : Fix for null pointer exception (bug id 951127) (DG); 066 * 05-Nov-2004 : Modified drawItem() signature (DG); 067 * 26-Jan-2005 : Provided override for getLegendItem() method (DG); 068 * 20-Apr-2005 : Generate legend labels, tooltips and URLs (DG); 069 * 18-May-2005 : Added configurable base value (DG); 070 * 09-Jun-2005 : Use addItemEntity() method from superclass (DG); 071 * 01-Dec-2005 : Update legend item to use/not use outline (DG); 072 * ------------: JFreeChart 1.0.x --------------------------------------------- 073 * 06-Dec-2005 : Fixed bug 1374222 (JDK 1.4 specific code) (DG); 074 * 11-Jan-2006 : Fixed bug 1401856 (bad rendering for non-zero base) (DG); 075 * 04-Aug-2006 : Fixed bug 1467706 (missing item labels for zero value 076 * bars) (DG); 077 * 04-Dec-2006 : Fixed bug in rendering to non-primary axis (DG); 078 * 13-Dec-2006 : Add support for GradientPaint display in legend items (DG); 079 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG); 080 * 11-May-2007 : Check for visibility in getLegendItem() (DG); 081 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG); 082 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); 083 * 07-May-2008 : If minimumBarLength is > 0.0, extend the non-base end of the 084 * bar (DG); 085 * 086 */ 087 088 package org.jfree.chart.renderer.category; 089 090 import java.awt.BasicStroke; 091 import java.awt.Color; 092 import java.awt.Font; 093 import java.awt.GradientPaint; 094 import java.awt.Graphics2D; 095 import java.awt.Paint; 096 import java.awt.Shape; 097 import java.awt.Stroke; 098 import java.awt.geom.Line2D; 099 import java.awt.geom.Point2D; 100 import java.awt.geom.Rectangle2D; 101 import java.io.Serializable; 102 103 import org.jfree.chart.LegendItem; 104 import org.jfree.chart.axis.CategoryAxis; 105 import org.jfree.chart.axis.ValueAxis; 106 import org.jfree.chart.entity.EntityCollection; 107 import org.jfree.chart.event.RendererChangeEvent; 108 import org.jfree.chart.labels.CategoryItemLabelGenerator; 109 import org.jfree.chart.labels.ItemLabelAnchor; 110 import org.jfree.chart.labels.ItemLabelPosition; 111 import org.jfree.chart.plot.CategoryPlot; 112 import org.jfree.chart.plot.PlotOrientation; 113 import org.jfree.chart.plot.PlotRenderingInfo; 114 import org.jfree.data.Range; 115 import org.jfree.data.category.CategoryDataset; 116 import org.jfree.data.general.DatasetUtilities; 117 import org.jfree.text.TextUtilities; 118 import org.jfree.ui.GradientPaintTransformer; 119 import org.jfree.ui.RectangleEdge; 120 import org.jfree.ui.StandardGradientPaintTransformer; 121 import org.jfree.util.ObjectUtilities; 122 import org.jfree.util.PublicCloneable; 123 124 /** 125 * A {@link CategoryItemRenderer} that draws individual data items as bars. 126 */ 127 public class BarRenderer extends AbstractCategoryItemRenderer 128 implements Cloneable, PublicCloneable, Serializable { 129 130 /** For serialization. */ 131 private static final long serialVersionUID = 6000649414965887481L; 132 133 /** The default item margin percentage. */ 134 public static final double DEFAULT_ITEM_MARGIN = 0.20; 135 136 /** 137 * Constant that controls the minimum width before a bar has an outline 138 * drawn. 139 */ 140 public static final double BAR_OUTLINE_WIDTH_THRESHOLD = 3.0; 141 142 /** The margin between items (bars) within a category. */ 143 private double itemMargin; 144 145 /** A flag that controls whether or not bar outlines are drawn. */ 146 private boolean drawBarOutline; 147 148 /** The maximum bar width as a percentage of the available space. */ 149 private double maximumBarWidth; 150 151 /** The minimum bar length (in Java2D units). */ 152 private double minimumBarLength; 153 154 /** 155 * An optional class used to transform gradient paint objects to fit each 156 * bar. 157 */ 158 private GradientPaintTransformer gradientPaintTransformer; 159 160 /** 161 * The fallback position if a positive item label doesn't fit inside the 162 * bar. 163 */ 164 private ItemLabelPosition positiveItemLabelPositionFallback; 165 166 /** 167 * The fallback position if a negative item label doesn't fit inside the 168 * bar. 169 */ 170 private ItemLabelPosition negativeItemLabelPositionFallback; 171 172 /** The upper clip (axis) value for the axis. */ 173 private double upperClip; 174 // TODO: this needs to move into the renderer state 175 176 /** The lower clip (axis) value for the axis. */ 177 private double lowerClip; 178 // TODO: this needs to move into the renderer state 179 180 /** The base value for the bars (defaults to 0.0). */ 181 private double base; 182 183 /** 184 * A flag that controls whether the base value is included in the range 185 * returned by the findRangeBounds() method. 186 */ 187 private boolean includeBaseInRange; 188 189 /** 190 * Creates a new bar renderer with default settings. 191 */ 192 public BarRenderer() { 193 super(); 194 this.base = 0.0; 195 this.includeBaseInRange = true; 196 this.itemMargin = DEFAULT_ITEM_MARGIN; 197 this.drawBarOutline = false; 198 this.maximumBarWidth = 1.0; 199 // 100 percent, so it will not apply unless changed 200 this.positiveItemLabelPositionFallback = null; 201 this.negativeItemLabelPositionFallback = null; 202 this.gradientPaintTransformer = new StandardGradientPaintTransformer(); 203 this.minimumBarLength = 0.0; 204 } 205 206 /** 207 * Returns the base value for the bars. The default value is 208 * <code>0.0</code>. 209 * 210 * @return The base value for the bars. 211 * 212 * @see #setBase(double) 213 */ 214 public double getBase() { 215 return this.base; 216 } 217 218 /** 219 * Sets the base value for the bars and sends a {@link RendererChangeEvent} 220 * to all registered listeners. 221 * 222 * @param base the new base value. 223 * 224 * @see #getBase() 225 */ 226 public void setBase(double base) { 227 this.base = base; 228 fireChangeEvent(); 229 } 230 231 /** 232 * Returns the item margin as a percentage of the available space for all 233 * bars. 234 * 235 * @return The margin percentage (where 0.10 is ten percent). 236 * 237 * @see #setItemMargin(double) 238 */ 239 public double getItemMargin() { 240 return this.itemMargin; 241 } 242 243 /** 244 * Sets the item margin and sends a {@link RendererChangeEvent} to all 245 * registered listeners. The value is expressed as a percentage of the 246 * available width for plotting all the bars, with the resulting amount to 247 * be distributed between all the bars evenly. 248 * 249 * @param percent the margin (where 0.10 is ten percent). 250 * 251 * @see #getItemMargin() 252 */ 253 public void setItemMargin(double percent) { 254 this.itemMargin = percent; 255 fireChangeEvent(); 256 } 257 258 /** 259 * Returns a flag that controls whether or not bar outlines are drawn. 260 * 261 * @return A boolean. 262 * 263 * @see #setDrawBarOutline(boolean) 264 */ 265 public boolean isDrawBarOutline() { 266 return this.drawBarOutline; 267 } 268 269 /** 270 * Sets the flag that controls whether or not bar outlines are drawn and 271 * sends a {@link RendererChangeEvent} to all registered listeners. 272 * 273 * @param draw the flag. 274 * 275 * @see #isDrawBarOutline() 276 */ 277 public void setDrawBarOutline(boolean draw) { 278 this.drawBarOutline = draw; 279 fireChangeEvent(); 280 } 281 282 /** 283 * Returns the maximum bar width, as a percentage of the available drawing 284 * space. 285 * 286 * @return The maximum bar width. 287 * 288 * @see #setMaximumBarWidth(double) 289 */ 290 public double getMaximumBarWidth() { 291 return this.maximumBarWidth; 292 } 293 294 /** 295 * Sets the maximum bar width, which is specified as a percentage of the 296 * available space for all bars, and sends a {@link RendererChangeEvent} to 297 * all registered listeners. 298 * 299 * @param percent the percent (where 0.05 is five percent). 300 * 301 * @see #getMaximumBarWidth() 302 */ 303 public void setMaximumBarWidth(double percent) { 304 this.maximumBarWidth = percent; 305 fireChangeEvent(); 306 } 307 308 /** 309 * Returns the minimum bar length (in Java2D units). The default value is 310 * 0.0. 311 * 312 * @return The minimum bar length. 313 * 314 * @see #setMinimumBarLength(double) 315 */ 316 public double getMinimumBarLength() { 317 return this.minimumBarLength; 318 } 319 320 /** 321 * Sets the minimum bar length and sends a {@link RendererChangeEvent} to 322 * all registered listeners. The minimum bar length is specified in Java2D 323 * units, and can be used to prevent bars that represent very small data 324 * values from disappearing when drawn on the screen. Typically you would 325 * set this to (say) 0.5 or 1.0 Java 2D units. Use this attribute with 326 * caution, however, because setting it to a non-zero value will 327 * artificially increase the length of bars representing small values, 328 * which may misrepresent your data. 329 * 330 * @param min the minimum bar length (in Java2D units, must be >= 0.0). 331 * 332 * @see #getMinimumBarLength() 333 */ 334 public void setMinimumBarLength(double min) { 335 if (min < 0.0) { 336 throw new IllegalArgumentException("Requires 'min' >= 0.0"); 337 } 338 this.minimumBarLength = min; 339 fireChangeEvent(); 340 } 341 342 /** 343 * Returns the gradient paint transformer (an object used to transform 344 * gradient paint objects to fit each bar). 345 * 346 * @return A transformer (<code>null</code> possible). 347 * 348 * @see #setGradientPaintTransformer(GradientPaintTransformer) 349 */ 350 public GradientPaintTransformer getGradientPaintTransformer() { 351 return this.gradientPaintTransformer; 352 } 353 354 /** 355 * Sets the gradient paint transformer and sends a 356 * {@link RendererChangeEvent} to all registered listeners. 357 * 358 * @param transformer the transformer (<code>null</code> permitted). 359 * 360 * @see #getGradientPaintTransformer() 361 */ 362 public void setGradientPaintTransformer( 363 GradientPaintTransformer transformer) { 364 this.gradientPaintTransformer = transformer; 365 fireChangeEvent(); 366 } 367 368 /** 369 * Returns the fallback position for positive item labels that don't fit 370 * within a bar. 371 * 372 * @return The fallback position (<code>null</code> possible). 373 * 374 * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) 375 */ 376 public ItemLabelPosition getPositiveItemLabelPositionFallback() { 377 return this.positiveItemLabelPositionFallback; 378 } 379 380 /** 381 * Sets the fallback position for positive item labels that don't fit 382 * within a bar, and sends a {@link RendererChangeEvent} to all registered 383 * listeners. 384 * 385 * @param position the position (<code>null</code> permitted). 386 * 387 * @see #getPositiveItemLabelPositionFallback() 388 */ 389 public void setPositiveItemLabelPositionFallback( 390 ItemLabelPosition position) { 391 this.positiveItemLabelPositionFallback = position; 392 fireChangeEvent(); 393 } 394 395 /** 396 * Returns the fallback position for negative item labels that don't fit 397 * within a bar. 398 * 399 * @return The fallback position (<code>null</code> possible). 400 * 401 * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) 402 */ 403 public ItemLabelPosition getNegativeItemLabelPositionFallback() { 404 return this.negativeItemLabelPositionFallback; 405 } 406 407 /** 408 * Sets the fallback position for negative item labels that don't fit 409 * within a bar, and sends a {@link RendererChangeEvent} to all registered 410 * listeners. 411 * 412 * @param position the position (<code>null</code> permitted). 413 * 414 * @see #getNegativeItemLabelPositionFallback() 415 */ 416 public void setNegativeItemLabelPositionFallback( 417 ItemLabelPosition position) { 418 this.negativeItemLabelPositionFallback = position; 419 fireChangeEvent(); 420 } 421 422 /** 423 * Returns the flag that controls whether or not the base value for the 424 * bars is included in the range calculated by 425 * {@link #findRangeBounds(CategoryDataset)}. 426 * 427 * @return <code>true</code> if the base is included in the range, and 428 * <code>false</code> otherwise. 429 * 430 * @since 1.0.1 431 * 432 * @see #setIncludeBaseInRange(boolean) 433 */ 434 public boolean getIncludeBaseInRange() { 435 return this.includeBaseInRange; 436 } 437 438 /** 439 * Sets the flag that controls whether or not the base value for the bars 440 * is included in the range calculated by 441 * {@link #findRangeBounds(CategoryDataset)}. If the flag is changed, 442 * a {@link RendererChangeEvent} is sent to all registered listeners. 443 * 444 * @param include the new value for the flag. 445 * 446 * @since 1.0.1 447 * 448 * @see #getIncludeBaseInRange() 449 */ 450 public void setIncludeBaseInRange(boolean include) { 451 if (this.includeBaseInRange != include) { 452 this.includeBaseInRange = include; 453 fireChangeEvent(); 454 } 455 } 456 457 /** 458 * Returns the lower clip value. This value is recalculated in the 459 * initialise() method. 460 * 461 * @return The value. 462 */ 463 public double getLowerClip() { 464 // TODO: this attribute should be transferred to the renderer state. 465 return this.lowerClip; 466 } 467 468 /** 469 * Returns the upper clip value. This value is recalculated in the 470 * initialise() method. 471 * 472 * @return The value. 473 */ 474 public double getUpperClip() { 475 // TODO: this attribute should be transferred to the renderer state. 476 return this.upperClip; 477 } 478 479 /** 480 * Initialises the renderer and returns a state object that will be passed 481 * to subsequent calls to the drawItem method. This method gets called 482 * once at the start of the process of drawing a chart. 483 * 484 * @param g2 the graphics device. 485 * @param dataArea the area in which the data is to be plotted. 486 * @param plot the plot. 487 * @param rendererIndex the renderer index. 488 * @param info collects chart rendering information for return to caller. 489 * 490 * @return The renderer state. 491 */ 492 public CategoryItemRendererState initialise(Graphics2D g2, 493 Rectangle2D dataArea, 494 CategoryPlot plot, 495 int rendererIndex, 496 PlotRenderingInfo info) { 497 498 CategoryItemRendererState state = super.initialise(g2, dataArea, plot, 499 rendererIndex, info); 500 501 // get the clipping values... 502 ValueAxis rangeAxis = plot.getRangeAxisForDataset(rendererIndex); 503 this.lowerClip = rangeAxis.getRange().getLowerBound(); 504 this.upperClip = rangeAxis.getRange().getUpperBound(); 505 506 // calculate the bar width 507 calculateBarWidth(plot, dataArea, rendererIndex, state); 508 509 return state; 510 511 } 512 513 /** 514 * Calculates the bar width and stores it in the renderer state. 515 * 516 * @param plot the plot. 517 * @param dataArea the data area. 518 * @param rendererIndex the renderer index. 519 * @param state the renderer state. 520 */ 521 protected void calculateBarWidth(CategoryPlot plot, 522 Rectangle2D dataArea, 523 int rendererIndex, 524 CategoryItemRendererState state) { 525 526 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); 527 CategoryDataset dataset = plot.getDataset(rendererIndex); 528 if (dataset != null) { 529 int columns = dataset.getColumnCount(); 530 int rows = dataset.getRowCount(); 531 double space = 0.0; 532 PlotOrientation orientation = plot.getOrientation(); 533 if (orientation == PlotOrientation.HORIZONTAL) { 534 space = dataArea.getHeight(); 535 } 536 else if (orientation == PlotOrientation.VERTICAL) { 537 space = dataArea.getWidth(); 538 } 539 double maxWidth = space * getMaximumBarWidth(); 540 double categoryMargin = 0.0; 541 double currentItemMargin = 0.0; 542 if (columns > 1) { 543 categoryMargin = domainAxis.getCategoryMargin(); 544 } 545 if (rows > 1) { 546 currentItemMargin = getItemMargin(); 547 } 548 double used = space * (1 - domainAxis.getLowerMargin() 549 - domainAxis.getUpperMargin() 550 - categoryMargin - currentItemMargin); 551 if ((rows * columns) > 0) { 552 state.setBarWidth(Math.min(used / (rows * columns), maxWidth)); 553 } 554 else { 555 state.setBarWidth(Math.min(used, maxWidth)); 556 } 557 } 558 } 559 560 /** 561 * Calculates the coordinate of the first "side" of a bar. This will be 562 * the minimum x-coordinate for a vertical bar, and the minimum 563 * y-coordinate for a horizontal bar. 564 * 565 * @param plot the plot. 566 * @param orientation the plot orientation. 567 * @param dataArea the data area. 568 * @param domainAxis the domain axis. 569 * @param state the renderer state (has the bar width precalculated). 570 * @param row the row index. 571 * @param column the column index. 572 * 573 * @return The coordinate. 574 */ 575 protected double calculateBarW0(CategoryPlot plot, 576 PlotOrientation orientation, 577 Rectangle2D dataArea, 578 CategoryAxis domainAxis, 579 CategoryItemRendererState state, 580 int row, 581 int column) { 582 // calculate bar width... 583 double space = 0.0; 584 if (orientation == PlotOrientation.HORIZONTAL) { 585 space = dataArea.getHeight(); 586 } 587 else { 588 space = dataArea.getWidth(); 589 } 590 double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 591 dataArea, plot.getDomainAxisEdge()); 592 int seriesCount = getRowCount(); 593 int categoryCount = getColumnCount(); 594 if (seriesCount > 1) { 595 double seriesGap = space * getItemMargin() 596 / (categoryCount * (seriesCount - 1)); 597 double seriesW = calculateSeriesWidth(space, domainAxis, 598 categoryCount, seriesCount); 599 barW0 = barW0 + row * (seriesW + seriesGap) 600 + (seriesW / 2.0) - (state.getBarWidth() / 2.0); 601 } 602 else { 603 barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 604 dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() 605 / 2.0; 606 } 607 return barW0; 608 } 609 610 /** 611 * Calculates the coordinates for the length of a single bar. 612 * 613 * @param value the value represented by the bar. 614 * 615 * @return The coordinates for each end of the bar (or <code>null</code> if 616 * the bar is not visible for the current axis range). 617 */ 618 protected double[] calculateBarL0L1(double value) { 619 double lclip = getLowerClip(); 620 double uclip = getUpperClip(); 621 double barLow = Math.min(this.base, value); 622 double barHigh = Math.max(this.base, value); 623 if (barHigh < lclip) { // bar is not visible 624 return null; 625 } 626 if (barLow > uclip) { // bar is not visible 627 return null; 628 } 629 barLow = Math.max(barLow, lclip); 630 barHigh = Math.min(barHigh, uclip); 631 return new double[] {barLow, barHigh}; 632 } 633 634 /** 635 * Returns the range of values the renderer requires to display all the 636 * items from the specified dataset. This takes into account the range 637 * of values in the dataset, plus the flag that determines whether or not 638 * the base value for the bars should be included in the range. 639 * 640 * @param dataset the dataset (<code>null</code> permitted). 641 * 642 * @return The range (or <code>null</code> if the dataset is 643 * <code>null</code> or empty). 644 */ 645 public Range findRangeBounds(CategoryDataset dataset) { 646 Range result = DatasetUtilities.findRangeBounds(dataset); 647 if (result != null) { 648 if (this.includeBaseInRange) { 649 result = Range.expandToInclude(result, this.base); 650 } 651 } 652 return result; 653 } 654 655 /** 656 * Returns a legend item for a series. 657 * 658 * @param datasetIndex the dataset index (zero-based). 659 * @param series the series index (zero-based). 660 * 661 * @return The legend item (possibly <code>null</code>). 662 */ 663 public LegendItem getLegendItem(int datasetIndex, int series) { 664 665 CategoryPlot cp = getPlot(); 666 if (cp == null) { 667 return null; 668 } 669 670 // check that a legend item needs to be displayed... 671 if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) { 672 return null; 673 } 674 675 CategoryDataset dataset = cp.getDataset(datasetIndex); 676 String label = getLegendItemLabelGenerator().generateLabel(dataset, 677 series); 678 String description = label; 679 String toolTipText = null; 680 if (getLegendItemToolTipGenerator() != null) { 681 toolTipText = getLegendItemToolTipGenerator().generateLabel( 682 dataset, series); 683 } 684 String urlText = null; 685 if (getLegendItemURLGenerator() != null) { 686 urlText = getLegendItemURLGenerator().generateLabel(dataset, 687 series); 688 } 689 Shape shape = new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0); 690 Paint paint = lookupSeriesPaint(series); 691 Paint outlinePaint = lookupSeriesOutlinePaint(series); 692 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 693 694 LegendItem result = new LegendItem(label, description, toolTipText, 695 urlText, true, shape, true, paint, isDrawBarOutline(), 696 outlinePaint, outlineStroke, false, new Line2D.Float(), 697 new BasicStroke(1.0f), Color.black); 698 result.setDataset(dataset); 699 result.setDatasetIndex(datasetIndex); 700 result.setSeriesKey(dataset.getRowKey(series)); 701 result.setSeriesIndex(series); 702 if (this.gradientPaintTransformer != null) { 703 result.setFillPaintTransformer(this.gradientPaintTransformer); 704 } 705 return result; 706 } 707 708 /** 709 * Draws the bar for a single (series, category) data item. 710 * 711 * @param g2 the graphics device. 712 * @param state the renderer state. 713 * @param dataArea the data area. 714 * @param plot the plot. 715 * @param domainAxis the domain axis. 716 * @param rangeAxis the range axis. 717 * @param dataset the dataset. 718 * @param row the row index (zero-based). 719 * @param column the column index (zero-based). 720 * @param pass the pass index. 721 */ 722 public void drawItem(Graphics2D g2, 723 CategoryItemRendererState state, 724 Rectangle2D dataArea, 725 CategoryPlot plot, 726 CategoryAxis domainAxis, 727 ValueAxis rangeAxis, 728 CategoryDataset dataset, 729 int row, 730 int column, 731 int pass) { 732 733 // nothing is drawn for null values... 734 Number dataValue = dataset.getValue(row, column); 735 if (dataValue == null) { 736 return; 737 } 738 739 double value = dataValue.doubleValue(); 740 PlotOrientation orientation = plot.getOrientation(); 741 double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, 742 state, row, column); 743 double[] barL0L1 = calculateBarL0L1(value); 744 if (barL0L1 == null) { 745 return; // the bar is not visible 746 } 747 748 RectangleEdge edge = plot.getRangeAxisEdge(); 749 double transL0 = rangeAxis.valueToJava2D(barL0L1[0], dataArea, edge); 750 double transL1 = rangeAxis.valueToJava2D(barL0L1[1], dataArea, edge); 751 752 // in the following code, barL0 is (in Java2D coordinates) the LEFT 753 // end of the bar for a horizontal bar chart, and the TOP end of the 754 // bar for a vertical bar chart. Whether this is the BASE of the bar 755 // or not depends also on (a) whether the data value is 'negative' 756 // relative to the base value and (b) whether or not the range axis is 757 // inverted. This only matters if/when we apply the minimumBarLength 758 // attribute, because we should extend the non-base end of the bar 759 boolean positive = (value >= this.base); 760 boolean inverted = rangeAxis.isInverted(); 761 double barL0 = Math.min(transL0, transL1); 762 double barLength = Math.abs(transL1 - transL0); 763 double barLengthAdj = 0.0; 764 if (barLength > 0.0 && barLength < getMinimumBarLength()) { 765 barLengthAdj = getMinimumBarLength() - barLength; 766 } 767 double barL0Adj = 0.0; 768 if (orientation == PlotOrientation.HORIZONTAL) { 769 if (positive && inverted || !positive && !inverted) { 770 barL0Adj = barLengthAdj; 771 } 772 } 773 else { 774 if (positive && !inverted || !positive && inverted) { 775 barL0Adj = barLengthAdj; 776 } 777 } 778 779 // draw the bar... 780 Rectangle2D bar = null; 781 if (orientation == PlotOrientation.HORIZONTAL) { 782 bar = new Rectangle2D.Double(barL0 - barL0Adj, barW0, 783 barLength + barLengthAdj, state.getBarWidth()); 784 } 785 else { 786 bar = new Rectangle2D.Double(barW0, barL0 - barL0Adj, 787 state.getBarWidth(), barLength + barLengthAdj); 788 } 789 Paint itemPaint = getItemPaint(row, column); 790 GradientPaintTransformer t = getGradientPaintTransformer(); 791 if (t != null && itemPaint instanceof GradientPaint) { 792 itemPaint = t.transform((GradientPaint) itemPaint, bar); 793 } 794 g2.setPaint(itemPaint); 795 g2.fill(bar); 796 797 // draw the outline... 798 if (isDrawBarOutline() 799 && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) { 800 Stroke stroke = getItemOutlineStroke(row, column); 801 Paint paint = getItemOutlinePaint(row, column); 802 if (stroke != null && paint != null) { 803 g2.setStroke(stroke); 804 g2.setPaint(paint); 805 g2.draw(bar); 806 } 807 } 808 809 CategoryItemLabelGenerator generator 810 = getItemLabelGenerator(row, column); 811 if (generator != null && isItemLabelVisible(row, column)) { 812 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 813 (value < 0.0)); 814 } 815 816 // add an item entity, if this information is being collected 817 EntityCollection entities = state.getEntityCollection(); 818 if (entities != null) { 819 addItemEntity(entities, dataset, row, column, bar); 820 } 821 822 } 823 824 /** 825 * Calculates the available space for each series. 826 * 827 * @param space the space along the entire axis (in Java2D units). 828 * @param axis the category axis. 829 * @param categories the number of categories. 830 * @param series the number of series. 831 * 832 * @return The width of one series. 833 */ 834 protected double calculateSeriesWidth(double space, CategoryAxis axis, 835 int categories, int series) { 836 double factor = 1.0 - getItemMargin() - axis.getLowerMargin() 837 - axis.getUpperMargin(); 838 if (categories > 1) { 839 factor = factor - axis.getCategoryMargin(); 840 } 841 return (space * factor) / (categories * series); 842 } 843 844 /** 845 * Draws an item label. This method is overridden so that the bar can be 846 * used to calculate the label anchor point. 847 * 848 * @param g2 the graphics device. 849 * @param data the dataset. 850 * @param row the row. 851 * @param column the column. 852 * @param plot the plot. 853 * @param generator the label generator. 854 * @param bar the bar. 855 * @param negative a flag indicating a negative value. 856 */ 857 protected void drawItemLabel(Graphics2D g2, 858 CategoryDataset data, 859 int row, 860 int column, 861 CategoryPlot plot, 862 CategoryItemLabelGenerator generator, 863 Rectangle2D bar, 864 boolean negative) { 865 866 String label = generator.generateLabel(data, row, column); 867 if (label == null) { 868 return; // nothing to do 869 } 870 871 Font labelFont = getItemLabelFont(row, column); 872 g2.setFont(labelFont); 873 Paint paint = getItemLabelPaint(row, column); 874 g2.setPaint(paint); 875 876 // find out where to place the label... 877 ItemLabelPosition position = null; 878 if (!negative) { 879 position = getPositiveItemLabelPosition(row, column); 880 } 881 else { 882 position = getNegativeItemLabelPosition(row, column); 883 } 884 885 // work out the label anchor point... 886 Point2D anchorPoint = calculateLabelAnchorPoint( 887 position.getItemLabelAnchor(), bar, plot.getOrientation()); 888 889 if (isInternalAnchor(position.getItemLabelAnchor())) { 890 Shape bounds = TextUtilities.calculateRotatedStringBounds(label, 891 g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(), 892 position.getTextAnchor(), position.getAngle(), 893 position.getRotationAnchor()); 894 895 if (bounds != null) { 896 if (!bar.contains(bounds.getBounds2D())) { 897 if (!negative) { 898 position = getPositiveItemLabelPositionFallback(); 899 } 900 else { 901 position = getNegativeItemLabelPositionFallback(); 902 } 903 if (position != null) { 904 anchorPoint = calculateLabelAnchorPoint( 905 position.getItemLabelAnchor(), bar, 906 plot.getOrientation()); 907 } 908 } 909 } 910 911 } 912 913 if (position != null) { 914 TextUtilities.drawRotatedString(label, g2, 915 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 916 position.getTextAnchor(), position.getAngle(), 917 position.getRotationAnchor()); 918 } 919 } 920 921 /** 922 * Calculates the item label anchor point. 923 * 924 * @param anchor the anchor. 925 * @param bar the bar. 926 * @param orientation the plot orientation. 927 * 928 * @return The anchor point. 929 */ 930 private Point2D calculateLabelAnchorPoint(ItemLabelAnchor anchor, 931 Rectangle2D bar, 932 PlotOrientation orientation) { 933 934 Point2D result = null; 935 double offset = getItemLabelAnchorOffset(); 936 double x0 = bar.getX() - offset; 937 double x1 = bar.getX(); 938 double x2 = bar.getX() + offset; 939 double x3 = bar.getCenterX(); 940 double x4 = bar.getMaxX() - offset; 941 double x5 = bar.getMaxX(); 942 double x6 = bar.getMaxX() + offset; 943 944 double y0 = bar.getMaxY() + offset; 945 double y1 = bar.getMaxY(); 946 double y2 = bar.getMaxY() - offset; 947 double y3 = bar.getCenterY(); 948 double y4 = bar.getMinY() + offset; 949 double y5 = bar.getMinY(); 950 double y6 = bar.getMinY() - offset; 951 952 if (anchor == ItemLabelAnchor.CENTER) { 953 result = new Point2D.Double(x3, y3); 954 } 955 else if (anchor == ItemLabelAnchor.INSIDE1) { 956 result = new Point2D.Double(x4, y4); 957 } 958 else if (anchor == ItemLabelAnchor.INSIDE2) { 959 result = new Point2D.Double(x4, y4); 960 } 961 else if (anchor == ItemLabelAnchor.INSIDE3) { 962 result = new Point2D.Double(x4, y3); 963 } 964 else if (anchor == ItemLabelAnchor.INSIDE4) { 965 result = new Point2D.Double(x4, y2); 966 } 967 else if (anchor == ItemLabelAnchor.INSIDE5) { 968 result = new Point2D.Double(x4, y2); 969 } 970 else if (anchor == ItemLabelAnchor.INSIDE6) { 971 result = new Point2D.Double(x3, y2); 972 } 973 else if (anchor == ItemLabelAnchor.INSIDE7) { 974 result = new Point2D.Double(x2, y2); 975 } 976 else if (anchor == ItemLabelAnchor.INSIDE8) { 977 result = new Point2D.Double(x2, y2); 978 } 979 else if (anchor == ItemLabelAnchor.INSIDE9) { 980 result = new Point2D.Double(x2, y3); 981 } 982 else if (anchor == ItemLabelAnchor.INSIDE10) { 983 result = new Point2D.Double(x2, y4); 984 } 985 else if (anchor == ItemLabelAnchor.INSIDE11) { 986 result = new Point2D.Double(x2, y4); 987 } 988 else if (anchor == ItemLabelAnchor.INSIDE12) { 989 result = new Point2D.Double(x3, y4); 990 } 991 else if (anchor == ItemLabelAnchor.OUTSIDE1) { 992 result = new Point2D.Double(x5, y6); 993 } 994 else if (anchor == ItemLabelAnchor.OUTSIDE2) { 995 result = new Point2D.Double(x6, y5); 996 } 997 else if (anchor == ItemLabelAnchor.OUTSIDE3) { 998 result = new Point2D.Double(x6, y3); 999 } 1000 else if (anchor == ItemLabelAnchor.OUTSIDE4) { 1001 result = new Point2D.Double(x6, y1); 1002 } 1003 else if (anchor == ItemLabelAnchor.OUTSIDE5) { 1004 result = new Point2D.Double(x5, y0); 1005 } 1006 else if (anchor == ItemLabelAnchor.OUTSIDE6) { 1007 result = new Point2D.Double(x3, y0); 1008 } 1009 else if (anchor == ItemLabelAnchor.OUTSIDE7) { 1010 result = new Point2D.Double(x1, y0); 1011 } 1012 else if (anchor == ItemLabelAnchor.OUTSIDE8) { 1013 result = new Point2D.Double(x0, y1); 1014 } 1015 else if (anchor == ItemLabelAnchor.OUTSIDE9) { 1016 result = new Point2D.Double(x0, y3); 1017 } 1018 else if (anchor == ItemLabelAnchor.OUTSIDE10) { 1019 result = new Point2D.Double(x0, y5); 1020 } 1021 else if (anchor == ItemLabelAnchor.OUTSIDE11) { 1022 result = new Point2D.Double(x1, y6); 1023 } 1024 else if (anchor == ItemLabelAnchor.OUTSIDE12) { 1025 result = new Point2D.Double(x3, y6); 1026 } 1027 1028 return result; 1029 1030 } 1031 1032 /** 1033 * Returns <code>true</code> if the specified anchor point is inside a bar. 1034 * 1035 * @param anchor the anchor point. 1036 * 1037 * @return A boolean. 1038 */ 1039 private boolean isInternalAnchor(ItemLabelAnchor anchor) { 1040 return anchor == ItemLabelAnchor.CENTER 1041 || anchor == ItemLabelAnchor.INSIDE1 1042 || anchor == ItemLabelAnchor.INSIDE2 1043 || anchor == ItemLabelAnchor.INSIDE3 1044 || anchor == ItemLabelAnchor.INSIDE4 1045 || anchor == ItemLabelAnchor.INSIDE5 1046 || anchor == ItemLabelAnchor.INSIDE6 1047 || anchor == ItemLabelAnchor.INSIDE7 1048 || anchor == ItemLabelAnchor.INSIDE8 1049 || anchor == ItemLabelAnchor.INSIDE9 1050 || anchor == ItemLabelAnchor.INSIDE10 1051 || anchor == ItemLabelAnchor.INSIDE11 1052 || anchor == ItemLabelAnchor.INSIDE12; 1053 } 1054 1055 /** 1056 * Tests this instance for equality with an arbitrary object. 1057 * 1058 * @param obj the object (<code>null</code> permitted). 1059 * 1060 * @return A boolean. 1061 */ 1062 public boolean equals(Object obj) { 1063 1064 if (obj == this) { 1065 return true; 1066 } 1067 if (!(obj instanceof BarRenderer)) { 1068 return false; 1069 } 1070 if (!super.equals(obj)) { 1071 return false; 1072 } 1073 BarRenderer that = (BarRenderer) obj; 1074 if (this.base != that.base) { 1075 return false; 1076 } 1077 if (this.itemMargin != that.itemMargin) { 1078 return false; 1079 } 1080 if (this.drawBarOutline != that.drawBarOutline) { 1081 return false; 1082 } 1083 if (this.maximumBarWidth != that.maximumBarWidth) { 1084 return false; 1085 } 1086 if (this.minimumBarLength != that.minimumBarLength) { 1087 return false; 1088 } 1089 if (!ObjectUtilities.equal(this.gradientPaintTransformer, 1090 that.gradientPaintTransformer)) { 1091 return false; 1092 } 1093 if (!ObjectUtilities.equal(this.positiveItemLabelPositionFallback, 1094 that.positiveItemLabelPositionFallback)) { 1095 return false; 1096 } 1097 if (!ObjectUtilities.equal(this.negativeItemLabelPositionFallback, 1098 that.negativeItemLabelPositionFallback)) { 1099 return false; 1100 } 1101 return true; 1102 1103 } 1104 1105 }