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