001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ---------------- 028 * BarRenderer.java 029 * ---------------- 030 * (C) Copyright 2002-2020, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Christian W. Zuckschwerdt; 034 * Peter Kolb (patches 2497611, 2791407); 035 * 036 */ 037 038package org.jfree.chart.renderer.category; 039 040import java.awt.BasicStroke; 041import java.awt.Color; 042import java.awt.Font; 043import java.awt.Graphics2D; 044import java.awt.Paint; 045import java.awt.Shape; 046import java.awt.Stroke; 047import java.awt.geom.Line2D; 048import java.awt.geom.Point2D; 049import java.awt.geom.Rectangle2D; 050import java.io.IOException; 051import java.io.ObjectInputStream; 052import java.io.ObjectOutputStream; 053import java.io.Serializable; 054 055import org.jfree.chart.LegendItem; 056import org.jfree.chart.axis.CategoryAxis; 057import org.jfree.chart.axis.ValueAxis; 058import org.jfree.chart.entity.EntityCollection; 059import org.jfree.chart.event.RendererChangeEvent; 060import org.jfree.chart.labels.CategoryItemLabelGenerator; 061import org.jfree.chart.labels.ItemLabelAnchor; 062import org.jfree.chart.labels.ItemLabelPosition; 063import org.jfree.chart.plot.CategoryPlot; 064import org.jfree.chart.plot.PlotOrientation; 065import org.jfree.chart.plot.PlotRenderingInfo; 066import org.jfree.chart.text.TextUtils; 067import org.jfree.chart.ui.GradientPaintTransformer; 068import org.jfree.chart.ui.RectangleEdge; 069import org.jfree.chart.ui.StandardGradientPaintTransformer; 070import org.jfree.chart.util.ObjectUtils; 071import org.jfree.chart.util.PaintUtils; 072import org.jfree.chart.util.Args; 073import org.jfree.chart.util.PublicCloneable; 074import org.jfree.chart.util.SerialUtils; 075import org.jfree.data.KeyedValues2DItemKey; 076import org.jfree.data.Range; 077import org.jfree.data.category.CategoryDataset; 078 079/** 080 * A {@link CategoryItemRenderer} that draws individual data items as bars. 081 * The example shown here is generated by the {@code BarChartDemo1.java} 082 * program included in the JFreeChart Demo Collection: 083 * <br><br> 084 * <img src="doc-files/BarChartDemo1.svg" alt="BarChartDemo1.svg"> 085 */ 086public class BarRenderer extends AbstractCategoryItemRenderer 087 implements Cloneable, PublicCloneable, Serializable { 088 089 /** For serialization. */ 090 private static final long serialVersionUID = 6000649414965887481L; 091 092 /** The default item margin percentage. */ 093 public static final double DEFAULT_ITEM_MARGIN = 0.20; 094 095 /** 096 * Constant that controls the minimum width before a bar has an outline 097 * drawn. 098 */ 099 public static final double BAR_OUTLINE_WIDTH_THRESHOLD = 3.0; 100 101 /** 102 * The default bar painter assigned to each new instance of this renderer. 103 * 104 * @since 1.0.11 105 */ 106 private static BarPainter defaultBarPainter = new GradientBarPainter(); 107 108 /** 109 * Returns the default bar painter. 110 * 111 * @return The default bar painter. 112 * 113 * @since 1.0.11 114 */ 115 public static BarPainter getDefaultBarPainter() { 116 return BarRenderer.defaultBarPainter; 117 } 118 119 /** 120 * Sets the default bar painter. 121 * 122 * @param painter the painter ({@code null} not permitted). 123 * 124 * @since 1.0.11 125 */ 126 public static void setDefaultBarPainter(BarPainter painter) { 127 Args.nullNotPermitted(painter, "painter"); 128 BarRenderer.defaultBarPainter = painter; 129 } 130 131 /** 132 * The default value for the initialisation of the shadowsVisible flag. 133 */ 134 private static boolean defaultShadowsVisible = true; 135 136 /** 137 * Returns the default value for the {@code shadowsVisible} flag. 138 * 139 * @return A boolean. 140 * 141 * @see #setDefaultShadowsVisible(boolean) 142 * 143 * @since 1.0.13 144 */ 145 public static boolean getDefaultShadowsVisible() { 146 return BarRenderer.defaultShadowsVisible; 147 } 148 149 /** 150 * Sets the default value for the shadows visible flag. 151 * 152 * @param visible the new value for the default. 153 * 154 * @see #getDefaultShadowsVisible() 155 * 156 * @since 1.0.13 157 */ 158 public static void setDefaultShadowsVisible(boolean visible) { 159 BarRenderer.defaultShadowsVisible = visible; 160 } 161 162 /** The margin between items (bars) within a category. */ 163 private double itemMargin; 164 165 /** A flag that controls whether or not bar outlines are drawn. */ 166 private boolean drawBarOutline; 167 168 /** The maximum bar width as a percentage of the available space. */ 169 private double maximumBarWidth; 170 171 /** The minimum bar length (in Java2D units). */ 172 private double minimumBarLength; 173 174 /** 175 * An optional class used to transform gradient paint objects to fit each 176 * bar. 177 */ 178 private GradientPaintTransformer gradientPaintTransformer; 179 180 /** 181 * The fallback position if a positive item label doesn't fit inside the 182 * bar. 183 */ 184 private ItemLabelPosition positiveItemLabelPositionFallback; 185 186 /** 187 * The fallback position if a negative item label doesn't fit inside the 188 * bar. 189 */ 190 private ItemLabelPosition negativeItemLabelPositionFallback; 191 192 /** The upper clip (axis) value for the axis. */ 193 private double upperClip; 194 // TODO: this needs to move into the renderer state 195 196 /** The lower clip (axis) value for the axis. */ 197 private double lowerClip; 198 // TODO: this needs to move into the renderer state 199 200 /** The base value for the bars (defaults to 0.0). */ 201 private double base; 202 203 /** 204 * A flag that controls whether the base value is included in the range 205 * returned by the findRangeBounds() method. 206 */ 207 private boolean includeBaseInRange; 208 209 /** 210 * The bar painter (never {@code null}). 211 * 212 * @since 1.0.11 213 */ 214 private BarPainter barPainter; 215 216 /** 217 * The flag that controls whether or not shadows are drawn for the bars. 218 * 219 * @since 1.0.11 220 */ 221 private boolean shadowsVisible; 222 223 /** 224 * The shadow paint. 225 * 226 * @since 1.0.11 227 */ 228 private transient Paint shadowPaint; 229 230 /** 231 * The x-offset for the shadow effect. 232 * 233 * @since 1.0.11 234 */ 235 private double shadowXOffset; 236 237 /** 238 * The y-offset for the shadow effect. 239 * 240 * @since 1.0.11 241 */ 242 private double shadowYOffset; 243 244 /** 245 * Creates a new bar renderer with default settings. 246 */ 247 public BarRenderer() { 248 super(); 249 this.base = 0.0; 250 this.includeBaseInRange = true; 251 this.itemMargin = DEFAULT_ITEM_MARGIN; 252 this.drawBarOutline = false; 253 this.maximumBarWidth = 1.0; 254 // 100 percent, so it will not apply unless changed 255 this.positiveItemLabelPositionFallback = null; 256 this.negativeItemLabelPositionFallback = null; 257 this.gradientPaintTransformer = new StandardGradientPaintTransformer(); 258 this.minimumBarLength = 0.0; 259 setDefaultLegendShape(new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0)); 260 this.barPainter = getDefaultBarPainter(); 261 this.shadowsVisible = getDefaultShadowsVisible(); 262 this.shadowPaint = Color.GRAY; 263 this.shadowXOffset = 4.0; 264 this.shadowYOffset = 4.0; 265 } 266 267 /** 268 * Returns the base value for the bars. The default value is 269 * {@code 0.0}. 270 * 271 * @return The base value for the bars. 272 * 273 * @see #setBase(double) 274 */ 275 public double getBase() { 276 return this.base; 277 } 278 279 /** 280 * Sets the base value for the bars and sends a {@link RendererChangeEvent} 281 * to all registered listeners. 282 * 283 * @param base the new base value. 284 * 285 * @see #getBase() 286 */ 287 public void setBase(double base) { 288 this.base = base; 289 fireChangeEvent(); 290 } 291 292 /** 293 * Returns the item margin as a percentage of the available space for all 294 * bars. 295 * 296 * @return The margin percentage (where 0.10 is ten percent). 297 * 298 * @see #setItemMargin(double) 299 */ 300 public double getItemMargin() { 301 return this.itemMargin; 302 } 303 304 /** 305 * Sets the item margin and sends a {@link RendererChangeEvent} to all 306 * registered listeners. The value is expressed as a percentage of the 307 * available width for plotting all the bars, with the resulting amount to 308 * be distributed between all the bars evenly. 309 * 310 * @param percent the margin (where 0.10 is ten percent). 311 * 312 * @see #getItemMargin() 313 */ 314 public void setItemMargin(double percent) { 315 this.itemMargin = percent; 316 fireChangeEvent(); 317 } 318 319 /** 320 * Returns a flag that controls whether or not bar outlines are drawn. 321 * 322 * @return A boolean. 323 * 324 * @see #setDrawBarOutline(boolean) 325 */ 326 public boolean isDrawBarOutline() { 327 return this.drawBarOutline; 328 } 329 330 /** 331 * Sets the flag that controls whether or not bar outlines are drawn and 332 * sends a {@link RendererChangeEvent} to all registered listeners. 333 * 334 * @param draw the flag. 335 * 336 * @see #isDrawBarOutline() 337 */ 338 public void setDrawBarOutline(boolean draw) { 339 this.drawBarOutline = draw; 340 fireChangeEvent(); 341 } 342 343 /** 344 * Returns the maximum bar width, as a percentage of the available drawing 345 * space. 346 * 347 * @return The maximum bar width. 348 * 349 * @see #setMaximumBarWidth(double) 350 */ 351 public double getMaximumBarWidth() { 352 return this.maximumBarWidth; 353 } 354 355 /** 356 * Sets the maximum bar width, which is specified as a percentage of the 357 * available space for all bars, and sends a {@link RendererChangeEvent} to 358 * all registered listeners. 359 * 360 * @param percent the percent (where 0.05 is five percent). 361 * 362 * @see #getMaximumBarWidth() 363 */ 364 public void setMaximumBarWidth(double percent) { 365 this.maximumBarWidth = percent; 366 fireChangeEvent(); 367 } 368 369 /** 370 * Returns the minimum bar length (in Java2D units). The default value is 371 * 0.0. 372 * 373 * @return The minimum bar length. 374 * 375 * @see #setMinimumBarLength(double) 376 */ 377 public double getMinimumBarLength() { 378 return this.minimumBarLength; 379 } 380 381 /** 382 * Sets the minimum bar length and sends a {@link RendererChangeEvent} to 383 * all registered listeners. The minimum bar length is specified in Java2D 384 * units, and can be used to prevent bars that represent very small data 385 * values from disappearing when drawn on the screen. Typically you would 386 * set this to (say) 0.5 or 1.0 Java 2D units. Use this attribute with 387 * caution, however, because setting it to a non-zero value will 388 * artificially increase the length of bars representing small values, 389 * which may misrepresent your data. 390 * 391 * @param min the minimum bar length (in Java2D units, must be >= 0.0). 392 * 393 * @see #getMinimumBarLength() 394 */ 395 public void setMinimumBarLength(double min) { 396 if (min < 0.0) { 397 throw new IllegalArgumentException("Requires 'min' >= 0.0"); 398 } 399 this.minimumBarLength = min; 400 fireChangeEvent(); 401 } 402 403 /** 404 * Returns the gradient paint transformer (an object used to transform 405 * gradient paint objects to fit each bar). 406 * 407 * @return A transformer ({@code null} possible). 408 * 409 * @see #setGradientPaintTransformer(GradientPaintTransformer) 410 */ 411 public GradientPaintTransformer getGradientPaintTransformer() { 412 return this.gradientPaintTransformer; 413 } 414 415 /** 416 * Sets the gradient paint transformer and sends a 417 * {@link RendererChangeEvent} to all registered listeners. 418 * 419 * @param transformer the transformer ({@code null} permitted). 420 * 421 * @see #getGradientPaintTransformer() 422 */ 423 public void setGradientPaintTransformer( 424 GradientPaintTransformer transformer) { 425 this.gradientPaintTransformer = transformer; 426 fireChangeEvent(); 427 } 428 429 /** 430 * Returns the fallback position for positive item labels that don't fit 431 * within a bar. 432 * 433 * @return The fallback position ({@code null} possible). 434 * 435 * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) 436 */ 437 public ItemLabelPosition getPositiveItemLabelPositionFallback() { 438 return this.positiveItemLabelPositionFallback; 439 } 440 441 /** 442 * Sets the fallback position for positive item labels that don't fit 443 * within a bar, and sends a {@link RendererChangeEvent} to all registered 444 * listeners. 445 * 446 * @param position the position ({@code null} permitted). 447 * 448 * @see #getPositiveItemLabelPositionFallback() 449 */ 450 public void setPositiveItemLabelPositionFallback( 451 ItemLabelPosition position) { 452 this.positiveItemLabelPositionFallback = position; 453 fireChangeEvent(); 454 } 455 456 /** 457 * Returns the fallback position for negative item labels that don't fit 458 * within a bar. 459 * 460 * @return The fallback position ({@code null} possible). 461 * 462 * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) 463 */ 464 public ItemLabelPosition getNegativeItemLabelPositionFallback() { 465 return this.negativeItemLabelPositionFallback; 466 } 467 468 /** 469 * Sets the fallback position for negative item labels that don't fit 470 * within a bar, and sends a {@link RendererChangeEvent} to all registered 471 * listeners. 472 * 473 * @param position the position ({@code null} permitted). 474 * 475 * @see #getNegativeItemLabelPositionFallback() 476 */ 477 public void setNegativeItemLabelPositionFallback( 478 ItemLabelPosition position) { 479 this.negativeItemLabelPositionFallback = position; 480 fireChangeEvent(); 481 } 482 483 /** 484 * Returns the flag that controls whether or not the base value for the 485 * bars is included in the range calculated by 486 * {@link #findRangeBounds(CategoryDataset)}. 487 * 488 * @return {@code true} if the base is included in the range, and 489 * {@code false} otherwise. 490 * 491 * @since 1.0.1 492 * 493 * @see #setIncludeBaseInRange(boolean) 494 */ 495 public boolean getIncludeBaseInRange() { 496 return this.includeBaseInRange; 497 } 498 499 /** 500 * Sets the flag that controls whether or not the base value for the bars 501 * is included in the range calculated by 502 * {@link #findRangeBounds(CategoryDataset)}. If the flag is changed, 503 * a {@link RendererChangeEvent} is sent to all registered listeners. 504 * 505 * @param include the new value for the flag. 506 * 507 * @since 1.0.1 508 * 509 * @see #getIncludeBaseInRange() 510 */ 511 public void setIncludeBaseInRange(boolean include) { 512 if (this.includeBaseInRange != include) { 513 this.includeBaseInRange = include; 514 fireChangeEvent(); 515 } 516 } 517 518 /** 519 * Returns the bar painter. 520 * 521 * @return The bar painter (never {@code null}). 522 * 523 * @see #setBarPainter(BarPainter) 524 * 525 * @since 1.0.11 526 */ 527 public BarPainter getBarPainter() { 528 return this.barPainter; 529 } 530 531 /** 532 * Sets the bar painter for this renderer and sends a 533 * {@link RendererChangeEvent} to all registered listeners. 534 * 535 * @param painter the painter ({@code null} not permitted). 536 * 537 * @see #getBarPainter() 538 * 539 * @since 1.0.11 540 */ 541 public void setBarPainter(BarPainter painter) { 542 Args.nullNotPermitted(painter, "painter"); 543 this.barPainter = painter; 544 fireChangeEvent(); 545 } 546 547 /** 548 * Returns the flag that controls whether or not shadows are drawn for 549 * the bars. 550 * 551 * @return A boolean. 552 * 553 * @since 1.0.11 554 */ 555 public boolean getShadowsVisible() { 556 return this.shadowsVisible; 557 } 558 559 /** 560 * Sets the flag that controls whether or not shadows are 561 * drawn by the renderer. 562 * 563 * @param visible the new flag value. 564 * 565 * @since 1.0.11 566 */ 567 public void setShadowVisible(boolean visible) { 568 this.shadowsVisible = visible; 569 fireChangeEvent(); 570 } 571 572 /** 573 * Returns the shadow paint. 574 * 575 * @return The shadow paint. 576 * 577 * @see #setShadowPaint(Paint) 578 * 579 * @since 1.0.11 580 */ 581 public Paint getShadowPaint() { 582 return this.shadowPaint; 583 } 584 585 /** 586 * Sets the shadow paint and sends a {@link RendererChangeEvent} to all 587 * registered listeners. 588 * 589 * @param paint the paint ({@code null} not permitted). 590 * 591 * @see #getShadowPaint() 592 * 593 * @since 1.0.11 594 */ 595 public void setShadowPaint(Paint paint) { 596 Args.nullNotPermitted(paint, "paint"); 597 this.shadowPaint = paint; 598 fireChangeEvent(); 599 } 600 601 /** 602 * Returns the shadow x-offset. 603 * 604 * @return The shadow x-offset. 605 * 606 * @since 1.0.11 607 */ 608 public double getShadowXOffset() { 609 return this.shadowXOffset; 610 } 611 612 /** 613 * Sets the x-offset for the bar shadow and sends a 614 * {@link RendererChangeEvent} to all registered listeners. 615 * 616 * @param offset the offset. 617 * 618 * @since 1.0.11 619 */ 620 public void setShadowXOffset(double offset) { 621 this.shadowXOffset = offset; 622 fireChangeEvent(); 623 } 624 625 /** 626 * Returns the shadow y-offset. 627 * 628 * @return The shadow y-offset. 629 * 630 * @since 1.0.11 631 */ 632 public double getShadowYOffset() { 633 return this.shadowYOffset; 634 } 635 636 /** 637 * Sets the y-offset for the bar shadow and sends a 638 * {@link RendererChangeEvent} to all registered listeners. 639 * 640 * @param offset the offset. 641 * 642 * @since 1.0.11 643 */ 644 public void setShadowYOffset(double offset) { 645 this.shadowYOffset = offset; 646 fireChangeEvent(); 647 } 648 649 /** 650 * Returns the lower clip value. This value is recalculated in the 651 * initialise() method. 652 * 653 * @return The value. 654 */ 655 public double getLowerClip() { 656 // TODO: this attribute should be transferred to the renderer state. 657 return this.lowerClip; 658 } 659 660 /** 661 * Returns the upper clip value. This value is recalculated in the 662 * initialise() method. 663 * 664 * @return The value. 665 */ 666 public double getUpperClip() { 667 // TODO: this attribute should be transferred to the renderer state. 668 return this.upperClip; 669 } 670 671 /** 672 * Initialises the renderer and returns a state object that will be passed 673 * to subsequent calls to the drawItem method. This method gets called 674 * once at the start of the process of drawing a chart. 675 * 676 * @param g2 the graphics device. 677 * @param dataArea the area in which the data is to be plotted. 678 * @param plot the plot. 679 * @param rendererIndex the renderer index. 680 * @param info collects chart rendering information for return to caller. 681 * 682 * @return The renderer state. 683 */ 684 @Override 685 public CategoryItemRendererState initialise(Graphics2D g2, 686 Rectangle2D dataArea, CategoryPlot plot, int rendererIndex, 687 PlotRenderingInfo info) { 688 689 CategoryItemRendererState state = super.initialise(g2, dataArea, plot, 690 rendererIndex, info); 691 692 // get the clipping values... 693 ValueAxis rangeAxis = plot.getRangeAxisForDataset(rendererIndex); 694 this.lowerClip = rangeAxis.getRange().getLowerBound(); 695 this.upperClip = rangeAxis.getRange().getUpperBound(); 696 697 // calculate the bar width 698 calculateBarWidth(plot, dataArea, rendererIndex, state); 699 700 return state; 701 702 } 703 704 /** 705 * Calculates the bar width and stores it in the renderer state. 706 * 707 * @param plot the plot. 708 * @param dataArea the data area. 709 * @param rendererIndex the renderer index. 710 * @param state the renderer state. 711 */ 712 protected void calculateBarWidth(CategoryPlot plot, 713 Rectangle2D dataArea, 714 int rendererIndex, 715 CategoryItemRendererState state) { 716 717 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); 718 CategoryDataset dataset = plot.getDataset(rendererIndex); 719 if (dataset != null) { 720 int columns = dataset.getColumnCount(); 721 int rows = state.getVisibleSeriesCount() >= 0 722 ? state.getVisibleSeriesCount() : dataset.getRowCount(); 723 double space = 0.0; 724 PlotOrientation orientation = plot.getOrientation(); 725 if (orientation == PlotOrientation.HORIZONTAL) { 726 space = dataArea.getHeight(); 727 } 728 else if (orientation == PlotOrientation.VERTICAL) { 729 space = dataArea.getWidth(); 730 } 731 double maxWidth = space * getMaximumBarWidth(); 732 double categoryMargin = 0.0; 733 double currentItemMargin = 0.0; 734 if (columns > 1) { 735 categoryMargin = domainAxis.getCategoryMargin(); 736 } 737 if (rows > 1) { 738 currentItemMargin = getItemMargin(); 739 } 740 double used = space * (1 - domainAxis.getLowerMargin() 741 - domainAxis.getUpperMargin() 742 - categoryMargin - currentItemMargin); 743 if ((rows * columns) > 0) { 744 state.setBarWidth(Math.min(used / (rows * columns), maxWidth)); 745 } 746 else { 747 state.setBarWidth(Math.min(used, maxWidth)); 748 } 749 } 750 } 751 752 /** 753 * Calculates the coordinate of the first "side" of a bar. This will be 754 * the minimum x-coordinate for a vertical bar, and the minimum 755 * y-coordinate for a horizontal bar. 756 * 757 * @param plot the plot. 758 * @param orientation the plot orientation. 759 * @param dataArea the data area. 760 * @param domainAxis the domain axis. 761 * @param state the renderer state (has the bar width precalculated). 762 * @param row the row index. 763 * @param column the column index. 764 * 765 * @return The coordinate. 766 */ 767 protected double calculateBarW0(CategoryPlot plot, 768 PlotOrientation orientation, Rectangle2D dataArea, 769 CategoryAxis domainAxis, CategoryItemRendererState state, 770 int row, int column) { 771 // calculate bar width... 772 double space; 773 if (orientation == PlotOrientation.HORIZONTAL) { 774 space = dataArea.getHeight(); 775 } 776 else { 777 space = dataArea.getWidth(); 778 } 779 double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 780 dataArea, plot.getDomainAxisEdge()); 781 int seriesCount = state.getVisibleSeriesCount() >= 0 782 ? state.getVisibleSeriesCount() : getRowCount(); 783 int categoryCount = getColumnCount(); 784 if (seriesCount > 1) { 785 double seriesGap = space * getItemMargin() 786 / (categoryCount * (seriesCount - 1)); 787 double seriesW = calculateSeriesWidth(space, domainAxis, 788 categoryCount, seriesCount); 789 barW0 = barW0 + row * (seriesW + seriesGap) 790 + (seriesW / 2.0) - (state.getBarWidth() / 2.0); 791 } 792 else { 793 barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 794 dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() 795 / 2.0; 796 } 797 return barW0; 798 } 799 800 /** 801 * Calculates the coordinates for the length of a single bar. 802 * 803 * @param value the value represented by the bar. 804 * 805 * @return The coordinates for each end of the bar (or {@code null} if 806 * the bar is not visible for the current axis range). 807 */ 808 protected double[] calculateBarL0L1(double value) { 809 double lclip = getLowerClip(); 810 double uclip = getUpperClip(); 811 double barLow = Math.min(this.base, value); 812 double barHigh = Math.max(this.base, value); 813 if (barHigh < lclip) { // bar is not visible 814 return null; 815 } 816 if (barLow > uclip) { // bar is not visible 817 return null; 818 } 819 barLow = Math.max(barLow, lclip); 820 barHigh = Math.min(barHigh, uclip); 821 return new double[] {barLow, barHigh}; 822 } 823 824 /** 825 * Returns the range of values the renderer requires to display all the 826 * items from the specified dataset. This takes into account the range 827 * of values in the dataset, plus the flag that determines whether or not 828 * the base value for the bars should be included in the range. 829 * 830 * @param dataset the dataset ({@code null} permitted). 831 * @param includeInterval include the interval if the dataset has one? 832 * 833 * @return The range (or {@code null} if the dataset is 834 * {@code null} or empty). 835 */ 836 @Override 837 public Range findRangeBounds(CategoryDataset dataset, 838 boolean includeInterval) { 839 if (dataset == null) { 840 return null; 841 } 842 Range result = super.findRangeBounds(dataset, includeInterval); 843 if (result != null) { 844 if (this.includeBaseInRange) { 845 result = Range.expandToInclude(result, this.base); 846 } 847 } 848 return result; 849 } 850 851 /** 852 * Returns a legend item for a series. 853 * 854 * @param datasetIndex the dataset index (zero-based). 855 * @param series the series index (zero-based). 856 * 857 * @return The legend item (possibly {@code null}). 858 */ 859 @Override 860 public LegendItem getLegendItem(int datasetIndex, int series) { 861 862 CategoryPlot cp = getPlot(); 863 if (cp == null) { 864 return null; 865 } 866 867 // check that a legend item needs to be displayed... 868 if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) { 869 return null; 870 } 871 872 CategoryDataset dataset = cp.getDataset(datasetIndex); 873 String label = getLegendItemLabelGenerator().generateLabel(dataset, 874 series); 875 String description = label; 876 String toolTipText = null; 877 if (getLegendItemToolTipGenerator() != null) { 878 toolTipText = getLegendItemToolTipGenerator().generateLabel( 879 dataset, series); 880 } 881 String urlText = null; 882 if (getLegendItemURLGenerator() != null) { 883 urlText = getLegendItemURLGenerator().generateLabel(dataset, 884 series); 885 } 886 Shape shape = lookupLegendShape(series); 887 Paint paint = lookupSeriesPaint(series); 888 Paint outlinePaint = lookupSeriesOutlinePaint(series); 889 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 890 891 LegendItem result = new LegendItem(label, description, toolTipText, 892 urlText, true, shape, true, paint, isDrawBarOutline(), 893 outlinePaint, outlineStroke, false, new Line2D.Float(), 894 new BasicStroke(1.0f), Color.BLACK); 895 result.setLabelFont(lookupLegendTextFont(series)); 896 Paint labelPaint = lookupLegendTextPaint(series); 897 if (labelPaint != null) { 898 result.setLabelPaint(labelPaint); 899 } 900 result.setDataset(dataset); 901 result.setDatasetIndex(datasetIndex); 902 result.setSeriesKey(dataset.getRowKey(series)); 903 result.setSeriesIndex(series); 904 if (this.gradientPaintTransformer != null) { 905 result.setFillPaintTransformer(this.gradientPaintTransformer); 906 } 907 return result; 908 } 909 910 /** 911 * Draws the bar for a single (series, category) data item. 912 * 913 * @param g2 the graphics device. 914 * @param state the renderer state. 915 * @param dataArea the data area. 916 * @param plot the plot. 917 * @param domainAxis the domain axis. 918 * @param rangeAxis the range axis. 919 * @param dataset the dataset. 920 * @param row the row index (zero-based). 921 * @param column the column index (zero-based). 922 * @param pass the pass index. 923 */ 924 @Override 925 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 926 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 927 ValueAxis rangeAxis, CategoryDataset dataset, int row, 928 int column, int pass) { 929 930 // nothing is drawn if the row index is not included in the list with 931 // the indices of the visible rows... 932 int visibleRow = state.getVisibleSeriesIndex(row); 933 if (visibleRow < 0) { 934 return; 935 } 936 // nothing is drawn for null values... 937 Number dataValue = dataset.getValue(row, column); 938 if (dataValue == null) { 939 return; 940 } 941 942 final double value = dataValue.doubleValue(); 943 PlotOrientation orientation = plot.getOrientation(); 944 double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, 945 state, visibleRow, column); 946 double[] barL0L1 = calculateBarL0L1(value); 947 if (barL0L1 == null) { 948 return; // the bar is not visible 949 } 950 951 RectangleEdge edge = plot.getRangeAxisEdge(); 952 double transL0 = rangeAxis.valueToJava2D(barL0L1[0], dataArea, edge); 953 double transL1 = rangeAxis.valueToJava2D(barL0L1[1], dataArea, edge); 954 955 // in the following code, barL0 is (in Java2D coordinates) the LEFT 956 // end of the bar for a horizontal bar chart, and the TOP end of the 957 // bar for a vertical bar chart. Whether this is the BASE of the bar 958 // or not depends also on (a) whether the data value is 'negative' 959 // relative to the base value and (b) whether or not the range axis is 960 // inverted. This only matters if/when we apply the minimumBarLength 961 // attribute, because we should extend the non-base end of the bar 962 boolean positive = (value >= this.base); 963 boolean inverted = rangeAxis.isInverted(); 964 double barL0 = Math.min(transL0, transL1); 965 double barLength = Math.abs(transL1 - transL0); 966 double barLengthAdj = 0.0; 967 if (barLength > 0.0 && barLength < getMinimumBarLength()) { 968 barLengthAdj = getMinimumBarLength() - barLength; 969 } 970 double barL0Adj = 0.0; 971 RectangleEdge barBase; 972 if (orientation == PlotOrientation.HORIZONTAL) { 973 if (positive && inverted || !positive && !inverted) { 974 barL0Adj = barLengthAdj; 975 barBase = RectangleEdge.RIGHT; 976 } 977 else { 978 barBase = RectangleEdge.LEFT; 979 } 980 } 981 else { 982 if (positive && !inverted || !positive && inverted) { 983 barL0Adj = barLengthAdj; 984 barBase = RectangleEdge.BOTTOM; 985 } 986 else { 987 barBase = RectangleEdge.TOP; 988 } 989 } 990 991 // draw the bar... 992 Rectangle2D bar; 993 if (orientation == PlotOrientation.HORIZONTAL) { 994 bar = new Rectangle2D.Double(barL0 - barL0Adj, barW0, 995 barLength + barLengthAdj, state.getBarWidth()); 996 } 997 else { 998 bar = new Rectangle2D.Double(barW0, barL0 - barL0Adj, 999 state.getBarWidth(), barLength + barLengthAdj); 1000 } 1001 if (state.getElementHinting()) { 1002 KeyedValues2DItemKey key = new KeyedValues2DItemKey( 1003 dataset.getRowKey(row), dataset.getColumnKey(column)); 1004 beginElementGroup(g2, key); 1005 } 1006 if (getShadowsVisible()) { 1007 this.barPainter.paintBarShadow(g2, this, row, column, bar, barBase, 1008 true); 1009 } 1010 this.barPainter.paintBar(g2, this, row, column, bar, barBase); 1011 if (state.getElementHinting()) { 1012 endElementGroup(g2); 1013 } 1014 1015 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 1016 column); 1017 if (generator != null && isItemLabelVisible(row, column)) { 1018 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 1019 (value < 0.0)); 1020 } 1021 1022 // submit the current data point as a crosshair candidate 1023 int datasetIndex = plot.indexOf(dataset); 1024 updateCrosshairValues(state.getCrosshairState(), 1025 dataset.getRowKey(row), dataset.getColumnKey(column), value, 1026 datasetIndex, barW0, barL0, orientation); 1027 1028 // add an item entity, if this information is being collected 1029 EntityCollection entities = state.getEntityCollection(); 1030 if (entities != null) { 1031 addItemEntity(entities, dataset, row, column, bar); 1032 } 1033 1034 } 1035 1036 /** 1037 * Calculates the available space for each series. 1038 * 1039 * @param space the space along the entire axis (in Java2D units). 1040 * @param axis the category axis. 1041 * @param categories the number of categories. 1042 * @param series the number of series. 1043 * 1044 * @return The width of one series. 1045 */ 1046 protected double calculateSeriesWidth(double space, CategoryAxis axis, 1047 int categories, int series) { 1048 double factor = 1.0 - getItemMargin() - axis.getLowerMargin() 1049 - axis.getUpperMargin(); 1050 if (categories > 1) { 1051 factor = factor - axis.getCategoryMargin(); 1052 } 1053 return (space * factor) / (categories * series); 1054 } 1055 1056 /** 1057 * Draws an item label. This method is overridden so that the bar can be 1058 * used to calculate the label anchor point. 1059 * 1060 * @param g2 the graphics device. 1061 * @param data the dataset. 1062 * @param row the row. 1063 * @param column the column. 1064 * @param plot the plot. 1065 * @param generator the label generator. 1066 * @param bar the bar. 1067 * @param negative a flag indicating a negative value. 1068 */ 1069 protected void drawItemLabel(Graphics2D g2, 1070 CategoryDataset data, 1071 int row, 1072 int column, 1073 CategoryPlot plot, 1074 CategoryItemLabelGenerator generator, 1075 Rectangle2D bar, 1076 boolean negative) { 1077 1078 String label = generator.generateLabel(data, row, column); 1079 if (label == null) { 1080 return; // nothing to do 1081 } 1082 1083 Font labelFont = getItemLabelFont(row, column); 1084 g2.setFont(labelFont); 1085 Paint paint = getItemLabelPaint(row, column); 1086 g2.setPaint(paint); 1087 1088 // find out where to place the label... 1089 ItemLabelPosition position; 1090 if (!negative) { 1091 position = getPositiveItemLabelPosition(row, column); 1092 } 1093 else { 1094 position = getNegativeItemLabelPosition(row, column); 1095 } 1096 1097 // work out the label anchor point... 1098 Point2D anchorPoint = calculateLabelAnchorPoint( 1099 position.getItemLabelAnchor(), bar, plot.getOrientation()); 1100 1101 if (isInternalAnchor(position.getItemLabelAnchor())) { 1102 Shape bounds = TextUtils.calculateRotatedStringBounds(label, 1103 g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(), 1104 position.getTextAnchor(), position.getAngle(), 1105 position.getRotationAnchor()); 1106 1107 if (bounds != null) { 1108 if (!bar.contains(bounds.getBounds2D())) { 1109 if (!negative) { 1110 position = getPositiveItemLabelPositionFallback(); 1111 } 1112 else { 1113 position = getNegativeItemLabelPositionFallback(); 1114 } 1115 if (position != null) { 1116 anchorPoint = calculateLabelAnchorPoint( 1117 position.getItemLabelAnchor(), bar, 1118 plot.getOrientation()); 1119 } 1120 } 1121 } 1122 1123 } 1124 1125 if (position != null) { 1126 TextUtils.drawRotatedString(label, g2, 1127 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 1128 position.getTextAnchor(), position.getAngle(), 1129 position.getRotationAnchor()); 1130 } 1131 } 1132 1133 /** 1134 * Calculates the item label anchor point. 1135 * 1136 * @param anchor the anchor. 1137 * @param bar the bar. 1138 * @param orientation the plot orientation. 1139 * 1140 * @return The anchor point. 1141 */ 1142 private Point2D calculateLabelAnchorPoint(ItemLabelAnchor anchor, 1143 Rectangle2D bar, 1144 PlotOrientation orientation) { 1145 1146 Point2D result = null; 1147 double offset = getItemLabelAnchorOffset(); 1148 double x0 = bar.getX() - offset; 1149 double x1 = bar.getX(); 1150 double x2 = bar.getX() + offset; 1151 double x3 = bar.getCenterX(); 1152 double x4 = bar.getMaxX() - offset; 1153 double x5 = bar.getMaxX(); 1154 double x6 = bar.getMaxX() + offset; 1155 1156 double y0 = bar.getMaxY() + offset; 1157 double y1 = bar.getMaxY(); 1158 double y2 = bar.getMaxY() - offset; 1159 double y3 = bar.getCenterY(); 1160 double y4 = bar.getMinY() + offset; 1161 double y5 = bar.getMinY(); 1162 double y6 = bar.getMinY() - offset; 1163 1164 if (anchor == ItemLabelAnchor.CENTER) { 1165 result = new Point2D.Double(x3, y3); 1166 } 1167 else if (anchor == ItemLabelAnchor.INSIDE1) { 1168 result = new Point2D.Double(x4, y4); 1169 } 1170 else if (anchor == ItemLabelAnchor.INSIDE2) { 1171 result = new Point2D.Double(x4, y4); 1172 } 1173 else if (anchor == ItemLabelAnchor.INSIDE3) { 1174 result = new Point2D.Double(x4, y3); 1175 } 1176 else if (anchor == ItemLabelAnchor.INSIDE4) { 1177 result = new Point2D.Double(x4, y2); 1178 } 1179 else if (anchor == ItemLabelAnchor.INSIDE5) { 1180 result = new Point2D.Double(x4, y2); 1181 } 1182 else if (anchor == ItemLabelAnchor.INSIDE6) { 1183 result = new Point2D.Double(x3, y2); 1184 } 1185 else if (anchor == ItemLabelAnchor.INSIDE7) { 1186 result = new Point2D.Double(x2, y2); 1187 } 1188 else if (anchor == ItemLabelAnchor.INSIDE8) { 1189 result = new Point2D.Double(x2, y2); 1190 } 1191 else if (anchor == ItemLabelAnchor.INSIDE9) { 1192 result = new Point2D.Double(x2, y3); 1193 } 1194 else if (anchor == ItemLabelAnchor.INSIDE10) { 1195 result = new Point2D.Double(x2, y4); 1196 } 1197 else if (anchor == ItemLabelAnchor.INSIDE11) { 1198 result = new Point2D.Double(x2, y4); 1199 } 1200 else if (anchor == ItemLabelAnchor.INSIDE12) { 1201 result = new Point2D.Double(x3, y4); 1202 } 1203 else if (anchor == ItemLabelAnchor.OUTSIDE1) { 1204 result = new Point2D.Double(x5, y6); 1205 } 1206 else if (anchor == ItemLabelAnchor.OUTSIDE2) { 1207 result = new Point2D.Double(x6, y5); 1208 } 1209 else if (anchor == ItemLabelAnchor.OUTSIDE3) { 1210 result = new Point2D.Double(x6, y3); 1211 } 1212 else if (anchor == ItemLabelAnchor.OUTSIDE4) { 1213 result = new Point2D.Double(x6, y1); 1214 } 1215 else if (anchor == ItemLabelAnchor.OUTSIDE5) { 1216 result = new Point2D.Double(x5, y0); 1217 } 1218 else if (anchor == ItemLabelAnchor.OUTSIDE6) { 1219 result = new Point2D.Double(x3, y0); 1220 } 1221 else if (anchor == ItemLabelAnchor.OUTSIDE7) { 1222 result = new Point2D.Double(x1, y0); 1223 } 1224 else if (anchor == ItemLabelAnchor.OUTSIDE8) { 1225 result = new Point2D.Double(x0, y1); 1226 } 1227 else if (anchor == ItemLabelAnchor.OUTSIDE9) { 1228 result = new Point2D.Double(x0, y3); 1229 } 1230 else if (anchor == ItemLabelAnchor.OUTSIDE10) { 1231 result = new Point2D.Double(x0, y5); 1232 } 1233 else if (anchor == ItemLabelAnchor.OUTSIDE11) { 1234 result = new Point2D.Double(x1, y6); 1235 } 1236 else if (anchor == ItemLabelAnchor.OUTSIDE12) { 1237 result = new Point2D.Double(x3, y6); 1238 } 1239 1240 return result; 1241 1242 } 1243 1244 /** 1245 * Returns {@code true} if the specified anchor point is inside a bar. 1246 * 1247 * @param anchor the anchor point. 1248 * 1249 * @return A boolean. 1250 */ 1251 private boolean isInternalAnchor(ItemLabelAnchor anchor) { 1252 return anchor == ItemLabelAnchor.CENTER 1253 || anchor == ItemLabelAnchor.INSIDE1 1254 || anchor == ItemLabelAnchor.INSIDE2 1255 || anchor == ItemLabelAnchor.INSIDE3 1256 || anchor == ItemLabelAnchor.INSIDE4 1257 || anchor == ItemLabelAnchor.INSIDE5 1258 || anchor == ItemLabelAnchor.INSIDE6 1259 || anchor == ItemLabelAnchor.INSIDE7 1260 || anchor == ItemLabelAnchor.INSIDE8 1261 || anchor == ItemLabelAnchor.INSIDE9 1262 || anchor == ItemLabelAnchor.INSIDE10 1263 || anchor == ItemLabelAnchor.INSIDE11 1264 || anchor == ItemLabelAnchor.INSIDE12; 1265 } 1266 1267 /** 1268 * Tests this instance for equality with an arbitrary object. 1269 * 1270 * @param obj the object ({@code null} permitted). 1271 * 1272 * @return A boolean. 1273 */ 1274 @Override 1275 public boolean equals(Object obj) { 1276 if (obj == this) { 1277 return true; 1278 } 1279 if (!(obj instanceof BarRenderer)) { 1280 return false; 1281 } 1282 BarRenderer that = (BarRenderer) obj; 1283 if (this.base != that.base) { 1284 return false; 1285 } 1286 if (this.itemMargin != that.itemMargin) { 1287 return false; 1288 } 1289 if (this.drawBarOutline != that.drawBarOutline) { 1290 return false; 1291 } 1292 if (this.maximumBarWidth != that.maximumBarWidth) { 1293 return false; 1294 } 1295 if (this.minimumBarLength != that.minimumBarLength) { 1296 return false; 1297 } 1298 if (!ObjectUtils.equal(this.gradientPaintTransformer, 1299 that.gradientPaintTransformer)) { 1300 return false; 1301 } 1302 if (!ObjectUtils.equal(this.positiveItemLabelPositionFallback, 1303 that.positiveItemLabelPositionFallback)) { 1304 return false; 1305 } 1306 if (!ObjectUtils.equal(this.negativeItemLabelPositionFallback, 1307 that.negativeItemLabelPositionFallback)) { 1308 return false; 1309 } 1310 if (!this.barPainter.equals(that.barPainter)) { 1311 return false; 1312 } 1313 if (this.shadowsVisible != that.shadowsVisible) { 1314 return false; 1315 } 1316 if (!PaintUtils.equal(this.shadowPaint, that.shadowPaint)) { 1317 return false; 1318 } 1319 if (this.shadowXOffset != that.shadowXOffset) { 1320 return false; 1321 } 1322 if (this.shadowYOffset != that.shadowYOffset) { 1323 return false; 1324 } 1325 return super.equals(obj); 1326 } 1327 1328 /** 1329 * Provides serialization support. 1330 * 1331 * @param stream the output stream. 1332 * 1333 * @throws IOException if there is an I/O error. 1334 */ 1335 private void writeObject(ObjectOutputStream stream) throws IOException { 1336 stream.defaultWriteObject(); 1337 SerialUtils.writePaint(this.shadowPaint, stream); 1338 } 1339 1340 /** 1341 * Provides serialization support. 1342 * 1343 * @param stream the input stream. 1344 * 1345 * @throws IOException if there is an I/O error. 1346 * @throws ClassNotFoundException if there is a classpath problem. 1347 */ 1348 private void readObject(ObjectInputStream stream) 1349 throws IOException, ClassNotFoundException { 1350 stream.defaultReadObject(); 1351 this.shadowPaint = SerialUtils.readPaint(stream); 1352 } 1353 1354}