/* =========================================================== * Orson Charts : a 3D chart library for the Java(tm) platform * =========================================================== * * (C)opyright 2013-2016, by Object Refinery Limited. All rights reserved. * * http://www.object-refinery.com/orsoncharts/index.html * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * If you do not wish to be bound by the terms of the GPL, an alternative * commercial license can be purchased. For details, please see visit the * Orson Charts home page: * * http://www.object-refinery.com/orsoncharts/index.html * */ package com.orsoncharts.renderer.category; import java.awt.Color; import java.io.Serializable; import com.orsoncharts.Chart3DFactory; import com.orsoncharts.Range; import com.orsoncharts.axis.CategoryAxis3D; import com.orsoncharts.axis.ValueAxis3D; import com.orsoncharts.data.category.CategoryDataset3D; import com.orsoncharts.data.DataUtils; import com.orsoncharts.data.KeyedValues3DItemKey; import com.orsoncharts.data.Values3D; import com.orsoncharts.graphics3d.Dimension3D; import com.orsoncharts.graphics3d.Object3D; import com.orsoncharts.graphics3d.World; import com.orsoncharts.label.ItemLabelPositioning; import com.orsoncharts.plot.CategoryPlot3D; import com.orsoncharts.renderer.Renderer3DChangeEvent; import com.orsoncharts.util.ObjectUtils; /** * A renderer for creating 3D bar charts from a {@link CategoryDataset3D} (for * use with a {@link CategoryPlot3D}). For example: * <div> * <object id="ABC" data="../../../../doc-files/BarChart3DDemo1.svg" type="image/svg+xml" * width="500" height="359"> * </object> * </div> * (refer to {@code BarChart3DDemo1.java} for the code to generate the * above chart). * <br><br> * There is a factory method to create a chart using this renderer - see * {@link Chart3DFactory#createBarChart(String, String, CategoryDataset3D, * String, String, String)}. * <br><br> * NOTE: This class is serializable, but the serialization format is subject * to change in future releases and should not be relied upon for persisting * instances of this class. */ @SuppressWarnings("serial") public class BarRenderer3D extends AbstractCategoryRenderer3D implements Serializable { /** The base of the bars - defaults to 0.0. */ private double base; /** The bar width as a percentage of the column width. */ private double barXWidth; /** The bar width as a percentage of the row width. */ private double barZWidth; /** * The color source used to fetch the color for the base of bars where * the actual base of the bar is *outside* of the current axis range * (that is, the bar is "cropped"). If this is {@code null}, then * the regular bar color is used. */ private CategoryColorSource baseColorSource; /** * The paint source used to fetch the color for the top of bars where * the actual top of the bar is *outside* of the current axis range * (that is, the bar is "cropped"). If this is {@code null} then the * bar top is always drawn using the series paint. */ private CategoryColorSource topColorSource; /** * Creates a new renderer with default attribute values. */ public BarRenderer3D() { this.base = 0.0; this.barXWidth = 0.8; this.barZWidth = 0.5; this.baseColorSource = new StandardCategoryColorSource(Color.WHITE); this.topColorSource = new StandardCategoryColorSource(Color.BLACK); } /** * Returns the base value for the bars. The default value * is {@code 0.0}. * * @return The base value for the bars. * * @see #setBase(double) */ public double getBase() { return this.base; } /** * Sets the base value for the bars and fires a * {@link com.orsoncharts.renderer.Renderer3DChangeEvent}. * * @param base the new base value. * * @see #getBase() */ public void setBase(double base) { this.base = base; fireChangeEvent(true); } /** * Returns the bar width as a percentage of the column width. * The default value is {@code 0.8} (the total width of each column * in world units is {@code 1.0}, so the default leaves a small gap * between each bar). * * @return The bar width (in world units). */ public double getBarXWidth() { return this.barXWidth; } /** * Sets the the bar width as a percentage of the column width and * fires a {@link Renderer3DChangeEvent}. * * @param barXWidth the new width. */ public void setBarXWidth(double barXWidth) { this.barXWidth = barXWidth; fireChangeEvent(true); } /** * Returns the bar width as a percentage of the row width. * The default value is {@code 0.8}. * * @return The bar width. */ public double getBarZWidth() { return this.barZWidth; } /** * Sets the the bar width as a percentage of the row width and * fires a {@link com.orsoncharts.renderer.Renderer3DChangeEvent}. * * @param barZWidth the new width. */ public void setBarZWidth(double barZWidth) { this.barZWidth = barZWidth; fireChangeEvent(true); } /** * Returns the object used to fetch the color for the base of bars * where the base of the bar is "cropped" (on account of the base value * falling outside of the bounds of the y-axis). This is used to give a * visual indication to the end-user that the bar on display is cropped. * If this paint source is {@code null}, the regular series color * will be used for the top of the bars. * * @return A paint source (possibly {@code null}). */ public CategoryColorSource getBaseColorSource() { return this.baseColorSource; } /** * Sets the object that determines the color to use for the base of bars * where the base value falls outside the axis range, and sends a * {@link Renderer3DChangeEvent} to all registered listeners. If you set * this to {@code null}, the regular series color will be used to draw * the base of the bar, but it will be harder for the end-user to know that * only a section of the bar is visible in the chart. Note that the * default base paint source returns {@code Color.WHITE} always. * * @param source the source ({@code null} permitted). * * @see #getBaseColorSource() * @see #getTopColorSource() */ public void setBaseColorSource(CategoryColorSource source) { this.baseColorSource = source; fireChangeEvent(true); } /** * Returns the object used to fetch the color for the top of bars * where the top of the bar is "cropped" (on account of the data value * falling outside of the bounds of the y-axis). This is used to give a * visual indication to the end-user that the bar on display is cropped. * If this paint source is {@code null}, the regular series color * will be used for the top of the bars. * * @return A paint source (possibly {@code null}). */ public CategoryColorSource getTopColorSource() { return this.topColorSource; } /** * Sets the object used to fetch the color for the top of bars where the * top of the bar is "cropped", and sends a {@link Renderer3DChangeEvent} * to all registered listeners. * * @param source the source ({@code null} permitted). * * @see #getTopColorSource() * @see #getBaseColorSource() */ public void setTopColorSource(CategoryColorSource source) { this.topColorSource = source; fireChangeEvent(true); } /** * Returns the range of values that will be required on the value axis * to see all the data from the dataset. We override the method to * include in the range the base value for the bars. * * @param data the data ({@code null} not permitted). * * @return The range (possibly {@code null}) */ @Override public Range findValueRange(Values3D<? extends Number> data) { return DataUtils.findValueRange(data, this.base); } /** * Constructs and places one item from the specified dataset into the given * world. This method will be called by the {@link CategoryPlot3D} class * while iterating over the items in the dataset. * * @param dataset the dataset ({@code null} not permitted). * @param series the series index. * @param row the row index. * @param column the column index. * @param world the world ({@code null} not permitted). * @param dimensions the plot dimensions ({@code null} not permitted). * @param xOffset the x-offset. * @param yOffset the y-offset. * @param zOffset the z-offset. */ @Override public void composeItem(CategoryDataset3D dataset, int series, int row, int column, World world, Dimension3D dimensions, double xOffset, double yOffset, double zOffset) { double value = dataset.getDoubleValue(series, row, column); if (Double.isNaN(value)) { return; } // delegate to a separate method that is reused by the // StackedBarRenderer3D subclass... composeItem(value, this.base, dataset, series, row, column, world, dimensions, xOffset, yOffset, zOffset); } /** * Performs the actual work of composing a bar to represent one item in the * dataset. This method is reused by the {@link StackedBarRenderer3D} * subclass. * * @param value the data value (top of the bar). * @param barBase the base value for the bar. * @param dataset the dataset. * @param series the series index. * @param row the row index. * @param column the column index. * @param world the world. * @param dimensions the plot dimensions. * @param xOffset the x-offset. * @param yOffset the y-offset. * @param zOffset the z-offset. */ @SuppressWarnings("unchecked") protected void composeItem(double value, double barBase, CategoryDataset3D dataset, int series, int row, int column, World world, Dimension3D dimensions, double xOffset, double yOffset, double zOffset) { Comparable<?> seriesKey = dataset.getSeriesKey(series); Comparable<?> rowKey = dataset.getRowKey(row); Comparable<?> columnKey = dataset.getColumnKey(column); double vlow = Math.min(barBase, value); double vhigh = Math.max(barBase, value); CategoryPlot3D plot = getPlot(); CategoryAxis3D rowAxis = plot.getRowAxis(); CategoryAxis3D columnAxis = plot.getColumnAxis(); ValueAxis3D valueAxis = plot.getValueAxis(); Range range = valueAxis.getRange(); if (!range.intersects(vlow, vhigh)) { return; // the bar is not visible for the given axis range } double vbase = range.peggedValue(vlow); double vtop = range.peggedValue(vhigh); boolean inverted = barBase > value; double rowValue = rowAxis.getCategoryValue(rowKey); double columnValue = columnAxis.getCategoryValue(columnKey); double width = dimensions.getWidth(); double height = dimensions.getHeight(); double depth = dimensions.getDepth(); double xx = columnAxis.translateToWorld(columnValue, width) + xOffset; double yy = valueAxis.translateToWorld(vtop, height) + yOffset; double zz = rowAxis.translateToWorld(rowValue, depth) + zOffset; double xw = this.barXWidth * columnAxis.getCategoryWidth(); double zw = this.barZWidth * rowAxis.getCategoryWidth(); double xxw = columnAxis.translateToWorld(xw, width); double xzw = rowAxis.translateToWorld(zw, depth); double basew = valueAxis.translateToWorld(vbase, height) + yOffset; Color color = getColorSource().getColor(series, row, column); Color baseColor = null; if (this.baseColorSource != null && !range.contains(this.base)) { baseColor = this.baseColorSource.getColor(series, row, column); } if (baseColor == null) { baseColor = color; } Color topColor = null; if (this.topColorSource != null && !range.contains(value)) { topColor = this.topColorSource.getColor(series, row, column); } if (topColor == null) { topColor = color; } Object3D bar = Object3D.createBar(xxw, xzw, xx, yy, zz, basew, color, baseColor, topColor, inverted); KeyedValues3DItemKey itemKey = new KeyedValues3DItemKey(seriesKey, rowKey, columnKey); bar.setProperty(Object3D.ITEM_KEY, itemKey); world.add(bar); drawItemLabels(world, dataset, itemKey, xx, yy, zz, basew, inverted); } protected void drawItemLabels(World world, CategoryDataset3D dataset, KeyedValues3DItemKey itemKey, double xw, double yw, double zw, double basew, boolean inverted) { ItemLabelPositioning positioning = getItemLabelPositioning(); if (getItemLabelGenerator() == null) { return; } String label = getItemLabelGenerator().generateItemLabel(dataset, itemKey.getSeriesKey(), itemKey.getRowKey(), itemKey.getColumnKey()); if (label != null) { Dimension3D dimensions = getPlot().getDimensions(); double dx = getItemLabelOffsets().getDX(); double dy = getItemLabelOffsets().getDY() * dimensions.getHeight(); double dz = getItemLabelOffsets().getDZ() * getBarZWidth(); double yy = yw; if (inverted) { yy = basew; dy = -dy; } if (positioning.equals(ItemLabelPositioning.CENTRAL)) { Object3D labelObj = Object3D.createLabelObject(label, getItemLabelFont(), getItemLabelColor(), getItemLabelBackgroundColor(), xw + dx, yy + dy, zw, false, true); labelObj.setProperty(Object3D.ITEM_KEY, itemKey); world.add(labelObj); } else if (positioning.equals( ItemLabelPositioning.FRONT_AND_BACK)) { Object3D labelObj1 = Object3D.createLabelObject(label, getItemLabelFont(), getItemLabelColor(), getItemLabelBackgroundColor(), xw + dx, yy + dy, zw + dz, false, false); labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); world.add(labelObj1); Object3D labelObj2 = Object3D.createLabelObject(label, getItemLabelFont(), getItemLabelColor(), getItemLabelBackgroundColor(), xw + dx, yy + dy, zw - dz, true, false); labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); world.add(labelObj2); } } } /** * Tests this renderer for equality with an arbitrary object. * * @param obj the object ({@code null} permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof BarRenderer3D)) { return false; } BarRenderer3D that = (BarRenderer3D) obj; if (this.base != that.base) { return false; } if (this.barXWidth != that.barXWidth) { return false; } if (this.barZWidth != that.barZWidth) { return false; } if (!ObjectUtils.equals(this.baseColorSource, that.baseColorSource)) { return false; } if (!ObjectUtils.equals(this.topColorSource, that.topColorSource)) { return false; } return super.equals(obj); } }