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