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