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