1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
6  *
7  * Project Info:  http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
22  * USA.
23  *
24  * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
25  * Other names may be trademarks of their respective owners.]
26  *
27  * --------------------
28  * SubCategoryAxis.java
29  * --------------------
30  * (C) Copyright 2004-2013, by Object Refinery Limited.
31  *
32  * Original Author:  David Gilbert;
33  * Contributor(s):   Adriaan Joubert;
34  *
35  * Changes
36  * -------
37  * 12-May-2004 : Version 1 (DG);
38  * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities
39  *               --> TextUtilities (DG);
40  * 26-Apr-2005 : Removed logger (DG);
41  * ------------- JFREECHART 1.0.x ---------------------------------------------
42  * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
43  *               Joubert (1277726) (DG);
44  * 30-May-2007 : Added argument check and event notification to
45  *               addSubCategory() (DG);
46  * 13-Nov-2008 : Fix NullPointerException when dataset is null - see bug
47  *               report 2275695 (DG);
48  * 02-Jul-2013 : Use ParamChecks (DG);
49  * 01-Aug-2013 : Added attributedLabel override to support superscripts,
50  *               subscripts and more (DG);
51  */
52 
53 package org.jfree.chart.axis;
54 
55 import java.awt.Color;
56 import java.awt.Font;
57 import java.awt.FontMetrics;
58 import java.awt.Graphics2D;
59 import java.awt.Paint;
60 import java.awt.geom.Rectangle2D;
61 import java.io.IOException;
62 import java.io.ObjectInputStream;
63 import java.io.ObjectOutputStream;
64 import java.io.Serializable;
65 import java.util.Iterator;
66 import java.util.List;
67 
68 import org.jfree.chart.event.AxisChangeEvent;
69 import org.jfree.chart.plot.CategoryPlot;
70 import org.jfree.chart.plot.Plot;
71 import org.jfree.chart.plot.PlotRenderingInfo;
72 import org.jfree.chart.util.ParamChecks;
73 import org.jfree.data.category.CategoryDataset;
74 import org.jfree.io.SerialUtilities;
75 import org.jfree.text.TextUtilities;
76 import org.jfree.ui.RectangleEdge;
77 import org.jfree.ui.TextAnchor;
78 
79 /**
80  * A specialised category axis that can display sub-categories.
81  */
82 public class SubCategoryAxis extends CategoryAxis
83         implements Cloneable, Serializable {
84 
85     /** For serialization. */
86     private static final long serialVersionUID = -1279463299793228344L;
87 
88     /** Storage for the sub-categories (these need to be set manually). */
89     private List subCategories;
90 
91     /** The font for the sub-category labels. */
92     private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10);
93 
94     /** The paint for the sub-category labels. */
95     private transient Paint subLabelPaint = Color.black;
96 
97     /**
98      * Creates a new axis.
99      *
100      * @param label  the axis label.
101      */
SubCategoryAxis(String label)102     public SubCategoryAxis(String label) {
103         super(label);
104         this.subCategories = new java.util.ArrayList();
105     }
106 
107     /**
108      * Adds a sub-category to the axis and sends an {@link AxisChangeEvent} to
109      * all registered listeners.
110      *
111      * @param subCategory  the sub-category (<code>null</code> not permitted).
112      */
addSubCategory(Comparable subCategory)113     public void addSubCategory(Comparable subCategory) {
114         ParamChecks.nullNotPermitted(subCategory, "subCategory");
115         this.subCategories.add(subCategory);
116         notifyListeners(new AxisChangeEvent(this));
117     }
118 
119     /**
120      * Returns the font used to display the sub-category labels.
121      *
122      * @return The font (never <code>null</code>).
123      *
124      * @see #setSubLabelFont(Font)
125      */
getSubLabelFont()126     public Font getSubLabelFont() {
127         return this.subLabelFont;
128     }
129 
130     /**
131      * Sets the font used to display the sub-category labels and sends an
132      * {@link AxisChangeEvent} to all registered listeners.
133      *
134      * @param font  the font (<code>null</code> not permitted).
135      *
136      * @see #getSubLabelFont()
137      */
setSubLabelFont(Font font)138     public void setSubLabelFont(Font font) {
139         ParamChecks.nullNotPermitted(font, "font");
140         this.subLabelFont = font;
141         notifyListeners(new AxisChangeEvent(this));
142     }
143 
144     /**
145      * Returns the paint used to display the sub-category labels.
146      *
147      * @return The paint (never <code>null</code>).
148      *
149      * @see #setSubLabelPaint(Paint)
150      */
getSubLabelPaint()151     public Paint getSubLabelPaint() {
152         return this.subLabelPaint;
153     }
154 
155     /**
156      * Sets the paint used to display the sub-category labels and sends an
157      * {@link AxisChangeEvent} to all registered listeners.
158      *
159      * @param paint  the paint (<code>null</code> not permitted).
160      *
161      * @see #getSubLabelPaint()
162      */
setSubLabelPaint(Paint paint)163     public void setSubLabelPaint(Paint paint) {
164         ParamChecks.nullNotPermitted(paint, "paint");
165         this.subLabelPaint = paint;
166         notifyListeners(new AxisChangeEvent(this));
167     }
168 
169     /**
170      * Estimates the space required for the axis, given a specific drawing area.
171      *
172      * @param g2  the graphics device (used to obtain font information).
173      * @param plot  the plot that the axis belongs to.
174      * @param plotArea  the area within which the axis should be drawn.
175      * @param edge  the axis location (top or bottom).
176      * @param space  the space already reserved.
177      *
178      * @return The space required to draw the axis.
179      */
180     @Override
reserveSpace(Graphics2D g2, Plot plot, Rectangle2D plotArea, RectangleEdge edge, AxisSpace space)181     public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
182             Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
183 
184         // create a new space object if one wasn't supplied...
185         if (space == null) {
186             space = new AxisSpace();
187         }
188 
189         // if the axis is not visible, no additional space is required...
190         if (!isVisible()) {
191             return space;
192         }
193 
194         space = super.reserveSpace(g2, plot, plotArea, edge, space);
195         double maxdim = getMaxDim(g2, edge);
196         if (RectangleEdge.isTopOrBottom(edge)) {
197             space.add(maxdim, edge);
198         }
199         else if (RectangleEdge.isLeftOrRight(edge)) {
200             space.add(maxdim, edge);
201         }
202         return space;
203     }
204 
205     /**
206      * Returns the maximum of the relevant dimension (height or width) of the
207      * subcategory labels.
208      *
209      * @param g2  the graphics device.
210      * @param edge  the edge.
211      *
212      * @return The maximum dimension.
213      */
getMaxDim(Graphics2D g2, RectangleEdge edge)214     private double getMaxDim(Graphics2D g2, RectangleEdge edge) {
215         double result = 0.0;
216         g2.setFont(this.subLabelFont);
217         FontMetrics fm = g2.getFontMetrics();
218         Iterator iterator = this.subCategories.iterator();
219         while (iterator.hasNext()) {
220             Comparable subcategory = (Comparable) iterator.next();
221             String label = subcategory.toString();
222             Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm);
223             double dim;
224             if (RectangleEdge.isLeftOrRight(edge)) {
225                 dim = bounds.getWidth();
226             }
227             else {  // must be top or bottom
228                 dim = bounds.getHeight();
229             }
230             result = Math.max(result, dim);
231         }
232         return result;
233     }
234 
235     /**
236      * Draws the axis on a Java 2D graphics device (such as the screen or a
237      * printer).
238      *
239      * @param g2  the graphics device (<code>null</code> not permitted).
240      * @param cursor  the cursor location.
241      * @param plotArea  the area within which the axis should be drawn
242      *                  (<code>null</code> not permitted).
243      * @param dataArea  the area within which the plot is being drawn
244      *                  (<code>null</code> not permitted).
245      * @param edge  the location of the axis (<code>null</code> not permitted).
246      * @param plotState  collects information about the plot
247      *                   (<code>null</code> permitted).
248      *
249      * @return The axis state (never <code>null</code>).
250      */
251     @Override
draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState)252     public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
253             Rectangle2D dataArea, RectangleEdge edge,
254             PlotRenderingInfo plotState) {
255 
256         // if the axis is not visible, don't draw it...
257         if (!isVisible()) {
258             return new AxisState(cursor);
259         }
260 
261         if (isAxisLineVisible()) {
262             drawAxisLine(g2, cursor, dataArea, edge);
263         }
264 
265         // draw the category labels and axis label
266         AxisState state = new AxisState(cursor);
267         state = drawSubCategoryLabels(g2, plotArea, dataArea, edge, state,
268                 plotState);
269         state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
270                 plotState);
271         if (getAttributedLabel() != null) {
272             state = drawAttributedLabel(getAttributedLabel(), g2, plotArea,
273                     dataArea, edge, state);
274         } else {
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      */
drawSubCategoryLabels(Graphics2D g2, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, AxisState state, PlotRenderingInfo plotState)295     protected AxisState drawSubCategoryLabels(Graphics2D g2,
296             Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
297             AxisState state, PlotRenderingInfo plotState) {
298 
299         ParamChecks.nullNotPermitted(state, "state");
300 
301         g2.setFont(this.subLabelFont);
302         g2.setPaint(this.subLabelPaint);
303         CategoryPlot plot = (CategoryPlot) getPlot();
304         int categoryCount = 0;
305         CategoryDataset dataset = plot.getDataset();
306         if (dataset != null) {
307             categoryCount = dataset.getColumnCount();
308         }
309 
310         double maxdim = getMaxDim(g2, edge);
311         for (int categoryIndex = 0; categoryIndex < categoryCount;
312              categoryIndex++) {
313 
314             double x0 = 0.0;
315             double x1 = 0.0;
316             double y0 = 0.0;
317             double y1 = 0.0;
318             if (edge == RectangleEdge.TOP) {
319                 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
320                         edge);
321                 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
322                         edge);
323                 y1 = state.getCursor();
324                 y0 = y1 - maxdim;
325             }
326             else if (edge == RectangleEdge.BOTTOM) {
327                 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
328                         edge);
329                 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
330                         edge);
331                 y0 = state.getCursor();
332                 y1 = y0 + maxdim;
333             }
334             else if (edge == RectangleEdge.LEFT) {
335                 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
336                         edge);
337                 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
338                         edge);
339                 x1 = state.getCursor();
340                 x0 = x1 - maxdim;
341             }
342             else if (edge == RectangleEdge.RIGHT) {
343                 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
344                         edge);
345                 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
346                         edge);
347                 x0 = state.getCursor();
348                 x1 = x0 + maxdim;
349             }
350             Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
351                     (y1 - y0));
352             int subCategoryCount = this.subCategories.size();
353             float width = (float) ((x1 - x0) / subCategoryCount);
354             float height = (float) ((y1 - y0) / subCategoryCount);
355             float xx, yy;
356             for (int i = 0; i < subCategoryCount; i++) {
357                 if (RectangleEdge.isTopOrBottom(edge)) {
358                     xx = (float) (x0 + (i + 0.5) * width);
359                     yy = (float) area.getCenterY();
360                 }
361                 else {
362                     xx = (float) area.getCenterX();
363                     yy = (float) (y0 + (i + 0.5) * height);
364                 }
365                 String label = this.subCategories.get(i).toString();
366                 TextUtilities.drawRotatedString(label, g2, xx, yy,
367                         TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
368             }
369         }
370 
371         if (edge.equals(RectangleEdge.TOP)) {
372             double h = maxdim;
373             state.cursorUp(h);
374         }
375         else if (edge.equals(RectangleEdge.BOTTOM)) {
376             double h = maxdim;
377             state.cursorDown(h);
378         }
379         else if (edge == RectangleEdge.LEFT) {
380             double w = maxdim;
381             state.cursorLeft(w);
382         }
383         else if (edge == RectangleEdge.RIGHT) {
384             double w = maxdim;
385             state.cursorRight(w);
386         }
387         return state;
388     }
389 
390     /**
391      * Tests the axis for equality with an arbitrary object.
392      *
393      * @param obj  the object (<code>null</code> permitted).
394      *
395      * @return A boolean.
396      */
397     @Override
equals(Object obj)398     public boolean equals(Object obj) {
399         if (obj == this) {
400             return true;
401         }
402         if (obj instanceof SubCategoryAxis && super.equals(obj)) {
403             SubCategoryAxis axis = (SubCategoryAxis) obj;
404             if (!this.subCategories.equals(axis.subCategories)) {
405                 return false;
406             }
407             if (!this.subLabelFont.equals(axis.subLabelFont)) {
408                 return false;
409             }
410             if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
411                 return false;
412             }
413             return true;
414         }
415         return false;
416     }
417 
418     /**
419      * Returns a hashcode for this instance.
420      *
421      * @return A hashcode for this instance.
422      */
423     @Override
hashCode()424     public int hashCode() {
425         return super.hashCode();
426     }
427 
428     /**
429      * Provides serialization support.
430      *
431      * @param stream  the output stream.
432      *
433      * @throws IOException  if there is an I/O error.
434      */
writeObject(ObjectOutputStream stream)435     private void writeObject(ObjectOutputStream stream) throws IOException {
436         stream.defaultWriteObject();
437         SerialUtilities.writePaint(this.subLabelPaint, stream);
438     }
439 
440     /**
441      * Provides serialization support.
442      *
443      * @param stream  the input stream.
444      *
445      * @throws IOException  if there is an I/O error.
446      * @throws ClassNotFoundException  if there is a classpath problem.
447      */
readObject(ObjectInputStream stream)448     private void readObject(ObjectInputStream stream)
449         throws IOException, ClassNotFoundException {
450         stream.defaultReadObject();
451         this.subLabelPaint = SerialUtilities.readPaint(stream);
452     }
453 
454 }
455