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
051package org.jfree.chart.axis;
052
053import java.awt.Color;
054import java.awt.Font;
055import java.awt.FontMetrics;
056import java.awt.Graphics2D;
057import java.awt.Paint;
058import java.awt.geom.Rectangle2D;
059import java.io.IOException;
060import java.io.ObjectInputStream;
061import java.io.ObjectOutputStream;
062import java.io.Serializable;
063import java.util.Iterator;
064import java.util.List;
065
066import org.jfree.chart.event.AxisChangeEvent;
067import org.jfree.chart.plot.CategoryPlot;
068import org.jfree.chart.plot.Plot;
069import org.jfree.chart.plot.PlotRenderingInfo;
070import org.jfree.data.category.CategoryDataset;
071import org.jfree.io.SerialUtilities;
072import org.jfree.text.TextUtilities;
073import org.jfree.ui.RectangleEdge;
074import org.jfree.ui.TextAnchor;
075
076/**
077 * A specialised category axis that can display sub-categories.
078 */
079public 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}