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.axis; 034 035import java.awt.BasicStroke; 036import java.awt.Color; 037import java.awt.Graphics2D; 038import java.awt.Paint; 039import java.awt.Shape; 040import java.awt.Stroke; 041import java.awt.font.LineMetrics; 042import java.awt.geom.Line2D; 043import java.awt.geom.Point2D; 044import java.io.IOException; 045import java.io.ObjectInputStream; 046import java.io.ObjectOutputStream; 047import java.io.Serializable; 048import java.util.ArrayList; 049import java.util.LinkedHashMap; 050import java.util.List; 051import java.util.HashMap; 052import java.util.Map; 053 054import org.jfree.chart3d.graphics3d.internal.Utils2D; 055import org.jfree.chart3d.Chart3DHints; 056import org.jfree.chart3d.ChartElementVisitor; 057import org.jfree.chart3d.data.Range; 058import org.jfree.chart3d.data.category.CategoryDataset3D; 059import org.jfree.chart3d.graphics2d.TextAnchor; 060import org.jfree.chart3d.graphics3d.RenderedElement; 061import org.jfree.chart3d.graphics3d.RenderingInfo; 062import org.jfree.chart3d.interaction.InteractiveElementType; 063import org.jfree.chart3d.internal.Args; 064import org.jfree.chart3d.internal.ObjectUtils; 065import org.jfree.chart3d.internal.SerialUtils; 066import org.jfree.chart3d.internal.TextUtils; 067import org.jfree.chart3d.label.CategoryLabelGenerator; 068import org.jfree.chart3d.label.StandardCategoryLabelGenerator; 069import org.jfree.chart3d.marker.CategoryMarker; 070import org.jfree.chart3d.marker.CategoryMarkerType; 071import org.jfree.chart3d.marker.Marker; 072import org.jfree.chart3d.marker.MarkerData; 073import org.jfree.chart3d.plot.CategoryPlot3D; 074import org.jfree.chart3d.renderer.category.AreaRenderer3D; 075 076/** 077 * An axis that displays categories. 078 * <br><br> 079 * NOTE: This class is serializable, but the serialization format is subject 080 * to change in future releases and should not be relied upon for persisting 081 * instances of this class. 082 */ 083@SuppressWarnings("serial") 084public class StandardCategoryAxis3D extends AbstractAxis3D 085 implements CategoryAxis3D, Serializable { 086 087 /** The categories. */ 088 private List<Comparable<?>> categories; 089 090 /** 091 * The axis range (never {@code null}). 092 */ 093 private Range range; 094 095 private boolean inverted; 096 097 /** The percentage margin to leave at the lower end of the axis. */ 098 private double lowerMargin; 099 100 /** The percentage margin to leave at the upper end of the axis. */ 101 private double upperMargin; 102 103 /** 104 * Hide half of the first category? This brings the category label 105 * closer to the beginning of the axis. It is useful if the renderer 106 * doesn't make full use of the category space for the first item. 107 */ 108 private boolean firstCategoryHalfWidth = false; 109 110 /** 111 * Hide half of the last category? This brings the category label 112 * closer to the end of the axis. It is useful if the renderer 113 * doesn't make full use of the category space for the last item. 114 */ 115 private boolean lastCategoryHalfWidth = false; 116 117 /** 118 * The tick mark length (in Java2D units). When this is 0.0, no tick 119 * marks will be drawn. 120 */ 121 private double tickMarkLength; 122 123 /** The tick mark stroke (never {@code null}). */ 124 private transient Stroke tickMarkStroke; 125 126 /** The tick mark paint (never {@code null}). */ 127 private transient Paint tickMarkPaint; 128 129 /** The tick label generator. */ 130 private CategoryLabelGenerator tickLabelGenerator; 131 132 /** 133 * The tick label offset (in Java2D units). This is the gap between the 134 * tick marks and their associated labels. 135 */ 136 private double tickLabelOffset; 137 138 /** The orientation for the tick labels. */ 139 private LabelOrientation tickLabelOrientation; 140 141 /** 142 * The maximum number of offset levels to use for tick labels on the axis. 143 */ 144 private int maxTickLabelLevels = 3; 145 146 /** 147 * The tick label factor (used as a multiplier for the tick label width 148 * when checking for overlapping labels). 149 */ 150 private double tickLabelFactor = 1.2; 151 152 /** 153 * The markers for the axis (this may be empty, but not {@code null}). 154 */ 155 private Map<String, CategoryMarker> markers; 156 157 /** A flag to indicate that this axis has been configured as a row axis. */ 158 private boolean isRowAxis; 159 160 /** 161 * A flag to indicate that this axis has been configured as a column 162 * axis. 163 */ 164 private boolean isColumnAxis; 165 166 /** 167 * Default constructor. 168 */ 169 public StandardCategoryAxis3D() { 170 this(null); 171 } 172 173 /** 174 * Creates a new axis with the specified label. 175 * 176 * @param label the axis label ({@code null} permitted). 177 */ 178 public StandardCategoryAxis3D(String label) { 179 super(label); 180 this.categories = new ArrayList<>(); 181 this.range = new Range(0.0, 1.0); 182 this.lowerMargin = 0.05; 183 this.upperMargin = 0.05; 184 this.firstCategoryHalfWidth = false; 185 this.lastCategoryHalfWidth = false; 186 this.tickMarkLength = 3.0; 187 this.tickMarkPaint = Color.GRAY; 188 this.tickMarkStroke = new BasicStroke(0.5f); 189 this.tickLabelGenerator = new StandardCategoryLabelGenerator(); 190 this.tickLabelOffset = 5.0; 191 this.tickLabelOrientation = LabelOrientation.PARALLEL; 192 this.tickLabelFactor = 1.4; 193 this.maxTickLabelLevels = 3; 194 this.markers = new LinkedHashMap<>(); 195 this.isRowAxis = false; 196 this.isColumnAxis = false; 197 } 198 199 /** 200 * Returns {@code true} if this axis has been configured as a 201 * row axis for the plot that it belongs to, and {@code false} 202 * otherwise. 203 * 204 * @return A boolean. 205 * 206 * @since 1.3 207 */ 208 @Override 209 public boolean isRowAxis() { 210 return isRowAxis; 211 } 212 213 /** 214 * Returns {@code true} if this axis has been configured as a 215 * column axis for the plot that it belongs to, and {@code false} 216 * otherwise. 217 * 218 * @return A boolean. 219 * 220 * @since 1.3 221 */ 222 @Override 223 public boolean isColumnAxis() { 224 return isColumnAxis; 225 } 226 227 /** 228 * Returns the range for the axis. By convention, the category axes have 229 * a range from 0.0 to 1.0. 230 * 231 * @return The range. 232 */ 233 @Override 234 public Range getRange() { 235 return this.range; 236 } 237 238 /** 239 * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to 240 * all registered listeners. 241 * 242 * @param lowerBound the lower bound. 243 * @param upperBound the upper bound. 244 */ 245 @Override 246 public void setRange(double lowerBound, double upperBound) { 247 setRange(new Range(lowerBound, upperBound)); 248 } 249 250 /** 251 * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to 252 * all registered listeners. Note that changing the range for the 253 * category axis will have no visible effect. 254 * 255 * @param range the range ({@code null} not permitted). 256 */ 257 @Override 258 public void setRange(Range range) { 259 Args.nullNotPermitted(range, "range"); 260 this.range = range; 261 fireChangeEvent(true); 262 } 263 264 /** 265 * Returns the margin to leave at the lower end of the axis, as a 266 * percentage of the axis length. The default is {@code 0.05} (five 267 * percent). 268 * 269 * @return The lower margin. 270 */ 271 public double getLowerMargin() { 272 return this.lowerMargin; 273 } 274 275 /** 276 * Sets the margin to leave at the lower end of the axis and sends an 277 * {@link Axis3DChangeEvent} to all registered listeners. 278 * 279 * @param margin the margin. 280 */ 281 public void setLowerMargin(double margin) { 282 this.lowerMargin = margin; 283 fireChangeEvent(true); 284 } 285 286 /** 287 * Returns the margin to leave at the upper end of the axis, as a 288 * percentage of the axis length. The default is {@code 0.05} (five 289 * percent). 290 * 291 * @return The lower margin. 292 */ 293 public double getUpperMargin() { 294 return this.upperMargin; 295 } 296 297 /** 298 * Sets the margin to leave at the upper end of the axis and sends an 299 * {@link Axis3DChangeEvent} to all registered listeners. 300 * 301 * @param margin the margin. 302 */ 303 public void setUpperMargin(double margin) { 304 this.upperMargin = margin; 305 fireChangeEvent(true); 306 } 307 308 /** 309 * Returns {@code true} if the first category on the axis should 310 * occupy half the normal width, and {@code false} otherwise. 311 * 312 * @return A boolean. 313 * 314 * @see #setFirstCategoryHalfWidth(boolean) 315 */ 316 public boolean isFirstCategoryHalfWidth() { 317 return this.firstCategoryHalfWidth; 318 } 319 320 /** 321 * Sets the flag that controls whether the first category on the axis 322 * occupies a full or half width, and sends an {@link Axis3DChangeEvent} 323 * to all registered listeners. There are some renderers where the 324 * charts look better when half-widths are used (for example, 325 * {@link AreaRenderer3D}). 326 * 327 * @param half half width? 328 * 329 * @see #setLastCategoryHalfWidth(boolean) 330 */ 331 public void setFirstCategoryHalfWidth(boolean half) { 332 this.firstCategoryHalfWidth = half; 333 fireChangeEvent(true); 334 } 335 336 /** 337 * Returns {@code true} if the last category on the axis should 338 * occupy half the normal width, and {@code false} otherwise. 339 * 340 * @return A boolean. 341 * 342 * @see #setLastCategoryHalfWidth(boolean) 343 */ 344 public boolean isLastCategoryHalfWidth() { 345 return this.lastCategoryHalfWidth; 346 } 347 348 /** 349 * Sets the flag that controls whether the last category on the axis 350 * occupies a full or half width, and sends an {@link Axis3DChangeEvent} 351 * to all registered listeners. There are some renderers where the 352 * charts look better when half-widths are used (for example, 353 * {@link AreaRenderer3D}). 354 * 355 * @param half half width? 356 * 357 * @see #setFirstCategoryHalfWidth(boolean) 358 */ 359 public void setLastCategoryHalfWidth(boolean half) { 360 this.lastCategoryHalfWidth = half; 361 fireChangeEvent(true); 362 } 363 364 /** 365 * Returns the tick mark length (in Java2D units). The default value 366 * is {@code 3.0}. 367 * 368 * @return The tick mark length. 369 */ 370 public double getTickMarkLength() { 371 return this.tickMarkLength; 372 } 373 374 /** 375 * Sets the tick mark length (in Java2D units) and sends an 376 * {@link Axis3DChangeEvent} to all registered listeners. You can set 377 * the length to {@code 0.0} if you don't want any tick marks on the 378 * axis. 379 * 380 * @param length the length (in Java2D units). 381 */ 382 public void setTickMarkLength(double length) { 383 this.tickMarkLength = length; 384 fireChangeEvent(false); 385 } 386 387 /** 388 * Returns the paint used to draw the tick marks, if they are visible. 389 * The default value is {@code Color.GRAY}. 390 * 391 * @return The paint used to draw the tick marks (never {@code null}). 392 */ 393 public Paint getTickMarkPaint() { 394 return this.tickMarkPaint; 395 } 396 397 /** 398 * Sets the paint used to draw the tick marks and sends an 399 * {@link Axis3DChangeEvent} to all registered listeners. 400 * 401 * @param paint the paint ({@code null} not permitted). 402 */ 403 public void setTickMarkPaint(Paint paint) { 404 Args.nullNotPermitted(paint, "paint"); 405 this.tickMarkPaint = paint; 406 fireChangeEvent(false); 407 } 408 409 /** 410 * Returns the stroke used to draw the tick marks, if they are visible. 411 * The default value is {@code new BasicStroke(0.5f)}. 412 * 413 * @return The stroke used to draw the tick marks (never {@code null}). 414 */ 415 public Stroke getTickMarkStroke() { 416 return this.tickMarkStroke; 417 } 418 419 /** 420 * Sets the stroke used to draw the tick marks and sends an 421 * {@link Axis3DChangeEvent} to all registered listeners. 422 * 423 * @param stroke the stroke ({@code null} not permitted). 424 */ 425 public void setTickMarkStroke(Stroke stroke) { 426 Args.nullNotPermitted(stroke, "stroke"); 427 this.tickMarkStroke = stroke; 428 fireChangeEvent(false); 429 } 430 431 /** 432 * Returns the tick label generator for the axis. This is an object that 433 * is responsible for creating the category labels on the axis. You can 434 * plug in your own instance to take full control over the generation 435 * of category labels. 436 * 437 * @return The tick label generator for the axis (never {@code null}). 438 * 439 * @since 1.2 440 */ 441 public CategoryLabelGenerator getTickLabelGenerator() { 442 return this.tickLabelGenerator; 443 } 444 445 /** 446 * Sets the tick label generator for the axis and sends a change event to 447 * all registered listeners. 448 * 449 * @param generator the generator ({@code null} not permitted). 450 * 451 * @since 1.2 452 */ 453 public void setTickLabelGenerator(CategoryLabelGenerator generator) { 454 Args.nullNotPermitted(generator, "generator"); 455 this.tickLabelGenerator = generator; 456 fireChangeEvent(false); 457 } 458 459 /** 460 * Returns the offset between the tick marks and the tick labels. The 461 * default value is {@code 5.0}. 462 * 463 * @return The offset between the tick marks and the tick labels (in Java2D 464 * units). 465 */ 466 public double getTickLabelOffset() { 467 return this.tickLabelOffset; 468 } 469 470 /** 471 * Sets the offset between the tick marks and the tick labels and sends 472 * a {@link Axis3DChangeEvent} to all registered listeners. 473 * 474 * @param offset the offset. 475 */ 476 public void setTickLabelOffset(double offset) { 477 this.tickLabelOffset = offset; 478 fireChangeEvent(false); 479 } 480 481 /** 482 * Returns the orientation for the tick labels. The default value is 483 * {@link LabelOrientation#PARALLEL}. 484 * 485 * @return The orientation for the tick labels (never {@code null}). 486 * 487 * @since 1.2 488 */ 489 public LabelOrientation getTickLabelOrientation() { 490 return this.tickLabelOrientation; 491 } 492 493 /** 494 * Sets the orientation for the tick labels and sends a change event to 495 * all registered listeners. 496 * 497 * @param orientation the orientation ({@code null} not permitted). 498 * 499 * @since 1.2 500 */ 501 public void setTickLabelOrientation(LabelOrientation orientation) { 502 Args.nullNotPermitted(orientation, "orientation"); 503 this.tickLabelOrientation = orientation; 504 fireChangeEvent(false); 505 } 506 507 /** 508 * Returns the maximum number of offset levels for the category labels on 509 * the axis. The default value is 3. 510 * 511 * @return The maximum number of offset levels. 512 * 513 * @since 1.2 514 */ 515 public int getMaxTickLabelLevels() { 516 return this.maxTickLabelLevels; 517 } 518 519 /** 520 * Sets the maximum number of offset levels for the category labels on the 521 * axis and sends a change event to all registered listeners. 522 * 523 * @param levels the maximum number of levels. 524 * 525 * @since 1.2 526 */ 527 public void setMaxTickLabelLevels(int levels) { 528 this.maxTickLabelLevels = levels; 529 fireChangeEvent(false); 530 } 531 532 /** 533 * Returns the tick label factor. The default value is {@code 1.4}. 534 * 535 * @return The tick label factor. 536 * 537 * @since 1.2 538 */ 539 public double getTickLabelFactor() { 540 return this.tickLabelFactor; 541 } 542 543 /** 544 * Sets the tick label factor and sends a change event to all registered 545 * listeners. 546 * 547 * @param factor the new factor (should be at least 1.0). 548 * 549 * @since 1.2 550 */ 551 public void setTickLabelFactor(double factor) { 552 this.tickLabelFactor = factor; 553 fireChangeEvent(false); 554 } 555 556 /** 557 * Returns the marker with the specified key, if there is one. 558 * 559 * @param key the key ({@code null} not permitted). 560 * 561 * @return The marker (possibly {@code null}). 562 * 563 * @since 1.2 564 */ 565 @Override 566 public CategoryMarker getMarker(String key) { 567 return this.markers.get(key); 568 } 569 570 /** 571 * Sets the marker for the specified key and sends a change event to 572 * all registered listeners. If there is an existing marker it is replaced 573 * (and the axis will no longer listen for change events on the previous 574 * marker). 575 * 576 * @param key the key that identifies the marker ({@code null} not 577 * permitted). 578 * @param marker the marker ({@code null} permitted). 579 * 580 * @since 1.2 581 */ 582 public void setMarker(String key, CategoryMarker marker) { 583 CategoryMarker existing = this.markers.get(key); 584 if (existing != null) { 585 existing.removeChangeListener(this); 586 } 587 this.markers.put(key, marker); 588 if (marker != null) { 589 marker.addChangeListener(this); 590 } 591 fireChangeEvent(false); 592 } 593 594 /** 595 * Returns a new map containing the markers that are assigned to this axis. 596 * 597 * @return A map. 598 * 599 * @since 1.2 600 */ 601 public Map<String, CategoryMarker> getMarkers() { 602 return new LinkedHashMap<>(this.markers); 603 } 604 605 /** 606 * Returns the width of a single category in the units of the axis 607 * range. 608 * 609 * @return The width of a single category. 610 */ 611 @Override 612 public double getCategoryWidth() { 613 double length = this.range.getLength(); 614 double start = this.range.getMin() + (this.lowerMargin * length); 615 double end = this.range.getMax() - (this.upperMargin * length); 616 double available = (end - start); 617 return available / this.categories.size(); 618 } 619 620 /** 621 * Configures the axis to be used as a row axis for the specified 622 * plot. This method is for internal use, you should not call it directly. 623 * 624 * @param plot the plot ({@code null} not permitted). 625 */ 626 @Override @SuppressWarnings("unchecked") 627 public void configureAsRowAxis(CategoryPlot3D plot) { 628 Args.nullNotPermitted(plot, "plot"); 629 this.categories = plot.getDataset().getRowKeys(); 630 this.isColumnAxis = false; 631 this.isRowAxis = true; 632 } 633 634 /** 635 * Configures the axis to be used as a column axis for the specified 636 * plot. This method is for internal use, you won't normally need to call 637 * it directly. 638 * 639 * @param plot the plot ({@code null} not permitted). 640 */ 641 @Override @SuppressWarnings("unchecked") 642 public void configureAsColumnAxis(CategoryPlot3D plot) { 643 Args.nullNotPermitted(plot, "plot"); 644 this.categories = plot.getDataset().getColumnKeys(); 645 this.isColumnAxis = true; 646 this.isRowAxis = false; 647 } 648 649 /** 650 * Returns the value for the specified category, or {@code Double.NaN} 651 * if the category is not registered on the axis. 652 * 653 * @param category the category ({@code null} not permitted). 654 * 655 * @return The value. 656 */ 657 @Override 658 public double getCategoryValue(Comparable<?> category) { 659 int index = this.categories.indexOf(category); 660 if (index < 0) { 661 return Double.NaN; 662 } 663 double length = this.range.getLength(); 664 double start = this.range.getMin() + (this.lowerMargin * length); 665 double end = this.range.getMax() - (this.upperMargin * length); 666 double available = (end - start); 667 double categoryCount = this.categories.size(); 668 if (categoryCount == 1) { 669 return (start + end) / 2.0; 670 } 671 if (this.firstCategoryHalfWidth) { 672 categoryCount -= 0.5; 673 } 674 if (this.lastCategoryHalfWidth) { 675 categoryCount -= 0.5; 676 } 677 double categoryWidth = 0.0; 678 if (categoryCount > 0.0) { 679 categoryWidth = available / categoryCount; 680 } 681 double adj = this.firstCategoryHalfWidth ? 0.0 : 0.5; 682 return start + (adj + index) * categoryWidth; 683 } 684 685 /** 686 * Translates a value on the axis to the equivalent coordinate in the 687 * 3D world used to construct a model of the chart. 688 * 689 * @param value the value along the axis. 690 * @param length the length of one side of the 3D box containing the model. 691 * 692 * @return A coordinate in 3D space. 693 */ 694 @Override 695 public double translateToWorld(double value, double length) { 696 double p = getRange().percent(value, isInverted()); 697 return length * p; 698 } 699 700 /** 701 * Draws the axis between the two points {@code pt0} and {@code pt1} in 702 * Java2D space. 703 * 704 * @param g2 the graphics target ({@code null} not permitted). 705 * @param pt0 the starting point for the axis ({@code null} not 706 * permitted). 707 * @param pt1 the ending point for the axis ({@code null} not 708 * permitted). 709 * @param opposingPt a point on the opposite side of the line from the 710 * labels ({@code null} not permitted). 711 * @param tickData the tick data, contains positioning anchors calculated 712 * by the 3D engine ({@code null} not permitted). 713 * @param info an object to be populated with rendering info 714 * ({@code null} permitted). 715 * @param hinting perform element hinting? 716 */ 717 @Override 718 public void draw(Graphics2D g2, Point2D pt0, Point2D pt1, 719 Point2D opposingPt, List<TickData> tickData, RenderingInfo info, 720 boolean hinting) { 721 722 if (!isVisible()) { 723 return; 724 } 725 if (pt0.equals(pt1)) { // if the axis starts and ends on the same point 726 return; // there is nothing we can draw 727 } 728 729 // draw the axis line (if you want no line, setting the line color 730 // to fully transparent will achieve this) 731 g2.setStroke(getLineStroke()); 732 g2.setPaint(getLineColor()); 733 Line2D axisLine = new Line2D.Float(pt0, pt1); 734 g2.draw(axisLine); 735 736 // draw the tick marks - during this pass we will also find the maximum 737 // tick label width 738 g2.setPaint(this.tickMarkPaint); 739 g2.setStroke(this.tickMarkStroke); 740 g2.setFont(getTickLabelFont()); 741 double maxTickLabelWidth = 0.0; 742 for (TickData t : tickData) { 743 if (this.tickMarkLength > 0.0) { 744 Line2D tickLine = Utils2D.createPerpendicularLine(axisLine, 745 t.getAnchorPt(), this.tickMarkLength, opposingPt); 746 g2.draw(tickLine); 747 } 748 String tickLabel = t.getKeyLabel(); 749 maxTickLabelWidth = Math.max(maxTickLabelWidth, 750 g2.getFontMetrics().stringWidth(tickLabel)); 751 } 752 753 double maxTickLabelDim = maxTickLabelWidth; 754 if (getTickLabelsVisible()) { 755 g2.setPaint(getTickLabelColor()); 756 if (this.tickLabelOrientation.equals( 757 LabelOrientation.PERPENDICULAR)) { 758 drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData, 759 info, hinting); 760 } else if (this.tickLabelOrientation.equals( 761 LabelOrientation.PARALLEL)) { 762 maxTickLabelDim = drawParallelTickLabels(g2, axisLine, 763 opposingPt, tickData, maxTickLabelWidth, info, hinting); 764 } 765 } else { 766 maxTickLabelDim = 0.0; 767 } 768 769 // draw the axis label if there is one 770 if (getLabel() != null) { 771 Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine, 772 opposingPt, maxTickLabelDim + this.tickMarkLength 773 + this.tickLabelOffset + getLabelOffset(), info, hinting); 774 } 775 } 776 777 /** 778 * Returns "row" if the axis has been configured as a row axis, "column" if 779 * the axis has been configured as a column axis, and the empty string ("") 780 * if the axis has not yet been configured. 781 * 782 * @return A string (never {@code null}). 783 * 784 * @since 1.3 785 */ 786 @Override 787 protected String axisStr() { 788 String result = ""; 789 if (this.isRowAxis) { 790 result = "row"; 791 } else if (this.isColumnAxis) { 792 result = "column"; 793 } 794 return result; 795 } 796 797 private double drawParallelTickLabels(Graphics2D g2, Line2D axisLine, 798 Point2D opposingPt, List<TickData> tickData, 799 double maxTickLabelWidth, RenderingInfo info, boolean hinting) { 800 int levels = 1; 801 LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2); 802 double height = lm.getHeight(); 803 if (tickData.size() > 1) { 804 805 // work out how many offset levels we need to display the 806 // categories without overlapping 807 Point2D p0 = tickData.get(0).getAnchorPt(); 808 Point2D pN = tickData.get(tickData.size() - 1).getAnchorPt(); 809 double availableWidth = pN.distance(p0) 810 * tickData.size() / (tickData.size() - 1); 811 int labelsPerLevel = (int) Math.floor(availableWidth / 812 (maxTickLabelWidth * tickLabelFactor)); 813 int levelsRequired = this.maxTickLabelLevels; 814 if (labelsPerLevel > 0) { 815 levelsRequired = this.categories.size() / labelsPerLevel + 1; 816 } 817 levels = Math.min(levelsRequired, this.maxTickLabelLevels); 818 } 819 820 int index = 0; 821 for (TickData t : tickData) { 822 int level = index % levels; 823 double adj = height * (level + 0.5); 824 Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 825 t.getAnchorPt(), this.tickMarkLength 826 + this.tickLabelOffset + adj, opposingPt); 827 double axisTheta = Utils2D.calculateTheta(axisLine); 828 TextAnchor textAnchor = TextAnchor.CENTER; 829 if (axisTheta >= Math.PI / 2.0) { 830 axisTheta = axisTheta - Math.PI; 831 } else if (axisTheta <= -Math.PI / 2) { 832 axisTheta = axisTheta + Math.PI; 833 } 834 String tickLabel = t.getKeyLabel(); 835 if (hinting) { 836 Map<String, String> m = new HashMap<>(); 837 m.put("ref", "{\"type\": \"categoryTickLabel\", \"axis\": \"" 838 + axisStr() + "\", \"key\": \"" 839 + t.getKey() + "\"}"); 840 g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); 841 } 842 843 Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 844 (float) perpLine.getX2(), (float) perpLine.getY2(), 845 textAnchor, axisTheta, textAnchor); 846 if (hinting) { 847 g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true); 848 } 849 if (info != null) { 850 RenderedElement tickLabelElement = new RenderedElement( 851 InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds); 852 tickLabelElement.setProperty("label", tickLabel); 853 tickLabelElement.setProperty("axis", axisStr()); 854 info.addOffsetElement(tickLabelElement); 855 } 856 index++; 857 } 858 return height * levels; 859 } 860 861 /** 862 * Draws the category labels perpendicular to the axis. 863 * 864 * @param g2 the graphics target. 865 * @param axisLine the axis line. 866 * @param opposingPt an opposing point (used to indicate which side the 867 * labels will appear on). 868 * @param tickData the tick data. 869 * @param info if not {@code null} this will be populated with 870 * {@link RenderedElement} instances for the tick labels. 871 * @param hinting 872 */ 873 @SuppressWarnings("unchecked") 874 private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine, 875 Point2D opposingPt, List<TickData> tickData, RenderingInfo info, 876 boolean hinting) { 877 878 for (TickData t : tickData) { 879 Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 880 t.getAnchorPt(), this.tickMarkLength 881 + this.tickLabelOffset, opposingPt); 882 double perpTheta = Utils2D.calculateTheta(perpLine); 883 TextAnchor textAnchor = TextAnchor.CENTER_LEFT; 884 if (perpTheta >= Math.PI / 2.0) { 885 perpTheta = perpTheta - Math.PI; 886 textAnchor = TextAnchor.CENTER_RIGHT; 887 } else if (perpTheta <= -Math.PI / 2) { 888 perpTheta = perpTheta + Math.PI; 889 textAnchor = TextAnchor.CENTER_RIGHT; 890 } 891 String tickLabel = t.getKeyLabel(); 892 if (hinting) { 893 Map m = new HashMap<String, String>(); 894 m.put("ref", "{\"type\": \"categoryAxisLabel\", \"axis\": \"" 895 + axisStr() + "\", \"key\": \"" 896 + t.getKey() + "\"}"); 897 g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); 898 } 899 Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 900 (float) perpLine.getX2(), (float) perpLine.getY2(), 901 textAnchor, perpTheta, textAnchor); 902 if (hinting) { 903 g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true); 904 } 905 if (info != null) { 906 RenderedElement tickLabelElement = new RenderedElement( 907 InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds); 908 tickLabelElement.setProperty("label", tickLabel); 909 tickLabelElement.setProperty("axis", axisStr()); 910 info.addOffsetElement(tickLabelElement); 911 } 912 } 913 } 914 915 /** 916 * Generates the tick data for the axis (assumes the axis is being used 917 * as the row axis). The dataset is passed as an argument to provide the 918 * opportunity to incorporate dataset-specific info into tick labels (for 919 * example, a row label might show the total for that row in the dataset) 920 * ---whether or not this is used depends on the axis implementation. 921 * 922 * @param dataset the dataset ({@code null} not permitted). 923 * 924 * @return The tick data. 925 * 926 * @since 1.2 927 */ 928 @Override @SuppressWarnings("unchecked") 929 public List<TickData> generateTickDataForRows(CategoryDataset3D dataset) { 930 Args.nullNotPermitted(dataset, "dataset"); 931 List<TickData> result = new ArrayList<>(this.categories.size()); 932 for (Comparable<?> key : this.categories) { 933 double pos = this.range.percent(getCategoryValue(key)); 934 String label = this.tickLabelGenerator.generateRowLabel(dataset, 935 key); 936 result.add(new TickData(pos, key, label)); 937 } 938 return result; 939 } 940 941 /** 942 * Generates the tick data for the axis (assumes the axis is being used 943 * as the row axis). The dataset is passed as an argument to provide the 944 * opportunity to incorporate dataset-specific info into tick labels (for 945 * example, a row label might show the total for that row in the dataset) 946 * ---whether or not this is used depends on the axis implementation. 947 * 948 * @param dataset the dataset ({@code null} not permitted). 949 * 950 * @return The tick data. 951 * 952 * @since 1.2 953 */ 954 @Override @SuppressWarnings("unchecked") 955 public List<TickData> generateTickDataForColumns( 956 CategoryDataset3D dataset) { 957 Args.nullNotPermitted(dataset, "dataset"); 958 List<TickData> result = new ArrayList<>(this.categories.size()); 959 for (Comparable<?> key : this.categories) { 960 double pos = this.range.percent(getCategoryValue(key)); 961 String label = this.tickLabelGenerator.generateColumnLabel(dataset, 962 key); 963 result.add(new TickData(pos, key, label)); 964 } 965 return result; 966 } 967 968 /** 969 * Generates and returns a list of marker data items for the axis. 970 * 971 * @return A list of marker data items (never {@code null}). 972 */ 973 @Override 974 public List<MarkerData> generateMarkerData() { 975 List<MarkerData> result = new ArrayList<>(); 976 for (Map.Entry<String, CategoryMarker> entry 977 : this.markers.entrySet()) { 978 CategoryMarker cm = entry.getValue(); 979 if (cm == null) { 980 continue; 981 } 982 MarkerData markerData; 983 if (cm.getType().equals(CategoryMarkerType.LINE)) { 984 double pos = getCategoryValue(cm.getCategory()); 985 markerData = new MarkerData(entry.getKey(), pos); 986 markerData.setLabelAnchor(cm.getLabel() != null 987 ? cm.getLabelAnchor() : null); 988 } else if (cm.getType().equals(CategoryMarkerType.BAND)) { 989 double pos = getCategoryValue(cm.getCategory()); 990 double width = getCategoryWidth(); 991 markerData = new MarkerData(entry.getKey(), pos - width / 2, 992 false, pos + width / 2, false); 993 markerData.setLabelAnchor(cm.getLabel() != null 994 ? cm.getLabelAnchor() : null); 995 } else { 996 throw new RuntimeException("Unrecognised marker: " 997 + cm.getType()); 998 } 999 result.add(markerData); 1000 } 1001 return result; 1002 } 1003 1004 /** 1005 * Receives a {@link ChartElementVisitor}. This method is part of a general 1006 * mechanism for traversing the chart structure and performing operations 1007 * on each element in the chart. You will not normally call this method 1008 * directly. 1009 * 1010 * @param visitor the visitor ({@code null} not permitted). 1011 * 1012 * @since 1.2 1013 */ 1014 @Override 1015 public void receive(ChartElementVisitor visitor) { 1016 for (Marker marker : this.markers.values()) { 1017 marker.receive(visitor); 1018 } 1019 visitor.visit(this); 1020 } 1021 1022 /** 1023 * Tests this instance for equality with an arbitrary object. 1024 * 1025 * @param obj the object to test against ({@code null} not permitted). 1026 * 1027 * @return A boolean. 1028 */ 1029 @Override 1030 public boolean equals(Object obj) { 1031 if (obj == this) { 1032 return true; 1033 } 1034 if (!(obj instanceof StandardCategoryAxis3D)) { 1035 return false; 1036 } 1037 StandardCategoryAxis3D that = (StandardCategoryAxis3D) obj; 1038 if (this.lowerMargin != that.lowerMargin) { 1039 return false; 1040 } 1041 if (this.upperMargin != that.upperMargin) { 1042 return false; 1043 } 1044 if (this.firstCategoryHalfWidth != that.firstCategoryHalfWidth) { 1045 return false; 1046 } 1047 if (this.lastCategoryHalfWidth != that.lastCategoryHalfWidth) { 1048 return false; 1049 } 1050 if (this.tickMarkLength != that.tickMarkLength) { 1051 return false; 1052 } 1053 if (!ObjectUtils.equalsPaint(this.tickMarkPaint, that.tickMarkPaint)) { 1054 return false; 1055 } 1056 if (!this.tickMarkStroke.equals(that.tickMarkStroke)) { 1057 return false; 1058 } 1059 if (!this.tickLabelGenerator.equals(that.tickLabelGenerator)) { 1060 return false; 1061 } 1062 if (this.tickLabelOffset != that.tickLabelOffset) { 1063 return false; 1064 } 1065 if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) { 1066 return false; 1067 } 1068 if (this.tickLabelFactor != that.tickLabelFactor) { 1069 return false; 1070 } 1071 if (this.maxTickLabelLevels != that.maxTickLabelLevels) { 1072 return false; 1073 } 1074 if (!this.markers.equals(that.markers)) { 1075 return false; 1076 } 1077 return super.equals(obj); 1078 } 1079 1080 /** 1081 * Provides serialization support. 1082 * 1083 * @param stream the output stream. 1084 * 1085 * @throws IOException if there is an I/O error. 1086 */ 1087 private void writeObject(ObjectOutputStream stream) throws IOException { 1088 stream.defaultWriteObject(); 1089 SerialUtils.writePaint(this.tickMarkPaint, stream); 1090 SerialUtils.writeStroke(this.tickMarkStroke, stream); 1091 } 1092 1093 /** 1094 * Provides serialization support. 1095 * 1096 * @param stream the input stream. 1097 * 1098 * @throws IOException if there is an I/O error. 1099 * @throws ClassNotFoundException if there is a classpath problem. 1100 */ 1101 private void readObject(ObjectInputStream stream) 1102 throws IOException, ClassNotFoundException { 1103 stream.defaultReadObject(); 1104 this.tickMarkPaint = SerialUtils.readPaint(stream); 1105 this.tickMarkStroke = SerialUtils.readStroke(stream); 1106 } 1107 1108 /** 1109 * Returns {@code true} if the axis inverts the order of the data items, 1110 * and {@code false} otherwise. 1111 * 1112 * @return A boolean. 1113 * 1114 * @since 1.5 1115 */ 1116 @Override 1117 public boolean isInverted() { 1118 return this.inverted; 1119 } 1120 1121 /** 1122 * Sets the flag that controls whether or not the axis inverts the order 1123 * of the data items and sends an {@link Axis3DChangeEvent} to all 1124 * registered listeners. 1125 * 1126 * @param inverted the new flag value. 1127 * 1128 * @since 1.5 1129 */ 1130 @Override 1131 public void setInverted(boolean inverted) { 1132 this.inverted = inverted; 1133 fireChangeEvent(true); 1134 } 1135}