/* ===========================================================
* 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.xyz;
import java.awt.Color;
import java.io.Serializable;
import com.orsoncharts.axis.Axis3D;
import com.orsoncharts.Range;
import com.orsoncharts.data.DataUtils;
import com.orsoncharts.data.xyz.XYZDataset;
import com.orsoncharts.plot.XYZPlot;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.Object3D;
import com.orsoncharts.graphics3d.World;
import com.orsoncharts.renderer.Renderer3DChangeEvent;
import com.orsoncharts.util.ObjectUtils;
/**
* A renderer that draws 3D bars on an {@link XYZPlot} using data from an
* {@link XYZDataset}. Here is a sample:
* <div>
* <object id="ABC" data="../../../../doc-files/XYZBarChart3DDemo1.svg"
* type="image/svg+xml" width="500" height="359"></object>
* </div>
* (refer to {@code XYZBarChart3DDemo1.java} for the code to generate
* the above chart).
* <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 BarXYZRenderer extends AbstractXYZRenderer implements XYZRenderer,
Serializable {
/** The base value (normally 0.0, but can be modified). */
private double base;
/** The width of the bars along the x-axis. */
private double barXWidth;
/** The width of the bars along the z-axis. */
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 XYZColorSource baseColorSource;
/**
* The color 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 XYZColorSource topColorSource;
/**
* Creates a new default instance.
*/
public BarXYZRenderer() {
this.base = 0.0;
this.barXWidth = 0.8;
this.barZWidth = 0.8;
this.baseColorSource = new StandardXYZColorSource(Color.WHITE);
this.topColorSource = new StandardXYZColorSource(Color.BLACK);
}
/**
* Returns the value for the base of the bars. The default is
* {@code 0.0}.
*
* @return The value for the base of the bars.
*/
public double getBase() {
return this.base;
}
/**
* Sets the base value for the bars and sends a
* {@link Renderer3DChangeEvent} to all registered listeners.
*
* @param base the base.
*/
public void setBase(double base) {
this.base = base;
fireChangeEvent(true);
}
/**
* Returns the width of the bars in the direction of the x-axis, in the
* units of the x-axis. The default value is {@code 0.8}.
*
* @return The width of the bars.
*/
public double getBarXWidth() {
return this.barXWidth;
}
/**
* Sets the width of the bars in the direction of the x-axis and sends a
* {@link Renderer3DChangeEvent} to all registered listeners.
*
* @param width the width.
*/
public void setBarXWidth(double width) {
this.barXWidth = width;
fireChangeEvent(true);
}
/**
* Returns the width of the bars in the direction of the z-axis, in the
* units of the z-axis. The default value is {@code 0.8}.
*
* @return The width of the bars.
*/
public double getBarZWidth() {
return this.barZWidth;
}
/**
* Sets the width of the bars in the direction of the z-axis and sends a
* {@link Renderer3DChangeEvent} to all registered listeners.
*
* @param width the width.
*/
public void setBarZWidth(double width) {
this.barZWidth = width;
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 XYZColorSource 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(XYZColorSource 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 XYZColorSource 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(XYZColorSource source) {
this.topColorSource = source;
fireChangeEvent(true);
}
/**
* Returns the range that needs to be set on the x-axis in order for this
* renderer to be able to display all the data in the supplied dataset.
*
* @param dataset the dataset ({@code null} not permitted).
*
* @return The range ({@code null} if there is no data in the dataset).
*/
@Override
public Range findXRange(XYZDataset dataset) {
// delegate argument check...
Range xRange = DataUtils.findXRange(dataset);
if (xRange == null) {
return null;
}
double delta = this.barXWidth / 2.0;
return new Range(xRange.getMin() - delta, xRange.getMax() + delta);
}
/**
* Returns the range to use for the y-axis to ensure that all data values
* are visible on the chart. This method is overridden to ensure that the
* base value is included.
*
* @param dataset the dataset ({@code null} not permitted).
*
* @return The range ({@code null} when there is no data).
*/
@Override
public Range findYRange(XYZDataset dataset) {
return DataUtils.findYRange(dataset, this.base);
}
/**
* Returns the range to use for the z-axis to ensure that all data values
* are visible on the chart. This method is overridden to account for the
* bar widths.
*
* @param dataset the dataset ({@code null} not permitted).
*
* @return The range ({@code null} when there is no data).
*/
@Override
public Range findZRange(XYZDataset dataset) {
Range zRange = DataUtils.findZRange(dataset);
if (zRange == null) {
return null;
}
double delta = this.barZWidth / 2.0;
return new Range(zRange.getMin() - delta, zRange.getMax() + delta);
}
/**
* Adds a single bar representing one item from the dataset.
*
* @param dataset the dataset.
* @param series the series index.
* @param item the item index.
* @param world the world used to model the 3D chart.
* @param dimensions the plot dimensions in 3D.
* @param xOffset the x-offset.
* @param yOffset the y-offset.
* @param zOffset the z-offset.
*/
@Override
public void composeItem(XYZDataset dataset, int series, int item,
World world, Dimension3D dimensions, double xOffset, double yOffset,
double zOffset) {
XYZPlot plot = getPlot();
Axis3D xAxis = plot.getXAxis();
Axis3D yAxis = plot.getYAxis();
Axis3D zAxis = plot.getZAxis();
double x = dataset.getX(series, item);
double y = dataset.getY(series, item);
double z = dataset.getZ(series, item);
double xdelta = this.barXWidth / 2.0;
double zdelta = this.barZWidth / 2.0;
double x0 = xAxis.getRange().peggedValue(x - xdelta);
double x1 = xAxis.getRange().peggedValue(x + xdelta);
double z0 = zAxis.getRange().peggedValue(z - zdelta);
double z1 = zAxis.getRange().peggedValue(z + zdelta);
if ((x1 <= x0) || (z1 <= z0)) {
return;
}
double ylow = Math.min(this.base, y);
double yhigh = Math.max(this.base, y);
Range range = yAxis.getRange();
if (!range.intersects(ylow, yhigh)) {
return; // the bar is not visible for the given axis range
}
double ybase = range.peggedValue(ylow);
double ytop = range.peggedValue(yhigh);
boolean inverted = this.base > y;
double wx0 = xAxis.translateToWorld(x0, dimensions.getWidth());
double wx1 = xAxis.translateToWorld(x1, dimensions.getWidth());
double wy0 = yAxis.translateToWorld(ybase, dimensions.getHeight());
double wy1 = yAxis.translateToWorld(ytop, dimensions.getHeight());
double wz0 = zAxis.translateToWorld(z0, dimensions.getDepth());
double wz1 = zAxis.translateToWorld(z1, dimensions.getDepth());
Color color = getColorSource().getColor(series, item);
Color baseColor = null;
if (this.baseColorSource != null && !range.contains(this.base)) {
baseColor = this.baseColorSource.getColor(series, item);
}
if (baseColor == null) {
baseColor = color;
}
Color topColor = null;
if (this.topColorSource != null && !range.contains(y)) {
topColor = this.topColorSource.getColor(series, item);
}
if (topColor == null) {
topColor = color;
}
Object3D bar = Object3D.createBar(wx1 - wx0, wz1 - wz0,
((wx0 + wx1) / 2.0) + xOffset, wy1 + yOffset,
((wz0 + wz1) / 2.0) + zOffset, wy0 + yOffset, color,
baseColor, topColor, inverted);
world.add(bar);
}
/**
* 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 BarXYZRenderer)) {
return false;
}
BarXYZRenderer that = (BarXYZRenderer) 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);
}
}