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