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