001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2005, 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 * CyclicXYItemRenderer.java 029 * --------------------------- 030 * (C) Copyright 2003, 2004, by Nicolas Brodu and Contributors. 031 * 032 * Original Author: Nicolas Brodu; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * 035 * $Id: CyclicXYItemRenderer.java,v 1.4.2.1 2005/10/25 20:56:21 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB); 040 * 23-Dec-2003 : Added missing Javadocs (DG); 041 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG); 042 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 043 * getYValue() (DG); 044 * 045 */ 046 047 package org.jfree.chart.renderer.xy; 048 049 import java.awt.Graphics2D; 050 import java.awt.geom.Rectangle2D; 051 import java.io.Serializable; 052 053 import org.jfree.chart.axis.CyclicNumberAxis; 054 import org.jfree.chart.axis.ValueAxis; 055 import org.jfree.chart.labels.XYToolTipGenerator; 056 import org.jfree.chart.plot.CrosshairState; 057 import org.jfree.chart.plot.PlotRenderingInfo; 058 import org.jfree.chart.plot.XYPlot; 059 import org.jfree.chart.urls.XYURLGenerator; 060 import org.jfree.data.DomainOrder; 061 import org.jfree.data.general.DatasetChangeListener; 062 import org.jfree.data.general.DatasetGroup; 063 import org.jfree.data.xy.XYDataset; 064 065 /** 066 * The Cyclic XY item renderer is specially designed to handle cyclic axis. 067 * While the standard renderer would draw a line across the plot when a cycling 068 * occurs, the cyclic renderer splits the line at each cycle end instead. This 069 * is done by interpolating new points at cycle boundary. Thus, correct 070 * appearance is restored. 071 * 072 * The Cyclic XY item renderer works exactly like a standard XY item renderer 073 * with non-cyclic axis. 074 * 075 * @author Nicolas Brodu 076 */ 077 public class CyclicXYItemRenderer extends StandardXYItemRenderer 078 implements Serializable { 079 080 /** For serialization. */ 081 private static final long serialVersionUID = 4035912243303764892L; 082 083 /** 084 * Default constructor. 085 */ 086 public CyclicXYItemRenderer() { 087 super(); 088 } 089 090 /** 091 * Creates a new renderer. 092 * 093 * @param type the renderer type. 094 */ 095 public CyclicXYItemRenderer(int type) { 096 super(type); 097 } 098 099 /** 100 * Creates a new renderer. 101 * 102 * @param type the renderer type. 103 * @param labelGenerator the tooltip generator. 104 */ 105 public CyclicXYItemRenderer(int type, XYToolTipGenerator labelGenerator) { 106 super(type, labelGenerator); 107 } 108 109 /** 110 * Creates a new renderer. 111 * 112 * @param type the renderer type. 113 * @param labelGenerator the tooltip generator. 114 * @param urlGenerator the url generator. 115 */ 116 public CyclicXYItemRenderer(int type, 117 XYToolTipGenerator labelGenerator, 118 XYURLGenerator urlGenerator) { 119 super(type, labelGenerator, urlGenerator); 120 } 121 122 123 /** 124 * Draws the visual representation of a single data item. 125 * When using cyclic axis, do not draw a line from right to left when 126 * cycling as would a standard XY item renderer, but instead draw a line 127 * from the previous point to the cycle bound in the last cycle, and a line 128 * from the cycle bound to current point in the current cycle. 129 * 130 * @param g2 the graphics device. 131 * @param state the renderer state. 132 * @param dataArea the data area. 133 * @param info the plot rendering info. 134 * @param plot the plot. 135 * @param domainAxis the domain axis. 136 * @param rangeAxis the range axis. 137 * @param dataset the dataset. 138 * @param series the series index. 139 * @param item the item index. 140 * @param crosshairState crosshair information for the plot 141 * (<code>null</code> permitted). 142 * @param pass the current pass index. 143 */ 144 public void drawItem(Graphics2D g2, 145 XYItemRendererState state, 146 Rectangle2D dataArea, 147 PlotRenderingInfo info, 148 XYPlot plot, 149 ValueAxis domainAxis, 150 ValueAxis rangeAxis, 151 XYDataset dataset, 152 int series, 153 int item, 154 CrosshairState crosshairState, 155 int pass) { 156 157 if ((!getPlotLines()) || ((!(domainAxis instanceof CyclicNumberAxis)) 158 && (!(rangeAxis instanceof CyclicNumberAxis))) || (item <= 0)) { 159 super.drawItem( 160 g2, state, dataArea, info, plot, domainAxis, rangeAxis, 161 dataset, series, item, 162 crosshairState, pass 163 ); 164 return; 165 } 166 167 // get the previous data point... 168 Number xn = dataset.getX(series, item - 1); 169 Number yn = dataset.getY(series, item - 1); 170 // If null, don't draw line => then delegate to parent 171 if (yn == null || xn == null) { 172 super.drawItem( 173 g2, state, dataArea, info, plot, domainAxis, rangeAxis, 174 dataset, series, item, 175 crosshairState, pass 176 ); 177 return; 178 } 179 double[] x = new double[2]; 180 double[] y = new double[2]; 181 x[0] = xn.doubleValue(); 182 y[0] = yn.doubleValue(); 183 184 // get the data point... 185 xn = dataset.getX(series, item); 186 yn = dataset.getY(series, item); 187 // If null, don't draw line at all 188 if (yn == null || xn == null) { 189 return; 190 } 191 x[1] = xn.doubleValue(); 192 y[1] = yn.doubleValue(); 193 194 // Now split the segment as needed 195 double xcycleBound = Double.NaN; 196 double ycycleBound = Double.NaN; 197 boolean xBoundMapping = false, yBoundMapping = false; 198 CyclicNumberAxis cnax = null, cnay = null; 199 200 if (domainAxis instanceof CyclicNumberAxis) { 201 cnax = (CyclicNumberAxis) domainAxis; 202 xcycleBound = cnax.getCycleBound(); 203 xBoundMapping = cnax.isBoundMappedToLastCycle(); 204 // If the segment must be splitted, insert a new point 205 // Strict test forces to have real segments (not 2 equal points) 206 // and avoids division by 0 207 if ((x[0] != x[1]) 208 && ((xcycleBound >= x[0]) 209 && (xcycleBound <= x[1]) 210 || (xcycleBound >= x[1]) 211 && (xcycleBound <= x[0]))) { 212 double[] nx = new double[3]; 213 double[] ny = new double[3]; 214 nx[0] = x[0]; nx[2] = x[1]; ny[0] = y[0]; ny[2] = y[1]; 215 nx[1] = xcycleBound; 216 ny[1] = (y[1] - y[0]) * (xcycleBound - x[0]) 217 / (x[1] - x[0]) + y[0]; 218 x = nx; y = ny; 219 } 220 } 221 222 if (rangeAxis instanceof CyclicNumberAxis) { 223 cnay = (CyclicNumberAxis) rangeAxis; 224 ycycleBound = cnay.getCycleBound(); 225 yBoundMapping = cnay.isBoundMappedToLastCycle(); 226 // The split may occur in either x splitted segments, if any, but 227 // not in both 228 if ((y[0] != y[1]) && ((ycycleBound >= y[0]) 229 && (ycycleBound <= y[1]) 230 || (ycycleBound >= y[1]) && (ycycleBound <= y[0]))) { 231 double[] nx = new double[x.length + 1]; 232 double[] ny = new double[y.length + 1]; 233 nx[0] = x[0]; nx[2] = x[1]; ny[0] = y[0]; ny[2] = y[1]; 234 ny[1] = ycycleBound; 235 nx[1] = (x[1] - x[0]) * (ycycleBound - y[0]) 236 / (y[1] - y[0]) + x[0]; 237 if (x.length == 3) { 238 nx[3] = x[2]; ny[3] = y[2]; 239 } 240 x = nx; y = ny; 241 } 242 else if ((x.length == 3) && (y[1] != y[2]) && ((ycycleBound >= y[1]) 243 && (ycycleBound <= y[2]) 244 || (ycycleBound >= y[2]) && (ycycleBound <= y[1]))) { 245 double[] nx = new double[4]; 246 double[] ny = new double[4]; 247 nx[0] = x[0]; nx[1] = x[1]; nx[3] = x[2]; 248 ny[0] = y[0]; ny[1] = y[1]; ny[3] = y[2]; 249 ny[2] = ycycleBound; 250 nx[2] = (x[2] - x[1]) * (ycycleBound - y[1]) 251 / (y[2] - y[1]) + x[1]; 252 x = nx; y = ny; 253 } 254 } 255 256 // If the line is not wrapping, then parent is OK 257 if (x.length == 2) { 258 super.drawItem( 259 g2, state, dataArea, info, plot, domainAxis, rangeAxis, dataset, 260 series, item, crosshairState, pass 261 ); 262 return; 263 } 264 265 OverwriteDataSet newset = new OverwriteDataSet(x, y, dataset); 266 267 if (cnax != null) { 268 if (xcycleBound == x[0]) { 269 cnax.setBoundMappedToLastCycle(x[1] <= xcycleBound); 270 } 271 if (xcycleBound == x[1]) { 272 cnax.setBoundMappedToLastCycle(x[0] <= xcycleBound); 273 } 274 } 275 if (cnay != null) { 276 if (ycycleBound == y[0]) { 277 cnay.setBoundMappedToLastCycle(y[1] <= ycycleBound); 278 } 279 if (ycycleBound == y[1]) { 280 cnay.setBoundMappedToLastCycle(y[0] <= ycycleBound); 281 } 282 } 283 super.drawItem( 284 g2, state, dataArea, info, plot, domainAxis, rangeAxis, 285 newset, series, 1, crosshairState, pass 286 ); 287 288 if (cnax != null) { 289 if (xcycleBound == x[1]) { 290 cnax.setBoundMappedToLastCycle(x[2] <= xcycleBound); 291 } 292 if (xcycleBound == x[2]) { 293 cnax.setBoundMappedToLastCycle(x[1] <= xcycleBound); 294 } 295 } 296 if (cnay != null) { 297 if (ycycleBound == y[1]) { 298 cnay.setBoundMappedToLastCycle(y[2] <= ycycleBound); 299 } 300 if (ycycleBound == y[2]) { 301 cnay.setBoundMappedToLastCycle(y[1] <= ycycleBound); 302 } 303 } 304 super.drawItem( 305 g2, state, dataArea, info, plot, domainAxis, rangeAxis, newset, 306 series, 2, crosshairState, pass 307 ); 308 309 if (x.length == 4) { 310 if (cnax != null) { 311 if (xcycleBound == x[2]) { 312 cnax.setBoundMappedToLastCycle(x[3] <= xcycleBound); 313 } 314 if (xcycleBound == x[3]) { 315 cnax.setBoundMappedToLastCycle(x[2] <= xcycleBound); 316 } 317 } 318 if (cnay != null) { 319 if (ycycleBound == y[2]) { 320 cnay.setBoundMappedToLastCycle(y[3] <= ycycleBound); 321 } 322 if (ycycleBound == y[3]) { 323 cnay.setBoundMappedToLastCycle(y[2] <= ycycleBound); 324 } 325 } 326 super.drawItem( 327 g2, state, dataArea, info, plot, domainAxis, rangeAxis, newset, 328 series, 3, crosshairState, pass 329 ); 330 } 331 332 if (cnax != null) { 333 cnax.setBoundMappedToLastCycle(xBoundMapping); 334 } 335 if (cnay != null) { 336 cnay.setBoundMappedToLastCycle(yBoundMapping); 337 } 338 } 339 340 /** 341 * A dataset to hold the interpolated points when drawing new lines. 342 */ 343 protected static class OverwriteDataSet implements XYDataset { 344 345 /** The delegate dataset. */ 346 protected XYDataset delegateSet; 347 348 /** Storage for the x and y values. */ 349 Double[] x, y; 350 351 /** 352 * Creates a new dataset. 353 * 354 * @param x the x values. 355 * @param y the y values. 356 * @param delegateSet the dataset. 357 */ 358 public OverwriteDataSet(double [] x, double[] y, 359 XYDataset delegateSet) { 360 this.delegateSet = delegateSet; 361 this.x = new Double[x.length]; this.y = new Double[y.length]; 362 for (int i = 0; i < x.length; ++i) { 363 this.x[i] = new Double(x[i]); 364 this.y[i] = new Double(y[i]); 365 } 366 } 367 368 /** 369 * Returns the order of the domain (X) values. 370 * 371 * @return The domain order. 372 */ 373 public DomainOrder getDomainOrder() { 374 return DomainOrder.NONE; 375 } 376 377 /** 378 * Returns the number of items for the given series. 379 * 380 * @param series the series index (zero-based). 381 * 382 * @return The item count. 383 */ 384 public int getItemCount(int series) { 385 return this.x.length; 386 } 387 388 /** 389 * Returns the x-value. 390 * 391 * @param series the series index (zero-based). 392 * @param item the item index (zero-based). 393 * 394 * @return The x-value. 395 */ 396 public Number getX(int series, int item) { 397 return this.x[item]; 398 } 399 400 /** 401 * Returns the x-value (as a double primitive) for an item within a 402 * series. 403 * 404 * @param series the series (zero-based index). 405 * @param item the item (zero-based index). 406 * 407 * @return The x-value. 408 */ 409 public double getXValue(int series, int item) { 410 double result = Double.NaN; 411 Number x = getX(series, item); 412 if (x != null) { 413 result = x.doubleValue(); 414 } 415 return result; 416 } 417 418 /** 419 * Returns the y-value. 420 * 421 * @param series the series index (zero-based). 422 * @param item the item index (zero-based). 423 * 424 * @return The y-value. 425 */ 426 public Number getY(int series, int item) { 427 return this.y[item]; 428 } 429 430 /** 431 * Returns the y-value (as a double primitive) for an item within a 432 * series. 433 * 434 * @param series the series (zero-based index). 435 * @param item the item (zero-based index). 436 * 437 * @return The y-value. 438 */ 439 public double getYValue(int series, int item) { 440 double result = Double.NaN; 441 Number y = getY(series, item); 442 if (y != null) { 443 result = y.doubleValue(); 444 } 445 return result; 446 } 447 448 /** 449 * Returns the number of series in the dataset. 450 * 451 * @return The series count. 452 */ 453 public int getSeriesCount() { 454 return this.delegateSet.getSeriesCount(); 455 } 456 457 /** 458 * Returns the name of the given series. 459 * 460 * @param series the series index (zero-based). 461 * 462 * @return The series name. 463 */ 464 public Comparable getSeriesKey(int series) { 465 return this.delegateSet.getSeriesKey(series); 466 } 467 468 /** 469 * Returns the index of the named series, or -1. 470 * 471 * @param seriesName the series name. 472 * 473 * @return The index. 474 */ 475 public int indexOf(Comparable seriesName) { 476 return this.delegateSet.indexOf(seriesName); 477 } 478 479 /** 480 * Does nothing. 481 * 482 * @param listener ignored. 483 */ 484 public void addChangeListener(DatasetChangeListener listener) { 485 // unused in parent 486 } 487 488 /** 489 * Does nothing. 490 * 491 * @param listener ignored. 492 */ 493 public void removeChangeListener(DatasetChangeListener listener) { 494 // unused in parent 495 } 496 497 /** 498 * Returns the dataset group. 499 * 500 * @return The dataset group. 501 */ 502 public DatasetGroup getGroup() { 503 // unused but must return something, so while we are at it... 504 return this.delegateSet.getGroup(); 505 } 506 507 /** 508 * Does nothing. 509 * 510 * @param group ignored. 511 */ 512 public void setGroup(DatasetGroup group) { 513 // unused in parent 514 } 515 516 } 517 518 } 519 520