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 * CombinedDomainCategoryPlot.java
029 * -------------------------------
030 * (C) Copyright 2003-2007, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Nicolas Brodu;
034 *
035 * Changes:
036 * --------
037 * 16-May-2003 : Version 1 (DG);
038 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
039 * 19-Aug-2003 : Added equals() method, implemented Cloneable and 
040 *               Serializable (DG);
041 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
042 * 15-Sep-2003 : Implemented PublicCloneable (DG);
043 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
044 * 17-Sep-2003 : Updated handling of 'clicks' (DG);
045 * 04-May-2004 : Added getter/setter methods for 'gap' attribute (DG);
046 * 12-Nov-2004 : Implemented the Zoomable interface (DG);
047 * 25-Nov-2004 : Small update to clone() implementation (DG);
048 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
049 *               items if set (DG);
050 * 05-May-2005 : Updated draw() method parameters (DG);
051 * ------------- JFREECHART 1.0.x ---------------------------------------------
052 * 13-Sep-2006 : Updated API docs (DG);
053 * 30-Oct-2006 : Added new getCategoriesForAxis() override (DG);
054 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
055 * 14-Nov-2007 : Updated setFixedRangeAxisSpaceForSubplots() method (DG);
056 */
057
058package org.jfree.chart.plot;
059
060import java.awt.Graphics2D;
061import java.awt.geom.Point2D;
062import java.awt.geom.Rectangle2D;
063import java.io.Serializable;
064import java.util.Collections;
065import java.util.Iterator;
066import java.util.List;
067
068import org.jfree.chart.LegendItemCollection;
069import org.jfree.chart.axis.AxisSpace;
070import org.jfree.chart.axis.AxisState;
071import org.jfree.chart.axis.CategoryAxis;
072import org.jfree.chart.event.PlotChangeEvent;
073import org.jfree.chart.event.PlotChangeListener;
074import org.jfree.ui.RectangleEdge;
075import org.jfree.ui.RectangleInsets;
076import org.jfree.util.ObjectUtilities;
077import org.jfree.util.PublicCloneable;
078
079/**
080 * A combined category plot where the domain axis is shared.
081 */
082public class CombinedDomainCategoryPlot extends CategoryPlot
083                                        implements Zoomable,
084                                                   Cloneable, PublicCloneable, 
085                                                   Serializable,
086                                                   PlotChangeListener {
087
088    /** For serialization. */
089    private static final long serialVersionUID = 8207194522653701572L;
090    
091    /** Storage for the subplot references. */
092    private List subplots;
093
094    /** Total weight of all charts. */
095    private int totalWeight;
096
097    /** The gap between subplots. */
098    private double gap;
099
100    /** Temporary storage for the subplot areas. */
101    private transient Rectangle2D[] subplotAreas;
102    // TODO:  move the above to the plot state
103    
104    /**
105     * Default constructor.
106     */
107    public CombinedDomainCategoryPlot() {
108        this(new CategoryAxis());
109    }
110    
111    /**
112     * Creates a new plot.
113     *
114     * @param domainAxis  the shared domain axis (<code>null</code> not 
115     *                    permitted).
116     */
117    public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
118        super(null, domainAxis, null, null);
119        this.subplots = new java.util.ArrayList();
120        this.totalWeight = 0;
121        this.gap = 5.0;
122    }
123
124    /**
125     * Returns the space between subplots.
126     *
127     * @return The gap (in Java2D units).
128     */
129    public double getGap() {
130        return this.gap;
131    }
132
133    /**
134     * Sets the amount of space between subplots and sends a 
135     * {@link PlotChangeEvent} to all registered listeners.
136     *
137     * @param gap  the gap between subplots (in Java2D units).
138     */
139    public void setGap(double gap) {
140        this.gap = gap;
141        notifyListeners(new PlotChangeEvent(this));
142    }
143
144    /**
145     * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
146     * to all registered listeners.
147     * <br><br>
148     * The domain axis for the subplot will be set to <code>null</code>.  You
149     * must ensure that the subplot has a non-null range axis.
150     * 
151     * @param subplot  the subplot (<code>null</code> not permitted).
152     */
153    public void add(CategoryPlot subplot) {
154        add(subplot, 1);    
155    }
156    
157    /**
158     * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
159     * to all registered listeners.
160     * <br><br>
161     * The domain axis for the subplot will be set to <code>null</code>.  You
162     * must ensure that the subplot has a non-null range axis.
163     *
164     * @param subplot  the subplot (<code>null</code> not permitted).
165     * @param weight  the weight (must be >= 1).
166     */
167    public void add(CategoryPlot subplot, int weight) {
168        if (subplot == null) {
169            throw new IllegalArgumentException("Null 'subplot' argument.");
170        }
171        if (weight < 1) {
172            throw new IllegalArgumentException("Require weight >= 1.");
173        }
174        subplot.setParent(this);
175        subplot.setWeight(weight);
176        subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
177        subplot.setDomainAxis(null);
178        subplot.setOrientation(getOrientation());
179        subplot.addChangeListener(this);
180        this.subplots.add(subplot);
181        this.totalWeight += weight;
182        CategoryAxis axis = getDomainAxis();
183        if (axis != null) {
184            axis.configure();
185        }
186        notifyListeners(new PlotChangeEvent(this));
187    }
188
189    /**
190     * Removes a subplot from the combined chart.  Potentially, this removes 
191     * some unique categories from the overall union of the datasets...so the 
192     * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to 
193     * all registered listeners.
194     *
195     * @param subplot  the subplot (<code>null</code> not permitted).
196     */
197    public void remove(CategoryPlot subplot) {
198        if (subplot == null) {
199            throw new IllegalArgumentException("Null 'subplot' argument.");
200        }
201        int position = -1;
202        int size = this.subplots.size();
203        int i = 0;
204        while (position == -1 && i < size) {
205            if (this.subplots.get(i) == subplot) {
206                position = i;
207            }
208            i++;
209        }
210        if (position != -1) {
211            this.subplots.remove(position);
212            subplot.setParent(null);
213            subplot.removeChangeListener(this);
214            this.totalWeight -= subplot.getWeight();
215
216            CategoryAxis domain = getDomainAxis();
217            if (domain != null) {
218                domain.configure();
219            }
220            notifyListeners(new PlotChangeEvent(this));
221        }
222    }
223
224    /**
225     * Returns the list of subplots.
226     *
227     * @return An unmodifiable list of subplots .
228     */
229    public List getSubplots() {
230        return Collections.unmodifiableList(this.subplots);
231    }
232
233    /**
234     * Returns the subplot (if any) that contains the (x, y) point (specified 
235     * in Java2D space).
236     * 
237     * @param info  the chart rendering info (<code>null</code> not permitted).
238     * @param source  the source point (<code>null</code> not permitted).
239     * 
240     * @return A subplot (possibly <code>null</code>).
241     */
242    public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
243        if (info == null) {
244            throw new IllegalArgumentException("Null 'info' argument.");
245        }
246        if (source == null) {
247            throw new IllegalArgumentException("Null 'source' argument.");
248        }
249        CategoryPlot result = null;
250        int subplotIndex = info.getSubplotIndex(source);
251        if (subplotIndex >= 0) {
252            result =  (CategoryPlot) this.subplots.get(subplotIndex);
253        }
254        return result;
255    }
256    
257    /**
258     * Multiplies the range on the range axis/axes by the specified factor.
259     *
260     * @param factor  the zoom factor.
261     * @param info  the plot rendering info (<code>null</code> not permitted).
262     * @param source  the source point (<code>null</code> not permitted).
263     */
264    public void zoomRangeAxes(double factor, PlotRenderingInfo info, 
265                              Point2D source) {
266        // delegate 'info' and 'source' argument checks...
267        CategoryPlot subplot = findSubplot(info, source);
268        if (subplot != null) {
269            subplot.zoomRangeAxes(factor, info, source);
270        }
271        else {
272            // if the source point doesn't fall within a subplot, we do the
273            // zoom on all subplots...
274            Iterator iterator = getSubplots().iterator();
275            while (iterator.hasNext()) {
276                subplot = (CategoryPlot) iterator.next();
277                subplot.zoomRangeAxes(factor, info, source);
278            }
279        }
280    }
281
282    /**
283     * Zooms in on the range axes.
284     *
285     * @param lowerPercent  the lower bound.
286     * @param upperPercent  the upper bound.
287     * @param info  the plot rendering info (<code>null</code> not permitted).
288     * @param source  the source point (<code>null</code> not permitted).
289     */
290    public void zoomRangeAxes(double lowerPercent, double upperPercent, 
291                              PlotRenderingInfo info, Point2D source) {
292        // delegate 'info' and 'source' argument checks...
293        CategoryPlot subplot = findSubplot(info, source);
294        if (subplot != null) {
295            subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
296        }
297        else {
298            // if the source point doesn't fall within a subplot, we do the
299            // zoom on all subplots...
300            Iterator iterator = getSubplots().iterator();
301            while (iterator.hasNext()) {
302                subplot = (CategoryPlot) iterator.next();
303                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
304            }
305        }
306    }
307
308    /**
309     * Calculates the space required for the axes.
310     * 
311     * @param g2  the graphics device.
312     * @param plotArea  the plot area.
313     * 
314     * @return The space required for the axes.
315     */
316    protected AxisSpace calculateAxisSpace(Graphics2D g2, 
317                                           Rectangle2D plotArea) {
318        
319        AxisSpace space = new AxisSpace();
320        PlotOrientation orientation = getOrientation();
321        
322        // work out the space required by the domain axis...
323        AxisSpace fixed = getFixedDomainAxisSpace();
324        if (fixed != null) {
325            if (orientation == PlotOrientation.HORIZONTAL) {
326                space.setLeft(fixed.getLeft());
327                space.setRight(fixed.getRight());
328            }
329            else if (orientation == PlotOrientation.VERTICAL) {
330                space.setTop(fixed.getTop());
331                space.setBottom(fixed.getBottom());                
332            }
333        }
334        else {
335            CategoryAxis categoryAxis = getDomainAxis();
336            RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
337                    getDomainAxisLocation(), orientation);
338            if (categoryAxis != null) {
339                space = categoryAxis.reserveSpace(g2, this, plotArea, 
340                        categoryEdge, space);
341            }
342            else {
343                if (getDrawSharedDomainAxis()) {
344                    space = getDomainAxis().reserveSpace(g2, this, plotArea, 
345                            categoryEdge, space);
346                }
347            }
348        }
349        
350        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
351        
352        // work out the maximum height or width of the non-shared axes...
353        int n = this.subplots.size();
354        this.subplotAreas = new Rectangle2D[n];
355        double x = adjustedPlotArea.getX();
356        double y = adjustedPlotArea.getY();
357        double usableSize = 0.0;
358        if (orientation == PlotOrientation.HORIZONTAL) {
359            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
360        }
361        else if (orientation == PlotOrientation.VERTICAL) {
362            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
363        }
364
365        for (int i = 0; i < n; i++) {
366            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
367
368            // calculate sub-plot area
369            if (orientation == PlotOrientation.HORIZONTAL) {
370                double w = usableSize * plot.getWeight() / this.totalWeight;
371                this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 
372                        adjustedPlotArea.getHeight());
373                x = x + w + this.gap;
374            }
375            else if (orientation == PlotOrientation.VERTICAL) {
376                double h = usableSize * plot.getWeight() / this.totalWeight;
377                this.subplotAreas[i] = new Rectangle2D.Double(x, y, 
378                        adjustedPlotArea.getWidth(), h);
379                y = y + h + this.gap;
380            }
381
382            AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 
383                    this.subplotAreas[i], null);
384            space.ensureAtLeast(subSpace);
385
386        }
387
388        return space;
389    }
390
391    /**
392     * Draws the plot on a Java 2D graphics device (such as the screen or a 
393     * printer).  Will perform all the placement calculations for each of the
394     * sub-plots and then tell these to draw themselves.
395     *
396     * @param g2  the graphics device.
397     * @param area  the area within which the plot (including axis labels) 
398     *              should be drawn.
399     * @param anchor  the anchor point (<code>null</code> permitted).
400     * @param parentState  the state from the parent plot, if there is one.
401     * @param info  collects information about the drawing (<code>null</code> 
402     *              permitted).
403     */
404    public void draw(Graphics2D g2, 
405                     Rectangle2D area, 
406                     Point2D anchor,
407                     PlotState parentState,
408                     PlotRenderingInfo info) {
409        
410        // set up info collection...
411        if (info != null) {
412            info.setPlotArea(area);
413        }
414
415        // adjust the drawing area for plot insets (if any)...
416        RectangleInsets insets = getInsets();
417        area.setRect(area.getX() + insets.getLeft(),
418                area.getY() + insets.getTop(),
419                area.getWidth() - insets.getLeft() - insets.getRight(),
420                area.getHeight() - insets.getTop() - insets.getBottom());
421
422
423        // calculate the data area...
424        setFixedRangeAxisSpaceForSubplots(null);
425        AxisSpace space = calculateAxisSpace(g2, area);
426        Rectangle2D dataArea = space.shrink(area, null);
427
428        // set the width and height of non-shared axis of all sub-plots
429        setFixedRangeAxisSpaceForSubplots(space);
430
431        // draw the shared axis
432        CategoryAxis axis = getDomainAxis();
433        RectangleEdge domainEdge = getDomainAxisEdge();
434        double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
435        AxisState axisState = axis.draw(g2, cursor, area, dataArea, 
436                domainEdge, info);
437        if (parentState == null) {
438            parentState = new PlotState();
439        }
440        parentState.getSharedAxisStates().put(axis, axisState);
441        
442        // draw all the subplots
443        for (int i = 0; i < this.subplots.size(); i++) {
444            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
445            PlotRenderingInfo subplotInfo = null;
446            if (info != null) {
447                subplotInfo = new PlotRenderingInfo(info.getOwner());
448                info.addSubplotInfo(subplotInfo);
449            }
450            plot.draw(g2, this.subplotAreas[i], null, parentState, subplotInfo);
451        }
452
453        if (info != null) {
454            info.setDataArea(dataArea);
455        }
456
457    }
458
459    /**
460     * Sets the size (width or height, depending on the orientation of the 
461     * plot) for the range axis of each subplot.
462     *
463     * @param space  the space (<code>null</code> permitted).
464     */
465    protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
466        Iterator iterator = this.subplots.iterator();
467        while (iterator.hasNext()) {
468            CategoryPlot plot = (CategoryPlot) iterator.next();
469            plot.setFixedRangeAxisSpace(space, false);
470        }
471    }
472
473    /**
474     * Sets the orientation of the plot (and all subplots).
475     * 
476     * @param orientation  the orientation (<code>null</code> not permitted).
477     */
478    public void setOrientation(PlotOrientation orientation) {
479
480        super.setOrientation(orientation);
481
482        Iterator iterator = this.subplots.iterator();
483        while (iterator.hasNext()) {
484            CategoryPlot plot = (CategoryPlot) iterator.next();
485            plot.setOrientation(orientation);
486        }
487
488    }
489    
490    /**
491     * Returns a collection of legend items for the plot.
492     *
493     * @return The legend items.
494     */
495    public LegendItemCollection getLegendItems() {
496        LegendItemCollection result = getFixedLegendItems();
497        if (result == null) {
498            result = new LegendItemCollection();
499            if (this.subplots != null) {
500                Iterator iterator = this.subplots.iterator();
501                while (iterator.hasNext()) {
502                    CategoryPlot plot = (CategoryPlot) iterator.next();
503                    LegendItemCollection more = plot.getLegendItems();
504                    result.addAll(more);
505                }
506            }
507        }
508        return result;
509    }
510    
511    /**
512     * Returns an unmodifiable list of the categories contained in all the 
513     * subplots.
514     * 
515     * @return The list.
516     */
517    public List getCategories() {
518        List result = new java.util.ArrayList();
519        if (this.subplots != null) {
520            Iterator iterator = this.subplots.iterator();
521            while (iterator.hasNext()) {
522                CategoryPlot plot = (CategoryPlot) iterator.next();
523                List more = plot.getCategories();
524                Iterator moreIterator = more.iterator();
525                while (moreIterator.hasNext()) {
526                    Comparable category = (Comparable) moreIterator.next();
527                    if (!result.contains(category)) {
528                        result.add(category);
529                    }
530                }
531            }
532        }
533        return Collections.unmodifiableList(result);
534    }
535    
536    /**
537     * Overridden to return the categories in the subplots.
538     * 
539     * @param axis  ignored.
540     * 
541     * @return A list of the categories in the subplots.
542     * 
543     * @since 1.0.3
544     */
545    public List getCategoriesForAxis(CategoryAxis axis) {
546        // FIXME:  this code means that it is not possible to use more than
547        // one domain axis for the combined plots...
548        return getCategories();    
549    }
550    
551    /**
552     * Handles a 'click' on the plot.
553     *
554     * @param x  x-coordinate of the click.
555     * @param y  y-coordinate of the click.
556     * @param info  information about the plot's dimensions.
557     *
558     */
559    public void handleClick(int x, int y, PlotRenderingInfo info) {
560
561        Rectangle2D dataArea = info.getDataArea();
562        if (dataArea.contains(x, y)) {
563            for (int i = 0; i < this.subplots.size(); i++) {
564                CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
565                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
566                subplot.handleClick(x, y, subplotInfo);
567            }
568        }
569
570    }
571    
572    /**
573     * Receives a {@link PlotChangeEvent} and responds by notifying all 
574     * listeners.
575     * 
576     * @param event  the event.
577     */
578    public void plotChanged(PlotChangeEvent event) {
579        notifyListeners(event);
580    }
581
582    /** 
583     * Tests the plot for equality with an arbitrary object.
584     * 
585     * @param obj  the object (<code>null</code> permitted).
586     * 
587     * @return A boolean.
588     */
589    public boolean equals(Object obj) {
590        if (obj == this) {
591            return true;
592        }
593        if (!(obj instanceof CombinedDomainCategoryPlot)) {
594            return false;
595        }
596        if (!super.equals(obj)) {
597            return false;
598        }
599        CombinedDomainCategoryPlot plot = (CombinedDomainCategoryPlot) obj;
600        if (!ObjectUtilities.equal(this.subplots, plot.subplots)) {
601            return false;
602        }
603        if (this.totalWeight != plot.totalWeight) {
604            return false;
605        }
606        if (this.gap != plot.gap) { 
607            return false;
608        }
609        return true;
610    }
611
612    /**
613     * Returns a clone of the plot.
614     * 
615     * @return A clone.
616     * 
617     * @throws CloneNotSupportedException  this class will not throw this 
618     *         exception, but subclasses (if any) might.
619     */
620    public Object clone() throws CloneNotSupportedException {
621        
622        CombinedDomainCategoryPlot result 
623            = (CombinedDomainCategoryPlot) super.clone(); 
624        result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
625        for (Iterator it = result.subplots.iterator(); it.hasNext();) {
626            Plot child = (Plot) it.next();
627            child.setParent(result);
628        }
629        return result;
630        
631    }
632    
633}