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