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 * SubCategoryAxis.java
029 * --------------------
030 * (C) Copyright 2004-2008, 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 * 13-Nov-2008 : Fix NullPointerException when dataset is null - see bug
047 * report 2275695 (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(g2, plotArea, dataArea, edge, state,
273 plotState);
274 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
275 plotState);
276 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
277
278 return state;
279
280 }
281
282 /**
283 * Draws the category labels and returns the updated axis state.
284 *
285 * @param g2 the graphics device (<code>null</code> not permitted).
286 * @param plotArea the plot area (<code>null</code> not permitted).
287 * @param dataArea the area inside the axes (<code>null</code> not
288 * permitted).
289 * @param edge the axis location (<code>null</code> not permitted).
290 * @param state the axis state (<code>null</code> not permitted).
291 * @param plotState collects information about the plot (<code>null</code>
292 * permitted).
293 *
294 * @return The updated axis state (never <code>null</code>).
295 */
296 protected AxisState drawSubCategoryLabels(Graphics2D g2,
297 Rectangle2D plotArea,
298 Rectangle2D dataArea,
299 RectangleEdge edge,
300 AxisState state,
301 PlotRenderingInfo plotState) {
302
303 if (state == null) {
304 throw new IllegalArgumentException("Null 'state' argument.");
305 }
306
307 g2.setFont(this.subLabelFont);
308 g2.setPaint(this.subLabelPaint);
309 CategoryPlot plot = (CategoryPlot) getPlot();
310 int categoryCount = 0;
311 CategoryDataset dataset = plot.getDataset();
312 if (dataset != null) {
313 categoryCount = dataset.getColumnCount();
314 }
315
316 double maxdim = getMaxDim(g2, edge);
317 for (int categoryIndex = 0; categoryIndex < categoryCount;
318 categoryIndex++) {
319
320 double x0 = 0.0;
321 double x1 = 0.0;
322 double y0 = 0.0;
323 double y1 = 0.0;
324 if (edge == RectangleEdge.TOP) {
325 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
326 edge);
327 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
328 edge);
329 y1 = state.getCursor();
330 y0 = y1 - maxdim;
331 }
332 else if (edge == RectangleEdge.BOTTOM) {
333 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
334 edge);
335 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
336 edge);
337 y0 = state.getCursor();
338 y1 = y0 + maxdim;
339 }
340 else if (edge == RectangleEdge.LEFT) {
341 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
342 edge);
343 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
344 edge);
345 x1 = state.getCursor();
346 x0 = x1 - maxdim;
347 }
348 else if (edge == RectangleEdge.RIGHT) {
349 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
350 edge);
351 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
352 edge);
353 x0 = state.getCursor();
354 x1 = x0 + maxdim;
355 }
356 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
357 (y1 - y0));
358 int subCategoryCount = this.subCategories.size();
359 float width = (float) ((x1 - x0) / subCategoryCount);
360 float height = (float) ((y1 - y0) / subCategoryCount);
361 float xx = 0.0f;
362 float yy = 0.0f;
363 for (int i = 0; i < subCategoryCount; i++) {
364 if (RectangleEdge.isTopOrBottom(edge)) {
365 xx = (float) (x0 + (i + 0.5) * width);
366 yy = (float) area.getCenterY();
367 }
368 else {
369 xx = (float) area.getCenterX();
370 yy = (float) (y0 + (i + 0.5) * height);
371 }
372 String label = this.subCategories.get(i).toString();
373 TextUtilities.drawRotatedString(label, g2, xx, yy,
374 TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
375 }
376 }
377
378 if (edge.equals(RectangleEdge.TOP)) {
379 double h = maxdim;
380 state.cursorUp(h);
381 }
382 else if (edge.equals(RectangleEdge.BOTTOM)) {
383 double h = maxdim;
384 state.cursorDown(h);
385 }
386 else if (edge == RectangleEdge.LEFT) {
387 double w = maxdim;
388 state.cursorLeft(w);
389 }
390 else if (edge == RectangleEdge.RIGHT) {
391 double w = maxdim;
392 state.cursorRight(w);
393 }
394 return state;
395 }
396
397 /**
398 * Tests the axis for equality with an arbitrary object.
399 *
400 * @param obj the object (<code>null</code> permitted).
401 *
402 * @return A boolean.
403 */
404 public boolean equals(Object obj) {
405 if (obj == this) {
406 return true;
407 }
408 if (obj instanceof SubCategoryAxis && super.equals(obj)) {
409 SubCategoryAxis axis = (SubCategoryAxis) obj;
410 if (!this.subCategories.equals(axis.subCategories)) {
411 return false;
412 }
413 if (!this.subLabelFont.equals(axis.subLabelFont)) {
414 return false;
415 }
416 if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
417 return false;
418 }
419 return true;
420 }
421 return false;
422 }
423
424 /**
425 * Provides serialization support.
426 *
427 * @param stream the output stream.
428 *
429 * @throws IOException if there is an I/O error.
430 */
431 private void writeObject(ObjectOutputStream stream) throws IOException {
432 stream.defaultWriteObject();
433 SerialUtilities.writePaint(this.subLabelPaint, stream);
434 }
435
436 /**
437 * Provides serialization support.
438 *
439 * @param stream the input stream.
440 *
441 * @throws IOException if there is an I/O error.
442 * @throws ClassNotFoundException if there is a classpath problem.
443 */
444 private void readObject(ObjectInputStream stream)
445 throws IOException, ClassNotFoundException {
446 stream.defaultReadObject();
447 this.subLabelPaint = SerialUtilities.readPaint(stream);
448 }
449
450 }