001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, 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 * CombinedDomainCategoryPlot.java
029 * -------------------------------
030 * (C) Copyright 2003-present, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Nicolas Brodu;
034 *                   Tracy Hiltbrand (equals/hashCode comply with EqualsVerifier);
035 *
036 */
037
038package org.jfree.chart.plot;
039
040import java.awt.Graphics2D;
041import java.awt.geom.Point2D;
042import java.awt.geom.Rectangle2D;
043import java.util.Collections;
044import java.util.Iterator;
045import java.util.List;
046import java.util.Objects;
047
048import org.jfree.chart.LegendItemCollection;
049import org.jfree.chart.axis.AxisSpace;
050import org.jfree.chart.axis.AxisState;
051import org.jfree.chart.axis.CategoryAxis;
052import org.jfree.chart.axis.ValueAxis;
053import org.jfree.chart.event.PlotChangeEvent;
054import org.jfree.chart.event.PlotChangeListener;
055import org.jfree.chart.ui.RectangleEdge;
056import org.jfree.chart.ui.RectangleInsets;
057import org.jfree.chart.util.ObjectUtils;
058import org.jfree.chart.util.Args;
059import org.jfree.chart.util.ShadowGenerator;
060import org.jfree.data.Range;
061
062/**
063 * A combined category plot where the domain axis is shared.
064 */
065public class CombinedDomainCategoryPlot extends CategoryPlot
066        implements PlotChangeListener {
067
068    /** For serialization. */
069    private static final long serialVersionUID = 8207194522653701572L;
070
071    /** Storage for the subplot references. */
072    private List subplots;
073
074    /** The gap between subplots. */
075    private double gap;
076
077    /** Temporary storage for the subplot areas. */
078    private transient Rectangle2D[] subplotAreas;
079    // TODO:  move the above to the plot state
080
081    /**
082     * Default constructor.
083     */
084    public CombinedDomainCategoryPlot() {
085        this(new CategoryAxis());
086    }
087
088    /**
089     * Creates a new plot.
090     *
091     * @param domainAxis  the shared domain axis ({@code null} not
092     *                    permitted).
093     */
094    public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
095        super(null, domainAxis, null, null);
096        this.subplots = new java.util.ArrayList();
097        this.gap = 5.0;
098    }
099
100    /**
101     * Returns the space between subplots.  The default value is 5.0.
102     *
103     * @return The gap (in Java2D units).
104     *
105     * @see #setGap(double)
106     */
107    public double getGap() {
108        return this.gap;
109    }
110
111    /**
112     * Sets the amount of space between subplots and sends a
113     * {@link PlotChangeEvent} to all registered listeners.
114     *
115     * @param gap  the gap between subplots (in Java2D units).
116     *
117     * @see #getGap()
118     */
119    public void setGap(double gap) {
120        this.gap = gap;
121        fireChangeEvent();
122    }
123
124    /**
125     * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
126     * to all registered listeners.
127     * <br><br>
128     * The domain axis for the subplot will be set to {@code null}.  You
129     * must ensure that the subplot has a non-null range axis.
130     *
131     * @param subplot  the subplot ({@code null} not permitted).
132     */
133    public void add(CategoryPlot subplot) {
134        add(subplot, 1);
135    }
136
137    /**
138     * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
139     * to all registered listeners.
140     * <br><br>
141     * The domain axis for the subplot will be set to {@code null}.  You
142     * must ensure that the subplot has a non-null range axis.
143     *
144     * @param subplot  the subplot ({@code null} not permitted).
145     * @param weight  the weight (must be &gt;= 1).
146     */
147    public void add(CategoryPlot subplot, int weight) {
148        Args.nullNotPermitted(subplot, "subplot");
149        if (weight < 1) {
150            throw new IllegalArgumentException("Require weight >= 1.");
151        }
152        subplot.setParent(this);
153        subplot.setWeight(weight);
154        subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
155        subplot.setDomainAxis(null);
156        subplot.setOrientation(getOrientation());
157        subplot.addChangeListener(this);
158        this.subplots.add(subplot);
159        CategoryAxis axis = getDomainAxis();
160        if (axis != null) {
161            axis.configure();
162        }
163        fireChangeEvent();
164    }
165
166    /**
167     * Removes a subplot from the combined chart.  Potentially, this removes
168     * some unique categories from the overall union of the datasets...so the
169     * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to
170     * all registered listeners.
171     *
172     * @param subplot  the subplot ({@code null} not permitted).
173     */
174    public void remove(CategoryPlot subplot) {
175        Args.nullNotPermitted(subplot, "subplot");
176        int position = -1;
177        int size = this.subplots.size();
178        int i = 0;
179        while (position == -1 && i < size) {
180            if (this.subplots.get(i) == subplot) {
181                position = i;
182            }
183            i++;
184        }
185        if (position != -1) {
186            this.subplots.remove(position);
187            subplot.setParent(null);
188            subplot.removeChangeListener(this);
189            CategoryAxis domain = getDomainAxis();
190            if (domain != null) {
191                domain.configure();
192            }
193            fireChangeEvent();
194        }
195    }
196
197    /**
198     * Returns the list of subplots.  The returned list may be empty, but is
199     * never {@code null}.
200     *
201     * @return An unmodifiable list of subplots.
202     */
203    public List getSubplots() {
204        if (this.subplots != null) {
205            return Collections.unmodifiableList(this.subplots);
206        }
207        else {
208            return Collections.EMPTY_LIST;
209        }
210    }
211
212    /**
213     * Returns the subplot (if any) that contains the (x, y) point (specified
214     * in Java2D space).
215     *
216     * @param info  the chart rendering info ({@code null} not permitted).
217     * @param source  the source point ({@code null} not permitted).
218     *
219     * @return A subplot (possibly {@code null}).
220     */
221    public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
222        Args.nullNotPermitted(info, "info");
223        Args.nullNotPermitted(source, "source");
224        CategoryPlot result = null;
225        int subplotIndex = info.getSubplotIndex(source);
226        if (subplotIndex >= 0) {
227            result =  (CategoryPlot) this.subplots.get(subplotIndex);
228        }
229        return result;
230    }
231
232    /**
233     * Multiplies the range on the range axis/axes by the specified factor.
234     *
235     * @param factor  the zoom factor.
236     * @param info  the plot rendering info ({@code null} not permitted).
237     * @param source  the source point ({@code null} not permitted).
238     */
239    @Override
240    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
241                              Point2D source) {
242        zoomRangeAxes(factor, info, source, false);
243    }
244
245    /**
246     * Multiplies the range on the range axis/axes by the specified factor.
247     *
248     * @param factor  the zoom factor.
249     * @param info  the plot rendering info ({@code null} not permitted).
250     * @param source  the source point ({@code null} not permitted).
251     * @param useAnchor  zoom about the anchor point?
252     */
253    @Override
254    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
255                              Point2D source, boolean useAnchor) {
256        // delegate 'info' and 'source' argument checks...
257        CategoryPlot subplot = findSubplot(info, source);
258        if (subplot != null) {
259            subplot.zoomRangeAxes(factor, info, source, useAnchor);
260        }
261        else {
262            // if the source point doesn't fall within a subplot, we do the
263            // zoom on all subplots...
264            Iterator iterator = getSubplots().iterator();
265            while (iterator.hasNext()) {
266                subplot = (CategoryPlot) iterator.next();
267                subplot.zoomRangeAxes(factor, info, source, useAnchor);
268            }
269        }
270    }
271
272    /**
273     * Zooms in on the range axes.
274     *
275     * @param lowerPercent  the lower bound.
276     * @param upperPercent  the upper bound.
277     * @param info  the plot rendering info ({@code null} not permitted).
278     * @param source  the source point ({@code null} not permitted).
279     */
280    @Override
281    public void zoomRangeAxes(double lowerPercent, double upperPercent,
282                              PlotRenderingInfo info, Point2D source) {
283        // delegate 'info' and 'source' argument checks...
284        CategoryPlot subplot = findSubplot(info, source);
285        if (subplot != null) {
286            subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
287        }
288        else {
289            // if the source point doesn't fall within a subplot, we do the
290            // zoom on all subplots...
291            Iterator iterator = getSubplots().iterator();
292            while (iterator.hasNext()) {
293                subplot = (CategoryPlot) iterator.next();
294                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
295            }
296        }
297    }
298
299    /**
300     * Calculates the space required for the axes.
301     *
302     * @param g2  the graphics device.
303     * @param plotArea  the plot area.
304     *
305     * @return The space required for the axes.
306     */
307    @Override
308    protected AxisSpace calculateAxisSpace(Graphics2D g2,
309                                           Rectangle2D plotArea) {
310
311        AxisSpace space = new AxisSpace();
312        PlotOrientation orientation = getOrientation();
313
314        // work out the space required by the domain axis...
315        AxisSpace fixed = getFixedDomainAxisSpace();
316        if (fixed != null) {
317            if (orientation == PlotOrientation.HORIZONTAL) {
318                space.setLeft(fixed.getLeft());
319                space.setRight(fixed.getRight());
320            }
321            else if (orientation == PlotOrientation.VERTICAL) {
322                space.setTop(fixed.getTop());
323                space.setBottom(fixed.getBottom());
324            }
325        }
326        else {
327            CategoryAxis categoryAxis = getDomainAxis();
328            RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
329                    getDomainAxisLocation(), orientation);
330            if (categoryAxis != null) {
331                space = categoryAxis.reserveSpace(g2, this, plotArea,
332                        categoryEdge, space);
333            }
334            else {
335                if (getDrawSharedDomainAxis()) {
336                    space = getDomainAxis().reserveSpace(g2, this, plotArea,
337                            categoryEdge, space);
338                }
339            }
340        }
341
342        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
343
344        // work out the maximum height or width of the non-shared axes...
345        int n = this.subplots.size();
346        int totalWeight = 0;
347        for (int i = 0; i < n; i++) {
348            CategoryPlot sub = (CategoryPlot) this.subplots.get(i);
349            totalWeight += sub.getWeight();
350        }
351        this.subplotAreas = new Rectangle2D[n];
352        double x = adjustedPlotArea.getX();
353        double y = adjustedPlotArea.getY();
354        double usableSize = 0.0;
355        if (orientation == PlotOrientation.HORIZONTAL) {
356            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
357        }
358        else if (orientation == PlotOrientation.VERTICAL) {
359            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
360        }
361
362        for (int i = 0; i < n; i++) {
363            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
364
365            // calculate sub-plot area
366            if (orientation == PlotOrientation.HORIZONTAL) {
367                double w = usableSize * plot.getWeight() / totalWeight;
368                this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
369                        adjustedPlotArea.getHeight());
370                x = x + w + this.gap;
371            }
372            else if (orientation == PlotOrientation.VERTICAL) {
373                double h = usableSize * plot.getWeight() / totalWeight;
374                this.subplotAreas[i] = new Rectangle2D.Double(x, y,
375                        adjustedPlotArea.getWidth(), h);
376                y = y + h + this.gap;
377            }
378
379            AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
380                    this.subplotAreas[i], null);
381            space.ensureAtLeast(subSpace);
382
383        }
384
385        return space;
386    }
387
388    /**
389     * Draws the plot on a Java 2D graphics device (such as the screen or a
390     * printer).  Will perform all the placement calculations for each of the
391     * sub-plots and then tell these to draw themselves.
392     *
393     * @param g2  the graphics device.
394     * @param area  the area within which the plot (including axis labels)
395     *              should be drawn.
396     * @param anchor  the anchor point ({@code null} permitted).
397     * @param parentState  the state from the parent plot, if there is one.
398     * @param info  collects information about the drawing ({@code null}
399     *              permitted).
400     */
401    @Override
402     public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
403            PlotState parentState, PlotRenderingInfo info) {
404
405        // set up info collection...
406        if (info != null) {
407            info.setPlotArea(area);
408        }
409
410        // adjust the drawing area for plot insets (if any)...
411        RectangleInsets insets = getInsets();
412        area.setRect(area.getX() + insets.getLeft(),
413                area.getY() + insets.getTop(),
414                area.getWidth() - insets.getLeft() - insets.getRight(),
415                area.getHeight() - insets.getTop() - insets.getBottom());
416
417
418        // calculate the data area...
419        setFixedRangeAxisSpaceForSubplots(null);
420        AxisSpace space = calculateAxisSpace(g2, area);
421        Rectangle2D dataArea = space.shrink(area, null);
422
423        // set the width and height of non-shared axis of all sub-plots
424        setFixedRangeAxisSpaceForSubplots(space);
425
426        // draw the shared axis
427        CategoryAxis axis = getDomainAxis();
428        RectangleEdge domainEdge = getDomainAxisEdge();
429        double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
430        AxisState axisState = axis.draw(g2, cursor, area, dataArea,
431                domainEdge, info);
432        if (parentState == null) {
433            parentState = new PlotState();
434        }
435        parentState.getSharedAxisStates().put(axis, axisState);
436
437        // draw all the subplots
438        for (int i = 0; i < this.subplots.size(); i++) {
439            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
440            PlotRenderingInfo subplotInfo = null;
441            if (info != null) {
442                subplotInfo = new PlotRenderingInfo(info.getOwner());
443                info.addSubplotInfo(subplotInfo);
444            }
445            Point2D subAnchor = null;
446            if (anchor != null && this.subplotAreas[i].contains(anchor)) {
447                subAnchor = anchor;
448            }
449            plot.draw(g2, this.subplotAreas[i], subAnchor, parentState,
450                    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} 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} not permitted).
477     */
478    @Override
479    public void setOrientation(PlotOrientation orientation) {
480        super.setOrientation(orientation);
481        Iterator iterator = this.subplots.iterator();
482        while (iterator.hasNext()) {
483            CategoryPlot plot = (CategoryPlot) iterator.next();
484            plot.setOrientation(orientation);
485        }
486
487    }
488
489    /**
490     * Sets the shadow generator for the plot (and all subplots) and sends
491     * a {@link PlotChangeEvent} to all registered listeners.
492     * 
493     * @param generator  the new generator ({@code null} permitted).
494     */
495    @Override
496    public void setShadowGenerator(ShadowGenerator generator) {
497        setNotify(false);
498        super.setShadowGenerator(generator);
499        Iterator iterator = this.subplots.iterator();
500        while (iterator.hasNext()) {
501            CategoryPlot plot = (CategoryPlot) iterator.next();
502            plot.setShadowGenerator(generator);
503        }
504        setNotify(true);
505    }
506
507    /**
508     * Returns a range representing the extent of the data values in this plot
509     * (obtained from the subplots) that will be rendered against the specified
510     * axis.  NOTE: This method is intended for internal JFreeChart use, and
511     * is public only so that code in the axis classes can call it.  Since,
512     * for this class, the domain axis is a {@link CategoryAxis}
513     * (not a {@code ValueAxis}) and subplots have independent range axes,
514     * the JFreeChart code will never call this method (although this is not
515     * checked/enforced).
516      *
517      * @param axis  the axis.
518      *
519      * @return The range.
520      */
521    @Override
522     public Range getDataRange(ValueAxis axis) {
523         // override is only for documentation purposes
524         return super.getDataRange(axis);
525     }
526
527     /**
528     * Returns a collection of legend items for the plot.
529     *
530     * @return The legend items.
531     */
532    @Override
533    public LegendItemCollection getLegendItems() {
534        LegendItemCollection result = getFixedLegendItems();
535        if (result == null) {
536            result = new LegendItemCollection();
537            if (this.subplots != null) {
538                Iterator iterator = this.subplots.iterator();
539                while (iterator.hasNext()) {
540                    CategoryPlot plot = (CategoryPlot) iterator.next();
541                    LegendItemCollection more = plot.getLegendItems();
542                    result.addAll(more);
543                }
544            }
545        }
546        return result;
547    }
548
549    /**
550     * Returns an unmodifiable list of the categories contained in all the
551     * subplots.
552     *
553     * @return The list.
554     */
555    @Override
556    public List getCategories() {
557        List result = new java.util.ArrayList();
558        if (this.subplots != null) {
559            Iterator iterator = this.subplots.iterator();
560            while (iterator.hasNext()) {
561                CategoryPlot plot = (CategoryPlot) iterator.next();
562                List more = plot.getCategories();
563                Iterator moreIterator = more.iterator();
564                while (moreIterator.hasNext()) {
565                    Comparable category = (Comparable) moreIterator.next();
566                    if (!result.contains(category)) {
567                        result.add(category);
568                    }
569                }
570            }
571        }
572        return Collections.unmodifiableList(result);
573    }
574
575    /**
576     * Overridden to return the categories in the subplots.
577     *
578     * @param axis  ignored.
579     *
580     * @return A list of the categories in the subplots.
581     */
582    @Override
583    public List getCategoriesForAxis(CategoryAxis axis) {
584        // FIXME:  this code means that it is not possible to use more than
585        // one domain axis for the combined plots...
586        return getCategories();
587    }
588
589    /**
590     * Handles a 'click' on the plot.
591     *
592     * @param x  x-coordinate of the click.
593     * @param y  y-coordinate of the click.
594     * @param info  information about the plot's dimensions.
595     *
596     */
597    @Override
598    public void handleClick(int x, int y, PlotRenderingInfo info) {
599
600        Rectangle2D dataArea = info.getDataArea();
601        if (dataArea.contains(x, y)) {
602            for (int i = 0; i < this.subplots.size(); i++) {
603                CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
604                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
605                subplot.handleClick(x, y, subplotInfo);
606            }
607        }
608
609    }
610
611    /**
612     * Receives a {@link PlotChangeEvent} and responds by notifying all
613     * listeners.
614     *
615     * @param event  the event.
616     */
617    @Override
618    public void plotChanged(PlotChangeEvent event) {
619        notifyListeners(event);
620    }
621
622    /**
623     * Tests the plot for equality with an arbitrary object.
624     *
625     * @param obj  the object ({@code null} permitted).
626     *
627     * @return A boolean.
628     */
629    @Override
630    public boolean equals(Object obj) {
631        if (obj == this) {
632            return true;
633        }
634        if (!(obj instanceof CombinedDomainCategoryPlot)) {
635            return false;
636        }
637        CombinedDomainCategoryPlot that = (CombinedDomainCategoryPlot) obj;
638        if (!that.canEqual(this)){
639            return false;
640        }
641        if (Double.compare(this.gap, that.gap) != 0) {
642            return false;
643        }
644        if (!Objects.equals(this.subplots, that.subplots)) {
645            return false;
646        }
647        return super.equals(obj);
648    }
649
650    /**
651     * Ensures symmetry between super/subclass implementations of equals. For
652     * more detail, see http://jqno.nl/equalsverifier/manual/inheritance.
653     *
654     * @param other Object
655     * 
656     * @return true ONLY if the parameter is THIS class type
657     */
658    @Override
659    public boolean canEqual(Object other) {
660        // Solves Problem: equals not symmetric
661        return (other instanceof CombinedDomainCategoryPlot);
662    }
663
664    @Override
665    public int hashCode() {
666        int hash = super.hashCode();
667        hash = 97 * hash + Objects.hashCode(this.subplots);
668        hash = 97 * hash + (int) (Double.doubleToLongBits(this.gap) ^ 
669                                 (Double.doubleToLongBits(this.gap) >>> 32));
670        return hash;
671    }
672
673    /**
674     * Returns a clone of the plot.
675     *
676     * @return A clone.
677     *
678     * @throws CloneNotSupportedException  this class will not throw this
679     *         exception, but subclasses (if any) might.
680     */
681    @Override
682    public Object clone() throws CloneNotSupportedException {
683
684        CombinedDomainCategoryPlot result
685            = (CombinedDomainCategoryPlot) super.clone();
686        result.subplots = (List) ObjectUtils.deepClone(this.subplots);
687        for (Iterator it = result.subplots.iterator(); it.hasNext();) {
688            Plot child = (Plot) it.next();
689            child.setParent(result);
690        }
691        return result;
692
693    }
694
695}