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.plot; 034 035import java.awt.Color; 036import java.awt.Font; 037import java.io.Serializable; 038import java.util.ArrayList; 039import java.util.List; 040 041import com.orsoncharts.Chart3D; 042import com.orsoncharts.data.PieDataset3D; 043import com.orsoncharts.util.ArgChecks; 044import com.orsoncharts.graphics3d.Dimension3D; 045import com.orsoncharts.graphics3d.Dot3D; 046import com.orsoncharts.graphics3d.Object3D; 047import com.orsoncharts.graphics3d.World; 048import com.orsoncharts.legend.LegendItemInfo; 049import com.orsoncharts.legend.StandardLegendItemInfo; 050import com.orsoncharts.Chart3DFactory; 051import com.orsoncharts.ChartElementVisitor; 052import com.orsoncharts.data.DataUtils; 053import com.orsoncharts.data.ItemKey; 054import com.orsoncharts.data.KeyedValuesItemKey; 055import com.orsoncharts.label.PieLabelGenerator; 056import com.orsoncharts.label.StandardPieLabelGenerator; 057 058/** 059 * A plot for creating 3D pie charts. To create a pie chart, you can use the 060 * {@code createPieChart()} method in the {@link Chart3DFactory} class. 061 * A typical pie chart will look like this: 062 * <div> 063 * <object id="ABC" data="../../../doc-files/PieChart3DDemo1.svg" 064 * type="image/svg+xml" width="500" height="359"> 065 * </object> 066 * </div> 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 PiePlot3D extends AbstractPlot3D implements Serializable { 074 075 /** The default font for section labels on the chart. */ 076 public static final Font DEFAULT_SECTION_LABEL_FONT 077 = new Font("Dialog", Font.PLAIN, 14); 078 079 /** The dataset. */ 080 private PieDataset3D<? extends Comparable> dataset; 081 082 /** The radius of the pie chart. */ 083 private double radius; 084 085 /** The depth of the pie chart. */ 086 private double depth; 087 088 /** The section color source. */ 089 private ColorSource sectionColorSource; 090 091 /** The section label generator. */ 092 private PieLabelGenerator sectionLabelGenerator; 093 094 /** The font source used to determine the font for section labels. */ 095 private FontSource sectionLabelFontSource; 096 097 /** 098 * The color source used to determine the foreground color for section 099 * labels. 100 */ 101 private ColorSource sectionLabelColorSource; 102 103 /** The legend label generator. */ 104 private PieLabelGenerator legendLabelGenerator; 105 106 /** 107 * The tool tip generator (can be null, in which case there will be no 108 * tool tips. */ 109 private PieLabelGenerator toolTipGenerator; 110 111 /** 112 * The number of segments used to render 360 degrees of the pie. A higher 113 * number will give better output but slower performance. 114 */ 115 private int segments = 40; 116 117 /** 118 * Creates a new pie plot in 3D. 119 * 120 * @param dataset the dataset ({@code null} not permitted). 121 */ 122 public PiePlot3D(PieDataset3D<? extends Comparable> dataset) { 123 ArgChecks.nullNotPermitted(dataset, "dataset"); 124 this.dataset = dataset; 125 this.dataset.addChangeListener(this); 126 this.radius = 4.0; 127 this.depth = 0.5; 128 this.sectionColorSource = new StandardColorSource(); 129 this.sectionLabelGenerator = new StandardPieLabelGenerator( 130 StandardPieLabelGenerator.KEY_ONLY_TEMPLATE); 131 this.sectionLabelFontSource = new StandardFontSource( 132 DEFAULT_SECTION_LABEL_FONT); 133 this.sectionLabelColorSource = new StandardColorSource(Color.BLACK); 134 this.legendLabelGenerator = new StandardPieLabelGenerator(); 135 this.toolTipGenerator = new StandardPieLabelGenerator( 136 StandardPieLabelGenerator.PERCENT_TEMPLATE_2DP); 137 } 138 139 /** 140 * Returns the dataset. 141 * 142 * @return The dataset (never {@code null}). 143 */ 144 public PieDataset3D<? extends Comparable> getDataset() { 145 return this.dataset; 146 } 147 148 /** 149 * Sets the dataset and notifies registered listeners that the dataset has 150 * been updated. 151 * 152 * @param dataset the dataset ({@code null} not permitted). 153 */ 154 public void setDataset(PieDataset3D<? extends Comparable> dataset) { 155 ArgChecks.nullNotPermitted(dataset, "dataset"); 156 this.dataset.removeChangeListener(this); 157 this.dataset = dataset; 158 this.dataset.addChangeListener(this); 159 fireChangeEvent(true); 160 } 161 162 /** 163 * Returns the radius of the pie (the default value is 8.0). 164 * 165 * @return The radius of the pie. 166 */ 167 public double getRadius() { 168 return this.radius; 169 } 170 171 /** 172 * Sets the radius of the pie chart and sends a change event to all 173 * registered listeners. 174 * 175 * @param radius the radius. 176 */ 177 public void setRadius(double radius) { 178 this.radius = radius; 179 fireChangeEvent(true); 180 } 181 182 /** 183 * Returns the depth of the pie (the default value is 2.0). 184 * 185 * @return The depth of the pie. 186 */ 187 public double getDepth() { 188 return this.depth; 189 } 190 191 /** 192 * Sets the depth of the pie chart and sends a change event to all 193 * registered listeners. 194 * 195 * @param depth the depth. 196 */ 197 public void setDepth(double depth) { 198 this.depth = depth; 199 fireChangeEvent(true); 200 } 201 202 /** 203 * Returns the color source for section colors. 204 * 205 * @return The color source (never {@code null}). 206 */ 207 public ColorSource getSectionColorSource() { 208 return this.sectionColorSource; 209 } 210 211 /** 212 * Sets the color source and sends a {@link Plot3DChangeEvent} to all 213 * registered listeners. 214 * 215 * @param source the color source ({@code null} not permitted). 216 */ 217 public void setSectionColorSource(ColorSource source) { 218 ArgChecks.nullNotPermitted(source, "source"); 219 this.sectionColorSource = source; 220 fireChangeEvent(true); 221 } 222 223 /** 224 * Sets a new color source for the plot using the specified colors and 225 * sends a {@link Plot3DChangeEvent} to all registered listeners. This 226 * is a convenience method that is equivalent to 227 * {@code setSectionColorSource(new StandardColorSource(colors))}. 228 * 229 * @param colors one or more colors ({@code null} not permitted). 230 * 231 * @since 1.2 232 */ 233 public void setSectionColors(Color... colors) { 234 setSectionColorSource(new StandardColorSource(colors)); 235 } 236 237 /** 238 * Returns the object that creates labels for each section of the pie 239 * chart. 240 * 241 * @return The section label generator (never {@code null}). 242 * 243 * @since 1.2 244 */ 245 public PieLabelGenerator getSectionLabelGenerator() { 246 return this.sectionLabelGenerator; 247 } 248 249 /** 250 * Sets the object that creates labels for each section of the pie chart, 251 * and sends a {@link Plot3DChangeEvent} to all registered listeners. 252 * 253 * @param generator the generator ({@code null} not permitted). 254 * 255 * @since 1.2 256 */ 257 public void setSectionLabelGenerator(PieLabelGenerator generator) { 258 ArgChecks.nullNotPermitted(generator, "generator"); 259 this.sectionLabelGenerator = generator; 260 fireChangeEvent(false); 261 } 262 263 /** 264 * Returns the font source that is used to determine the font to use for 265 * the section labels. 266 * 267 * @return The font source for the section labels (never {@code null}). 268 */ 269 public FontSource getSectionLabelFontSource() { 270 return this.sectionLabelFontSource; 271 } 272 273 /** 274 * Sets the font source and sends a {@link Plot3DChangeEvent} to all 275 * registered listeners. 276 * 277 * @param source the source ({@code null} not permitted). 278 */ 279 public void setSectionLabelFontSource(FontSource source) { 280 ArgChecks.nullNotPermitted(source, "source"); 281 this.sectionLabelFontSource = source; 282 fireChangeEvent(false); 283 } 284 285 /** 286 * Returns the color source for section labels. The default value is 287 * an instance of {@link StandardColorSource} that always returns 288 * {@code Color.BLACK}. 289 * 290 * @return The color source (never {@code null}). 291 * 292 * @see #setSectionLabelColorSource(ColorSource) 293 */ 294 public ColorSource getSectionLabelColorSource() { 295 return this.sectionLabelColorSource; 296 } 297 298 /** 299 * Sets the color source for the section labels and sends a 300 * {@link Plot3DChangeEvent} to all registered listeners. 301 * 302 * @param source the color source. 303 * 304 * @see #getSectionLabelColorSource() 305 */ 306 public void setSectionLabelColorSource(ColorSource source) { 307 ArgChecks.nullNotPermitted(source, "source"); 308 this.sectionLabelColorSource = source; 309 fireChangeEvent(false); 310 } 311 312 /** 313 * Returns the object that creates legend labels for each section of the pie 314 * chart. 315 * 316 * @return The legend label generator (never {@code null}). 317 * 318 * @since 1.2 319 */ 320 public PieLabelGenerator getLegendLabelGenerator() { 321 return this.legendLabelGenerator; 322 } 323 324 /** 325 * Sets the object that creates legend labels for each section of the pie 326 * chart, and sends a {@link Plot3DChangeEvent} to all registered 327 * listeners. 328 * 329 * @param generator the generator ({@code null} not permitted). 330 * 331 * @since 1.2 332 */ 333 public void setLegendLabelGenerator(PieLabelGenerator generator) { 334 ArgChecks.nullNotPermitted(generator, "generator"); 335 this.legendLabelGenerator = generator; 336 fireChangeEvent(false); 337 } 338 339 /** 340 * Returns the tool tip generator. 341 * 342 * @return The tool tip generator (possibly {@code null}). 343 * 344 * @since 1.3 345 */ 346 public PieLabelGenerator getToolTipGenerator() { 347 return this.toolTipGenerator; 348 } 349 350 /** 351 * Sets the tool tip generator and sends a change event to all registered 352 * listeners. 353 * 354 * @param generator the generator ({@code null} permitted). 355 * 356 * @since 1.3 357 */ 358 public void setToolTipGenerator(PieLabelGenerator generator) { 359 this.toolTipGenerator = generator; 360 fireChangeEvent(false); 361 } 362 363 /** 364 * Returns the dimensions for the plot. For the pie chart, it is more 365 * natural to specify the dimensions in terms of a radius and a depth, so 366 * we use those values to calculate the dimensions here. 367 * 368 * @return The dimensions for the plot. 369 */ 370 @Override 371 public Dimension3D getDimensions() { 372 return new Dimension3D(this.radius * 2, this.depth, this.radius * 2); 373 } 374 375 /** 376 * Returns the number of segments used when composing the 3D objects 377 * representing the pie chart. The default value is {@code 40}. 378 * 379 * @return The number of segments used to compose the pie chart. 380 */ 381 public int getSegmentCount() { 382 return this.segments; 383 } 384 385 /** 386 * Sets the number of segments used when composing the pie chart and 387 * sends a {@link Plot3DChangeEvent} to all registered listeners. A higher 388 * number will result in a more rounded pie chart, but will take longer 389 * to render. 390 * 391 * @param count the count. 392 */ 393 public void setSegmentCount(int count) { 394 this.segments = count; 395 fireChangeEvent(true); 396 } 397 398 /** 399 * Returns a list containing legend item info, typically one item for 400 * each series in the chart. This is intended for use in the construction 401 * of a chart legend. 402 * 403 * @return A list containing legend item info. 404 */ 405 @Override @SuppressWarnings("unchecked") 406 public List<LegendItemInfo> getLegendInfo() { 407 List<LegendItemInfo> result = new ArrayList<LegendItemInfo>(); 408 for (Comparable<?> key : (List<Comparable<?>>) 409 this.dataset.getKeys()) { 410 String label = this.legendLabelGenerator.generateLabel(dataset, 411 key); 412 LegendItemInfo info = new StandardLegendItemInfo(key, 413 label, this.sectionColorSource.getColor(key)); 414 result.add(info); 415 } 416 return result; 417 } 418 419 /** 420 * Adds 3D objects representing the current data for the plot to the 421 * specified world. After the world has been populated (or constructed) in 422 * this way, it is ready for rendering. This method is called by the 423 * {@link Chart3D} class, you won't normally call it directly. 424 * 425 * @param world the world ({@code null} not permitted). 426 * @param xOffset the x-offset. 427 * @param yOffset the y-offset. 428 * @param zOffset the z-offset. 429 */ 430 @Override 431 @SuppressWarnings("unchecked") 432 public void compose(World world, double xOffset, double yOffset, 433 double zOffset) { 434 double total = DataUtils.total(this.dataset); 435 double r = 0.0; 436 int count = this.dataset.getItemCount(); 437 for (int i = 0; i < count; i++) { 438 Comparable<?> key = this.dataset.getKey(i); 439 Number n = (Number) this.dataset.getValue(i); 440 if (n != null) { 441 double angle = Math.PI * 2 * (n.doubleValue() / total); 442 Color c = this.sectionColorSource.getColor( 443 this.dataset.getKey(i)); 444 Object3D segment = Object3D.createPieSegment(this.radius, 0.0, 445 yOffset, this.depth, r, r + angle, 446 Math.PI / this.segments, c); 447 segment.setProperty(Object3D.ITEM_KEY, 448 new KeyedValuesItemKey(key)); 449 world.add(segment); 450 r = r + angle; 451 } 452 } 453 } 454 455 /** 456 * Returns a list of label faces for the plot. These are non-visible 457 * objects added to the 3D model of the pie chart to track the positions 458 * for labels (which are added after the plot is projected and rendered). 459 * <br><br> 460 * NOTE: This method is public so that it can be called by the 461 * {@link Chart3D} class - you won't normally call it directly. 462 * 463 * @param xOffset the x-offset. 464 * @param yOffset the y-offset. 465 * @param zOffset the z-offset. 466 * 467 * @return A list of label faces. 468 */ 469 public List<Object3D> getLabelFaces(double xOffset, double yOffset, 470 double zOffset) { 471 double total = DataUtils.total(this.dataset); 472 List<Object3D> result = new ArrayList<Object3D>(); 473 // this adds the centre points 474 result.add(new Dot3D(0.0f, 0.0f, 0.0f, Color.RED)); 475 result.add(new Dot3D(0.0f, (float) yOffset, 0.0f, Color.RED)); 476 double r = 0.0; 477 int count = this.dataset.getItemCount(); 478 for (int i = 0; i < count; i++) { 479 Number n = (Number) this.dataset.getValue(i); 480 double angle = 0.0; 481 if (n != null) { 482 angle = Math.PI * 2 * (n.doubleValue() / total); 483 } 484 result.addAll(Object3D.createPieLabelMarkers(this.radius * 1.2, 485 0.0, yOffset - this.depth * 0.05, this.depth * 1.1, r, 486 r + angle)); 487 r = r + angle; 488 } 489 return result; 490 } 491 492 @Override 493 public String generateToolTipText(ItemKey itemKey) { 494 if (!(itemKey instanceof KeyedValuesItemKey)) { 495 throw new IllegalArgumentException( 496 "The itemKey must be a ValuesItemKey instance."); 497 } 498 KeyedValuesItemKey vik = (KeyedValuesItemKey) itemKey; 499 return this.toolTipGenerator.generateLabel(this.dataset, vik.getKey()); 500 } 501 502 /** 503 * Receives a visitor. This is a general purpose mechanism, but the main 504 * use is to apply chart style changes across all the elements of a 505 * chart. 506 * 507 * @param visitor the visitor ({@code null} not permitted). 508 * 509 * @since 1.2 510 */ 511 @Override 512 public void receive(ChartElementVisitor visitor) { 513 visitor.visit(this); 514 } 515 516 /** 517 * Tests this plot for equality with an arbitrary object. Note that the 518 * plot's dataset is NOT considered in the equality test. 519 * 520 * @param obj the object ({@code null} not permitted). 521 * 522 * @return A boolean. 523 */ 524 @Override 525 public boolean equals(Object obj) { 526 if (obj == this) { 527 return true; 528 } 529 if (!(obj instanceof PiePlot3D)) { 530 return false; 531 } 532 PiePlot3D that = (PiePlot3D) obj; 533 if (this.radius != that.radius) { 534 return false; 535 } 536 if (this.depth != that.depth) { 537 return false; 538 } 539 if (!this.sectionColorSource.equals(that.sectionColorSource)) { 540 return false; 541 } 542 if (!this.sectionLabelGenerator.equals(that.sectionLabelGenerator)) { 543 return false; 544 } 545 if (!this.sectionLabelFontSource.equals(that.sectionLabelFontSource)) { 546 return false; 547 } 548 if (!this.sectionLabelColorSource.equals( 549 that.sectionLabelColorSource)) { 550 return false; 551 } 552 if (!this.legendLabelGenerator.equals(that.legendLabelGenerator)) { 553 return false; 554 } 555 if (!this.toolTipGenerator.equals(that.toolTipGenerator)) { 556 return false; 557 } 558 if (this.segments != that.segments) { 559 return false; 560 } 561 return super.equals(obj); 562 } 563 564 @Override 565 public int hashCode() { 566 int hash = 5; 567 hash = 97 * hash + (int) (Double.doubleToLongBits(this.radius) 568 ^ (Double.doubleToLongBits(this.radius) >>> 32)); 569 hash = 97 * hash + (int) (Double.doubleToLongBits(this.depth) 570 ^ (Double.doubleToLongBits(this.depth) >>> 32)); 571 hash = 97 * hash + this.sectionColorSource.hashCode(); 572 hash = 97 * hash + this.sectionLabelGenerator.hashCode(); 573 hash = 97 * hash + this.sectionLabelFontSource.hashCode(); 574 hash = 97 * hash + this.sectionLabelColorSource.hashCode(); 575 hash = 97 * hash + this.segments; 576 return hash; 577 } 578 579}