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 * Changes
036 * -------
037 * 12-May-2004 : Version 1 (DG);
038 * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities
039 * --> TextUtilities (DG);
040 * 26-Apr-2005 : Removed logger (DG);
041 * ------------- JFREECHART 1.0.x ---------------------------------------------
042 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
043 * Joubert (1277726) (DG);
044 * 30-May-2007 : Added argument check and event notification to
045 * addSubCategory() (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 and sends an {@link AxisChangeEvent} to
104 * all registered listeners.
105 *
106 * @param subCategory the sub-category (<code>null</code> not permitted).
107 */
108 public void addSubCategory(Comparable subCategory) {
109 if (subCategory == null) {
110 throw new IllegalArgumentException("Null 'subcategory' axis.");
111 }
112 this.subCategories.add(subCategory);
113 notifyListeners(new AxisChangeEvent(this));
114 }
115
116 /**
117 * Returns the font used to display the sub-category labels.
118 *
119 * @return The font (never <code>null</code>).
120 *
121 * @see #setSubLabelFont(Font)
122 */
123 public Font getSubLabelFont() {
124 return this.subLabelFont;
125 }
126
127 /**
128 * Sets the font used to display the sub-category labels and sends an
129 * {@link AxisChangeEvent} to all registered listeners.
130 *
131 * @param font the font (<code>null</code> not permitted).
132 *
133 * @see #getSubLabelFont()
134 */
135 public void setSubLabelFont(Font font) {
136 if (font == null) {
137 throw new IllegalArgumentException("Null 'font' argument.");
138 }
139 this.subLabelFont = font;
140 notifyListeners(new AxisChangeEvent(this));
141 }
142
143 /**
144 * Returns the paint used to display the sub-category labels.
145 *
146 * @return The paint (never <code>null</code>).
147 *
148 * @see #setSubLabelPaint(Paint)
149 */
150 public Paint getSubLabelPaint() {
151 return this.subLabelPaint;
152 }
153
154 /**
155 * Sets the paint used to display the sub-category labels and sends an
156 * {@link AxisChangeEvent} to all registered listeners.
157 *
158 * @param paint the paint (<code>null</code> not permitted).
159 *
160 * @see #getSubLabelPaint()
161 */
162 public void setSubLabelPaint(Paint paint) {
163 if (paint == null) {
164 throw new IllegalArgumentException("Null 'paint' argument.");
165 }
166 this.subLabelPaint = paint;
167 notifyListeners(new AxisChangeEvent(this));
168 }
169
170 /**
171 * Estimates the space required for the axis, given a specific drawing area.
172 *
173 * @param g2 the graphics device (used to obtain font information).
174 * @param plot the plot that the axis belongs to.
175 * @param plotArea the area within which the axis should be drawn.
176 * @param edge the axis location (top or bottom).
177 * @param space the space already reserved.
178 *
179 * @return The space required to draw the axis.
180 */
181 public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
182 Rectangle2D plotArea,
183 RectangleEdge edge, AxisSpace space) {
184
185 // create a new space object if one wasn't supplied...
186 if (space == null) {
187 space = new AxisSpace();
188 }
189
190 // if the axis is not visible, no additional space is required...
191 if (!isVisible()) {
192 return space;
193 }
194
195 space = super.reserveSpace(g2, plot, plotArea, edge, space);
196 double maxdim = getMaxDim(g2, edge);
197 if (RectangleEdge.isTopOrBottom(edge)) {
198 space.add(maxdim, edge);
199 }
200 else if (RectangleEdge.isLeftOrRight(edge)) {
201 space.add(maxdim, edge);
202 }
203 return space;
204 }
205
206 /**
207 * Returns the maximum of the relevant dimension (height or width) of the
208 * subcategory labels.
209 *
210 * @param g2 the graphics device.
211 * @param edge the edge.
212 *
213 * @return The maximum dimension.
214 */
215 private double getMaxDim(Graphics2D g2, RectangleEdge edge) {
216 double result = 0.0;
217 g2.setFont(this.subLabelFont);
218 FontMetrics fm = g2.getFontMetrics();
219 Iterator iterator = this.subCategories.iterator();
220 while (iterator.hasNext()) {
221 Comparable subcategory = (Comparable) iterator.next();
222 String label = subcategory.toString();
223 Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm);
224 double dim = 0.0;
225 if (RectangleEdge.isLeftOrRight(edge)) {
226 dim = bounds.getWidth();
227 }
228 else { // must be top or bottom
229 dim = bounds.getHeight();
230 }
231 result = Math.max(result, dim);
232 }
233 return result;
234 }
235
236 /**
237 * Draws the axis on a Java 2D graphics device (such as the screen or a
238 * printer).
239 *
240 * @param g2 the graphics device (<code>null</code> not permitted).
241 * @param cursor the cursor location.
242 * @param plotArea the area within which the axis should be drawn
243 * (<code>null</code> not permitted).
244 * @param dataArea the area within which the plot is being drawn
245 * (<code>null</code> not permitted).
246 * @param edge the location of the axis (<code>null</code> not permitted).
247 * @param plotState collects information about the plot
248 * (<code>null</code> permitted).
249 *
250 * @return The axis state (never <code>null</code>).
251 */
252 public AxisState draw(Graphics2D g2,
253 double cursor,
254 Rectangle2D plotArea,
255 Rectangle2D dataArea,
256 RectangleEdge edge,
257 PlotRenderingInfo plotState) {
258
259 // if the axis is not visible, don't draw it...
260 if (!isVisible()) {
261 return new AxisState(cursor);
262 }
263
264 if (isAxisLineVisible()) {
265 drawAxisLine(g2, cursor, dataArea, edge);
266 }
267
268 // draw the category labels and axis label
269 AxisState state = new AxisState(cursor);
270 state = drawSubCategoryLabels(
271 g2, plotArea, dataArea, edge, state, plotState
272 );
273 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
274 plotState);
275 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
276
277 return state;
278
279 }
280
281 /**
282 * Draws the category labels and returns the updated axis state.
283 *
284 * @param g2 the graphics device (<code>null</code> not permitted).
285 * @param plotArea the plot area (<code>null</code> not permitted).
286 * @param dataArea the area inside the axes (<code>null</code> not
287 * permitted).
288 * @param edge the axis location (<code>null</code> not permitted).
289 * @param state the axis state (<code>null</code> not permitted).
290 * @param plotState collects information about the plot (<code>null</code>
291 * permitted).
292 *
293 * @return The updated axis state (never <code>null</code>).
294 */
295 protected AxisState drawSubCategoryLabels(Graphics2D g2,
296 Rectangle2D plotArea,
297 Rectangle2D dataArea,
298 RectangleEdge edge,
299 AxisState state,
300 PlotRenderingInfo plotState) {
301
302 if (state == null) {
303 throw new IllegalArgumentException("Null 'state' argument.");
304 }
305
306 g2.setFont(this.subLabelFont);
307 g2.setPaint(this.subLabelPaint);
308 CategoryPlot plot = (CategoryPlot) getPlot();
309 CategoryDataset dataset = plot.getDataset();
310 int categoryCount = dataset.getColumnCount();
311
312 double maxdim = getMaxDim(g2, edge);
313 for (int categoryIndex = 0; categoryIndex < categoryCount;
314 categoryIndex++) {
315
316 double x0 = 0.0;
317 double x1 = 0.0;
318 double y0 = 0.0;
319 double y1 = 0.0;
320 if (edge == RectangleEdge.TOP) {
321 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
322 edge);
323 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
324 edge);
325 y1 = state.getCursor();
326 y0 = y1 - maxdim;
327 }
328 else if (edge == RectangleEdge.BOTTOM) {
329 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
330 edge);
331 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
332 edge);
333 y0 = state.getCursor();
334 y1 = y0 + maxdim;
335 }
336 else if (edge == RectangleEdge.LEFT) {
337 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
338 edge);
339 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
340 edge);
341 x1 = state.getCursor();
342 x0 = x1 - maxdim;
343 }
344 else if (edge == RectangleEdge.RIGHT) {
345 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
346 edge);
347 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
348 edge);
349 x0 = state.getCursor();
350 x1 = x0 + maxdim;
351 }
352 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
353 (y1 - y0));
354 int subCategoryCount = this.subCategories.size();
355 float width = (float) ((x1 - x0) / subCategoryCount);
356 float height = (float) ((y1 - y0) / subCategoryCount);
357 float xx = 0.0f;
358 float yy = 0.0f;
359 for (int i = 0; i < subCategoryCount; i++) {
360 if (RectangleEdge.isTopOrBottom(edge)) {
361 xx = (float) (x0 + (i + 0.5) * width);
362 yy = (float) area.getCenterY();
363 }
364 else {
365 xx = (float) area.getCenterX();
366 yy = (float) (y0 + (i + 0.5) * height);
367 }
368 String label = this.subCategories.get(i).toString();
369 TextUtilities.drawRotatedString(label, g2, xx, yy,
370 TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
371 }
372 }
373
374 if (edge.equals(RectangleEdge.TOP)) {
375 double h = maxdim;
376 state.cursorUp(h);
377 }
378 else if (edge.equals(RectangleEdge.BOTTOM)) {
379 double h = maxdim;
380 state.cursorDown(h);
381 }
382 else if (edge == RectangleEdge.LEFT) {
383 double w = maxdim;
384 state.cursorLeft(w);
385 }
386 else if (edge == RectangleEdge.RIGHT) {
387 double w = maxdim;
388 state.cursorRight(w);
389 }
390 return state;
391 }
392
393 /**
394 * Tests the axis for equality with an arbitrary object.
395 *
396 * @param obj the object (<code>null</code> permitted).
397 *
398 * @return A boolean.
399 */
400 public boolean equals(Object obj) {
401 if (obj == this) {
402 return true;
403 }
404 if (obj instanceof SubCategoryAxis && super.equals(obj)) {
405 SubCategoryAxis axis = (SubCategoryAxis) obj;
406 if (!this.subCategories.equals(axis.subCategories)) {
407 return false;
408 }
409 if (!this.subLabelFont.equals(axis.subLabelFont)) {
410 return false;
411 }
412 if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
413 return false;
414 }
415 return true;
416 }
417 return false;
418 }
419
420 /**
421 * Provides serialization support.
422 *
423 * @param stream the output stream.
424 *
425 * @throws IOException if there is an I/O error.
426 */
427 private void writeObject(ObjectOutputStream stream) throws IOException {
428 stream.defaultWriteObject();
429 SerialUtilities.writePaint(this.subLabelPaint, stream);
430 }
431
432 /**
433 * Provides serialization support.
434 *
435 * @param stream the input stream.
436 *
437 * @throws IOException if there is an I/O error.
438 * @throws ClassNotFoundException if there is a classpath problem.
439 */
440 private void readObject(ObjectInputStream stream)
441 throws IOException, ClassNotFoundException {
442 stream.defaultReadObject();
443 this.subLabelPaint = SerialUtilities.readPaint(stream);
444 }
445
446 }