001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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 * LayeredBarRenderer.java 029 * ----------------------- 030 * (C) Copyright 2003-2021, by Arnaud Lelievre and Contributors. 031 * 032 * Original Author: Arnaud Lelievre (for Garden); 033 * Contributor(s): David Gilbert; 034 * Zoheb Borbora; 035 * 036 */ 037 038package org.jfree.chart.renderer.category; 039 040import java.awt.GradientPaint; 041import java.awt.Graphics2D; 042import java.awt.Paint; 043import java.awt.Stroke; 044import java.awt.geom.Rectangle2D; 045import java.io.Serializable; 046import java.util.HashMap; 047import java.util.Map; 048import java.util.Objects; 049 050import org.jfree.chart.axis.CategoryAxis; 051import org.jfree.chart.axis.ValueAxis; 052import org.jfree.chart.entity.EntityCollection; 053import org.jfree.chart.labels.CategoryItemLabelGenerator; 054import org.jfree.chart.plot.CategoryPlot; 055import org.jfree.chart.plot.PlotOrientation; 056import org.jfree.chart.util.GradientPaintTransformer; 057import org.jfree.chart.api.RectangleEdge; 058import org.jfree.data.category.CategoryDataset; 059 060/** 061 * A {@link CategoryItemRenderer} that represents data using bars which are 062 * superimposed. The example shown here is generated by the 063 * {@code LayeredBarChartDemo1.java} program included in the JFreeChart 064 * Demo Collection: 065 * <br><br> 066 * <img src="doc-files/LayeredBarRendererSample.png" 067 * alt="LayeredBarRendererSample.png"> 068 */ 069public class LayeredBarRenderer extends BarRenderer implements Serializable { 070 071 /** For serialization. */ 072 private static final long serialVersionUID = -8716572894780469487L; 073 074 /** A list of the width of each series bar. */ 075 protected Map<Integer, Double> seriesBarWidths; 076 077 /** 078 * Default constructor. 079 */ 080 public LayeredBarRenderer() { 081 super(); 082 this.seriesBarWidths = new HashMap<>(); 083 } 084 085 /** 086 * Returns the bar width for a series, or {@code Double.NaN} if no 087 * width has been set. 088 * 089 * @param series the series index (zero based). 090 * 091 * @return The width for the series (1.0=100%, it is the maximum). 092 */ 093 public double getSeriesBarWidth(int series) { 094 double result = Double.NaN; 095 Number n = (Number) this.seriesBarWidths.get(series); 096 if (n != null) { 097 result = n.doubleValue(); 098 } 099 return result; 100 } 101 102 /** 103 * Sets the width of the bars of a series. 104 * 105 * @param series the series index (zero based). 106 * @param width the width of the series bar in percentage (1.0=100%, it is 107 * the maximum). 108 */ 109 public void setSeriesBarWidth(int series, double width) { 110 this.seriesBarWidths.put(series, width); 111 } 112 113 /** 114 * Calculates the bar width and stores it in the renderer state. 115 * 116 * @param plot the plot. 117 * @param dataArea the data area. 118 * @param rendererIndex the renderer index. 119 * @param state the renderer state. 120 */ 121 @Override 122 protected void calculateBarWidth(CategoryPlot plot, Rectangle2D dataArea, 123 int rendererIndex, CategoryItemRendererState state) { 124 125 // calculate the bar width - this calculation differs from the 126 // BarRenderer calculation because the bars are layered on top of one 127 // another, so there is effectively only one bar per category for 128 // the purpose of the bar width calculation 129 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); 130 CategoryDataset dataset = plot.getDataset(rendererIndex); 131 if (dataset != null) { 132 int columns = dataset.getColumnCount(); 133 int rows = dataset.getRowCount(); 134 double space = 0.0; 135 PlotOrientation orientation = plot.getOrientation(); 136 if (orientation == PlotOrientation.HORIZONTAL) { 137 space = dataArea.getHeight(); 138 } 139 else if (orientation == PlotOrientation.VERTICAL) { 140 space = dataArea.getWidth(); 141 } 142 double maxWidth = space * getMaximumBarWidth(); 143 double categoryMargin = 0.0; 144 if (columns > 1) { 145 categoryMargin = domainAxis.getCategoryMargin(); 146 } 147 double used = space * (1 - domainAxis.getLowerMargin() 148 - domainAxis.getUpperMargin() - categoryMargin); 149 if ((rows * columns) > 0) { 150 state.setBarWidth(Math.min(used / (dataset.getColumnCount()), 151 maxWidth)); 152 } 153 else { 154 state.setBarWidth(Math.min(used, maxWidth)); 155 } 156 } 157 } 158 159 /** 160 * Draws the bar for one item in the dataset. 161 * 162 * @param g2 the graphics device. 163 * @param state the renderer state. 164 * @param dataArea the plot area. 165 * @param plot the plot. 166 * @param domainAxis the domain (category) axis. 167 * @param rangeAxis the range (value) axis. 168 * @param data the data. 169 * @param row the row index (zero-based). 170 * @param column the column index (zero-based). 171 * @param pass the pass index. 172 */ 173 @Override 174 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 175 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 176 ValueAxis rangeAxis, CategoryDataset data, int row, int column, 177 int pass) { 178 179 PlotOrientation orientation = plot.getOrientation(); 180 if (orientation.isHorizontal()) { 181 drawHorizontalItem(g2, state, dataArea, plot, domainAxis, 182 rangeAxis, data, row, column); 183 } else if (orientation.isVertical()) { 184 drawVerticalItem(g2, state, dataArea, plot, domainAxis, rangeAxis, 185 data, row, column); 186 } 187 188 } 189 190 /** 191 * Draws the bar for a single (series, category) data item. 192 * 193 * @param g2 the graphics device. 194 * @param state the renderer state. 195 * @param dataArea the data area. 196 * @param plot the plot. 197 * @param domainAxis the domain axis. 198 * @param rangeAxis the range axis. 199 * @param dataset the dataset. 200 * @param row the row index (zero-based). 201 * @param column the column index (zero-based). 202 */ 203 protected void drawHorizontalItem(Graphics2D g2, 204 CategoryItemRendererState state, Rectangle2D dataArea, 205 CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, 206 CategoryDataset dataset, int row, int column) { 207 208 // nothing is drawn for null values... 209 Number dataValue = dataset.getValue(row, column); 210 if (dataValue == null) { 211 return; 212 } 213 214 // X 215 double value = dataValue.doubleValue(); 216 double base = getBase(); 217 double lclip = getLowerClip(); 218 double uclip = getUpperClip(); 219 if (uclip <= 0.0) { // cases 1, 2, 3 and 4 220 if (value >= uclip) { 221 return; // bar is not visible 222 } 223 base = uclip; 224 if (value <= lclip) { 225 value = lclip; 226 } 227 } else if (lclip <= 0.0) { // cases 5, 6, 7 and 8 228 if (value >= uclip) { 229 value = uclip; 230 } 231 else { 232 if (value <= lclip) { 233 value = lclip; 234 } 235 } 236 } else { // cases 9, 10, 11 and 12 237 if (value <= lclip) { 238 return; // bar is not visible 239 } 240 base = lclip; 241 if (value >= uclip) { 242 value = uclip; 243 } 244 } 245 246 RectangleEdge edge = plot.getRangeAxisEdge(); 247 double transX1 = rangeAxis.valueToJava2D(base, dataArea, edge); 248 double transX2 = rangeAxis.valueToJava2D(value, dataArea, edge); 249 double rectX = Math.min(transX1, transX2); 250 double rectWidth = Math.abs(transX2 - transX1); 251 252 // Y 253 double rectY = domainAxis.getCategoryMiddle(column, getColumnCount(), 254 dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() / 2.0; 255 256 int seriesCount = getRowCount(); 257 258 // draw the bar... 259 double shift = 0.0; 260 double rectHeight; 261 double widthFactor = 1.0; 262 double seriesBarWidth = getSeriesBarWidth(row); 263 if (!Double.isNaN(seriesBarWidth)) { 264 widthFactor = seriesBarWidth; 265 } 266 rectHeight = widthFactor * state.getBarWidth(); 267 rectY = rectY + (1 - widthFactor) * state.getBarWidth() / 2.0; 268 if (seriesCount > 1) { 269 shift = rectHeight * 0.20 / (seriesCount - 1); 270 } 271 272 Rectangle2D bar = new Rectangle2D.Double(rectX, 273 (rectY + ((seriesCount - 1 - row) * shift)), rectWidth, 274 (rectHeight - (seriesCount - 1 - row) * shift * 2)); 275 276 if (state.getElementHinting()) { 277 beginElementGroup(g2, dataset.getRowKey(row), 278 dataset.getColumnKey(column)); 279 } 280 281 Paint itemPaint = getItemPaint(row, column); 282 GradientPaintTransformer t = getGradientPaintTransformer(); 283 if (t != null && itemPaint instanceof GradientPaint) { 284 itemPaint = t.transform((GradientPaint) itemPaint, bar); 285 } 286 g2.setPaint(itemPaint); 287 g2.fill(bar); 288 289 // draw the outline... 290 if (isDrawBarOutline() 291 && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) { 292 Stroke stroke = getItemOutlineStroke(row, column); 293 Paint paint = getItemOutlinePaint(row, column); 294 if (stroke != null && paint != null) { 295 g2.setStroke(stroke); 296 g2.setPaint(paint); 297 g2.draw(bar); 298 } 299 } 300 301 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 302 column); 303 if (generator != null && isItemLabelVisible(row, column)) { 304 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 305 value < base); 306 } 307 308 // collect entity and tool tip information... 309 EntityCollection entities = state.getEntityCollection(); 310 if (entities != null) { 311 addItemEntity(entities, dataset, row, column, bar); 312 } 313 } 314 315 /** 316 * Draws the bar for a single (series, category) data item. 317 * 318 * @param g2 the graphics device. 319 * @param state the renderer state. 320 * @param dataArea the data area. 321 * @param plot the plot. 322 * @param domainAxis the domain axis. 323 * @param rangeAxis the range axis. 324 * @param dataset the dataset. 325 * @param row the row index (zero-based). 326 * @param column the column index (zero-based). 327 */ 328 protected void drawVerticalItem(Graphics2D g2, 329 CategoryItemRendererState state, Rectangle2D dataArea, 330 CategoryPlot plot, CategoryAxis domainAxis, ValueAxis rangeAxis, 331 CategoryDataset dataset, int row, int column) { 332 333 // nothing is drawn for null values... 334 Number dataValue = dataset.getValue(row, column); 335 if (dataValue == null) { 336 return; 337 } 338 339 // BAR X 340 double rectX = domainAxis.getCategoryMiddle(column, getColumnCount(), 341 dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() / 2.0; 342 343 int seriesCount = getRowCount(); 344 345 // BAR Y 346 double value = dataValue.doubleValue(); 347 double base = getBase(); 348 double lclip = getLowerClip(); 349 double uclip = getUpperClip(); 350 351 if (uclip <= 0.0) { // cases 1, 2, 3 and 4 352 if (value >= uclip) { 353 return; // bar is not visible 354 } 355 base = uclip; 356 if (value <= lclip) { 357 value = lclip; 358 } 359 } else if (lclip <= 0.0) { // cases 5, 6, 7 and 8 360 if (value >= uclip) { 361 value = uclip; 362 } else { 363 if (value <= lclip) { 364 value = lclip; 365 } 366 } 367 } else { // cases 9, 10, 11 and 12 368 if (value <= lclip) { 369 return; // bar is not visible 370 } 371 base = getLowerClip(); 372 if (value >= uclip) { 373 value = uclip; 374 } 375 } 376 377 RectangleEdge edge = plot.getRangeAxisEdge(); 378 double transY1 = rangeAxis.valueToJava2D(base, dataArea, edge); 379 double transY2 = rangeAxis.valueToJava2D(value, dataArea, edge); 380 double rectY = Math.min(transY2, transY1); 381 382 double rectWidth; 383 double rectHeight = Math.abs(transY2 - transY1); 384 385 // draw the bar... 386 double shift = 0.0; 387 double widthFactor = 1.0; 388 double seriesBarWidth = getSeriesBarWidth(row); 389 if (!Double.isNaN(seriesBarWidth)) { 390 widthFactor = seriesBarWidth; 391 } 392 rectWidth = widthFactor * state.getBarWidth(); 393 rectX = rectX + (1 - widthFactor) * state.getBarWidth() / 2.0; 394 if (seriesCount > 1) { 395 // needs to be improved !!! 396 shift = rectWidth * 0.20 / (seriesCount - 1); 397 } 398 399 Rectangle2D bar = new Rectangle2D.Double( 400 (rectX + ((seriesCount - 1 - row) * shift)), rectY, 401 (rectWidth - (seriesCount - 1 - row) * shift * 2), rectHeight); 402 403 if (state.getElementHinting()) { 404 beginElementGroup(g2, dataset.getRowKey(row), 405 dataset.getColumnKey(column)); 406 } 407 408 Paint itemPaint = getItemPaint(row, column); 409 GradientPaintTransformer t = getGradientPaintTransformer(); 410 if (t != null && itemPaint instanceof GradientPaint) { 411 itemPaint = t.transform((GradientPaint) itemPaint, bar); 412 } 413 g2.setPaint(itemPaint); 414 g2.fill(bar); 415 416 if (isDrawBarOutline() && state.getBarWidth() 417 > BAR_OUTLINE_WIDTH_THRESHOLD) { 418 g2.setStroke(getItemOutlineStroke(row, column)); 419 g2.setPaint(getItemOutlinePaint(row, column)); 420 g2.draw(bar); 421 } 422 423 if (state.getElementHinting()) { 424 endElementGroup(g2); 425 } 426 427 // draw the item labels if there are any... 428 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 429 column); 430 if (generator != null && isItemLabelVisible(row, column)) { 431 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 432 value < base); 433 } 434 435 // collect entity and tool tip information... 436 EntityCollection entities = state.getEntityCollection(); 437 if (entities != null) { 438 addItemEntity(entities, dataset, row, column, bar); 439 } 440 } 441 442 @Override 443 public int hashCode() { 444 int hash = 7; 445 hash = 17 * hash + Objects.hashCode(this.seriesBarWidths); 446 return hash; 447 } 448 449 /** 450 * Tests the entity for equality with an arbitrary object. 451 * 452 * @param obj the object to test against ({@code null} permitted). 453 * 454 * @return A boolean. 455 */ 456 @Override 457 public boolean equals(Object obj) { 458 if (this == obj) { 459 return true; 460 } 461 if (obj == null) { 462 return false; 463 } 464 if (getClass() != obj.getClass()) { 465 return false; 466 } 467 final LayeredBarRenderer other = (LayeredBarRenderer) obj; 468 if (!Objects.equals(this.seriesBarWidths, other.seriesBarWidths)) { 469 return false; 470 } 471 return super.equals(obj); 472 } 473 474}