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.category; 034 035import java.awt.Color; 036import java.io.Serializable; 037 038import org.jfree.chart3d.Chart3DFactory; 039import org.jfree.chart3d.axis.CategoryAxis3D; 040import org.jfree.chart3d.axis.ValueAxis3D; 041import org.jfree.chart3d.data.DataUtils; 042import org.jfree.chart3d.data.KeyedValues3DItemKey; 043import org.jfree.chart3d.data.Range; 044import org.jfree.chart3d.data.Values3D; 045import org.jfree.chart3d.data.category.CategoryDataset3D; 046import org.jfree.chart3d.graphics3d.Dimension3D; 047import org.jfree.chart3d.graphics3d.Object3D; 048import org.jfree.chart3d.graphics3d.World; 049import org.jfree.chart3d.internal.ObjectUtils; 050import org.jfree.chart3d.label.ItemLabelPositioning; 051import org.jfree.chart3d.plot.CategoryPlot3D; 052import org.jfree.chart3d.renderer.Renderer3DChangeEvent; 053 054/** 055 * A renderer for creating 3D bar charts from a {@link CategoryDataset3D} (for 056 * use with a {@link CategoryPlot3D}). For example: 057 * <div> 058 * <img src="../../../../../../doc-files/BarChart3DDemo1.svg" alt="BarChart3DDemo1.svg" 059 * width="500" height="359"> 060 * </div> 061 * (refer to {@code BarChart3DDemo1.java} for the code to generate the 062 * above chart). 063 * <br><br> 064 * There is a factory method to create a chart using this renderer - see 065 * {@link Chart3DFactory#createBarChart(String, String, CategoryDataset3D, 066 * String, String, String)}. 067 * <br><br> 068 * NOTE: This class is serializable, but the serialization format is subject 069 * to change in future releases and should not be relied upon for persisting 070 * instances of this class. 071 */ 072@SuppressWarnings("serial") 073public class BarRenderer3D extends AbstractCategoryRenderer3D 074 implements Serializable { 075 076 /** The base of the bars - defaults to 0.0. */ 077 private double base; 078 079 /** The bar width as a percentage of the column width. */ 080 private double barXWidth; 081 082 /** The bar width as a percentage of the row width. */ 083 private double barZWidth; 084 085 /** 086 * The color source used to fetch the color for the base of bars where 087 * the actual base of the bar is *outside* of the current axis range 088 * (that is, the bar is "cropped"). If this is {@code null}, then 089 * the regular bar color is used. 090 */ 091 private CategoryColorSource baseColorSource; 092 093 /** 094 * The paint source used to fetch the color for the top of bars where 095 * the actual top of the bar is *outside* of the current axis range 096 * (that is, the bar is "cropped"). If this is {@code null} then the 097 * bar top is always drawn using the series paint. 098 */ 099 private CategoryColorSource topColorSource; 100 101 /** 102 * Creates a new renderer with default attribute values. 103 */ 104 public BarRenderer3D() { 105 this.base = 0.0; 106 this.barXWidth = 0.8; 107 this.barZWidth = 0.5; 108 this.baseColorSource = new StandardCategoryColorSource(Color.WHITE); 109 this.topColorSource = new StandardCategoryColorSource(Color.BLACK); 110 } 111 112 /** 113 * Returns the base value for the bars. The default value 114 * is {@code 0.0}. 115 * 116 * @return The base value for the bars. 117 * 118 * @see #setBase(double) 119 */ 120 public double getBase() { 121 return this.base; 122 } 123 124 /** 125 * Sets the base value for the bars and fires a 126 * {@link org.jfree.chart3d.renderer.Renderer3DChangeEvent}. 127 * 128 * @param base the new base value. 129 * 130 * @see #getBase() 131 */ 132 public void setBase(double base) { 133 this.base = base; 134 fireChangeEvent(true); 135 } 136 137 /** 138 * Returns the bar width as a percentage of the column width. 139 * The default value is {@code 0.8} (the total width of each column 140 * in world units is {@code 1.0}, so the default leaves a small gap 141 * between each bar). 142 * 143 * @return The bar width (in world units). 144 */ 145 public double getBarXWidth() { 146 return this.barXWidth; 147 } 148 149 /** 150 * Sets the the bar width as a percentage of the column width and 151 * fires a {@link Renderer3DChangeEvent}. 152 * 153 * @param barXWidth the new width. 154 */ 155 public void setBarXWidth(double barXWidth) { 156 this.barXWidth = barXWidth; 157 fireChangeEvent(true); 158 } 159 160 /** 161 * Returns the bar width as a percentage of the row width. 162 * The default value is {@code 0.8}. 163 * 164 * @return The bar width. 165 */ 166 public double getBarZWidth() { 167 return this.barZWidth; 168 } 169 170 /** 171 * Sets the the bar width as a percentage of the row width and 172 * fires a {@link org.jfree.chart3d.renderer.Renderer3DChangeEvent}. 173 * 174 * @param barZWidth the new width. 175 */ 176 public void setBarZWidth(double barZWidth) { 177 this.barZWidth = barZWidth; 178 fireChangeEvent(true); 179 } 180 181 /** 182 * Returns the object used to fetch the color for the base of bars 183 * where the base of the bar is "cropped" (on account of the base value 184 * falling outside of the bounds of the y-axis). This is used to give a 185 * visual indication to the end-user that the bar on display is cropped. 186 * If this paint source is {@code null}, the regular series color 187 * will be used for the top of the bars. 188 * 189 * @return A paint source (possibly {@code null}). 190 */ 191 public CategoryColorSource getBaseColorSource() { 192 return this.baseColorSource; 193 } 194 195 /** 196 * Sets the object that determines the color to use for the base of bars 197 * where the base value falls outside the axis range, and sends a 198 * {@link Renderer3DChangeEvent} to all registered listeners. If you set 199 * this to {@code null}, the regular series color will be used to draw 200 * the base of the bar, but it will be harder for the end-user to know that 201 * only a section of the bar is visible in the chart. Note that the 202 * default base paint source returns {@code Color.WHITE} always. 203 * 204 * @param source the source ({@code null} permitted). 205 * 206 * @see #getBaseColorSource() 207 * @see #getTopColorSource() 208 */ 209 public void setBaseColorSource(CategoryColorSource source) { 210 this.baseColorSource = source; 211 fireChangeEvent(true); 212 } 213 214 /** 215 * Returns the object used to fetch the color for the top of bars 216 * where the top of the bar is "cropped" (on account of the data value 217 * falling outside of the bounds of the y-axis). This is used to give a 218 * visual indication to the end-user that the bar on display is cropped. 219 * If this paint source is {@code null}, the regular series color 220 * will be used for the top of the bars. 221 * 222 * @return A paint source (possibly {@code null}). 223 */ 224 public CategoryColorSource getTopColorSource() { 225 return this.topColorSource; 226 } 227 228 /** 229 * Sets the object used to fetch the color for the top of bars where the 230 * top of the bar is "cropped", and sends a {@link Renderer3DChangeEvent} 231 * to all registered listeners. 232 * 233 * @param source the source ({@code null} permitted). 234 * 235 * @see #getTopColorSource() 236 * @see #getBaseColorSource() 237 */ 238 public void setTopColorSource(CategoryColorSource source) { 239 this.topColorSource = source; 240 fireChangeEvent(true); 241 } 242 243 /** 244 * Returns the range of values that will be required on the value axis 245 * to see all the data from the dataset. We override the method to 246 * include in the range the base value for the bars. 247 * 248 * @param data the data ({@code null} not permitted). 249 * 250 * @return The range (possibly {@code null}) 251 */ 252 @Override 253 public Range findValueRange(Values3D<? extends Number> data) { 254 return DataUtils.findValueRange(data, this.base); 255 } 256 257 /** 258 * Constructs and places one item from the specified dataset into the given 259 * world. This method will be called by the {@link CategoryPlot3D} class 260 * while iterating over the items in the dataset. 261 * 262 * @param dataset the dataset ({@code null} not permitted). 263 * @param series the series index. 264 * @param row the row index. 265 * @param column the column index. 266 * @param world the world ({@code null} not permitted). 267 * @param dimensions the plot dimensions ({@code null} not permitted). 268 * @param xOffset the x-offset. 269 * @param yOffset the y-offset. 270 * @param zOffset the z-offset. 271 */ 272 @Override 273 public void composeItem(CategoryDataset3D dataset, int series, int row, 274 int column, World world, Dimension3D dimensions, 275 double xOffset, double yOffset, double zOffset) { 276 277 double value = dataset.getDoubleValue(series, row, column); 278 if (Double.isNaN(value)) { 279 return; 280 } 281 // delegate to a separate method that is reused by the 282 // StackedBarRenderer3D subclass... 283 composeItem(value, this.base, dataset, series, row, column, world, 284 dimensions, xOffset, yOffset, zOffset); 285 } 286 287 /** 288 * Performs the actual work of composing a bar to represent one item in the 289 * dataset. This method is reused by the {@link StackedBarRenderer3D} 290 * subclass. 291 * 292 * @param value the data value (top of the bar). 293 * @param barBase the base value for the bar. 294 * @param dataset the dataset. 295 * @param series the series index. 296 * @param row the row index. 297 * @param column the column index. 298 * @param world the world. 299 * @param dimensions the plot dimensions. 300 * @param xOffset the x-offset. 301 * @param yOffset the y-offset. 302 * @param zOffset the z-offset. 303 */ 304 @SuppressWarnings("unchecked") 305 protected void composeItem(double value, double barBase, 306 CategoryDataset3D dataset, int series, int row, int column, 307 World world, Dimension3D dimensions, double xOffset, 308 double yOffset, double zOffset) { 309 310 Comparable<?> seriesKey = dataset.getSeriesKey(series); 311 Comparable<?> rowKey = dataset.getRowKey(row); 312 Comparable<?> columnKey = dataset.getColumnKey(column); 313 314 double vlow = Math.min(barBase, value); 315 double vhigh = Math.max(barBase, value); 316 317 CategoryPlot3D plot = getPlot(); 318 CategoryAxis3D rowAxis = plot.getRowAxis(); 319 CategoryAxis3D columnAxis = plot.getColumnAxis(); 320 ValueAxis3D valueAxis = plot.getValueAxis(); 321 Range range = valueAxis.getRange(); 322 if (!range.intersects(vlow, vhigh)) { 323 return; // the bar is not visible for the given axis range 324 } 325 326 double vbase = range.peggedValue(vlow); 327 double vtop = range.peggedValue(vhigh); 328 boolean inverted = barBase > value; 329 330 double rowValue = rowAxis.getCategoryValue(rowKey); 331 double columnValue = columnAxis.getCategoryValue(columnKey); 332 333 double width = dimensions.getWidth(); 334 double height = dimensions.getHeight(); 335 double depth = dimensions.getDepth(); 336 double xx = columnAxis.translateToWorld(columnValue, width) + xOffset; 337 double yy = valueAxis.translateToWorld(vtop, height) + yOffset; 338 double zz = rowAxis.translateToWorld(rowValue, depth) + zOffset; 339 340 double xw = this.barXWidth * columnAxis.getCategoryWidth(); 341 double zw = this.barZWidth * rowAxis.getCategoryWidth(); 342 double xxw = columnAxis.translateToWorld(xw, width); 343 double xzw = rowAxis.translateToWorld(zw, depth); 344 double basew = valueAxis.translateToWorld(vbase, height) + yOffset; 345 346 Color color = getColorSource().getColor(series, row, column); 347 Color baseColor = null; 348 if (this.baseColorSource != null && !range.contains(this.base)) { 349 baseColor = this.baseColorSource.getColor(series, row, column); 350 } 351 if (baseColor == null) { 352 baseColor = color; 353 } 354 355 Color topColor = null; 356 if (this.topColorSource != null && !range.contains(value)) { 357 topColor = this.topColorSource.getColor(series, row, column); 358 } 359 if (topColor == null) { 360 topColor = color; 361 } 362 Object3D bar = Object3D.createBar(xxw, xzw, xx, yy, zz, basew, 363 color, baseColor, topColor, inverted); 364 KeyedValues3DItemKey itemKey = new KeyedValues3DItemKey(seriesKey, 365 rowKey, columnKey); 366 bar.setProperty(Object3D.ITEM_KEY, itemKey); 367 world.add(bar); 368 drawItemLabels(world, dataset, itemKey, xx, yy, zz, basew, inverted); 369 } 370 371 /** 372 * Draws the item labels. 373 * 374 * @param world the world. 375 * @param dataset the dataset. 376 * @param itemKey the item key. 377 * @param xw the x-coordinate. 378 * @param yw the y-coordinate. 379 * @param zw the z-coordinate. 380 * @param basew the base coordinate. 381 * @param inverted is the y-axis inverted? 382 */ 383 protected void drawItemLabels(World world, CategoryDataset3D dataset, 384 KeyedValues3DItemKey itemKey, double xw, double yw, double zw, 385 double basew, boolean inverted) { 386 ItemLabelPositioning positioning = getItemLabelPositioning(); 387 if (getItemLabelGenerator() == null) { 388 return; 389 } 390 String label = getItemLabelGenerator().generateItemLabel(dataset, 391 itemKey.getSeriesKey(), itemKey.getRowKey(), 392 itemKey.getColumnKey()); 393 if (label != null) { 394 Dimension3D dimensions = getPlot().getDimensions(); 395 double dx = getItemLabelOffsets().getDX(); 396 double dy = getItemLabelOffsets().getDY() * dimensions.getHeight(); 397 double dz = getItemLabelOffsets().getDZ() * getBarZWidth(); 398 double yy = yw; 399 if (inverted) { 400 yy = basew; 401 dy = -dy; 402 } 403 if (positioning.equals(ItemLabelPositioning.CENTRAL)) { 404 Object3D labelObj = Object3D.createLabelObject(label, 405 getItemLabelFont(), getItemLabelColor(), 406 getItemLabelBackgroundColor(), xw + dx, yy + dy, zw, 407 false, true); 408 labelObj.setProperty(Object3D.ITEM_KEY, itemKey); 409 world.add(labelObj); 410 } else if (positioning.equals( 411 ItemLabelPositioning.FRONT_AND_BACK)) { 412 Object3D labelObj1 = Object3D.createLabelObject(label, 413 getItemLabelFont(), getItemLabelColor(), 414 getItemLabelBackgroundColor(), xw + dx, yy + dy, 415 zw + dz, false, false); 416 labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); 417 world.add(labelObj1); 418 Object3D labelObj2 = Object3D.createLabelObject(label, 419 getItemLabelFont(), getItemLabelColor(), 420 getItemLabelBackgroundColor(), xw + dx, yy + dy, 421 zw - dz, true, false); 422 labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); 423 world.add(labelObj2); 424 } 425 } 426 } 427 428 /** 429 * Tests this renderer for equality with an arbitrary object. 430 * 431 * @param obj the object ({@code null} permitted). 432 * 433 * @return A boolean. 434 */ 435 @Override 436 public boolean equals(Object obj) { 437 if (obj == this) { 438 return true; 439 } 440 if (!(obj instanceof BarRenderer3D)) { 441 return false; 442 } 443 BarRenderer3D that = (BarRenderer3D) obj; 444 if (this.base != that.base) { 445 return false; 446 } 447 if (this.barXWidth != that.barXWidth) { 448 return false; 449 } 450 if (this.barZWidth != that.barZWidth) { 451 return false; 452 } 453 if (!ObjectUtils.equals(this.baseColorSource, that.baseColorSource)) { 454 return false; 455 } 456 if (!ObjectUtils.equals(this.topColorSource, that.topColorSource)) { 457 return false; 458 } 459 return super.equals(obj); 460 } 461}