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 * CombinedDomainXYPlot.java
029 * -------------------------
030 * (C) Copyright 2001-present, by Bill Kelemen and Contributors.
031 *
032 * Original Author:  Bill Kelemen;
033 * Contributor(s):   David Gilbert;
034 *                   Anthony Boulestreau;
035 *                   David Basten;
036 *                   Kevin Frechette (for ISTI);
037 *                   Nicolas Brodu;
038 *                   Petr Kubanek (bug 1606205);
039 *                   Vladimir Shirokov (bug 986);
040 */
041
042package org.jfree.chart.plot;
043
044import java.awt.Graphics2D;
045import java.awt.geom.Point2D;
046import java.awt.geom.Rectangle2D;
047import java.util.Collections;
048import java.util.Iterator;
049import java.util.List;
050import java.util.Objects;
051
052import org.jfree.chart.LegendItemCollection;
053import org.jfree.chart.axis.AxisSpace;
054import org.jfree.chart.axis.AxisState;
055import org.jfree.chart.axis.NumberAxis;
056import org.jfree.chart.axis.ValueAxis;
057import org.jfree.chart.event.PlotChangeEvent;
058import org.jfree.chart.event.PlotChangeListener;
059import org.jfree.chart.renderer.xy.XYItemRenderer;
060import org.jfree.chart.ui.RectangleEdge;
061import org.jfree.chart.ui.RectangleInsets;
062import org.jfree.chart.util.ObjectUtils;
063import org.jfree.chart.util.Args;
064import org.jfree.chart.util.ShadowGenerator;
065import org.jfree.data.Range;
066import org.jfree.data.general.DatasetChangeEvent;
067import org.jfree.data.xy.XYDataset;
068
069/**
070 * An extension of {@link XYPlot} that contains multiple subplots that share a
071 * common domain axis.
072 */
073public class CombinedDomainXYPlot extends XYPlot
074        implements PlotChangeListener {
075
076    /** For serialization. */
077    private static final long serialVersionUID = -7765545541261907383L;
078
079    /** Storage for the subplot references (possibly empty but never null). */
080    private List<XYPlot> subplots;
081
082    /** The gap between subplots. */
083    private double gap = 5.0;
084
085    /** Temporary storage for the subplot areas. */
086    private transient Rectangle2D[] subplotAreas;
087    // TODO:  the subplot areas needs to be moved out of the plot into the plot
088    //        state
089
090    /**
091     * Default constructor.
092     */
093    public CombinedDomainXYPlot() {
094        this(new NumberAxis());
095    }
096
097    /**
098     * Creates a new combined plot that shares a domain axis among multiple
099     * subplots.
100     *
101     * @param domainAxis  the shared axis.
102     */
103    public CombinedDomainXYPlot(ValueAxis domainAxis) {
104        super(null,        // no data in the parent plot
105              domainAxis,
106              null,        // no range axis
107              null);       // no renderer
108        this.subplots = new java.util.ArrayList<>();
109    }
110
111    /**
112     * Returns a string describing the type of plot.
113     *
114     * @return The type of plot.
115     */
116    @Override
117    public String getPlotType() {
118        return "Combined_Domain_XYPlot";
119    }
120
121    /**
122     * Returns the gap between subplots, measured in Java2D units.
123     *
124     * @return The gap (in Java2D units).
125     *
126     * @see #setGap(double)
127     */
128    public double getGap() {
129        return this.gap;
130    }
131
132    /**
133     * Sets the amount of space between subplots and sends a
134     * {@link PlotChangeEvent} to all registered listeners.
135     *
136     * @param gap  the gap between subplots (in Java2D units).
137     *
138     * @see #getGap()
139     */
140    public void setGap(double gap) {
141        this.gap = gap;
142        fireChangeEvent();
143    }
144
145    /**
146     * Returns {@code true} if the range is pannable for at least one subplot,
147     * and {@code false} otherwise.
148     * 
149     * @return A boolean. 
150     */
151    @Override
152    public boolean isRangePannable() {
153        for (XYPlot subplot : this.subplots) {
154            if (subplot.isRangePannable()) {
155                return true;
156            }
157        }
158        return false;
159    }
160    
161    /**
162     * Sets the flag, on each of the subplots, that controls whether or not the 
163     * range is pannable.
164     * 
165     * @param pannable  the new flag value. 
166     */
167    @Override
168    public void setRangePannable(boolean pannable) {
169        for (XYPlot subplot : this.subplots) {
170            subplot.setRangePannable(pannable);
171        }        
172    }
173
174    /**
175     * Sets the orientation for the plot (also changes the orientation for all
176     * the subplots to match).
177     *
178     * @param orientation  the orientation ({@code null} not allowed).
179     */
180    @Override
181    public void setOrientation(PlotOrientation orientation) {
182        super.setOrientation(orientation);
183        for (XYPlot p : this.subplots) {
184            p.setOrientation(orientation);
185        }
186    }
187
188    /**
189     * Sets the shadow generator for the plot (and all subplots) and sends
190     * a {@link PlotChangeEvent} to all registered listeners.
191     * 
192     * @param generator  the new generator ({@code null} permitted).
193     */
194    @Override
195    public void setShadowGenerator(ShadowGenerator generator) {
196        setNotify(false);
197        super.setShadowGenerator(generator);
198        for (XYPlot p : this.subplots) {
199            p.setShadowGenerator(generator);
200        }
201        setNotify(true);
202    }
203
204    /**
205     * Returns a range representing the extent of the data values in this plot
206     * (obtained from the subplots) that will be rendered against the specified
207     * axis.  NOTE: This method is intended for internal JFreeChart use, and
208     * is public only so that code in the axis classes can call it.  Since
209     * only the domain axis is shared between subplots, the JFreeChart code
210     * will only call this method for the domain values (although this is not
211     * checked/enforced).
212     *
213     * @param axis  the axis.
214     *
215     * @return The range (possibly {@code null}).
216     */
217    @Override
218    public Range getDataRange(ValueAxis axis) {
219        if (this.subplots == null) {
220            return null;
221        }
222        Range result = null;
223        for (XYPlot p : this.subplots) {
224            result = Range.combine(result, p.getDataRange(axis));
225        }
226        return result;
227    }
228
229    /**
230     * Adds a subplot (with a default 'weight' of 1) and sends a
231     * {@link PlotChangeEvent} to all registered listeners.
232     * <P>
233     * The domain axis for the subplot will be set to {@code null}.  You
234     * must ensure that the subplot has a non-null range axis.
235     *
236     * @param subplot  the subplot ({@code null} not permitted).
237     */
238    public void add(XYPlot subplot) {
239        // defer argument checking
240        add(subplot, 1);
241    }
242
243    /**
244     * Adds a subplot with the specified weight and sends a
245     * {@link PlotChangeEvent} to all registered listeners.  The weight
246     * determines how much space is allocated to the subplot relative to all
247     * the other subplots.
248     * <P>
249     * The domain axis for the subplot will be set to {@code null}.  You
250     * must ensure that the subplot has a non-null range axis.
251     *
252     * @param subplot  the subplot ({@code null} not permitted).
253     * @param weight  the weight (must be &gt;= 1).
254     */
255    public void add(XYPlot subplot, int weight) {
256        Args.nullNotPermitted(subplot, "subplot");
257        if (weight <= 0) {
258            throw new IllegalArgumentException("Require weight >= 1.");
259        }
260
261        // store the plot and its weight
262        subplot.setParent(this);
263        subplot.setWeight(weight);
264        subplot.setInsets(RectangleInsets.ZERO_INSETS, false);
265        subplot.setDomainAxis(null);
266        subplot.addChangeListener(this);
267        this.subplots.add(subplot);
268
269        ValueAxis axis = getDomainAxis();
270        if (axis != null) {
271            axis.configure();
272        }
273        fireChangeEvent();
274    }
275
276    /**
277     * Removes a subplot from the combined chart and sends a
278     * {@link PlotChangeEvent} to all registered listeners.
279     *
280     * @param subplot  the subplot ({@code null} not permitted).
281     */
282    public void remove(XYPlot subplot) {
283        Args.nullNotPermitted(subplot, "subplot");
284        int position = -1;
285        int size = this.subplots.size();
286        int i = 0;
287        while (position == -1 && i < size) {
288            if (this.subplots.get(i) == subplot) {
289                position = i;
290            }
291            i++;
292        }
293        if (position != -1) {
294            this.subplots.remove(position);
295            subplot.setParent(null);
296            subplot.removeChangeListener(this);
297            ValueAxis domain = getDomainAxis();
298            if (domain != null) {
299                domain.configure();
300            }
301            fireChangeEvent();
302        }
303    }
304
305    /**
306     * Returns the list of subplots.  The returned list may be empty, but is
307     * never {@code null}.
308     *
309     * @return An unmodifiable list of subplots.
310     */
311    public List<XYPlot> getSubplots() {
312        return Collections.unmodifiableList(this.subplots);
313    }
314
315    /**
316     * Calculates the axis space required.
317     *
318     * @param g2  the graphics device.
319     * @param plotArea  the plot area.
320     *
321     * @return The space.
322     */
323    @Override
324    protected AxisSpace calculateAxisSpace(Graphics2D g2,
325            Rectangle2D plotArea) {
326
327        AxisSpace space = new AxisSpace();
328        PlotOrientation orientation = getOrientation();
329
330        // work out the space required by the domain axis...
331        AxisSpace fixed = getFixedDomainAxisSpace();
332        if (fixed != null) {
333            if (orientation == PlotOrientation.HORIZONTAL) {
334                space.setLeft(fixed.getLeft());
335                space.setRight(fixed.getRight());
336            }
337            else if (orientation == PlotOrientation.VERTICAL) {
338                space.setTop(fixed.getTop());
339                space.setBottom(fixed.getBottom());
340            }
341        }
342        else {
343            ValueAxis xAxis = getDomainAxis();
344            RectangleEdge xEdge = Plot.resolveDomainAxisLocation(
345                    getDomainAxisLocation(), orientation);
346            if (xAxis != null) {
347                space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space);
348            }
349        }
350
351        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
352
353        // work out the maximum height or width of the non-shared axes...
354        int n = this.subplots.size();
355        int totalWeight = 0;
356        for (int i = 0; i < n; i++) {
357            XYPlot sub = (XYPlot) this.subplots.get(i);
358            totalWeight += sub.getWeight();
359        }
360        this.subplotAreas = new Rectangle2D[n];
361        double x = adjustedPlotArea.getX();
362        double y = adjustedPlotArea.getY();
363        double usableSize = 0.0;
364        if (orientation == PlotOrientation.HORIZONTAL) {
365            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
366        }
367        else if (orientation == PlotOrientation.VERTICAL) {
368            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
369        }
370
371        for (int i = 0; i < n; i++) {
372            XYPlot plot = (XYPlot) this.subplots.get(i);
373
374            // calculate sub-plot area
375            if (orientation == PlotOrientation.HORIZONTAL) {
376                double w = usableSize * plot.getWeight() / totalWeight;
377                this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
378                        adjustedPlotArea.getHeight());
379                x = x + w + this.gap;
380            }
381            else if (orientation == PlotOrientation.VERTICAL) {
382                double h = usableSize * plot.getWeight() / totalWeight;
383                this.subplotAreas[i] = new Rectangle2D.Double(x, y,
384                        adjustedPlotArea.getWidth(), h);
385                y = y + h + this.gap;
386            }
387
388            AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
389                    this.subplotAreas[i], null);
390            space.ensureAtLeast(subSpace);
391
392        }
393
394        return space;
395    }
396
397    /**
398     * Draws the plot within the specified area on a graphics device.
399     *
400     * @param g2  the graphics device.
401     * @param area  the plot area (in Java2D space).
402     * @param anchor  an anchor point in Java2D space ({@code null}
403     *                permitted).
404     * @param parentState  the state from the parent plot, if there is one
405     *                     ({@code null} permitted).
406     * @param info  collects chart drawing information ({@code null}
407     *              permitted).
408     */
409    @Override
410    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
411            PlotState parentState, PlotRenderingInfo info) {
412
413        // set up info collection...
414        if (info != null) {
415            info.setPlotArea(area);
416        }
417
418        // adjust the drawing area for plot insets (if any)...
419        RectangleInsets insets = getInsets();
420        insets.trim(area);
421
422        setFixedRangeAxisSpaceForSubplots(null);
423        AxisSpace space = calculateAxisSpace(g2, area);
424        Rectangle2D dataArea = space.shrink(area, null);
425
426        // set the width and height of non-shared axis of all sub-plots
427        setFixedRangeAxisSpaceForSubplots(space);
428
429        // draw the shared axis
430        ValueAxis axis = getDomainAxis();
431        RectangleEdge edge = getDomainAxisEdge();
432        double cursor = RectangleEdge.coordinate(dataArea, edge);
433        AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
434        if (parentState == null) {
435            parentState = new PlotState();
436        }
437        parentState.getSharedAxisStates().put(axis, axisState);
438
439        // draw all the subplots
440        for (int i = 0; i < this.subplots.size(); i++) {
441            XYPlot plot = (XYPlot) this.subplots.get(i);
442            PlotRenderingInfo subplotInfo = null;
443            if (info != null) {
444                subplotInfo = new PlotRenderingInfo(info.getOwner());
445                info.addSubplotInfo(subplotInfo);
446            }
447            plot.draw(g2, this.subplotAreas[i], anchor, parentState,
448                    subplotInfo);
449        }
450
451        if (info != null) {
452            info.setDataArea(dataArea);
453        }
454
455    }
456
457    /**
458     * Returns a collection of legend items for the plot.
459     *
460     * @return The legend items.
461     */
462    @Override
463    public LegendItemCollection getLegendItems() {
464        LegendItemCollection result = getFixedLegendItems();
465        if (result == null) {
466            result = new LegendItemCollection();
467            if (this.subplots != null) {
468                Iterator iterator = this.subplots.iterator();
469                while (iterator.hasNext()) {
470                    XYPlot plot = (XYPlot) iterator.next();
471                    LegendItemCollection more = plot.getLegendItems();
472                    result.addAll(more);
473                }
474            }
475        }
476        return result;
477    }
478
479    /**
480     * Multiplies the range on the range axis/axes by the specified factor.
481     *
482     * @param factor  the zoom factor.
483     * @param info  the plot rendering info ({@code null} not permitted).
484     * @param source  the source point ({@code null} not permitted).
485     */
486    @Override
487    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
488                              Point2D source) {
489        zoomRangeAxes(factor, info, source, false);
490    }
491
492    /**
493     * Multiplies the range on the range axis/axes by the specified factor.
494     *
495     * @param factor  the zoom factor.
496     * @param state  the plot state.
497     * @param source  the source point (in Java2D coordinates).
498     * @param useAnchor  use source point as zoom anchor?
499     */
500    @Override
501    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
502            Point2D source, boolean useAnchor) {
503        // delegate 'state' and 'source' argument checks...
504        XYPlot subplot = findSubplot(state, source);
505        if (subplot != null) {
506            subplot.zoomRangeAxes(factor, state, source, useAnchor);
507        } else {
508            // if the source point doesn't fall within a subplot, we do the
509            // zoom on all subplots...
510            for (XYPlot p : this.subplots) {
511                p.zoomRangeAxes(factor, state, source, useAnchor);
512            }
513        }
514    }
515
516    /**
517     * Zooms in on the range axes.
518     *
519     * @param lowerPercent  the lower bound.
520     * @param upperPercent  the upper bound.
521     * @param info  the plot rendering info ({@code null} not permitted).
522     * @param source  the source point ({@code null} not permitted).
523     */
524    @Override
525    public void zoomRangeAxes(double lowerPercent, double upperPercent,
526                              PlotRenderingInfo info, Point2D source) {
527        // delegate 'info' and 'source' argument checks...
528        XYPlot subplot = findSubplot(info, source);
529        if (subplot != null) {
530            subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
531        } else {
532            // if the source point doesn't fall within a subplot, we do the
533            // zoom on all subplots...
534            for (XYPlot p : this.subplots) {
535                p.zoomRangeAxes(lowerPercent, upperPercent, info, source);
536            }
537        }
538    }
539
540    /**
541     * Pans all range axes by the specified percentage.
542     *
543     * @param panRange the distance to pan (as a percentage of the axis length).
544     * @param info  the plot info ({@code null} not permitted).
545     * @param source the source point where the pan action started.
546     */
547    @Override
548    public void panRangeAxes(double panRange, PlotRenderingInfo info,
549            Point2D source) {
550        XYPlot subplot = findSubplot(info, source);
551        if (subplot == null) {
552            return;
553        }
554        if (!subplot.isRangePannable()) {
555            return;
556        }
557        PlotRenderingInfo subplotInfo = info.getSubplotInfo(
558                info.getSubplotIndex(source));
559        if (subplotInfo == null) {
560            return;
561        }
562        for (int i = 0; i < subplot.getRangeAxisCount(); i++) {
563            ValueAxis rangeAxis = subplot.getRangeAxis(i);
564            if (rangeAxis != null) {
565                rangeAxis.pan(panRange);
566            }
567        }
568    }
569
570    /**
571     * Returns the subplot (if any) that contains the (x, y) point (specified
572     * in Java2D space).
573     *
574     * @param info  the chart rendering info ({@code null} not permitted).
575     * @param source  the source point ({@code null} not permitted).
576     *
577     * @return A subplot (possibly {@code null}).
578     */
579    public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
580        Args.nullNotPermitted(info, "info");
581        Args.nullNotPermitted(source, "source");
582        XYPlot result = null;
583        int subplotIndex = info.getSubplotIndex(source);
584        if (subplotIndex >= 0) {
585            result =  (XYPlot) this.subplots.get(subplotIndex);
586        }
587        return result;
588    }
589
590    /**
591     * Sets the item renderer FOR ALL SUBPLOTS.  Registered listeners are
592     * notified that the plot has been modified.
593     * <P>
594     * Note: usually you will want to set the renderer independently for each
595     * subplot, which is NOT what this method does.
596     *
597     * @param renderer the new renderer.
598     */
599    @Override
600    public void setRenderer(XYItemRenderer renderer) {
601        super.setRenderer(renderer);  // not strictly necessary, since the
602                                      // renderer set for the
603                                      // parent plot is not used
604        for (XYPlot p : this.subplots) {
605            p.setRenderer(renderer);
606        }
607    }
608
609    /**
610     * Sets the fixed range axis space and sends a {@link PlotChangeEvent} to
611     * all registered listeners.
612     *
613     * @param space  the space ({@code null} permitted).
614     */
615    @Override
616    public void setFixedRangeAxisSpace(AxisSpace space) {
617        super.setFixedRangeAxisSpace(space);
618        setFixedRangeAxisSpaceForSubplots(space);
619        fireChangeEvent();
620    }
621
622    /**
623     * Sets the size (width or height, depending on the orientation of the
624     * plot) for the domain axis of each subplot.
625     *
626     * @param space  the space.
627     */
628    protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
629        for (XYPlot p : this.subplots) {
630            p.setFixedRangeAxisSpace(space, false);
631        }
632    }
633
634    /**
635     * Handles a 'click' on the plot by updating the anchor values.
636     *
637     * @param x  x-coordinate, where the click occurred.
638     * @param y  y-coordinate, where the click occurred.
639     * @param info  object containing information about the plot dimensions.
640     */
641    @Override
642    public void handleClick(int x, int y, PlotRenderingInfo info) {
643        Rectangle2D dataArea = info.getDataArea();
644        if (dataArea.contains(x, y)) {
645            for (int i = 0; i < this.subplots.size(); i++) {
646                XYPlot subplot = (XYPlot) this.subplots.get(i);
647                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
648                subplot.handleClick(x, y, subplotInfo);
649            }
650        }
651    }
652
653    /**
654     * Receives notification of a change to the plot's dataset.
655     * <P>
656     * The axis ranges are updated if necessary.
657     *
658     * @param event  information about the event (not used here).
659     */
660    @Override
661    public void datasetChanged(DatasetChangeEvent event) {
662        super.datasetChanged(event);
663        if (this.subplots == null) {
664            return;  // this can happen during plot construction
665        }
666        XYDataset dataset = null;
667        if (event.getDataset() instanceof XYDataset) {
668            dataset = (XYDataset) event.getDataset();
669        }
670        for (XYPlot subplot : this.subplots) {
671            if (subplot.indexOf(dataset) >= 0) {
672                subplot.configureRangeAxes();
673            }
674        }
675    }
676
677    /**
678     * Receives a {@link PlotChangeEvent} and responds by notifying all
679     * listeners.
680     *
681     * @param event  the event.
682     */
683    @Override
684    public void plotChanged(PlotChangeEvent event) {
685        notifyListeners(event);
686    }
687
688    /**
689     * Tests this plot for equality with another object.
690     *
691     * @param obj  the other object.
692     *
693     * @return {@code true} or {@code false}.
694     */
695    @Override
696    public boolean equals(Object obj) {
697        if (obj == this) {
698            return true;
699        }
700        if (!(obj instanceof CombinedDomainXYPlot)) {
701            return false;
702        }
703        CombinedDomainXYPlot that = (CombinedDomainXYPlot) obj;
704        if (this.gap != that.gap) {
705            return false;
706        }
707        if (!Objects.equals(this.subplots, that.subplots)) {
708            return false;
709        }
710        return super.equals(obj);
711    }
712
713    /**
714     * Returns a clone of the annotation.
715     *
716     * @return A clone.
717     *
718     * @throws CloneNotSupportedException  this class will not throw this
719     *         exception, but subclasses (if any) might.
720     */
721    @Override
722    public Object clone() throws CloneNotSupportedException {
723
724        CombinedDomainXYPlot result = (CombinedDomainXYPlot) super.clone();
725        result.subplots = (List) ObjectUtils.deepClone(this.subplots);
726        for (Iterator it = result.subplots.iterator(); it.hasNext();) {
727            Plot child = (Plot) it.next();
728            child.setParent(result);
729        }
730
731        // after setting up all the subplots, the shared domain axis may need
732        // reconfiguring
733        ValueAxis domainAxis = result.getDomainAxis();
734        if (domainAxis != null) {
735            domainAxis.configure();
736        }
737
738        return result;
739
740    }
741
742}