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 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 protected void drawItemLabels(World world, CategoryDataset3D dataset, 372 KeyedValues3DItemKey itemKey, double xw, double yw, double zw, 373 double basew, boolean inverted) { 374 ItemLabelPositioning positioning = getItemLabelPositioning(); 375 if (getItemLabelGenerator() == null) { 376 return; 377 } 378 String label = getItemLabelGenerator().generateItemLabel(dataset, 379 itemKey.getSeriesKey(), itemKey.getRowKey(), 380 itemKey.getColumnKey()); 381 if (label != null) { 382 Dimension3D dimensions = getPlot().getDimensions(); 383 double dx = getItemLabelOffsets().getDX(); 384 double dy = getItemLabelOffsets().getDY() * dimensions.getHeight(); 385 double dz = getItemLabelOffsets().getDZ() * getBarZWidth(); 386 double yy = yw; 387 if (inverted) { 388 yy = basew; 389 dy = -dy; 390 } 391 if (positioning.equals(ItemLabelPositioning.CENTRAL)) { 392 Object3D labelObj = Object3D.createLabelObject(label, 393 getItemLabelFont(), getItemLabelColor(), 394 getItemLabelBackgroundColor(), xw + dx, yy + dy, zw, 395 false, true); 396 labelObj.setProperty(Object3D.ITEM_KEY, itemKey); 397 world.add(labelObj); 398 } else if (positioning.equals( 399 ItemLabelPositioning.FRONT_AND_BACK)) { 400 Object3D labelObj1 = Object3D.createLabelObject(label, 401 getItemLabelFont(), getItemLabelColor(), 402 getItemLabelBackgroundColor(), xw + dx, yy + dy, 403 zw + dz, false, false); 404 labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); 405 world.add(labelObj1); 406 Object3D labelObj2 = Object3D.createLabelObject(label, 407 getItemLabelFont(), getItemLabelColor(), 408 getItemLabelBackgroundColor(), xw + dx, yy + dy, 409 zw - dz, true, false); 410 labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); 411 world.add(labelObj2); 412 } 413 } 414 } 415 416 /** 417 * Tests this renderer for equality with an arbitrary object. 418 * 419 * @param obj the object ({@code null} permitted). 420 * 421 * @return A boolean. 422 */ 423 @Override 424 public boolean equals(Object obj) { 425 if (obj == this) { 426 return true; 427 } 428 if (!(obj instanceof BarRenderer3D)) { 429 return false; 430 } 431 BarRenderer3D that = (BarRenderer3D) obj; 432 if (this.base != that.base) { 433 return false; 434 } 435 if (this.barXWidth != that.barXWidth) { 436 return false; 437 } 438 if (this.barZWidth != that.barZWidth) { 439 return false; 440 } 441 if (!ObjectUtils.equals(this.baseColorSource, that.baseColorSource)) { 442 return false; 443 } 444 if (!ObjectUtils.equals(this.topColorSource, that.topColorSource)) { 445 return false; 446 } 447 return super.equals(obj); 448 } 449}