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