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