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 * ClusteredXYBarRenderer.java
029 * ---------------------------
030 * (C) Copyright 2003-present, by Paolo Cova and Contributors.
031 *
032 * Original Author:  Paolo Cova;
033 * Contributor(s):   David Gilbert;
034 *                   Christian W. Zuckschwerdt;
035 *                   Matthias Rose;
036 *
037 */
038
039package org.jfree.chart.renderer.xy;
040
041import java.awt.Graphics2D;
042import java.awt.geom.Rectangle2D;
043import java.io.Serializable;
044import java.util.ArrayList;
045import java.util.List;
046
047import org.jfree.chart.axis.ValueAxis;
048import org.jfree.chart.entity.EntityCollection;
049import org.jfree.chart.labels.XYItemLabelGenerator;
050import org.jfree.chart.plot.CrosshairState;
051import org.jfree.chart.plot.PlotOrientation;
052import org.jfree.chart.plot.PlotRenderingInfo;
053import org.jfree.chart.plot.XYPlot;
054import org.jfree.chart.ui.RectangleEdge;
055import org.jfree.chart.util.Args;
056import org.jfree.chart.util.PublicCloneable;
057import org.jfree.data.Range;
058import org.jfree.data.xy.IntervalXYDataset;
059import org.jfree.data.xy.XYDataset;
060
061/**
062 * An extension of {@link XYBarRenderer} that displays bars for different
063 * series values at the same x next to each other. The assumption here is
064 * that for each x (time or else) there is a y value for each series. If
065 * this is not the case, there will be spaces between bars for a given x.
066 * The example shown here is generated by the
067 * {@code ClusteredXYBarRendererDemo1.java} program included in the
068 * JFreeChart demo collection:
069 * <br><br>
070 * <img src="doc-files/ClusteredXYBarRendererSample.png"
071 * alt="ClusteredXYBarRendererSample.png">
072 * <P>
073 * This renderer does not include code to calculate the crosshair point for the
074 * plot.
075 */
076public class ClusteredXYBarRenderer extends XYBarRenderer
077        implements Cloneable, PublicCloneable, Serializable {
078
079    /** For serialization. */
080    private static final long serialVersionUID = 5864462149177133147L;
081
082    /** Determines whether bar center should be interval start. */
083    private final boolean centerBarAtStartValue;
084
085    /**
086     * Default constructor. Bar margin is set to 0.0.
087     */
088    public ClusteredXYBarRenderer() {
089        this(0.0, false);
090    }
091
092    /**
093     * Constructs a new XY clustered bar renderer.
094     *
095     * @param margin  the percentage amount to trim from the width of each bar.
096     * @param centerBarAtStartValue  if true, bars will be centered on the
097     *         start of the time period.
098     */
099    public ClusteredXYBarRenderer(double margin,
100                                  boolean centerBarAtStartValue) {
101        super(margin);
102        this.centerBarAtStartValue = centerBarAtStartValue;
103    }
104
105    /**
106     * Returns the number of passes through the dataset that this renderer
107     * requires.  In this case, two passes are required, the first for drawing
108     * the shadows (if visible), and the second for drawing the bars.
109     *
110     * @return {@code 2}.
111     */
112    @Override
113    public int getPassCount() {
114        return 2;
115    }
116
117    /**
118     * Returns the x-value bounds for the specified dataset.
119     *
120     * @param dataset  the dataset ({@code null} permitted).
121     *
122     * @return The bounds (possibly {@code null}).
123     */
124    @Override
125    public Range findDomainBounds(XYDataset dataset) {
126        if (dataset == null) {
127            return null;
128        }
129        // need to handle cluster centering as a special case
130        if (this.centerBarAtStartValue) {
131            return findDomainBoundsWithOffset((IntervalXYDataset) dataset);
132        } else {
133            return super.findDomainBounds(dataset);
134        }
135    }
136
137    /**
138     * Iterates over the items in an {@link IntervalXYDataset} to find
139     * the range of x-values including the interval OFFSET so that it centers
140     * the interval around the start value.
141     *
142     * @param dataset  the dataset ({@code null} not permitted).
143     *
144     * @return The range (possibly {@code null}).
145     */
146    protected Range findDomainBoundsWithOffset(IntervalXYDataset dataset) {
147        Args.nullNotPermitted(dataset, "dataset");
148        double minimum = Double.POSITIVE_INFINITY;
149        double maximum = Double.NEGATIVE_INFINITY;
150        int seriesCount = dataset.getSeriesCount();
151        double lvalue;
152        double uvalue;
153        for (int series = 0; series < seriesCount; series++) {
154            int itemCount = dataset.getItemCount(series);
155            for (int item = 0; item < itemCount; item++) {
156                lvalue = dataset.getStartXValue(series, item);
157                uvalue = dataset.getEndXValue(series, item);
158                double offset = (uvalue - lvalue) / 2.0;
159                lvalue = lvalue - offset;
160                uvalue = uvalue - offset;
161                minimum = Math.min(minimum, lvalue);
162                maximum = Math.max(maximum, uvalue);
163            }
164        }
165
166        if (minimum > maximum) {
167            return null;
168        } else {
169            return new Range(minimum, maximum);
170        }
171    }
172
173    /**
174     * Draws the visual representation of a single data item. This method
175     * is mostly copied from the superclass, the change is that in the
176     * calculated space for a singe bar we draw bars for each series next to
177     * each other. The width of each bar is the available width divided by
178     * the number of series. Bars for each series are drawn in order left to
179     * right.
180     *
181     * @param g2  the graphics device.
182     * @param state  the renderer state.
183     * @param dataArea  the area within which the plot is being drawn.
184     * @param info  collects information about the drawing.
185     * @param plot  the plot (can be used to obtain standard color
186     *              information etc).
187     * @param domainAxis  the domain axis.
188     * @param rangeAxis  the range axis.
189     * @param dataset  the dataset.
190     * @param series  the series index.
191     * @param item  the item index.
192     * @param crosshairState  crosshair information for the plot
193     *                        ({@code null} permitted).
194     * @param pass  the pass index.
195     */
196    @Override
197    public void drawItem(Graphics2D g2, XYItemRendererState state, 
198            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
199            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 
200            int series, int item, CrosshairState crosshairState, int pass) {
201
202        if (!getItemVisible(series, item)) {
203            return;
204        }
205
206        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
207
208        double y0;
209        double y1;
210        if (getUseYInterval()) {
211            y0 = intervalDataset.getStartYValue(series, item);
212            y1 = intervalDataset.getEndYValue(series, item);
213        } else {
214            y0 = getBase();
215            y1 = intervalDataset.getYValue(series, item);
216        }
217        if (Double.isNaN(y0) || Double.isNaN(y1)) {
218            return;
219        }
220
221        double yy0 = rangeAxis.valueToJava2D(y0, dataArea,
222                plot.getRangeAxisEdge());
223        double yy1 = rangeAxis.valueToJava2D(y1, dataArea,
224                plot.getRangeAxisEdge());
225
226        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
227        double x0 = intervalDataset.getStartXValue(series, item);
228        double xx0 = domainAxis.valueToJava2D(x0, dataArea, xAxisLocation);
229
230        double x1 = intervalDataset.getEndXValue(series, item);
231        double xx1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
232
233        double intervalW = xx1 - xx0;  // this may be negative
234        double baseX = xx0;
235        if (this.centerBarAtStartValue) {
236            baseX = baseX - intervalW / 2.0;
237        }
238        double m = getMargin();
239        if (m > 0.0) {
240            double cut = intervalW * getMargin();
241            intervalW = intervalW - cut;
242            baseX = baseX + (cut / 2);
243        }
244
245        double intervalH = Math.abs(yy0 - yy1);  // we don't need the sign
246
247        PlotOrientation orientation = plot.getOrientation();
248
249        List<Integer> visibleSeries = new ArrayList<>();
250        for (int i = 0; i < dataset.getSeriesCount(); i++) {
251            if (isSeriesVisible(i)) {
252                visibleSeries.add(i);
253            }
254        }
255
256        int numSeries = visibleSeries.size();
257        double seriesBarWidth = intervalW / numSeries;  // may be negative
258        int visibleSeriesIndex = visibleSeries.indexOf(series);
259
260        Rectangle2D bar = null;
261        if (orientation == PlotOrientation.HORIZONTAL) {
262            double barY0 = baseX + (seriesBarWidth * visibleSeriesIndex);
263            double barY1 = barY0 + seriesBarWidth;
264            double rx = Math.min(yy0, yy1);
265            double rw = intervalH;
266            double ry = Math.min(barY0, barY1);
267            double rh = Math.abs(barY1 - barY0);
268            bar = new Rectangle2D.Double(rx, ry, rw, rh);
269        } else if (orientation == PlotOrientation.VERTICAL) {
270            double barX0 = baseX + (seriesBarWidth * visibleSeriesIndex);
271            double barX1 = barX0 + seriesBarWidth;
272            double rx = Math.min(barX0, barX1);
273            double rw = Math.abs(barX1 - barX0);
274            double ry = Math.min(yy0, yy1);
275            double rh = intervalH;
276            bar = new Rectangle2D.Double(rx, ry, rw, rh);
277        } else {
278            throw new IllegalStateException();
279        }
280        boolean positive = (y1 > 0.0);
281        boolean inverted = rangeAxis.isInverted();
282        RectangleEdge barBase;
283        if (orientation == PlotOrientation.HORIZONTAL) {
284            if (positive && inverted || !positive && !inverted) {
285                barBase = RectangleEdge.RIGHT;
286            } else {
287                barBase = RectangleEdge.LEFT;
288            }
289        } else {
290            if (positive && !inverted || !positive && inverted) {
291                barBase = RectangleEdge.BOTTOM;
292            } else {
293                barBase = RectangleEdge.TOP;
294            }
295        }
296        if (pass == 0 && getShadowsVisible()) {
297            getBarPainter().paintBarShadow(g2, this, series, item, bar, barBase,
298                !getUseYInterval());
299        }
300        if (pass == 1) {
301            getBarPainter().paintBar(g2, this, series, item, bar, barBase);
302
303            if (isItemLabelVisible(series, item)) {
304                XYItemLabelGenerator generator = getItemLabelGenerator(series,
305                        item);
306                drawItemLabel(g2, dataset, series, item, plot, generator, bar,
307                        y1 < 0.0);
308            }
309
310            // add an entity for the item...
311            if (info != null) {
312                EntityCollection entities
313                        = info.getOwner().getEntityCollection();
314                if (entities != null) {
315                    addEntity(entities, bar, dataset, series, item,
316                            bar.getCenterX(), bar.getCenterY());
317                }
318            }
319        }
320
321    }
322
323    /**
324     * Tests this renderer for equality with an arbitrary object, returning
325     * {@code true} if {@code obj} is a {@code ClusteredXYBarRenderer} with the
326     * same settings as this renderer, and {@code false} otherwise.
327     *
328     * @param obj  the object ({@code null} permitted).
329     *
330     * @return A boolean.
331     */
332    @Override
333    public boolean equals(Object obj) {
334        if (obj == this) {
335            return true;
336        }
337        if (!(obj instanceof ClusteredXYBarRenderer)) {
338            return false;
339        }
340        ClusteredXYBarRenderer that = (ClusteredXYBarRenderer) obj;
341        if (this.centerBarAtStartValue != that.centerBarAtStartValue) {
342            return false;
343        }
344        return super.equals(obj);
345    }
346
347    /**
348     * Returns a clone of the renderer.
349     *
350     * @return A clone.
351     *
352     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
353     */
354    @Override
355    public Object clone() throws CloneNotSupportedException {
356        return super.clone();
357    }
358
359}