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