001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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 * LineRenderer3D.java 029 * ------------------- 030 * (C) Copyright 2004-2007, by Tobias Selb and Contributors. 031 * 032 * Original Author: Tobias Selb (http://www.uepselon.com); 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * 035 * $Id: LineRenderer3D.java,v 1.10.2.7 2007/01/17 14:16:11 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 15-Oct-2004 : Version 1 (TS); 040 * 05-Nov-2004 : Modified drawItem() signature (DG); 041 * 11-Nov-2004 : Now uses ShapeUtilities class to translate shapes (DG); 042 * 26-Jan-2005 : Update for changes in super class (DG); 043 * 13-Apr-2005 : Check item visibility in drawItem() method (DG); 044 * 09-Jun-2005 : Use addItemEntity() in drawItem() method (DG); 045 * 10-Jun-2005 : Fixed capitalisation of setXOffset() and setYOffset() (DG); 046 * ------------- JFREECHART 1.0.x --------------------------------------------- 047 * 01-Dec-2006 : Fixed equals() and serialization (DG); 048 * 17-Jan-2007 : Fixed bug in drawDomainGridline() method and added 049 * argument check to setWallPaint() (DG); 050 * 051 */ 052 053 package org.jfree.chart.renderer.category; 054 055 import java.awt.AlphaComposite; 056 import java.awt.Color; 057 import java.awt.Composite; 058 import java.awt.Graphics2D; 059 import java.awt.Image; 060 import java.awt.Paint; 061 import java.awt.Shape; 062 import java.awt.Stroke; 063 import java.awt.geom.GeneralPath; 064 import java.awt.geom.Line2D; 065 import java.awt.geom.Rectangle2D; 066 import java.io.IOException; 067 import java.io.ObjectInputStream; 068 import java.io.ObjectOutputStream; 069 import java.io.Serializable; 070 071 import org.jfree.chart.Effect3D; 072 import org.jfree.chart.axis.CategoryAxis; 073 import org.jfree.chart.axis.ValueAxis; 074 import org.jfree.chart.entity.EntityCollection; 075 import org.jfree.chart.event.RendererChangeEvent; 076 import org.jfree.chart.plot.CategoryPlot; 077 import org.jfree.chart.plot.Marker; 078 import org.jfree.chart.plot.PlotOrientation; 079 import org.jfree.chart.plot.ValueMarker; 080 import org.jfree.data.Range; 081 import org.jfree.data.category.CategoryDataset; 082 import org.jfree.io.SerialUtilities; 083 import org.jfree.util.PaintUtilities; 084 import org.jfree.util.ShapeUtilities; 085 086 /** 087 * A line renderer with a 3D effect. 088 */ 089 public class LineRenderer3D extends LineAndShapeRenderer 090 implements Effect3D, Serializable { 091 092 /** For serialization. */ 093 private static final long serialVersionUID = 5467931468380928736L; 094 095 /** The default x-offset for the 3D effect. */ 096 public static final double DEFAULT_X_OFFSET = 12.0; 097 098 /** The default y-offset for the 3D effect. */ 099 public static final double DEFAULT_Y_OFFSET = 8.0; 100 101 /** The default wall paint. */ 102 public static final Paint DEFAULT_WALL_PAINT = new Color(0xDD, 0xDD, 0xDD); 103 104 /** The size of x-offset for the 3D effect. */ 105 private double xOffset; 106 107 /** The size of y-offset for the 3D effect. */ 108 private double yOffset; 109 110 /** The paint used to shade the left and lower 3D wall. */ 111 private transient Paint wallPaint; 112 113 /** 114 * Creates a new renderer. 115 */ 116 public LineRenderer3D() { 117 super(true, false); //Create a line renderer only 118 this.xOffset = DEFAULT_X_OFFSET; 119 this.yOffset = DEFAULT_Y_OFFSET; 120 this.wallPaint = DEFAULT_WALL_PAINT; 121 } 122 123 /** 124 * Returns the x-offset for the 3D effect. 125 * 126 * @return The x-offset. 127 * 128 * @see #setXOffset(double) 129 * @see #getYOffset() 130 */ 131 public double getXOffset() { 132 return this.xOffset; 133 } 134 135 /** 136 * Returns the y-offset for the 3D effect. 137 * 138 * @return The y-offset. 139 * 140 * @see #setYOffset(double) 141 * @see #getXOffset() 142 */ 143 public double getYOffset() { 144 return this.yOffset; 145 } 146 147 /** 148 * Sets the x-offset and sends a {@link RendererChangeEvent} to all 149 * registered listeners. 150 * 151 * @param xOffset the x-offset. 152 * 153 * @see #getXOffset() 154 */ 155 public void setXOffset(double xOffset) { 156 this.xOffset = xOffset; 157 notifyListeners(new RendererChangeEvent(this)); 158 } 159 160 /** 161 * Sets the y-offset and sends a {@link RendererChangeEvent} to all 162 * registered listeners. 163 * 164 * @param yOffset the y-offset. 165 * 166 * @see #getYOffset() 167 */ 168 public void setYOffset(double yOffset) { 169 this.yOffset = yOffset; 170 notifyListeners(new RendererChangeEvent(this)); 171 } 172 173 /** 174 * Returns the paint used to highlight the left and bottom wall in the plot 175 * background. 176 * 177 * @return The paint. 178 * 179 * @see #setWallPaint(Paint) 180 */ 181 public Paint getWallPaint() { 182 return this.wallPaint; 183 } 184 185 /** 186 * Sets the paint used to hightlight the left and bottom walls in the plot 187 * background, and sends a {@link RendererChangeEvent} to all 188 * registered listeners. 189 * 190 * @param paint the paint (<code>null</code> not permitted). 191 * 192 * @see #getWallPaint() 193 */ 194 public void setWallPaint(Paint paint) { 195 if (paint == null) { 196 throw new IllegalArgumentException("Null 'paint' argument."); 197 } 198 this.wallPaint = paint; 199 notifyListeners(new RendererChangeEvent(this)); 200 } 201 202 /** 203 * Draws the background for the plot. 204 * 205 * @param g2 the graphics device. 206 * @param plot the plot. 207 * @param dataArea the area inside the axes. 208 */ 209 public void drawBackground(Graphics2D g2, CategoryPlot plot, 210 Rectangle2D dataArea) { 211 212 float x0 = (float) dataArea.getX(); 213 float x1 = x0 + (float) Math.abs(this.xOffset); 214 float x3 = (float) dataArea.getMaxX(); 215 float x2 = x3 - (float) Math.abs(this.xOffset); 216 217 float y0 = (float) dataArea.getMaxY(); 218 float y1 = y0 - (float) Math.abs(this.yOffset); 219 float y3 = (float) dataArea.getMinY(); 220 float y2 = y3 + (float) Math.abs(this.yOffset); 221 222 GeneralPath clip = new GeneralPath(); 223 clip.moveTo(x0, y0); 224 clip.lineTo(x0, y2); 225 clip.lineTo(x1, y3); 226 clip.lineTo(x3, y3); 227 clip.lineTo(x3, y1); 228 clip.lineTo(x2, y0); 229 clip.closePath(); 230 231 // fill background... 232 Paint backgroundPaint = plot.getBackgroundPaint(); 233 if (backgroundPaint != null) { 234 g2.setPaint(backgroundPaint); 235 g2.fill(clip); 236 } 237 238 GeneralPath leftWall = new GeneralPath(); 239 leftWall.moveTo(x0, y0); 240 leftWall.lineTo(x0, y2); 241 leftWall.lineTo(x1, y3); 242 leftWall.lineTo(x1, y1); 243 leftWall.closePath(); 244 g2.setPaint(getWallPaint()); 245 g2.fill(leftWall); 246 247 GeneralPath bottomWall = new GeneralPath(); 248 bottomWall.moveTo(x0, y0); 249 bottomWall.lineTo(x1, y1); 250 bottomWall.lineTo(x3, y1); 251 bottomWall.lineTo(x2, y0); 252 bottomWall.closePath(); 253 g2.setPaint(getWallPaint()); 254 g2.fill(bottomWall); 255 256 // higlight the background corners... 257 g2.setPaint(Color.lightGray); 258 Line2D corner = new Line2D.Double(x0, y0, x1, y1); 259 g2.draw(corner); 260 corner.setLine(x1, y1, x1, y3); 261 g2.draw(corner); 262 corner.setLine(x1, y1, x3, y1); 263 g2.draw(corner); 264 265 // draw background image, if there is one... 266 Image backgroundImage = plot.getBackgroundImage(); 267 if (backgroundImage != null) { 268 Composite originalComposite = g2.getComposite(); 269 g2.setComposite(AlphaComposite.getInstance( 270 AlphaComposite.SRC, plot.getBackgroundAlpha())); 271 g2.drawImage(backgroundImage, (int) x1, (int) y3, 272 (int) (x3 - x1 + 1), (int) (y1 - y3 + 1), null); 273 g2.setComposite(originalComposite); 274 } 275 276 } 277 278 /** 279 * Draws the outline for the plot. 280 * 281 * @param g2 the graphics device. 282 * @param plot the plot. 283 * @param dataArea the area inside the axes. 284 */ 285 public void drawOutline(Graphics2D g2, CategoryPlot plot, 286 Rectangle2D dataArea) { 287 288 float x0 = (float) dataArea.getX(); 289 float x1 = x0 + (float) Math.abs(this.xOffset); 290 float x3 = (float) dataArea.getMaxX(); 291 float x2 = x3 - (float) Math.abs(this.xOffset); 292 293 float y0 = (float) dataArea.getMaxY(); 294 float y1 = y0 - (float) Math.abs(this.yOffset); 295 float y3 = (float) dataArea.getMinY(); 296 float y2 = y3 + (float) Math.abs(this.yOffset); 297 298 GeneralPath clip = new GeneralPath(); 299 clip.moveTo(x0, y0); 300 clip.lineTo(x0, y2); 301 clip.lineTo(x1, y3); 302 clip.lineTo(x3, y3); 303 clip.lineTo(x3, y1); 304 clip.lineTo(x2, y0); 305 clip.closePath(); 306 307 // put an outline around the data area... 308 Stroke outlineStroke = plot.getOutlineStroke(); 309 Paint outlinePaint = plot.getOutlinePaint(); 310 if ((outlineStroke != null) && (outlinePaint != null)) { 311 g2.setStroke(outlineStroke); 312 g2.setPaint(outlinePaint); 313 g2.draw(clip); 314 } 315 316 } 317 318 /** 319 * Draws a grid line against the domain axis. 320 * 321 * @param g2 the graphics device. 322 * @param plot the plot. 323 * @param dataArea the area for plotting data (not yet adjusted for any 324 * 3D effect). 325 * @param value the Java2D value at which the grid line should be drawn. 326 * 327 */ 328 public void drawDomainGridline(Graphics2D g2, 329 CategoryPlot plot, 330 Rectangle2D dataArea, 331 double value) { 332 333 Line2D line1 = null; 334 Line2D line2 = null; 335 PlotOrientation orientation = plot.getOrientation(); 336 if (orientation == PlotOrientation.HORIZONTAL) { 337 double y0 = value; 338 double y1 = value - getYOffset(); 339 double x0 = dataArea.getMinX(); 340 double x1 = x0 + getXOffset(); 341 double x2 = dataArea.getMaxX(); 342 line1 = new Line2D.Double(x0, y0, x1, y1); 343 line2 = new Line2D.Double(x1, y1, x2, y1); 344 } 345 else if (orientation == PlotOrientation.VERTICAL) { 346 double x0 = value; 347 double x1 = value + getXOffset(); 348 double y0 = dataArea.getMaxY(); 349 double y1 = y0 - getYOffset(); 350 double y2 = dataArea.getMinY(); 351 line1 = new Line2D.Double(x0, y0, x1, y1); 352 line2 = new Line2D.Double(x1, y1, x1, y2); 353 } 354 g2.setPaint(plot.getDomainGridlinePaint()); 355 g2.setStroke(plot.getDomainGridlineStroke()); 356 g2.draw(line1); 357 g2.draw(line2); 358 359 } 360 361 /** 362 * Draws a grid line against the range axis. 363 * 364 * @param g2 the graphics device. 365 * @param plot the plot. 366 * @param axis the value axis. 367 * @param dataArea the area for plotting data (not yet adjusted for any 368 * 3D effect). 369 * @param value the value at which the grid line should be drawn. 370 * 371 */ 372 public void drawRangeGridline(Graphics2D g2, 373 CategoryPlot plot, 374 ValueAxis axis, 375 Rectangle2D dataArea, 376 double value) { 377 378 Range range = axis.getRange(); 379 380 if (!range.contains(value)) { 381 return; 382 } 383 384 Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(), 385 dataArea.getY() + getYOffset(), 386 dataArea.getWidth() - getXOffset(), 387 dataArea.getHeight() - getYOffset()); 388 389 Line2D line1 = null; 390 Line2D line2 = null; 391 PlotOrientation orientation = plot.getOrientation(); 392 if (orientation == PlotOrientation.HORIZONTAL) { 393 double x0 = axis.valueToJava2D(value, adjusted, 394 plot.getRangeAxisEdge()); 395 double x1 = x0 + getXOffset(); 396 double y0 = dataArea.getMaxY(); 397 double y1 = y0 - getYOffset(); 398 double y2 = dataArea.getMinY(); 399 line1 = new Line2D.Double(x0, y0, x1, y1); 400 line2 = new Line2D.Double(x1, y1, x1, y2); 401 } 402 else if (orientation == PlotOrientation.VERTICAL) { 403 double y0 = axis.valueToJava2D(value, adjusted, 404 plot.getRangeAxisEdge()); 405 double y1 = y0 - getYOffset(); 406 double x0 = dataArea.getMinX(); 407 double x1 = x0 + getXOffset(); 408 double x2 = dataArea.getMaxX(); 409 line1 = new Line2D.Double(x0, y0, x1, y1); 410 line2 = new Line2D.Double(x1, y1, x2, y1); 411 } 412 g2.setPaint(plot.getRangeGridlinePaint()); 413 g2.setStroke(plot.getRangeGridlineStroke()); 414 g2.draw(line1); 415 g2.draw(line2); 416 417 } 418 419 /** 420 * Draws a range marker. 421 * 422 * @param g2 the graphics device. 423 * @param plot the plot. 424 * @param axis the value axis. 425 * @param marker the marker. 426 * @param dataArea the area for plotting data (not including 3D effect). 427 */ 428 public void drawRangeMarker(Graphics2D g2, 429 CategoryPlot plot, 430 ValueAxis axis, 431 Marker marker, 432 Rectangle2D dataArea) { 433 434 if (marker instanceof ValueMarker) { 435 ValueMarker vm = (ValueMarker) marker; 436 double value = vm.getValue(); 437 Range range = axis.getRange(); 438 if (!range.contains(value)) { 439 return; 440 } 441 442 Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(), 443 dataArea.getY() + getYOffset(), 444 dataArea.getWidth() - getXOffset(), 445 dataArea.getHeight() - getYOffset()); 446 447 GeneralPath path = null; 448 PlotOrientation orientation = plot.getOrientation(); 449 if (orientation == PlotOrientation.HORIZONTAL) { 450 float x = (float) axis.valueToJava2D(value, adjusted, 451 plot.getRangeAxisEdge()); 452 float y = (float) adjusted.getMaxY(); 453 path = new GeneralPath(); 454 path.moveTo(x, y); 455 path.lineTo((float) (x + getXOffset()), 456 y - (float) getYOffset()); 457 path.lineTo((float) (x + getXOffset()), 458 (float) (adjusted.getMinY() - getYOffset())); 459 path.lineTo(x, (float) adjusted.getMinY()); 460 path.closePath(); 461 } 462 else if (orientation == PlotOrientation.VERTICAL) { 463 float y = (float) axis.valueToJava2D(value, adjusted, 464 plot.getRangeAxisEdge()); 465 float x = (float) dataArea.getX(); 466 path = new GeneralPath(); 467 path.moveTo(x, y); 468 path.lineTo(x + (float) this.xOffset, y - (float) this.yOffset); 469 path.lineTo((float) (adjusted.getMaxX() + this.xOffset), 470 y - (float) this.yOffset); 471 path.lineTo((float) (adjusted.getMaxX()), y); 472 path.closePath(); 473 } 474 g2.setPaint(marker.getPaint()); 475 g2.fill(path); 476 g2.setPaint(marker.getOutlinePaint()); 477 g2.draw(path); 478 } 479 } 480 481 /** 482 * Draw a single data item. 483 * 484 * @param g2 the graphics device. 485 * @param state the renderer state. 486 * @param dataArea the area in which the data is drawn. 487 * @param plot the plot. 488 * @param domainAxis the domain axis. 489 * @param rangeAxis the range axis. 490 * @param dataset the dataset. 491 * @param row the row index (zero-based). 492 * @param column the column index (zero-based). 493 * @param pass the pass index. 494 */ 495 public void drawItem(Graphics2D g2, 496 CategoryItemRendererState state, 497 Rectangle2D dataArea, 498 CategoryPlot plot, 499 CategoryAxis domainAxis, 500 ValueAxis rangeAxis, 501 CategoryDataset dataset, 502 int row, 503 int column, 504 int pass) { 505 506 if (!getItemVisible(row, column)) { 507 return; 508 } 509 510 // nothing is drawn for null... 511 Number v = dataset.getValue(row, column); 512 if (v == null) { 513 return; 514 } 515 516 Rectangle2D adjusted = new Rectangle2D.Double(dataArea.getX(), 517 dataArea.getY() + getYOffset(), 518 dataArea.getWidth() - getXOffset(), 519 dataArea.getHeight() - getYOffset()); 520 521 PlotOrientation orientation = plot.getOrientation(); 522 523 // current data point... 524 double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 525 adjusted, plot.getDomainAxisEdge()); 526 double value = v.doubleValue(); 527 double y1 = rangeAxis.valueToJava2D(value, adjusted, 528 plot.getRangeAxisEdge()); 529 530 Shape shape = getItemShape(row, column); 531 if (orientation == PlotOrientation.HORIZONTAL) { 532 shape = ShapeUtilities.createTranslatedShape(shape, y1, x1); 533 } 534 else if (orientation == PlotOrientation.VERTICAL) { 535 shape = ShapeUtilities.createTranslatedShape(shape, x1, y1); 536 } 537 538 if (getItemLineVisible(row, column)) { 539 if (column != 0) { 540 541 Number previousValue = dataset.getValue(row, column - 1); 542 if (previousValue != null) { 543 544 // previous data point... 545 double previous = previousValue.doubleValue(); 546 double x0 = domainAxis.getCategoryMiddle(column - 1, 547 getColumnCount(), adjusted, 548 plot.getDomainAxisEdge()); 549 double y0 = rangeAxis.valueToJava2D(previous, adjusted, 550 plot.getRangeAxisEdge()); 551 552 double x2 = x0 + getXOffset(); 553 double y2 = y0 - getYOffset(); 554 double x3 = x1 + getXOffset(); 555 double y3 = y1 - getYOffset(); 556 557 GeneralPath clip = new GeneralPath(); 558 559 if (orientation == PlotOrientation.HORIZONTAL) { 560 clip.moveTo((float) y0, (float) x0); 561 clip.lineTo((float) y1, (float) x1); 562 clip.lineTo((float) y3, (float) x3); 563 clip.lineTo((float) y2, (float) x2); 564 clip.lineTo((float) y0, (float) x0); 565 clip.closePath(); 566 } 567 else if (orientation == PlotOrientation.VERTICAL) { 568 clip.moveTo((float) x0, (float) y0); 569 clip.lineTo((float) x1, (float) y1); 570 clip.lineTo((float) x3, (float) y3); 571 clip.lineTo((float) x2, (float) y2); 572 clip.lineTo((float) x0, (float) y0); 573 clip.closePath(); 574 } 575 576 g2.setPaint(getItemPaint(row, column)); 577 g2.fill(clip); 578 g2.setStroke(getItemOutlineStroke(row, column)); 579 g2.setPaint(getItemOutlinePaint(row, column)); 580 g2.draw(clip); 581 } 582 } 583 } 584 585 // draw the item label if there is one... 586 if (isItemLabelVisible(row, column)) { 587 drawItemLabel(g2, orientation, dataset, row, column, x1, y1, 588 (value < 0.0)); 589 } 590 591 // add an item entity, if this information is being collected 592 EntityCollection entities = state.getEntityCollection(); 593 if (entities != null) { 594 addItemEntity(entities, dataset, row, column, shape); 595 } 596 597 } 598 599 /** 600 * Checks this renderer for equality with an arbitrary object. 601 * 602 * @param obj the object (<code>null</code> permitted). 603 * 604 * @return A boolean. 605 */ 606 public boolean equals(Object obj) { 607 if (obj == this) { 608 return true; 609 } 610 if (!(obj instanceof LineRenderer3D)) { 611 return false; 612 } 613 LineRenderer3D that = (LineRenderer3D) obj; 614 if (this.xOffset != that.xOffset) { 615 return false; 616 } 617 if (this.yOffset != that.yOffset) { 618 return false; 619 } 620 if (!PaintUtilities.equal(this.wallPaint, that.wallPaint)) { 621 return false; 622 } 623 return super.equals(obj); 624 } 625 626 /** 627 * Provides serialization support. 628 * 629 * @param stream the output stream. 630 * 631 * @throws IOException if there is an I/O error. 632 */ 633 private void writeObject(ObjectOutputStream stream) throws IOException { 634 stream.defaultWriteObject(); 635 SerialUtilities.writePaint(this.wallPaint, stream); 636 } 637 638 /** 639 * Provides serialization support. 640 * 641 * @param stream the input stream. 642 * 643 * @throws IOException if there is an I/O error. 644 * @throws ClassNotFoundException if there is a classpath problem. 645 */ 646 private void readObject(ObjectInputStream stream) 647 throws IOException, ClassNotFoundException { 648 stream.defaultReadObject(); 649 this.wallPaint = SerialUtilities.readPaint(stream); 650 } 651 652 }