001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2006, 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 * SubCategoryAxis.java
029 * --------------------
030 * (C) Copyright 2004-2006, by Object Refinery Limited.
031 *
032 * Original Author: David Gilbert;
033 * Contributor(s): Adriaan Joubert;
034 *
035 * $Id: SubCategoryAxis.java,v 1.6.2.2 2006/08/18 14:48:31 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 12-May-2004 : Version 1 (DG);
040 * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities
041 * --> TextUtilities (DG);
042 * 26-Apr-2005 : Removed logger (DG);
043 * ------------- JFREECHART 1.0.0 ---------------------------------------------
044 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
045 * Joubert (1277726) (DG);
046 *
047 */
048
049 package org.jfree.chart.axis;
050
051 import java.awt.Color;
052 import java.awt.Font;
053 import java.awt.FontMetrics;
054 import java.awt.Graphics2D;
055 import java.awt.Paint;
056 import java.awt.geom.Rectangle2D;
057 import java.io.IOException;
058 import java.io.ObjectInputStream;
059 import java.io.ObjectOutputStream;
060 import java.io.Serializable;
061 import java.util.Iterator;
062 import java.util.List;
063
064 import org.jfree.chart.event.AxisChangeEvent;
065 import org.jfree.chart.plot.CategoryPlot;
066 import org.jfree.chart.plot.Plot;
067 import org.jfree.chart.plot.PlotRenderingInfo;
068 import org.jfree.data.category.CategoryDataset;
069 import org.jfree.io.SerialUtilities;
070 import org.jfree.text.TextUtilities;
071 import org.jfree.ui.RectangleEdge;
072 import org.jfree.ui.TextAnchor;
073
074 /**
075 * A specialised category axis that can display sub-categories.
076 */
077 public class SubCategoryAxis extends CategoryAxis
078 implements Cloneable, Serializable {
079
080 /** For serialization. */
081 private static final long serialVersionUID = -1279463299793228344L;
082
083 /** Storage for the sub-categories (these need to be set manually). */
084 private List subCategories;
085
086 /** The font for the sub-category labels. */
087 private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10);
088
089 /** The paint for the sub-category labels. */
090 private transient Paint subLabelPaint = Color.black;
091
092 /**
093 * Creates a new axis.
094 *
095 * @param label the axis label.
096 */
097 public SubCategoryAxis(String label) {
098 super(label);
099 this.subCategories = new java.util.ArrayList();
100 }
101
102 /**
103 * Adds a sub-category to the axis.
104 *
105 * @param subCategory the sub-category.
106 */
107 public void addSubCategory(Comparable subCategory) {
108 this.subCategories.add(subCategory);
109 }
110
111 /**
112 * Returns the font used to display the sub-category labels.
113 *
114 * @return The font (never <code>null</code>).
115 */
116 public Font getSubLabelFont() {
117 return this.subLabelFont;
118 }
119
120 /**
121 * Sets the font used to display the sub-category labels and sends an
122 * {@link AxisChangeEvent} to all registered listeners.
123 *
124 * @param font the font (<code>null</code> not permitted).
125 */
126 public void setSubLabelFont(Font font) {
127 if (font == null) {
128 throw new IllegalArgumentException("Null 'font' argument.");
129 }
130 this.subLabelFont = font;
131 notifyListeners(new AxisChangeEvent(this));
132 }
133
134 /**
135 * Returns the paint used to display the sub-category labels.
136 *
137 * @return The paint (never <code>null</code>).
138 */
139 public Paint getSubLabelPaint() {
140 return this.subLabelPaint;
141 }
142
143 /**
144 * Sets the paint used to display the sub-category labels and sends an
145 * {@link AxisChangeEvent} to all registered listeners.
146 *
147 * @param paint the paint (<code>null</code> not permitted).
148 */
149 public void setSubLabelPaint(Paint paint) {
150 if (paint == null) {
151 throw new IllegalArgumentException("Null 'paint' argument.");
152 }
153 this.subLabelPaint = paint;
154 notifyListeners(new AxisChangeEvent(this));
155 }
156
157 /**
158 * Estimates the space required for the axis, given a specific drawing area.
159 *
160 * @param g2 the graphics device (used to obtain font information).
161 * @param plot the plot that the axis belongs to.
162 * @param plotArea the area within which the axis should be drawn.
163 * @param edge the axis location (top or bottom).
164 * @param space the space already reserved.
165 *
166 * @return The space required to draw the axis.
167 */
168 public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
169 Rectangle2D plotArea,
170 RectangleEdge edge, AxisSpace space) {
171
172 // create a new space object if one wasn't supplied...
173 if (space == null) {
174 space = new AxisSpace();
175 }
176
177 // if the axis is not visible, no additional space is required...
178 if (!isVisible()) {
179 return space;
180 }
181
182 space = super.reserveSpace(g2, plot, plotArea, edge, space);
183 double maxdim = getMaxDim(g2, edge);
184 if (RectangleEdge.isTopOrBottom(edge)) {
185 space.add(maxdim, edge);
186 }
187 else if (RectangleEdge.isLeftOrRight(edge)) {
188 space.add(maxdim, edge);
189 }
190 return space;
191 }
192
193 /**
194 * Returns the maximum of the relevant dimension (height or width) of the
195 * subcategory labels.
196 *
197 * @param g2 the graphics device.
198 * @param edge the edge.
199 *
200 * @return The maximum dimension.
201 */
202 private double getMaxDim(Graphics2D g2, RectangleEdge edge) {
203 double result = 0.0;
204 g2.setFont(this.subLabelFont);
205 FontMetrics fm = g2.getFontMetrics();
206 Iterator iterator = this.subCategories.iterator();
207 while (iterator.hasNext()) {
208 Comparable subcategory = (Comparable) iterator.next();
209 String label = subcategory.toString();
210 Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm);
211 double dim = 0.0;
212 if (RectangleEdge.isLeftOrRight(edge)) {
213 dim = bounds.getWidth();
214 }
215 else { // must be top or bottom
216 dim = bounds.getHeight();
217 }
218 result = Math.max(result, dim);
219 }
220 return result;
221 }
222
223 /**
224 * Draws the axis on a Java 2D graphics device (such as the screen or a
225 * printer).
226 *
227 * @param g2 the graphics device (<code>null</code> not permitted).
228 * @param cursor the cursor location.
229 * @param plotArea the area within which the axis should be drawn
230 * (<code>null</code> not permitted).
231 * @param dataArea the area within which the plot is being drawn
232 * (<code>null</code> not permitted).
233 * @param edge the location of the axis (<code>null</code> not permitted).
234 * @param plotState collects information about the plot
235 * (<code>null</code> permitted).
236 *
237 * @return The axis state (never <code>null</code>).
238 */
239 public AxisState draw(Graphics2D g2,
240 double cursor,
241 Rectangle2D plotArea,
242 Rectangle2D dataArea,
243 RectangleEdge edge,
244 PlotRenderingInfo plotState) {
245
246 // if the axis is not visible, don't draw it...
247 if (!isVisible()) {
248 return new AxisState(cursor);
249 }
250
251 if (isAxisLineVisible()) {
252 drawAxisLine(g2, cursor, dataArea, edge);
253 }
254
255 // draw the category labels and axis label
256 AxisState state = new AxisState(cursor);
257 state = drawSubCategoryLabels(
258 g2, plotArea, dataArea, edge, state, plotState
259 );
260 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
261 plotState);
262 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
263
264 return state;
265
266 }
267
268 /**
269 * Draws the category labels and returns the updated axis state.
270 *
271 * @param g2 the graphics device (<code>null</code> not permitted).
272 * @param plotArea the plot area (<code>null</code> not permitted).
273 * @param dataArea the area inside the axes (<code>null</code> not
274 * permitted).
275 * @param edge the axis location (<code>null</code> not permitted).
276 * @param state the axis state (<code>null</code> not permitted).
277 * @param plotState collects information about the plot (<code>null</code>
278 * permitted).
279 *
280 * @return The updated axis state (never <code>null</code>).
281 */
282 protected AxisState drawSubCategoryLabels(Graphics2D g2,
283 Rectangle2D plotArea,
284 Rectangle2D dataArea,
285 RectangleEdge edge,
286 AxisState state,
287 PlotRenderingInfo plotState) {
288
289 if (state == null) {
290 throw new IllegalArgumentException("Null 'state' argument.");
291 }
292
293 g2.setFont(this.subLabelFont);
294 g2.setPaint(this.subLabelPaint);
295 CategoryPlot plot = (CategoryPlot) getPlot();
296 CategoryDataset dataset = plot.getDataset();
297 int categoryCount = dataset.getColumnCount();
298
299 double maxdim = getMaxDim(g2, edge);
300 for (int categoryIndex = 0; categoryIndex < categoryCount;
301 categoryIndex++) {
302
303 double x0 = 0.0;
304 double x1 = 0.0;
305 double y0 = 0.0;
306 double y1 = 0.0;
307 if (edge == RectangleEdge.TOP) {
308 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
309 edge);
310 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
311 edge);
312 y1 = state.getCursor();
313 y0 = y1 - maxdim;
314 }
315 else if (edge == RectangleEdge.BOTTOM) {
316 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
317 edge);
318 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
319 edge);
320 y0 = state.getCursor();
321 y1 = y0 + maxdim;
322 }
323 else if (edge == RectangleEdge.LEFT) {
324 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
325 edge);
326 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
327 edge);
328 x1 = state.getCursor();
329 x0 = x1 - maxdim;
330 }
331 else if (edge == RectangleEdge.RIGHT) {
332 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
333 edge);
334 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
335 edge);
336 x0 = state.getCursor();
337 x1 = x0 + maxdim;
338 }
339 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
340 (y1 - y0));
341 int subCategoryCount = this.subCategories.size();
342 float width = (float) ((x1 - x0) / subCategoryCount);
343 float height = (float) ((y1 - y0) / subCategoryCount);
344 float xx = 0.0f;
345 float yy = 0.0f;
346 for (int i = 0; i < subCategoryCount; i++) {
347 if (RectangleEdge.isTopOrBottom(edge)) {
348 xx = (float) (x0 + (i + 0.5) * width);
349 yy = (float) area.getCenterY();
350 }
351 else {
352 xx = (float) area.getCenterX();
353 yy = (float) (y0 + (i + 0.5) * height);
354 }
355 String label = this.subCategories.get(i).toString();
356 TextUtilities.drawRotatedString(label, g2, xx, yy,
357 TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
358 }
359 }
360
361 if (edge.equals(RectangleEdge.TOP)) {
362 double h = maxdim;
363 state.cursorUp(h);
364 }
365 else if (edge.equals(RectangleEdge.BOTTOM)) {
366 double h = maxdim;
367 state.cursorDown(h);
368 }
369 else if (edge == RectangleEdge.LEFT) {
370 double w = maxdim;
371 state.cursorLeft(w);
372 }
373 else if (edge == RectangleEdge.RIGHT) {
374 double w = maxdim;
375 state.cursorRight(w);
376 }
377 return state;
378 }
379
380 /**
381 * Tests the axis for equality with an arbitrary object.
382 *
383 * @param obj the object (<code>null</code> permitted).
384 *
385 * @return A boolean.
386 */
387 public boolean equals(Object obj) {
388 if (obj == this) {
389 return true;
390 }
391 if (obj instanceof SubCategoryAxis && super.equals(obj)) {
392 SubCategoryAxis axis = (SubCategoryAxis) obj;
393 if (!this.subCategories.equals(axis.subCategories)) {
394 return false;
395 }
396 if (!this.subLabelFont.equals(axis.subLabelFont)) {
397 return false;
398 }
399 if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
400 return false;
401 }
402 return true;
403 }
404 return false;
405 }
406
407 /**
408 * Provides serialization support.
409 *
410 * @param stream the output stream.
411 *
412 * @throws IOException if there is an I/O error.
413 */
414 private void writeObject(ObjectOutputStream stream) throws IOException {
415 stream.defaultWriteObject();
416 SerialUtilities.writePaint(this.subLabelPaint, stream);
417 }
418
419 /**
420 * Provides serialization support.
421 *
422 * @param stream the input stream.
423 *
424 * @throws IOException if there is an I/O error.
425 * @throws ClassNotFoundException if there is a classpath problem.
426 */
427 private void readObject(ObjectInputStream stream)
428 throws IOException, ClassNotFoundException {
429 stream.defaultReadObject();
430 this.subLabelPaint = SerialUtilities.readPaint(stream);
431 }
432
433 }