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