001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info: http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * -----------------
028 * CategoryAxis.java
029 * -----------------
030 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert;
033 * Contributor(s): Pady Srinivasan (patch 1217634);
034 *
035 * $Id: CategoryAxis.java,v 1.18.2.12 2007/03/07 11:14:11 mungady Exp $
036 *
037 * Changes (from 21-Aug-2001)
038 * --------------------------
039 * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG);
040 * 18-Sep-2001 : Updated header (DG);
041 * 04-Dec-2001 : Changed constructors to protected, and tidied up default
042 * values (DG);
043 * 19-Apr-2002 : Updated import statements (DG);
044 * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
045 * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG);
046 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
047 * 22-Jan-2002 : Removed monolithic constructor (DG);
048 * 26-Mar-2003 : Implemented Serializable (DG);
049 * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into
050 * this class (DG);
051 * 13-Aug-2003 : Implemented Cloneable (DG);
052 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
053 * 05-Nov-2003 : Fixed serialization bug (DG);
054 * 26-Nov-2003 : Added category label offset (DG);
055 * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised
056 * category label position attributes (DG);
057 * 07-Jan-2004 : Added new implementation for linewrapping of category
058 * labels (DG);
059 * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG);
060 * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG);
061 * 16-Mar-2004 : Added support for tooltips on category labels (DG);
062 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D
063 * because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
064 * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG);
065 * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
066 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
067 * release (DG);
068 * 21-Jan-2005 : Modified return type for RectangleAnchor.coordinates()
069 * method (DG);
070 * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
071 * 26-Apr-2005 : Removed LOGGER (DG);
072 * 08-Jun-2005 : Fixed bug in axis layout (DG);
073 * 22-Nov-2005 : Added a method to access the tool tip text for a category
074 * label (DG);
075 * 23-Nov-2005 : Added per-category font and paint options - see patch
076 * 1217634 (DG);
077 * ------------- JFreeChart 1.0.x ---------------------------------------------
078 * 11-Jan-2006 : Fixed null pointer exception in drawCategoryLabels - see bug
079 * 1403043 (DG);
080 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
081 * Joubert (1277726) (DG);
082 * 02-Oct-2006 : Updated category label entity (DG);
083 * 30-Oct-2006 : Updated refreshTicks() method to account for possibility of
084 * multiple domain axes (DG);
085 * 07-Mar-2007 : Fixed bug in axis label positioning (DG);
086 *
087 */
088
089 package org.jfree.chart.axis;
090
091 import java.awt.Font;
092 import java.awt.Graphics2D;
093 import java.awt.Paint;
094 import java.awt.Shape;
095 import java.awt.geom.Point2D;
096 import java.awt.geom.Rectangle2D;
097 import java.io.IOException;
098 import java.io.ObjectInputStream;
099 import java.io.ObjectOutputStream;
100 import java.io.Serializable;
101 import java.util.HashMap;
102 import java.util.Iterator;
103 import java.util.List;
104 import java.util.Map;
105 import java.util.Set;
106
107 import org.jfree.chart.entity.CategoryLabelEntity;
108 import org.jfree.chart.entity.EntityCollection;
109 import org.jfree.chart.event.AxisChangeEvent;
110 import org.jfree.chart.plot.CategoryPlot;
111 import org.jfree.chart.plot.Plot;
112 import org.jfree.chart.plot.PlotRenderingInfo;
113 import org.jfree.io.SerialUtilities;
114 import org.jfree.text.G2TextMeasurer;
115 import org.jfree.text.TextBlock;
116 import org.jfree.text.TextUtilities;
117 import org.jfree.ui.RectangleAnchor;
118 import org.jfree.ui.RectangleEdge;
119 import org.jfree.ui.RectangleInsets;
120 import org.jfree.ui.Size2D;
121 import org.jfree.util.ObjectUtilities;
122 import org.jfree.util.PaintUtilities;
123 import org.jfree.util.ShapeUtilities;
124
125 /**
126 * An axis that displays categories.
127 */
128 public class CategoryAxis extends Axis implements Cloneable, Serializable {
129
130 /** For serialization. */
131 private static final long serialVersionUID = 5886554608114265863L;
132
133 /**
134 * The default margin for the axis (used for both lower and upper margins).
135 */
136 public static final double DEFAULT_AXIS_MARGIN = 0.05;
137
138 /**
139 * The default margin between categories (a percentage of the overall axis
140 * length).
141 */
142 public static final double DEFAULT_CATEGORY_MARGIN = 0.20;
143
144 /** The amount of space reserved at the start of the axis. */
145 private double lowerMargin;
146
147 /** The amount of space reserved at the end of the axis. */
148 private double upperMargin;
149
150 /** The amount of space reserved between categories. */
151 private double categoryMargin;
152
153 /** The maximum number of lines for category labels. */
154 private int maximumCategoryLabelLines;
155
156 /**
157 * A ratio that is multiplied by the width of one category to determine the
158 * maximum label width.
159 */
160 private float maximumCategoryLabelWidthRatio;
161
162 /** The category label offset. */
163 private int categoryLabelPositionOffset;
164
165 /**
166 * A structure defining the category label positions for each axis
167 * location.
168 */
169 private CategoryLabelPositions categoryLabelPositions;
170
171 /** Storage for tick label font overrides (if any). */
172 private Map tickLabelFontMap;
173
174 /** Storage for tick label paint overrides (if any). */
175 private transient Map tickLabelPaintMap;
176
177 /** Storage for the category label tooltips (if any). */
178 private Map categoryLabelToolTips;
179
180 /**
181 * Creates a new category axis with no label.
182 */
183 public CategoryAxis() {
184 this(null);
185 }
186
187 /**
188 * Constructs a category axis, using default values where necessary.
189 *
190 * @param label the axis label (<code>null</code> permitted).
191 */
192 public CategoryAxis(String label) {
193
194 super(label);
195
196 this.lowerMargin = DEFAULT_AXIS_MARGIN;
197 this.upperMargin = DEFAULT_AXIS_MARGIN;
198 this.categoryMargin = DEFAULT_CATEGORY_MARGIN;
199 this.maximumCategoryLabelLines = 1;
200 this.maximumCategoryLabelWidthRatio = 0.0f;
201
202 setTickMarksVisible(false); // not supported by this axis type yet
203
204 this.categoryLabelPositionOffset = 4;
205 this.categoryLabelPositions = CategoryLabelPositions.STANDARD;
206 this.tickLabelFontMap = new HashMap();
207 this.tickLabelPaintMap = new HashMap();
208 this.categoryLabelToolTips = new HashMap();
209
210 }
211
212 /**
213 * Returns the lower margin for the axis.
214 *
215 * @return The margin.
216 *
217 * @see #getUpperMargin()
218 * @see #setLowerMargin(double)
219 */
220 public double getLowerMargin() {
221 return this.lowerMargin;
222 }
223
224 /**
225 * Sets the lower margin for the axis and sends an {@link AxisChangeEvent}
226 * to all registered listeners.
227 *
228 * @param margin the margin as a percentage of the axis length (for
229 * example, 0.05 is five percent).
230 *
231 * @see #getLowerMargin()
232 */
233 public void setLowerMargin(double margin) {
234 this.lowerMargin = margin;
235 notifyListeners(new AxisChangeEvent(this));
236 }
237
238 /**
239 * Returns the upper margin for the axis.
240 *
241 * @return The margin.
242 *
243 * @see #getLowerMargin()
244 * @see #setUpperMargin(double)
245 */
246 public double getUpperMargin() {
247 return this.upperMargin;
248 }
249
250 /**
251 * Sets the upper margin for the axis and sends an {@link AxisChangeEvent}
252 * to all registered listeners.
253 *
254 * @param margin the margin as a percentage of the axis length (for
255 * example, 0.05 is five percent).
256 *
257 * @see #getUpperMargin()
258 */
259 public void setUpperMargin(double margin) {
260 this.upperMargin = margin;
261 notifyListeners(new AxisChangeEvent(this));
262 }
263
264 /**
265 * Returns the category margin.
266 *
267 * @return The margin.
268 *
269 * @see #setCategoryMargin(double)
270 */
271 public double getCategoryMargin() {
272 return this.categoryMargin;
273 }
274
275 /**
276 * Sets the category margin and sends an {@link AxisChangeEvent} to all
277 * registered listeners. The overall category margin is distributed over
278 * N-1 gaps, where N is the number of categories on the axis.
279 *
280 * @param margin the margin as a percentage of the axis length (for
281 * example, 0.05 is five percent).
282 *
283 * @see #getCategoryMargin()
284 */
285 public void setCategoryMargin(double margin) {
286 this.categoryMargin = margin;
287 notifyListeners(new AxisChangeEvent(this));
288 }
289
290 /**
291 * Returns the maximum number of lines to use for each category label.
292 *
293 * @return The maximum number of lines.
294 *
295 * @see #setMaximumCategoryLabelLines(int)
296 */
297 public int getMaximumCategoryLabelLines() {
298 return this.maximumCategoryLabelLines;
299 }
300
301 /**
302 * Sets the maximum number of lines to use for each category label and
303 * sends an {@link AxisChangeEvent} to all registered listeners.
304 *
305 * @param lines the maximum number of lines.
306 *
307 * @see #getMaximumCategoryLabelLines()
308 */
309 public void setMaximumCategoryLabelLines(int lines) {
310 this.maximumCategoryLabelLines = lines;
311 notifyListeners(new AxisChangeEvent(this));
312 }
313
314 /**
315 * Returns the category label width ratio.
316 *
317 * @return The ratio.
318 *
319 * @see #setMaximumCategoryLabelWidthRatio(float)
320 */
321 public float getMaximumCategoryLabelWidthRatio() {
322 return this.maximumCategoryLabelWidthRatio;
323 }
324
325 /**
326 * Sets the maximum category label width ratio and sends an
327 * {@link AxisChangeEvent} to all registered listeners.
328 *
329 * @param ratio the ratio.
330 *
331 * @see #getMaximumCategoryLabelWidthRatio()
332 */
333 public void setMaximumCategoryLabelWidthRatio(float ratio) {
334 this.maximumCategoryLabelWidthRatio = ratio;
335 notifyListeners(new AxisChangeEvent(this));
336 }
337
338 /**
339 * Returns the offset between the axis and the category labels (before
340 * label positioning is taken into account).
341 *
342 * @return The offset (in Java2D units).
343 *
344 * @see #setCategoryLabelPositionOffset(int)
345 */
346 public int getCategoryLabelPositionOffset() {
347 return this.categoryLabelPositionOffset;
348 }
349
350 /**
351 * Sets the offset between the axis and the category labels (before label
352 * positioning is taken into account).
353 *
354 * @param offset the offset (in Java2D units).
355 *
356 * @see #getCategoryLabelPositionOffset()
357 */
358 public void setCategoryLabelPositionOffset(int offset) {
359 this.categoryLabelPositionOffset = offset;
360 notifyListeners(new AxisChangeEvent(this));
361 }
362
363 /**
364 * Returns the category label position specification (this contains label
365 * positioning info for all four possible axis locations).
366 *
367 * @return The positions (never <code>null</code>).
368 *
369 * @see #setCategoryLabelPositions(CategoryLabelPositions)
370 */
371 public CategoryLabelPositions getCategoryLabelPositions() {
372 return this.categoryLabelPositions;
373 }
374
375 /**
376 * Sets the category label position specification for the axis and sends an
377 * {@link AxisChangeEvent} to all registered listeners.
378 *
379 * @param positions the positions (<code>null</code> not permitted).
380 *
381 * @see #getCategoryLabelPositions()
382 */
383 public void setCategoryLabelPositions(CategoryLabelPositions positions) {
384 if (positions == null) {
385 throw new IllegalArgumentException("Null 'positions' argument.");
386 }
387 this.categoryLabelPositions = positions;
388 notifyListeners(new AxisChangeEvent(this));
389 }
390
391 /**
392 * Returns the font for the tick label for the given category.
393 *
394 * @param category the category (<code>null</code> not permitted).
395 *
396 * @return The font (never <code>null</code>).
397 *
398 * @see #setTickLabelFont(Comparable, Font)
399 */
400 public Font getTickLabelFont(Comparable category) {
401 if (category == null) {
402 throw new IllegalArgumentException("Null 'category' argument.");
403 }
404 Font result = (Font) this.tickLabelFontMap.get(category);
405 // if there is no specific font, use the general one...
406 if (result == null) {
407 result = getTickLabelFont();
408 }
409 return result;
410 }
411
412 /**
413 * Sets the font for the tick label for the specified category and sends
414 * an {@link AxisChangeEvent} to all registered listeners.
415 *
416 * @param category the category (<code>null</code> not permitted).
417 * @param font the font (<code>null</code> permitted).
418 *
419 * @see #getTickLabelFont(Comparable)
420 */
421 public void setTickLabelFont(Comparable category, Font font) {
422 if (category == null) {
423 throw new IllegalArgumentException("Null 'category' argument.");
424 }
425 if (font == null) {
426 this.tickLabelFontMap.remove(category);
427 }
428 else {
429 this.tickLabelFontMap.put(category, font);
430 }
431 notifyListeners(new AxisChangeEvent(this));
432 }
433
434 /**
435 * Returns the paint for the tick label for the given category.
436 *
437 * @param category the category (<code>null</code> not permitted).
438 *
439 * @return The paint (never <code>null</code>).
440 *
441 * @see #setTickLabelPaint(Paint)
442 */
443 public Paint getTickLabelPaint(Comparable category) {
444 if (category == null) {
445 throw new IllegalArgumentException("Null 'category' argument.");
446 }
447 Paint result = (Paint) this.tickLabelPaintMap.get(category);
448 // if there is no specific paint, use the general one...
449 if (result == null) {
450 result = getTickLabelPaint();
451 }
452 return result;
453 }
454
455 /**
456 * Sets the paint for the tick label for the specified category and sends
457 * an {@link AxisChangeEvent} to all registered listeners.
458 *
459 * @param category the category (<code>null</code> not permitted).
460 * @param paint the paint (<code>null</code> permitted).
461 *
462 * @see #getTickLabelPaint(Comparable)
463 */
464 public void setTickLabelPaint(Comparable category, Paint paint) {
465 if (category == null) {
466 throw new IllegalArgumentException("Null 'category' argument.");
467 }
468 if (paint == null) {
469 this.tickLabelPaintMap.remove(category);
470 }
471 else {
472 this.tickLabelPaintMap.put(category, paint);
473 }
474 notifyListeners(new AxisChangeEvent(this));
475 }
476
477 /**
478 * Adds a tooltip to the specified category and sends an
479 * {@link AxisChangeEvent} to all registered listeners.
480 *
481 * @param category the category (<code>null<code> not permitted).
482 * @param tooltip the tooltip text (<code>null</code> permitted).
483 *
484 * @see #removeCategoryLabelToolTip(Comparable)
485 */
486 public void addCategoryLabelToolTip(Comparable category, String tooltip) {
487 if (category == null) {
488 throw new IllegalArgumentException("Null 'category' argument.");
489 }
490 this.categoryLabelToolTips.put(category, tooltip);
491 notifyListeners(new AxisChangeEvent(this));
492 }
493
494 /**
495 * Returns the tool tip text for the label belonging to the specified
496 * category.
497 *
498 * @param category the category (<code>null</code> not permitted).
499 *
500 * @return The tool tip text (possibly <code>null</code>).
501 *
502 * @see #addCategoryLabelToolTip(Comparable, String)
503 * @see #removeCategoryLabelToolTip(Comparable)
504 */
505 public String getCategoryLabelToolTip(Comparable category) {
506 if (category == null) {
507 throw new IllegalArgumentException("Null 'category' argument.");
508 }
509 return (String) this.categoryLabelToolTips.get(category);
510 }
511
512 /**
513 * Removes the tooltip for the specified category and sends an
514 * {@link AxisChangeEvent} to all registered listeners.
515 *
516 * @param category the category (<code>null<code> not permitted).
517 *
518 * @see #addCategoryLabelToolTip(Comparable, String)
519 * @see #clearCategoryLabelToolTips()
520 */
521 public void removeCategoryLabelToolTip(Comparable category) {
522 if (category == null) {
523 throw new IllegalArgumentException("Null 'category' argument.");
524 }
525 this.categoryLabelToolTips.remove(category);
526 notifyListeners(new AxisChangeEvent(this));
527 }
528
529 /**
530 * Clears the category label tooltips and sends an {@link AxisChangeEvent}
531 * to all registered listeners.
532 *
533 * @see #addCategoryLabelToolTip(Comparable, String)
534 * @see #removeCategoryLabelToolTip(Comparable)
535 */
536 public void clearCategoryLabelToolTips() {
537 this.categoryLabelToolTips.clear();
538 notifyListeners(new AxisChangeEvent(this));
539 }
540
541 /**
542 * Returns the Java 2D coordinate for a category.
543 *
544 * @param anchor the anchor point.
545 * @param category the category index.
546 * @param categoryCount the category count.
547 * @param area the data area.
548 * @param edge the location of the axis.
549 *
550 * @return The coordinate.
551 */
552 public double getCategoryJava2DCoordinate(CategoryAnchor anchor,
553 int category,
554 int categoryCount,
555 Rectangle2D area,
556 RectangleEdge edge) {
557
558 double result = 0.0;
559 if (anchor == CategoryAnchor.START) {
560 result = getCategoryStart(category, categoryCount, area, edge);
561 }
562 else if (anchor == CategoryAnchor.MIDDLE) {
563 result = getCategoryMiddle(category, categoryCount, area, edge);
564 }
565 else if (anchor == CategoryAnchor.END) {
566 result = getCategoryEnd(category, categoryCount, area, edge);
567 }
568 return result;
569
570 }
571
572 /**
573 * Returns the starting coordinate for the specified category.
574 *
575 * @param category the category.
576 * @param categoryCount the number of categories.
577 * @param area the data area.
578 * @param edge the axis location.
579 *
580 * @return The coordinate.
581 *
582 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
583 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
584 */
585 public double getCategoryStart(int category, int categoryCount,
586 Rectangle2D area,
587 RectangleEdge edge) {
588
589 double result = 0.0;
590 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
591 result = area.getX() + area.getWidth() * getLowerMargin();
592 }
593 else if ((edge == RectangleEdge.LEFT)
594 || (edge == RectangleEdge.RIGHT)) {
595 result = area.getMinY() + area.getHeight() * getLowerMargin();
596 }
597
598 double categorySize = calculateCategorySize(categoryCount, area, edge);
599 double categoryGapWidth = calculateCategoryGapSize(categoryCount, area,
600 edge);
601
602 result = result + category * (categorySize + categoryGapWidth);
603 return result;
604
605 }
606
607 /**
608 * Returns the middle coordinate for the specified category.
609 *
610 * @param category the category.
611 * @param categoryCount the number of categories.
612 * @param area the data area.
613 * @param edge the axis location.
614 *
615 * @return The coordinate.
616 *
617 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
618 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
619 */
620 public double getCategoryMiddle(int category, int categoryCount,
621 Rectangle2D area, RectangleEdge edge) {
622
623 return getCategoryStart(category, categoryCount, area, edge)
624 + calculateCategorySize(categoryCount, area, edge) / 2;
625
626 }
627
628 /**
629 * Returns the end coordinate for the specified category.
630 *
631 * @param category the category.
632 * @param categoryCount the number of categories.
633 * @param area the data area.
634 * @param edge the axis location.
635 *
636 * @return The coordinate.
637 *
638 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
639 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
640 */
641 public double getCategoryEnd(int category, int categoryCount,
642 Rectangle2D area, RectangleEdge edge) {
643
644 return getCategoryStart(category, categoryCount, area, edge)
645 + calculateCategorySize(categoryCount, area, edge);
646
647 }
648
649 /**
650 * Calculates the size (width or height, depending on the location of the
651 * axis) of a category.
652 *
653 * @param categoryCount the number of categories.
654 * @param area the area within which the categories will be drawn.
655 * @param edge the axis location.
656 *
657 * @return The category size.
658 */
659 protected double calculateCategorySize(int categoryCount, Rectangle2D area,
660 RectangleEdge edge) {
661
662 double result = 0.0;
663 double available = 0.0;
664
665 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
666 available = area.getWidth();
667 }
668 else if ((edge == RectangleEdge.LEFT)
669 || (edge == RectangleEdge.RIGHT)) {
670 available = area.getHeight();
671 }
672 if (categoryCount > 1) {
673 result = available * (1 - getLowerMargin() - getUpperMargin()
674 - getCategoryMargin());
675 result = result / categoryCount;
676 }
677 else {
678 result = available * (1 - getLowerMargin() - getUpperMargin());
679 }
680 return result;
681
682 }
683
684 /**
685 * Calculates the size (width or height, depending on the location of the
686 * axis) of a category gap.
687 *
688 * @param categoryCount the number of categories.
689 * @param area the area within which the categories will be drawn.
690 * @param edge the axis location.
691 *
692 * @return The category gap width.
693 */
694 protected double calculateCategoryGapSize(int categoryCount,
695 Rectangle2D area,
696 RectangleEdge edge) {
697
698 double result = 0.0;
699 double available = 0.0;
700
701 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
702 available = area.getWidth();
703 }
704 else if ((edge == RectangleEdge.LEFT)
705 || (edge == RectangleEdge.RIGHT)) {
706 available = area.getHeight();
707 }
708
709 if (categoryCount > 1) {
710 result = available * getCategoryMargin() / (categoryCount - 1);
711 }
712
713 return result;
714
715 }
716
717 /**
718 * Estimates the space required for the axis, given a specific drawing area.
719 *
720 * @param g2 the graphics device (used to obtain font information).
721 * @param plot the plot that the axis belongs to.
722 * @param plotArea the area within which the axis should be drawn.
723 * @param edge the axis location (top or bottom).
724 * @param space the space already reserved.
725 *
726 * @return The space required to draw the axis.
727 */
728 public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
729 Rectangle2D plotArea,
730 RectangleEdge edge, AxisSpace space) {
731
732 // create a new space object if one wasn't supplied...
733 if (space == null) {
734 space = new AxisSpace();
735 }
736
737 // if the axis is not visible, no additional space is required...
738 if (!isVisible()) {
739 return space;
740 }
741
742 // calculate the max size of the tick labels (if visible)...
743 double tickLabelHeight = 0.0;
744 double tickLabelWidth = 0.0;
745 if (isTickLabelsVisible()) {
746 g2.setFont(getTickLabelFont());
747 AxisState state = new AxisState();
748 // we call refresh ticks just to get the maximum width or height
749 refreshTicks(g2, state, plotArea, edge);
750 if (edge == RectangleEdge.TOP) {
751 tickLabelHeight = state.getMax();
752 }
753 else if (edge == RectangleEdge.BOTTOM) {
754 tickLabelHeight = state.getMax();
755 }
756 else if (edge == RectangleEdge.LEFT) {
757 tickLabelWidth = state.getMax();
758 }
759 else if (edge == RectangleEdge.RIGHT) {
760 tickLabelWidth = state.getMax();
761 }
762 }
763
764 // get the axis label size and update the space object...
765 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
766 double labelHeight = 0.0;
767 double labelWidth = 0.0;
768 if (RectangleEdge.isTopOrBottom(edge)) {
769 labelHeight = labelEnclosure.getHeight();
770 space.add(labelHeight + tickLabelHeight
771 + this.categoryLabelPositionOffset, edge);
772 }
773 else if (RectangleEdge.isLeftOrRight(edge)) {
774 labelWidth = labelEnclosure.getWidth();
775 space.add(labelWidth + tickLabelWidth
776 + this.categoryLabelPositionOffset, edge);
777 }
778 return space;
779
780 }
781
782 /**
783 * Configures the axis against the current plot.
784 */
785 public void configure() {
786 // nothing required
787 }
788
789 /**
790 * Draws the axis on a Java 2D graphics device (such as the screen or a
791 * printer).
792 *
793 * @param g2 the graphics device (<code>null</code> not permitted).
794 * @param cursor the cursor location.
795 * @param plotArea the area within which the axis should be drawn
796 * (<code>null</code> not permitted).
797 * @param dataArea the area within which the plot is being drawn
798 * (<code>null</code> not permitted).
799 * @param edge the location of the axis (<code>null</code> not permitted).
800 * @param plotState collects information about the plot
801 * (<code>null</code> permitted).
802 *
803 * @return The axis state (never <code>null</code>).
804 */
805 public AxisState draw(Graphics2D g2,
806 double cursor,
807 Rectangle2D plotArea,
808 Rectangle2D dataArea,
809 RectangleEdge edge,
810 PlotRenderingInfo plotState) {
811
812 // if the axis is not visible, don't draw it...
813 if (!isVisible()) {
814 return new AxisState(cursor);
815 }
816
817 if (isAxisLineVisible()) {
818 drawAxisLine(g2, cursor, dataArea, edge);
819 }
820
821 // draw the category labels and axis label
822 AxisState state = new AxisState(cursor);
823 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
824 plotState);
825 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
826
827 return state;
828
829 }
830
831 /**
832 * Draws the category labels and returns the updated axis state.
833 *
834 * @param g2 the graphics device (<code>null</code> not permitted).
835 * @param dataArea the area inside the axes (<code>null</code> not
836 * permitted).
837 * @param edge the axis location (<code>null</code> not permitted).
838 * @param state the axis state (<code>null</code> not permitted).
839 * @param plotState collects information about the plot (<code>null</code>
840 * permitted).
841 *
842 * @return The updated axis state (never <code>null</code>).
843 *
844 * @deprecated Use {@link #drawCategoryLabels(Graphics2D, Rectangle2D,
845 * Rectangle2D, RectangleEdge, AxisState, PlotRenderingInfo)}.
846 */
847 protected AxisState drawCategoryLabels(Graphics2D g2,
848 Rectangle2D dataArea,
849 RectangleEdge edge,
850 AxisState state,
851 PlotRenderingInfo plotState) {
852
853 // this method is deprecated because we really need the plotArea
854 // when drawing the labels - see bug 1277726
855 return drawCategoryLabels(g2, dataArea, dataArea, edge, state,
856 plotState);
857 }
858
859 /**
860 * Draws the category labels and returns the updated axis state.
861 *
862 * @param g2 the graphics device (<code>null</code> not permitted).
863 * @param plotArea the plot area (<code>null</code> not permitted).
864 * @param dataArea the area inside the axes (<code>null</code> not
865 * permitted).
866 * @param edge the axis location (<code>null</code> not permitted).
867 * @param state the axis state (<code>null</code> not permitted).
868 * @param plotState collects information about the plot (<code>null</code>
869 * permitted).
870 *
871 * @return The updated axis state (never <code>null</code>).
872 */
873 protected AxisState drawCategoryLabels(Graphics2D g2,
874 Rectangle2D plotArea,
875 Rectangle2D dataArea,
876 RectangleEdge edge,
877 AxisState state,
878 PlotRenderingInfo plotState) {
879
880 if (state == null) {
881 throw new IllegalArgumentException("Null 'state' argument.");
882 }
883
884 if (isTickLabelsVisible()) {
885 List ticks = refreshTicks(g2, state, plotArea, edge);
886 state.setTicks(ticks);
887
888 int categoryIndex = 0;
889 Iterator iterator = ticks.iterator();
890 while (iterator.hasNext()) {
891
892 CategoryTick tick = (CategoryTick) iterator.next();
893 g2.setFont(getTickLabelFont(tick.getCategory()));
894 g2.setPaint(getTickLabelPaint(tick.getCategory()));
895
896 CategoryLabelPosition position
897 = this.categoryLabelPositions.getLabelPosition(edge);
898 double x0 = 0.0;
899 double x1 = 0.0;
900 double y0 = 0.0;
901 double y1 = 0.0;
902 if (edge == RectangleEdge.TOP) {
903 x0 = getCategoryStart(categoryIndex, ticks.size(),
904 dataArea, edge);
905 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
906 edge);
907 y1 = state.getCursor() - this.categoryLabelPositionOffset;
908 y0 = y1 - state.getMax();
909 }
910 else if (edge == RectangleEdge.BOTTOM) {
911 x0 = getCategoryStart(categoryIndex, ticks.size(),
912 dataArea, edge);
913 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
914 edge);
915 y0 = state.getCursor() + this.categoryLabelPositionOffset;
916 y1 = y0 + state.getMax();
917 }
918 else if (edge == RectangleEdge.LEFT) {
919 y0 = getCategoryStart(categoryIndex, ticks.size(),
920 dataArea, edge);
921 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
922 edge);
923 x1 = state.getCursor() - this.categoryLabelPositionOffset;
924 x0 = x1 - state.getMax();
925 }
926 else if (edge == RectangleEdge.RIGHT) {
927 y0 = getCategoryStart(categoryIndex, ticks.size(),
928 dataArea, edge);
929 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
930 edge);
931 x0 = state.getCursor() + this.categoryLabelPositionOffset;
932 x1 = x0 - state.getMax();
933 }
934 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
935 (y1 - y0));
936 Point2D anchorPoint = RectangleAnchor.coordinates(area,
937 position.getCategoryAnchor());
938 TextBlock block = tick.getLabel();
939 block.draw(g2, (float) anchorPoint.getX(),
940 (float) anchorPoint.getY(), position.getLabelAnchor(),
941 (float) anchorPoint.getX(), (float) anchorPoint.getY(),
942 position.getAngle());
943 Shape bounds = block.calculateBounds(g2,
944 (float) anchorPoint.getX(), (float) anchorPoint.getY(),
945 position.getLabelAnchor(), (float) anchorPoint.getX(),
946 (float) anchorPoint.getY(), position.getAngle());
947 if (plotState != null && plotState.getOwner() != null) {
948 EntityCollection entities
949 = plotState.getOwner().getEntityCollection();
950 if (entities != null) {
951 String tooltip = getCategoryLabelToolTip(
952 tick.getCategory());
953 entities.add(new CategoryLabelEntity(tick.getCategory(),
954 bounds, tooltip, null));
955 }
956 }
957 categoryIndex++;
958 }
959
960 if (edge.equals(RectangleEdge.TOP)) {
961 double h = state.getMax() + this.categoryLabelPositionOffset;
962 state.cursorUp(h);
963 }
964 else if (edge.equals(RectangleEdge.BOTTOM)) {
965 double h = state.getMax() + this.categoryLabelPositionOffset;
966 state.cursorDown(h);
967 }
968 else if (edge == RectangleEdge.LEFT) {
969 double w = state.getMax() + this.categoryLabelPositionOffset;
970 state.cursorLeft(w);
971 }
972 else if (edge == RectangleEdge.RIGHT) {
973 double w = state.getMax() + this.categoryLabelPositionOffset;
974 state.cursorRight(w);
975 }
976 }
977 return state;
978 }
979
980 /**
981 * Creates a temporary list of ticks that can be used when drawing the axis.
982 *
983 * @param g2 the graphics device (used to get font measurements).
984 * @param state the axis state.
985 * @param dataArea the area inside the axes.
986 * @param edge the location of the axis.
987 *
988 * @return A list of ticks.
989 */
990 public List refreshTicks(Graphics2D g2,
991 AxisState state,
992 Rectangle2D dataArea,
993 RectangleEdge edge) {
994
995 List ticks = new java.util.ArrayList();
996
997 // sanity check for data area...
998 if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
999 return ticks;
1000 }
1001
1002 CategoryPlot plot = (CategoryPlot) getPlot();
1003 List categories = plot.getCategoriesForAxis(this);
1004 double max = 0.0;
1005
1006 if (categories != null) {
1007 CategoryLabelPosition position
1008 = this.categoryLabelPositions.getLabelPosition(edge);
1009 float r = this.maximumCategoryLabelWidthRatio;
1010 if (r <= 0.0) {
1011 r = position.getWidthRatio();
1012 }
1013
1014 float l = 0.0f;
1015 if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
1016 l = (float) calculateCategorySize(categories.size(), dataArea,
1017 edge);
1018 }
1019 else {
1020 if (RectangleEdge.isLeftOrRight(edge)) {
1021 l = (float) dataArea.getWidth();
1022 }
1023 else {
1024 l = (float) dataArea.getHeight();
1025 }
1026 }
1027 int categoryIndex = 0;
1028 Iterator iterator = categories.iterator();
1029 while (iterator.hasNext()) {
1030 Comparable category = (Comparable) iterator.next();
1031 TextBlock label = createLabel(category, l * r, edge, g2);
1032 if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
1033 max = Math.max(max, calculateTextBlockHeight(label,
1034 position, g2));
1035 }
1036 else if (edge == RectangleEdge.LEFT
1037 || edge == RectangleEdge.RIGHT) {
1038 max = Math.max(max, calculateTextBlockWidth(label,
1039 position, g2));
1040 }
1041 Tick tick = new CategoryTick(category, label,
1042 position.getLabelAnchor(), position.getRotationAnchor(),
1043 position.getAngle());
1044 ticks.add(tick);
1045 categoryIndex = categoryIndex + 1;
1046 }
1047 }
1048 state.setMax(max);
1049 return ticks;
1050
1051 }
1052
1053 /**
1054 * Creates a label.
1055 *
1056 * @param category the category.
1057 * @param width the available width.
1058 * @param edge the edge on which the axis appears.
1059 * @param g2 the graphics device.
1060 *
1061 * @return A label.
1062 */
1063 protected TextBlock createLabel(Comparable category, float width,
1064 RectangleEdge edge, Graphics2D g2) {
1065 TextBlock label = TextUtilities.createTextBlock(category.toString(),
1066 getTickLabelFont(category), getTickLabelPaint(category), width,
1067 this.maximumCategoryLabelLines, new G2TextMeasurer(g2));
1068 return label;
1069 }
1070
1071 /**
1072 * A utility method for determining the width of a text block.
1073 *
1074 * @param block the text block.
1075 * @param position the position.
1076 * @param g2 the graphics device.
1077 *
1078 * @return The width.
1079 */
1080 protected double calculateTextBlockWidth(TextBlock block,
1081 CategoryLabelPosition position,
1082 Graphics2D g2) {
1083
1084 RectangleInsets insets = getTickLabelInsets();
1085 Size2D size = block.calculateDimensions(g2);
1086 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1087 size.getHeight());
1088 Shape rotatedBox = ShapeUtilities.rotateShape(box, position.getAngle(),
1089 0.0f, 0.0f);
1090 double w = rotatedBox.getBounds2D().getWidth() + insets.getTop()
1091 + insets.getBottom();
1092 return w;
1093
1094 }
1095
1096 /**
1097 * A utility method for determining the height of a text block.
1098 *
1099 * @param block the text block.
1100 * @param position the label position.
1101 * @param g2 the graphics device.
1102 *
1103 * @return The height.
1104 */
1105 protected double calculateTextBlockHeight(TextBlock block,
1106 CategoryLabelPosition position,
1107 Graphics2D g2) {
1108
1109 RectangleInsets insets = getTickLabelInsets();
1110 Size2D size = block.calculateDimensions(g2);
1111 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1112 size.getHeight());
1113 Shape rotatedBox = ShapeUtilities.rotateShape(box, position.getAngle(),
1114 0.0f, 0.0f);
1115 double h = rotatedBox.getBounds2D().getHeight()
1116 + insets.getTop() + insets.getBottom();
1117 return h;
1118
1119 }
1120
1121 /**
1122 * Creates a clone of the axis.
1123 *
1124 * @return A clone.
1125 *
1126 * @throws CloneNotSupportedException if some component of the axis does
1127 * not support cloning.
1128 */
1129 public Object clone() throws CloneNotSupportedException {
1130 CategoryAxis clone = (CategoryAxis) super.clone();
1131 clone.tickLabelFontMap = new HashMap(this.tickLabelFontMap);
1132 clone.tickLabelPaintMap = new HashMap(this.tickLabelPaintMap);
1133 clone.categoryLabelToolTips = new HashMap(this.categoryLabelToolTips);
1134 return clone;
1135 }
1136
1137 /**
1138 * Tests this axis for equality with an arbitrary object.
1139 *
1140 * @param obj the object (<code>null</code> permitted).
1141 *
1142 * @return A boolean.
1143 */
1144 public boolean equals(Object obj) {
1145 if (obj == this) {
1146 return true;
1147 }
1148 if (!(obj instanceof CategoryAxis)) {
1149 return false;
1150 }
1151 if (!super.equals(obj)) {
1152 return false;
1153 }
1154 CategoryAxis that = (CategoryAxis) obj;
1155 if (that.lowerMargin != this.lowerMargin) {
1156 return false;
1157 }
1158 if (that.upperMargin != this.upperMargin) {
1159 return false;
1160 }
1161 if (that.categoryMargin != this.categoryMargin) {
1162 return false;
1163 }
1164 if (that.maximumCategoryLabelWidthRatio
1165 != this.maximumCategoryLabelWidthRatio) {
1166 return false;
1167 }
1168 if (that.categoryLabelPositionOffset
1169 != this.categoryLabelPositionOffset) {
1170 return false;
1171 }
1172 if (!ObjectUtilities.equal(that.categoryLabelPositions,
1173 this.categoryLabelPositions)) {
1174 return false;
1175 }
1176 if (!ObjectUtilities.equal(that.categoryLabelToolTips,
1177 this.categoryLabelToolTips)) {
1178 return false;
1179 }
1180 if (!ObjectUtilities.equal(this.tickLabelFontMap,
1181 that.tickLabelFontMap)) {
1182 return false;
1183 }
1184 if (!equalPaintMaps(this.tickLabelPaintMap, that.tickLabelPaintMap)) {
1185 return false;
1186 }
1187 return true;
1188 }
1189
1190 /**
1191 * Returns a hash code for this object.
1192 *
1193 * @return A hash code.
1194 */
1195 public int hashCode() {
1196 if (getLabel() != null) {
1197 return getLabel().hashCode();
1198 }
1199 else {
1200 return 0;
1201 }
1202 }
1203
1204 /**
1205 * Provides serialization support.
1206 *
1207 * @param stream the output stream.
1208 *
1209 * @throws IOException if there is an I/O error.
1210 */
1211 private void writeObject(ObjectOutputStream stream) throws IOException {
1212 stream.defaultWriteObject();
1213 writePaintMap(this.tickLabelPaintMap, stream);
1214 }
1215
1216 /**
1217 * Provides serialization support.
1218 *
1219 * @param stream the input stream.
1220 *
1221 * @throws IOException if there is an I/O error.
1222 * @throws ClassNotFoundException if there is a classpath problem.
1223 */
1224 private void readObject(ObjectInputStream stream)
1225 throws IOException, ClassNotFoundException {
1226 stream.defaultReadObject();
1227 this.tickLabelPaintMap = readPaintMap(stream);
1228 }
1229
1230 /**
1231 * Reads a <code>Map</code> of (<code>Comparable</code>, <code>Paint</code>)
1232 * elements from a stream.
1233 *
1234 * @param in the input stream.
1235 *
1236 * @return The map.
1237 *
1238 * @throws IOException
1239 * @throws ClassNotFoundException
1240 *
1241 * @see #writePaintMap(Map, ObjectOutputStream)
1242 */
1243 private Map readPaintMap(ObjectInputStream in)
1244 throws IOException, ClassNotFoundException {
1245 boolean isNull = in.readBoolean();
1246 if (isNull) {
1247 return null;
1248 }
1249 Map result = new HashMap();
1250 int count = in.readInt();
1251 for (int i = 0; i < count; i++) {
1252 Comparable category = (Comparable) in.readObject();
1253 Paint paint = SerialUtilities.readPaint(in);
1254 result.put(category, paint);
1255 }
1256 return result;
1257 }
1258
1259 /**
1260 * Writes a map of (<code>Comparable</code>, <code>Paint</code>)
1261 * elements to a stream.
1262 *
1263 * @param map the map (<code>null</code> permitted).
1264 *
1265 * @param out
1266 * @throws IOException
1267 *
1268 * @see #readPaintMap(ObjectInputStream)
1269 */
1270 private void writePaintMap(Map map, ObjectOutputStream out)
1271 throws IOException {
1272 if (map == null) {
1273 out.writeBoolean(true);
1274 }
1275 else {
1276 out.writeBoolean(false);
1277 Set keys = map.keySet();
1278 int count = keys.size();
1279 out.writeInt(count);
1280 Iterator iterator = keys.iterator();
1281 while (iterator.hasNext()) {
1282 Comparable key = (Comparable) iterator.next();
1283 out.writeObject(key);
1284 SerialUtilities.writePaint((Paint) map.get(key), out);
1285 }
1286 }
1287 }
1288
1289 /**
1290 * Tests two maps containing (<code>Comparable</code>, <code>Paint</code>)
1291 * elements for equality.
1292 *
1293 * @param map1 the first map (<code>null</code> not permitted).
1294 * @param map2 the second map (<code>null</code> not permitted).
1295 *
1296 * @return A boolean.
1297 */
1298 private boolean equalPaintMaps(Map map1, Map map2) {
1299 if (map1.size() != map2.size()) {
1300 return false;
1301 }
1302 Set keys = map1.keySet();
1303 Iterator iterator = keys.iterator();
1304 while (iterator.hasNext()) {
1305 Comparable key = (Comparable) iterator.next();
1306 Paint p1 = (Paint) map1.get(key);
1307 Paint p2 = (Paint) map2.get(key);
1308 if (!PaintUtilities.equal(p1, p2)) {
1309 return false;
1310 }
1311 }
1312 return true;
1313 }
1314
1315 }