001/* =========================================================== 002 * Orson Charts : a 3D chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C)opyright 2013-2022, by David Gilbert. All rights reserved. 006 * 007 * https://github.com/jfree/orson-charts 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * Orson Charts home page: 028 * 029 * http://www.object-refinery.com/orsoncharts/index.html 030 * 031 */ 032 033package org.jfree.chart3d.renderer.xyz; 034 035import java.awt.Color; 036import java.io.Serializable; 037 038import org.jfree.chart3d.axis.Axis3D; 039import org.jfree.chart3d.data.DataUtils; 040import org.jfree.chart3d.data.Range; 041import org.jfree.chart3d.data.xyz.XYZDataset; 042import org.jfree.chart3d.graphics3d.Dimension3D; 043import org.jfree.chart3d.graphics3d.Object3D; 044import org.jfree.chart3d.graphics3d.World; 045import org.jfree.chart3d.internal.ObjectUtils; 046import org.jfree.chart3d.plot.XYZPlot; 047import org.jfree.chart3d.renderer.Renderer3DChangeEvent; 048 049/** 050 * A renderer that draws 3D bars on an {@link XYZPlot} using data from an 051 * {@link XYZDataset}. Here is a sample: 052 * <div> 053 * <img src="../../../../../../doc-files/XYZBarChart3DDemo1.svg" 054 * alt="XYZBarChart3DDemo1.svg" width="500" height="359"> 055 * </div> 056 * (refer to {@code XYZBarChart3DDemo1.java} for the code to generate 057 * the above chart). 058 * <br><br> 059 * NOTE: This class is serializable, but the serialization format is subject 060 * to change in future releases and should not be relied upon for persisting 061 * instances of this class. 062 */ 063@SuppressWarnings("serial") 064public class BarXYZRenderer extends AbstractXYZRenderer implements XYZRenderer, 065 Serializable { 066 067 /** The base value (normally 0.0, but can be modified). */ 068 private double base; 069 070 /** The width of the bars along the x-axis. */ 071 private double barXWidth; 072 073 /** The width of the bars along the z-axis. */ 074 private double barZWidth; 075 076 /** 077 * The color source used to fetch the color for the base of bars where 078 * the actual base of the bar is *outside* of the current axis range 079 * (that is, the bar is "cropped"). If this is {@code null}, then 080 * the regular bar color is used. 081 */ 082 private XYZColorSource baseColorSource; 083 084 /** 085 * The color source used to fetch the color for the top of bars where 086 * the actual top of the bar is *outside* of the current axis range 087 * (that is, the bar is "cropped"). If this is {@code null} then the 088 * bar top is always drawn using the series paint. 089 */ 090 private XYZColorSource topColorSource; 091 092 /** 093 * Creates a new default instance. 094 */ 095 public BarXYZRenderer() { 096 this.base = 0.0; 097 this.barXWidth = 0.8; 098 this.barZWidth = 0.8; 099 this.baseColorSource = new StandardXYZColorSource(Color.WHITE); 100 this.topColorSource = new StandardXYZColorSource(Color.BLACK); 101 102 } 103 104 /** 105 * Returns the value for the base of the bars. The default is 106 * {@code 0.0}. 107 * 108 * @return The value for the base of the bars. 109 */ 110 public double getBase() { 111 return this.base; 112 } 113 114 /** 115 * Sets the base value for the bars and sends a 116 * {@link Renderer3DChangeEvent} to all registered listeners. 117 * 118 * @param base the base. 119 */ 120 public void setBase(double base) { 121 this.base = base; 122 fireChangeEvent(true); 123 } 124 125 /** 126 * Returns the width of the bars in the direction of the x-axis, in the 127 * units of the x-axis. The default value is {@code 0.8}. 128 * 129 * @return The width of the bars. 130 */ 131 public double getBarXWidth() { 132 return this.barXWidth; 133 } 134 135 /** 136 * Sets the width of the bars in the direction of the x-axis and sends a 137 * {@link Renderer3DChangeEvent} to all registered listeners. 138 * 139 * @param width the width. 140 */ 141 public void setBarXWidth(double width) { 142 this.barXWidth = width; 143 fireChangeEvent(true); 144 } 145 146 /** 147 * Returns the width of the bars in the direction of the z-axis, in the 148 * units of the z-axis. The default value is {@code 0.8}. 149 * 150 * @return The width of the bars. 151 */ 152 public double getBarZWidth() { 153 return this.barZWidth; 154 } 155 156 /** 157 * Sets the width of the bars in the direction of the z-axis and sends a 158 * {@link Renderer3DChangeEvent} to all registered listeners. 159 * 160 * @param width the width. 161 */ 162 public void setBarZWidth(double width) { 163 this.barZWidth = width; 164 fireChangeEvent(true); 165 } 166 167 /** 168 * Returns the object used to fetch the color for the base of bars 169 * where the base of the bar is "cropped" (on account of the base value 170 * falling outside of the bounds of the y-axis). This is used to give a 171 * visual indication to the end-user that the bar on display is cropped. 172 * If this paint source is {@code null}, the regular series color 173 * will be used for the top of the bars. 174 * 175 * @return A paint source (possibly {@code null}). 176 */ 177 public XYZColorSource getBaseColorSource() { 178 return this.baseColorSource; 179 } 180 181 /** 182 * Sets the object that determines the color to use for the base of bars 183 * where the base value falls outside the axis range, and sends a 184 * {@link Renderer3DChangeEvent} to all registered listeners. If you set 185 * this to {@code null}, the regular series color will be used to draw 186 * the base of the bar, but it will be harder for the end-user to know that 187 * only a section of the bar is visible in the chart. Note that the 188 * default base paint source returns {@code Color.WHITE} always. 189 * 190 * @param source the source ({@code null} permitted). 191 * 192 * @see #getBaseColorSource() 193 * @see #getTopColorSource() 194 */ 195 public void setBaseColorSource(XYZColorSource source) { 196 this.baseColorSource = source; 197 fireChangeEvent(true); 198 } 199 200 /** 201 * Returns the object used to fetch the color for the top of bars 202 * where the top of the bar is "cropped" (on account of the data value 203 * falling outside of the bounds of the y-axis). This is used to give a 204 * visual indication to the end-user that the bar on display is cropped. 205 * If this paint source is {@code null}, the regular series color 206 * will be used for the top of the bars. 207 * 208 * @return A paint source (possibly {@code null}). 209 */ 210 public XYZColorSource getTopColorSource() { 211 return this.topColorSource; 212 } 213 214 /** 215 * Sets the object used to fetch the color for the top of bars where the 216 * top of the bar is "cropped", and sends a {@link Renderer3DChangeEvent} 217 * to all registered listeners. 218 * 219 * @param source the source ({@code null} permitted). 220 * 221 * @see #getTopColorSource() 222 * @see #getBaseColorSource() 223 */ 224 public void setTopColorSource(XYZColorSource source) { 225 this.topColorSource = source; 226 fireChangeEvent(true); 227 } 228 229 /** 230 * Returns the range that needs to be set on the x-axis in order for this 231 * renderer to be able to display all the data in the supplied dataset. 232 * 233 * @param dataset the dataset ({@code null} not permitted). 234 * 235 * @return The range ({@code null} if there is no data in the dataset). 236 */ 237 @Override 238 public Range findXRange(XYZDataset dataset) { 239 // delegate argument check... 240 Range xRange = DataUtils.findXRange(dataset); 241 if (xRange == null) { 242 return null; 243 } 244 double delta = this.barXWidth / 2.0; 245 return new Range(xRange.getMin() - delta, xRange.getMax() + delta); 246 } 247 248 /** 249 * Returns the range to use for the y-axis to ensure that all data values 250 * are visible on the chart. This method is overridden to ensure that the 251 * base value is included. 252 * 253 * @param dataset the dataset ({@code null} not permitted). 254 * 255 * @return The range ({@code null} when there is no data). 256 */ 257 @Override 258 public Range findYRange(XYZDataset dataset) { 259 return DataUtils.findYRange(dataset, this.base); 260 } 261 262 /** 263 * Returns the range to use for the z-axis to ensure that all data values 264 * are visible on the chart. This method is overridden to account for the 265 * bar widths. 266 * 267 * @param dataset the dataset ({@code null} not permitted). 268 * 269 * @return The range ({@code null} when there is no data). 270 */ 271 @Override 272 public Range findZRange(XYZDataset dataset) { 273 Range zRange = DataUtils.findZRange(dataset); 274 if (zRange == null) { 275 return null; 276 } 277 double delta = this.barZWidth / 2.0; 278 return new Range(zRange.getMin() - delta, zRange.getMax() + delta); 279 } 280 281 /** 282 * Adds a single bar representing one item from the dataset. 283 * 284 * @param dataset the dataset. 285 * @param series the series index. 286 * @param item the item index. 287 * @param world the world used to model the 3D chart. 288 * @param dimensions the plot dimensions in 3D. 289 * @param xOffset the x-offset. 290 * @param yOffset the y-offset. 291 * @param zOffset the z-offset. 292 */ 293 @Override 294 public void composeItem(XYZDataset dataset, int series, int item, 295 World world, Dimension3D dimensions, double xOffset, double yOffset, 296 double zOffset) { 297 298 XYZPlot plot = getPlot(); 299 Axis3D xAxis = plot.getXAxis(); 300 Axis3D yAxis = plot.getYAxis(); 301 Axis3D zAxis = plot.getZAxis(); 302 double x = dataset.getX(series, item); 303 double y = dataset.getY(series, item); 304 double z = dataset.getZ(series, item); 305 double xdelta = this.barXWidth / 2.0; 306 double zdelta = this.barZWidth / 2.0; 307 308 double x0 = xAxis.getRange().peggedValue(x - xdelta); 309 double x1 = xAxis.getRange().peggedValue(x + xdelta); 310 double z0 = zAxis.getRange().peggedValue(z - zdelta); 311 double z1 = zAxis.getRange().peggedValue(z + zdelta); 312 if ((x1 <= x0) || (z1 <= z0)) { 313 return; 314 } 315 double ylow = Math.min(this.base, y); 316 double yhigh = Math.max(this.base, y); 317 Range range = yAxis.getRange(); 318 if (!range.intersects(ylow, yhigh)) { 319 return; // the bar is not visible for the given axis range 320 } 321 double ybase = range.peggedValue(ylow); 322 double ytop = range.peggedValue(yhigh); 323 boolean inverted = this.base > y; 324 325 double wx0 = xAxis.translateToWorld(x0, dimensions.getWidth()); 326 double wx1 = xAxis.translateToWorld(x1, dimensions.getWidth()); 327 double wy0 = yAxis.translateToWorld(ybase, dimensions.getHeight()); 328 double wy1 = yAxis.translateToWorld(ytop, dimensions.getHeight()); 329 double wz0 = zAxis.translateToWorld(z0, dimensions.getDepth()); 330 double wz1 = zAxis.translateToWorld(z1, dimensions.getDepth()); 331 332 Color color = getColorSource().getColor(series, item); 333 Color baseColor = null; 334 if (this.baseColorSource != null && !range.contains(this.base)) { 335 baseColor = this.baseColorSource.getColor(series, item); 336 } 337 if (baseColor == null) { 338 baseColor = color; 339 } 340 341 Color topColor = null; 342 if (this.topColorSource != null && !range.contains(y)) { 343 topColor = this.topColorSource.getColor(series, item); 344 } 345 if (topColor == null) { 346 topColor = color; 347 } 348 349 Object3D bar = Object3D.createBar(wx1 - wx0, wz1 - wz0, 350 ((wx0 + wx1) / 2.0) + xOffset, wy1 + yOffset, 351 ((wz0 + wz1) / 2.0) + zOffset, wy0 + yOffset, color, 352 baseColor, topColor, inverted); 353 world.add(bar); 354 } 355 356 /** 357 * Tests this renderer for equality with an arbitrary object. 358 * 359 * @param obj the object ({@code null} permitted). 360 * 361 * @return A boolean. 362 */ 363 @Override 364 public boolean equals(Object obj) { 365 if (obj == this) { 366 return true; 367 } 368 if (!(obj instanceof BarXYZRenderer)) { 369 return false; 370 } 371 BarXYZRenderer that = (BarXYZRenderer) obj; 372 if (this.base != that.base) { 373 return false; 374 } 375 if (this.barXWidth != that.barXWidth) { 376 return false; 377 } 378 if (this.barZWidth != that.barZWidth) { 379 return false; 380 } 381 if (!ObjectUtils.equals(this.baseColorSource, that.baseColorSource)) { 382 return false; 383 } 384 if (!ObjectUtils.equals(this.topColorSource, that.topColorSource)) { 385 return false; 386 } 387 return super.equals(obj); 388 } 389 390}