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}