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