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