    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * [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-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}