org.jfree.chart3d.Chart3D Maven / Gradle / Ivy
Show all versions of org.jfree.chart3d Show documentation
/* ===========================================================
* Orson Charts : a 3D chart library for the Java(tm) platform
* ===========================================================
*
* (C)opyright 2013-2020, by Object Refinery Limited. All rights reserved.
*
* https://github.com/jfree/orson-charts
*
* 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 .
*
* [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 org.jfree.chart3d;
import java.awt.BasicStroke;
import java.awt.Stroke;
import java.awt.geom.Line2D;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import javax.swing.event.EventListenerList;
import org.jfree.chart3d.graphics3d.internal.FaceSorter;
import org.jfree.chart3d.graphics3d.internal.StandardFaceSorter;
import org.jfree.chart3d.graphics3d.internal.Utils2D;
import org.jfree.chart3d.internal.ChartBox3D;
import org.jfree.chart3d.internal.ChartBox3D.ChartBoxFace;
import org.jfree.chart3d.internal.OnDrawHandler;
import org.jfree.chart3d.internal.Args;
import org.jfree.chart3d.internal.ObjectUtils;
import org.jfree.chart3d.internal.TextUtils;
import org.jfree.chart3d.axis.Axis3D;
import org.jfree.chart3d.axis.TickData;
import org.jfree.chart3d.axis.ValueAxis3D;
import org.jfree.chart3d.data.ItemKey;
import org.jfree.chart3d.graphics2d.Anchor2D;
import org.jfree.chart3d.graphics2d.RefPt2D;
import org.jfree.chart3d.graphics2d.TextAnchor;
import org.jfree.chart3d.graphics3d.Dimension3D;
import org.jfree.chart3d.graphics3d.DoubleSidedFace;
import org.jfree.chart3d.graphics3d.Drawable3D;
import org.jfree.chart3d.graphics3d.Face;
import org.jfree.chart3d.graphics3d.LabelFace;
import org.jfree.chart3d.graphics3d.Object3D;
import org.jfree.chart3d.graphics3d.Offset2D;
import org.jfree.chart3d.graphics3d.Point3D;
import org.jfree.chart3d.graphics3d.RenderedElement;
import org.jfree.chart3d.graphics3d.RenderingInfo;
import org.jfree.chart3d.graphics3d.ViewPoint3D;
import org.jfree.chart3d.graphics3d.World;
import org.jfree.chart3d.interaction.InteractiveElementType;
import org.jfree.chart3d.legend.LegendAnchor;
import org.jfree.chart3d.legend.LegendBuilder;
import org.jfree.chart3d.legend.StandardLegendBuilder;
import org.jfree.chart3d.marker.Marker;
import org.jfree.chart3d.marker.MarkerData;
import org.jfree.chart3d.plot.CategoryPlot3D;
import org.jfree.chart3d.plot.PiePlot3D;
import org.jfree.chart3d.plot.Plot3D;
import org.jfree.chart3d.plot.Plot3DChangeEvent;
import org.jfree.chart3d.plot.Plot3DChangeListener;
import org.jfree.chart3d.plot.XYZPlot;
import org.jfree.chart3d.style.ChartStyle;
import org.jfree.chart3d.style.ChartStyleChangeEvent;
import org.jfree.chart3d.style.ChartStyleChangeListener;
import org.jfree.chart3d.style.ChartStyler;
import org.jfree.chart3d.table.GradientRectanglePainter;
import org.jfree.chart3d.table.RectanglePainter;
import org.jfree.chart3d.table.StandardRectanglePainter;
import org.jfree.chart3d.table.TableElement;
import org.jfree.chart3d.table.TextElement;
/**
* A chart object for 3D charts (this is the umbrella object that manages all
* the components of the chart). The {@link Chart3DFactory} class provides
* some factory methods to construct common types of charts.
*
* All rendering is done via the Java2D API, so this object is able to draw to
* any implementation of the Graphics2D API (including
* JFreeSVG for
* SVG output, and
* OrsonPDF
* for PDF output).
*
* In the step prior to rendering, a chart is composed in a 3D model that is
* referred to as the "world". The dimensions of this 3D model are measured
* in "world units" and the overall size is controlled by the plot. You will
* see some attributes in the API that are specified in "world units", and these
* can be used to modify how objects are composed within the 3D world model.
* Once the objects (for example, bars in a bar chart) within the world have
* been composed, they are projected onto a 2D plane and rendered to the
* {@code Graphics2D} target (such as the screen, image, SVG file or
* PDF file).
*
* Charts can have simple titles or composite titles (anything that can be
* constructed as a {@link TableElement} instance. The {@link TitleUtils}
* class contains methods to create a common title/subtitle composite title.
* This is illustrated in some of the demo applications. The chart title
* and legend (and also the axis labels) are not part of the 3D world model,
* they are overlaid on the output after the 3D components have been
* rendered.
*
* 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.
*
* @see Chart3DFactory
* @see Chart3DPanel
*/
@SuppressWarnings("serial")
public class Chart3D implements Drawable3D, ChartElement,
Plot3DChangeListener, ChartStyleChangeListener, Serializable {
/**
* The default projection distance.
*
* @since 1.2
*/
public static final double DEFAULT_PROJ_DIST = 1500.0;
/**
* The key for a property that stores the interactive element type.
*
* @since 1.3
*/
public static final String INTERACTIVE_ELEMENT_TYPE
= "interactive_element_type";
/**
* The key for a property that stores the series key. This is used to
* store the series key on the {@link TableElement} representing a legend
* item, and also on a corresponding {@link RenderedElement} after
* chart rendering (in the {@link RenderingInfo}).
*
* @since 1.3
*/
public static final String SERIES_KEY = "series_key";
/** The chart id. */
private String id;
/** A background rectangle painter, if any. */
private RectanglePainter background;
/** The chart title (can be {@code null}). */
private TableElement title;
/** The anchor point for the title (never {@code null}). */
private Anchor2D titleAnchor;
/** A builder for the chart legend (can be {@code null}). */
private LegendBuilder legendBuilder;
/** The anchor point for the legend (never {@code null}). */
private Anchor2D legendAnchor;
/** The orientation for the legend (never {@code null}). */
private Orientation legendOrientation;
/** The plot. */
private Plot3D plot;
/** The view point. */
private ViewPoint3D viewPoint;
/** The projection distance. */
private double projDist;
/** The chart box color (never {@code null}). */
private Color chartBoxColor;
/**
* A translation factor applied to the chart when drawing. We use this
* to allow the user (optionally) to drag the chart from its center
* location to better align it with the chart title and legend.
*/
private Offset2D translate2D;
/** Storage for registered change listeners. */
private transient EventListenerList listenerList;
/**
* A flag that controls whether or not the chart will notify listeners
* of changes (defaults to {@code true}, but sometimes it is useful
* to disable this).
*/
private boolean notify;
/**
* Rendering hints that will be used for chart drawing. This can be
* empty but it should never be {@code null}.
*
* @since 1.1
*/
private transient RenderingHints renderingHints;
/**
* The chart style.
*
* @since 1.2
*/
private ChartStyle style;
/** A 3D model of the world (represents the chart). */
private transient World world;
/** An object that sorts faces for rendering (painter's algorithm). */
private FaceSorter faceSorter;
/**
* A flag that controls whether or not element hints are added to the
* {@code Graphics2D} output.
*/
private boolean elementHinting;
/**
* Creates a 3D chart for the specified plot using the default chart
* style. Note that a plot instance must be used in one chart instance
* only.
*
* @param title the chart title ({@code null} permitted).
* @param subtitle the chart subtitle ({@code null} permitted).
* @param plot the plot ({@code null} not permitted).
*
* @see Chart3DFactory
*/
public Chart3D(String title, String subtitle, Plot3D plot) {
this(title, subtitle, plot, Chart3DFactory.getDefaultChartStyle());
}
/**
* Creates a 3D chart for the specified plot using the supplied style.
*
* @param title the chart title ({@code null} permitted).
* @param subtitle the chart subtitle ({@code null} permitted).
* @param plot the plot ({@code null} not permitted).
* @param style the chart style ({@code null} not permitted).
*
* @since 1.2
*/
public Chart3D(String title, String subtitle, Plot3D plot,
ChartStyle style) {
Args.nullNotPermitted(plot, "plot");
Args.nullNotPermitted(style, "style");
plot.setChart(this);
this.background = new StandardRectanglePainter(Color.WHITE);
if (title != null) {
this.title = TitleUtils.createTitle(title, subtitle);
}
this.titleAnchor = TitleAnchor.TOP_LEFT;
this.legendBuilder = new StandardLegendBuilder();
this.legendAnchor = LegendAnchor.BOTTOM_RIGHT;
this.legendOrientation = Orientation.HORIZONTAL;
this.plot = plot;
this.plot.addChangeListener(this);
Dimension3D dim = this.plot.getDimensions();
float distance = (float) dim.getDiagonalLength() * 3.0f;
this.viewPoint = ViewPoint3D.createAboveViewPoint(distance);
this.projDist = DEFAULT_PROJ_DIST;
this.chartBoxColor = new Color(255, 255, 255, 100);
this.translate2D = new Offset2D();
this.faceSorter = new StandardFaceSorter();
this.renderingHints = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
this.elementHinting = false;
this.notify = true;
this.listenerList = new EventListenerList();
this.style = style;
this.style.addChangeListener(this);
receive(new ChartStyler(this.style));
}
/**
* Returns the chart id.
*
* @return The chart id (possibly {@code null}).
*
* @since 1.3
*/
public String getID() {
return this.id;
}
/**
* Sets the chart id.
*
* @param id the id ({@code null} permitted).
*
* @since 1.3
*/
public void setID(String id) {
this.id = id;
}
/**
* Returns the background painter (an object that is responsible for filling
* the background area before charts are rendered). The default value
* is an instance of {@link StandardRectanglePainter} that paints the
* background white.
*
* @return The background painter (possibly {@code null}).
*
* @see #setBackground(org.jfree.chart3d.table.RectanglePainter)
*/
public RectanglePainter getBackground() {
return this.background;
}
/**
* Sets the background painter and sends a {@link Chart3DChangeEvent} to
* all registered listeners. A background painter is used to fill in the
* background of the chart before the 3D rendering takes place. To fill
* the background with a color or image, you can use
* {@link StandardRectanglePainter}. To fill the background with a
* gradient paint, use {@link GradientRectanglePainter}.
*
* @param background the background painter ({@code null} permitted).
*
* @see #getBackground()
*/
public void setBackground(RectanglePainter background) {
this.background = background;
fireChangeEvent();
}
/**
* Returns the chart title. A {@link TableElement} is used for the title,
* since it allows a lot of flexibility in the types of title that can
* be displayed.
*
* @return The chart title (possibly {@code null}).
*/
public TableElement getTitle() {
return this.title;
}
/**
* Sets the chart title and sends a {@link Chart3DChangeEvent} to all
* registered listeners. This is a convenience method that constructs
* the required {@link TableElement} under-the-hood.
*
* @param title the title ({@code null} permitted).
*/
public void setTitle(String title) {
if (title == null) {
setTitle((TableElement) null);
} else {
setTitle(title, this.style.getTitleFont(),
TitleUtils.DEFAULT_TITLE_COLOR);
}
}
/**
* Sets the chart title and sends a {@link Chart3DChangeEvent} to all
* registered listeners. This is a convenience method that constructs
* the required {@link TableElement} under-the-hood.
*
* @param title the title ({@code null} not permitted).
* @param font the font ({@code null} not permitted).
* @param color the foreground color ({@code null} not permitted).
*/
public void setTitle(String title, Font font, Color color) {
// defer 'title' null check
Args.nullNotPermitted(font, "font");
Args.nullNotPermitted(color, "color");
TextElement te = new TextElement(title);
te.setTag("CHART_TITLE");
te.setFont(font);
te.setColor(color);
setTitle(te);
}
/**
* Sets the chart title and sends a {@link Chart3DChangeEvent} to all
* registered listeners. You can set the title to {@code null}, in
* which case there will be no chart title.
*
* @param title the title ({@code null} permitted).
*/
public void setTitle(TableElement title) {
this.title = title;
fireChangeEvent();
}
/**
* Returns the title anchor. This controls the position of the title
* in the chart area.
*
* @return The title anchor (never {@code null}).
*
* @see #setTitleAnchor(org.jfree.chart3d.util.Anchor2D)
*/
public Anchor2D getTitleAnchor() {
return this.titleAnchor;
}
/**
* Sets the title anchor and sends a {@link Chart3DChangeEvent} to all
* registered listeners. There is a {@link TitleAnchor} class providing
* some useful default anchors.
*
* @param anchor the anchor ({@code null} not permitted).
*
* @see #getTitleAnchor()
*/
public void setTitleAnchor(Anchor2D anchor) {
Args.nullNotPermitted(anchor, "anchor");
this.titleAnchor = anchor;
fireChangeEvent();
}
/**
* Returns the plot, which manages the dataset, the axes (if any), the
* renderer (if any) and other attributes related to plotting data. The
* plot is specified via the constructor...there is no method to set a
* new plot for the chart, instead you need to create a new chart instance.
*
* @return The plot (never {@code null}).
*/
public Plot3D getPlot() {
return this.plot;
}
/**
* Returns the chart box color (the chart box is the visible, open-sided
* box inside which data is plotted for all charts except pie charts).
* The default value is {@code Color.WHITE}.
*
* @return The chart box color (never {@code null}).
*
* @see #setChartBoxColor(java.awt.Color)
*/
public Color getChartBoxColor() {
return this.chartBoxColor;
}
/**
* Sets the chart box color and sends a {@link Chart3DChangeEvent} to all
* registered listeners. Bear in mind that {@link PiePlot3D} does not
* display a chart box, so this attribute will be ignored for pie charts.
*
* @param color the color ({@code null} not permitted).
*
* @see #getChartBoxColor()
*/
public void setChartBoxColor(Color color) {
Args.nullNotPermitted(color, "color");
this.chartBoxColor = color;
fireChangeEvent();
}
/**
* Returns the dimensions of the 3D object.
*
* @return The dimensions (never {@code null}).
*/
@Override
public Dimension3D getDimensions() {
return this.plot.getDimensions();
}
/**
* Returns the view point.
*
* @return The view point (never {@code null}).
*/
@Override
public ViewPoint3D getViewPoint() {
return this.viewPoint;
}
/**
* Sets the view point.
*
* @param viewPoint the view point ({@code null} not permitted).
*/
@Override
public void setViewPoint(ViewPoint3D viewPoint) {
Args.nullNotPermitted(viewPoint, "viewPoint");
this.viewPoint = viewPoint;
fireChangeEvent();
}
/**
* Returns the projection distance. The default value is
* {@link #DEFAULT_PROJ_DIST}, higher numbers flatten out the perspective
* and reduce distortion in the projected image.
*
* @return The projection distance.
*
* @since 1.2
*/
@Override
public double getProjDistance() {
return this.projDist;
}
/**
* Sets the projection distance and sends a change event to all registered
* listeners.
*
* @param dist the distance.
*
* @since 1.2
*/
@Override
public void setProjDistance(double dist) {
this.projDist = dist;
fireChangeEvent();
}
/**
* Sets the offset in 2D-space for the rendering of the chart. The
* default value is {@code (0, 0)} but the user can modify it via
* ALT-mouse-drag in the chart panel, providing an easy way to get improved
* chart alignment in the panels (especially prior to export to PNG, SVG or
* PDF).
*
* @return The offset (never {@code null}).
*/
@Override
public Offset2D getTranslate2D() {
return this.translate2D;
}
/**
* Sets the offset in 2D-space for the rendering of the chart and sends a
* change event to all registered listeners.
*
* @param offset the new offset ({@code null} not permitted).
*/
@Override
public void setTranslate2D(Offset2D offset) {
Args.nullNotPermitted(offset, "offset");
this.translate2D = offset;
fireChangeEvent();
}
/**
* Returns the legend builder. The default value is an instance of
* {@link StandardLegendBuilder}. If the legend builder is {@code null},
* no legend will be displayed for the chart.
*
* @return The legend builder (possibly {@code null}).
*
* @see #setLegendBuilder(org.jfree.chart3d.legend.LegendBuilder)
* @see #setLegendAnchor(org.jfree.chart3d.util.Anchor2D)
*/
public LegendBuilder getLegendBuilder() {
return this.legendBuilder;
}
/**
* Sets the legend builder and sends a change event to all registered
* listeners. When the legend builder is {@code null}, no legend
* will be displayed on the chart.
*
* @param legendBuilder the legend builder ({@code null} permitted).
*
* @see #setLegendAnchor(org.jfree.chart3d.util.Anchor2D)
*/
public void setLegendBuilder(LegendBuilder legendBuilder) {
this.legendBuilder = legendBuilder;
fireChangeEvent();
}
/**
* Returns the legend anchor.
*
* @return The legend anchor (never {@code null}).
*
* @see #setLegendAnchor(org.jfree.chart3d.util.Anchor2D)
*/
public Anchor2D getLegendAnchor() {
return this.legendAnchor;
}
/**
* Sets the legend anchor and sends a {@link Chart3DChangeEvent} to all
* registered listeners. There is a {@link LegendAnchor} class providing
* some useful default anchors.
*
* @param anchor the anchor ({@code null} not permitted).
*
* @see #getLegendAnchor()
*/
public void setLegendAnchor(Anchor2D anchor) {
Args.nullNotPermitted(anchor, "anchor");
this.legendAnchor = anchor;
fireChangeEvent();
}
/**
* Returns the orientation for the legend.
*
* @return The orientation (never {@code null}).
*
* @since 1.1
*/
public Orientation getLegendOrientation() {
return this.legendOrientation;
}
/**
* Sets the legend orientation and sends a {@link Chart3DChangeEvent}
* to all registered listeners.
*
* @param orientation the orientation ({@code null} not permitted).
*
* @since 1.1
*/
public void setLegendOrientation(Orientation orientation) {
Args.nullNotPermitted(orientation, "orientation");
this.legendOrientation = orientation;
fireChangeEvent();
}
/**
* Sets the legend position (both the anchor point and the orientation) and
* sends a {@link Chart3DChangeEvent} to all registered listeners.
* This is a convenience method that calls both the
* {@link #setLegendAnchor(org.jfree.chart3d.util.Anchor2D)} and
* {@link #setLegendOrientation(org.jfree.chart3d.util.Orientation)}
* methods.
*
* @param anchor the anchor ({@code null} not permitted).
* @param orientation the orientation ({@code null} not permitted).
*
* @since 1.1
*/
public void setLegendPosition(Anchor2D anchor, Orientation orientation) {
setNotify(false);
setLegendAnchor(anchor);
setLegendOrientation(orientation);
setNotify(true);
}
/**
* Returns the collection of rendering hints for the chart.
*
* @return The rendering hints for the chart (never {@code null}).
*
* @see #setRenderingHints(RenderingHints)
*
* @since 1.1
*/
public RenderingHints getRenderingHints() {
return this.renderingHints;
}
/**
* Sets the rendering hints for the chart. These will be added (using the
* {@code Graphics2D.addRenderingHints()} method) near the start of
* the chart rendering. Note that calling this method will replace all
* existing hints assigned to the chart. If you simply wish to add an
* additional hint, you can use {@code getRenderingHints().put(key, value)}.
*
* @param hints the rendering hints ({@code null} not permitted).
*
* @see #getRenderingHints()
*
* @since 1.1
*/
public void setRenderingHints(RenderingHints hints) {
Args.nullNotPermitted(hints, "hints");
this.renderingHints = hints;
fireChangeEvent();
}
/**
* Returns a flag that indicates whether or not anti-aliasing is used when
* the chart is drawn.
*
* @return The flag.
*
* @see #setAntiAlias(boolean)
* @since 1.1
*/
public boolean getAntiAlias() {
Object val = this.renderingHints.get(RenderingHints.KEY_ANTIALIASING);
return RenderingHints.VALUE_ANTIALIAS_ON.equals(val);
}
/**
* Sets a flag that indicates whether or not anti-aliasing is used when the
* chart is drawn.
*
* Anti-aliasing usually improves the appearance of charts, but is slower.
*
* @param flag the new value of the flag.
*
* @see #getAntiAlias()
* @since 1.1
*/
public void setAntiAlias(boolean flag) {
if (flag) {
this.renderingHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
} else {
this.renderingHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
}
fireChangeEvent();
}
/**
* Returns the flag that controls whether or not element hints will be
* added to the {@code Graphics2D} output when the chart is rendered.
* The default value is {@code false}.
*
* @return A boolean.
*
* @since 1.3
*/
public boolean getElementHinting() {
return this.elementHinting;
}
/**
* Sets the flag that controls whether or not element hints will be
* added to the {@code Graphics2D} output when the chart is rendered
* and sends a change event to all registered listeners.
*
* @param hinting the new flag value.
*
* @since 1.3
*/
public void setElementHinting(boolean hinting) {
this.elementHinting = hinting;
fireChangeEvent();
}
/**
* Returns the chart style.
*
* @return The chart style (never {@code null}).
*
* @since 1.2
*/
public ChartStyle getStyle() {
return this.style;
}
/**
* Sets (and applies) the specified chart style.
*
* @param style the chart style ({@code null} not permitted).
*
* @since 1.2
*/
public void setStyle(ChartStyle style) {
Args.nullNotPermitted(style, "style");
this.style.removeChangeListener(this);
this.style = style;
this.style.addChangeListener(this);
setNotify(false);
receive(new ChartStyler(this.style));
setNotify(true);
}
/**
* Creates a world containing the chart and the supplied chart box.
*
* @param chartBox the chart box ({@code null} permitted).
*/
private World createWorld(ChartBox3D chartBox) {
World result = new World();
Dimension3D dim = this.plot.getDimensions();
double w = dim.getWidth();
double h = dim.getHeight();
double d = dim.getDepth();
if (chartBox != null) {
result.add("chartbox", chartBox.createObject3D());
}
this.plot.compose(result, -w / 2, -h / 2, -d / 2);
return result;
}
/**
* Draws the chart to the specified output target.
*
* @param g2 the output target ({@code null} not permitted).
*
* @return Information about the items rendered.
*/
@Override
public RenderingInfo draw(Graphics2D g2, Rectangle2D bounds) {
beginElement(g2, this.id, "ORSON_CHART_TOP_LEVEL");
Shape savedClip = g2.getClip();
g2.clip(bounds);
g2.addRenderingHints(this.renderingHints);
g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, 1f));
Dimension3D dim3D = this.plot.getDimensions();
double w = dim3D.getWidth();
double h = dim3D.getHeight();
double depth = dim3D.getDepth();
ChartBox3D chartBox = null;
if (this.plot instanceof XYZPlot
|| this.plot instanceof CategoryPlot3D) {
double[] tickUnits = findAxisTickUnits(g2, w, h, depth);
chartBox = new ChartBox3D(w, h, depth, -w / 2, -h / 2, -depth / 2,
this.chartBoxColor);
chartBox.setXTicks(fetchXTickData(this.plot, tickUnits[0]));
chartBox.setYTicks(fetchYTickData(this.plot, tickUnits[1]));
chartBox.setZTicks(fetchZTickData(this.plot, tickUnits[2]));
chartBox.setXMarkers(fetchXMarkerData(this.plot));
chartBox.setYMarkers(fetchYMarkerData(this.plot));
chartBox.setZMarkers(fetchZMarkerData(this.plot));
}
if (this.world == null) {
this.world = createWorld(chartBox);
} else if (chartBox != null) {
this.world.clear("chartbox");
this.world.add("chartbox", chartBox.createObject3D());
}
if (this.background != null) {
this.background.fill(g2, bounds);
}
AffineTransform saved = g2.getTransform();
double dx = bounds.getX() + bounds.getWidth() / 2.0
+ this.translate2D.getDX();
double dy = bounds.getY() + bounds.getHeight() / 2.0
+ this.translate2D.getDY();
g2.translate(dx, dy);
Point3D[] eyePts = this.world.calculateEyeCoordinates(this.viewPoint);
Point2D[] pts = this.world.calculateProjectedPoints(this.viewPoint,
this.projDist);
// sort faces by z-order
List facesInPaintOrder = new ArrayList<>(world.getFaces());
facesInPaintOrder = this.faceSorter.sort(facesInPaintOrder, eyePts);
Line2D line = null;
Stroke stroke = new BasicStroke(1.0f);
for (Face f : facesInPaintOrder) {
// check for the special case where the face is just a line
if (f.getVertexCount() == 2) {
g2.setPaint(f.getColor());
if (line == null) {
line = new Line2D.Float();
}
int v0 = f.getVertexIndex(0);
int v1 = f.getVertexIndex(1);
line.setLine(pts[v0].getX(), pts[v0].getY(), pts[v1].getX(),
pts[v1].getY());
g2.setStroke(stroke);
g2.draw(line);
continue;
}
boolean drawOutline = f.getOutline();
double[] plane = f.calculateNormal(eyePts);
double inprod = plane[0] * world.getSunX() + plane[1]
* world.getSunY() + plane[2] * world.getSunZ();
double shade = (inprod + 1) / 2.0;
if (f instanceof DoubleSidedFace
|| Utils2D.area2(pts[f.getVertexIndex(0)],
pts[f.getVertexIndex(1)], pts[f.getVertexIndex(2)]) > 0.0) {
Color c = f.getColor();
Path2D p = f.createPath(pts);
g2.setPaint(new Color((int) (c.getRed() * shade),
(int) (c.getGreen() * shade),
(int) (c.getBlue() * shade), c.getAlpha()));
if (this.elementHinting) {
beginElementGroup(f, g2);
}
g2.fill(p);
if (drawOutline) {
g2.draw(p);
}
if (this.elementHinting) {
endElementGroup(f, g2);
}
if (f instanceof ChartBoxFace
&& (this.plot instanceof CategoryPlot3D
|| this.plot instanceof XYZPlot)) {
Stroke savedStroke = g2.getStroke();
ChartBoxFace cbf = (ChartBoxFace) f;
drawGridlines(g2, cbf, pts);
drawMarkers(g2, cbf, pts);
g2.setStroke(savedStroke);
}
} else if (f instanceof LabelFace) {
LabelFace lf = (LabelFace) f;
Path2D p = lf.createPath(pts);
Rectangle2D lb = p.getBounds2D();
g2.setFont(lf.getFont());
g2.setColor(lf.getBackgroundColor());
Rectangle2D bb = TextUtils.calcAlignedStringBounds(
lf.getLabel(), g2,
(float) lb.getCenterX(), (float) lb.getCenterY(),
TextAnchor.CENTER);
g2.fill(bb);
g2.setColor(lf.getTextColor());
Rectangle2D r = TextUtils.drawAlignedString(lf.getLabel(), g2,
(float) lb.getCenterX(), (float) lb.getCenterY(),
TextAnchor.CENTER);
lf.getOwner().setProperty("labelBounds", r);
}
}
RenderingInfo info = new RenderingInfo(facesInPaintOrder, pts, dx, dy);
OnDrawHandler onDrawHandler = new OnDrawHandler(info,
this.elementHinting);
// handle labels on pie plots...
if (this.plot instanceof PiePlot3D) {
drawPieLabels(g2, w, h, depth, info);
}
// handle axis labelling on non-pie plots...
if (this.plot instanceof XYZPlot || this.plot instanceof
CategoryPlot3D) {
drawAxes(g2, chartBox, pts, info);
}
g2.setTransform(saved);
// generate and draw the legend...
if (this.legendBuilder != null) {
TableElement legend = this.legendBuilder.createLegend(this.plot,
this.legendAnchor, this.legendOrientation, this.style);
if (legend != null) {
Dimension2D legendSize = legend.preferredSize(g2, bounds);
Rectangle2D legendArea = calculateDrawArea(legendSize,
this.legendAnchor, bounds);
legend.draw(g2, legendArea, onDrawHandler);
}
}
// draw the title...
if (this.title != null) {
Dimension2D titleSize = this.title.preferredSize(g2, bounds);
Rectangle2D titleArea = calculateDrawArea(titleSize,
this.titleAnchor, bounds);
this.title.draw(g2, titleArea, onDrawHandler);
}
g2.setClip(savedClip);
endElement(g2);
return info;
}
private void beginElementGroup(Face face, Graphics2D g2) {
Object3D owner = face.getOwner();
ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY);
if (itemKey != null) {
Map m = new HashMap<>();
m.put("ref", itemKey.toJSONString());
g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
}
}
private void endElementGroup(Face face, Graphics2D g2) {
Object3D owner = face.getOwner();
ItemKey itemKey = (ItemKey) owner.getProperty(Object3D.ITEM_KEY);
if (itemKey != null) {
g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE);
}
}
/**
* An implementation method that fetches x-axis tick data from the plot,
* assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}.
* On a category plot, the x-axis is the column axis (and the tickUnit is
* ignored).
*
* @param plot the plot.
* @param tickUnit the tick unit.
*
* @return A list of tick data instances representing the tick marks and
* values along the x-axis.
*/
private List fetchXTickData(Plot3D plot, double tickUnit) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getColumnAxis().generateTickDataForColumns(
cp.getDataset());
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getXAxis().generateTickData(tickUnit);
}
return Collections.emptyList();
}
/**
* An implementation method that fetches y-axis tick data from the plot,
* assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}.
* On a category plot, the y-axis is the value axis.
*
* @param plot the plot.
* @param tickUnit the tick unit.
*
* @return A list of tick data instances representing the tick marks and
* values along the y-axis.
*/
private List fetchYTickData(Plot3D plot, double tickUnit) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getValueAxis().generateTickData(tickUnit);
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getYAxis().generateTickData(tickUnit);
}
return Collections.emptyList();
}
/**
* An implementation method that fetches z-axis tick data from the plot,
* assuming it is either a {@link CategoryPlot3D} or an {@link XYZPlot}.
* On a category plot, the z-axis is the row axis (and the tickUnit is
* ignored).
*
* @param plot the plot.
* @param tickUnit the tick unit.
*
* @return A list of tick data instances representing the tick marks and
* values along the y-axis.
*/
private List fetchZTickData(Plot3D plot, double tickUnit) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getRowAxis().generateTickDataForRows(cp.getDataset());
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getZAxis().generateTickData(tickUnit);
}
return Collections.emptyList();
}
/**
* Fetches marker data for the plot's x-axis.
*
* @param plot the plot ({@code null} not permitted).
*
* @return A list of marker data items (possibly empty but never
* {@code null}).
*/
private List fetchXMarkerData(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
return ((CategoryPlot3D) plot).getColumnAxis().generateMarkerData();
}
if (plot instanceof XYZPlot) {
return ((XYZPlot) plot).getXAxis().generateMarkerData();
}
return new ArrayList<>(0);
}
/**
* Fetches marker data for the plot's x-axis.
*
* @param plot the plot ({@code null} not permitted).
*
* @return A list of marker data items (possibly empty but never
* {@code null}).
*/
private List fetchYMarkerData(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
return ((CategoryPlot3D) plot).getValueAxis().generateMarkerData();
}
if (plot instanceof XYZPlot) {
return ((XYZPlot) plot).getYAxis().generateMarkerData();
}
return new ArrayList<>(0);
}
/**
* Fetches marker data for the plot's x-axis.
*
* @param plot the plot ({@code null} not permitted).
*
* @return A list of marker data items (possibly empty but never
* {@code null}).
*/
private List fetchZMarkerData(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
return ((CategoryPlot3D) plot).getRowAxis().generateMarkerData();
}
if (plot instanceof XYZPlot) {
return ((XYZPlot) plot).getZAxis().generateMarkerData();
}
return new ArrayList<>(0);
}
/**
* Draw the gridlines for one chart box face.
*
* @param g2 the graphics target.
* @param face the face.
* @param pts the projection points.
*/
private void drawGridlines(Graphics2D g2, ChartBoxFace face,
Point2D[] pts) {
if (isGridlinesVisibleForX(this.plot)) {
g2.setPaint(fetchGridlinePaintX(this.plot));
g2.setStroke(fetchGridlineStrokeX(this.plot));
List xA = face.getXTicksA();
List xB = face.getXTicksB();
for (int i = 0; i < xA.size(); i++) {
Line2D line = new Line2D.Double(
pts[face.getOffset() + xA.get(i).getVertexIndex()],
pts[face.getOffset() + xB.get(i).getVertexIndex()]);
g2.draw(line);
}
}
if (isGridlinesVisibleForY(this.plot)) {
g2.setPaint(fetchGridlinePaintY(this.plot));
g2.setStroke(fetchGridlineStrokeY(this.plot));
List yA = face.getYTicksA();
List yB = face.getYTicksB();
for (int i = 0; i < yA.size(); i++) {
Line2D line = new Line2D.Double(
pts[face.getOffset() + yA.get(i).getVertexIndex()],
pts[face.getOffset() + yB.get(i).getVertexIndex()]);
g2.draw(line);
}
}
if (isGridlinesVisibleForZ(this.plot)) {
g2.setPaint(fetchGridlinePaintZ(this.plot));
g2.setStroke(fetchGridlineStrokeZ(this.plot));
List zA = face.getZTicksA();
List zB = face.getZTicksB();
for (int i = 0; i < zA.size(); i++) {
Line2D line = new Line2D.Double(
pts[face.getOffset() + zA.get(i).getVertexIndex()],
pts[face.getOffset() + zB.get(i).getVertexIndex()]);
g2.draw(line);
}
}
}
/**
* Returns {@code true} if gridlines are visible for the x-axis
* (column axis in the case of a {@link CategoryPlot3D}) and
* {@code false} otherwise. For pie charts, this method will always
* return {@code false}.
*
* @param plot the plot.
*
* @return A boolean.
*/
private boolean isGridlinesVisibleForX(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlinesVisibleForColumns();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.isGridlinesVisibleX();
}
return false;
}
/**
* Returns {@code true} if gridlines are visible for the y-axis
* (value axis in the case of a {@link CategoryPlot3D}) and
* {@code false} otherwise.
*
* @param plot the plot.
*
* @return A boolean.
*/
private boolean isGridlinesVisibleForY(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlinesVisibleForValues();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.isGridlinesVisibleY();
}
return false;
}
/**
* Returns {@code true} if gridlines are visible for the z-axis
* (row axis in the case of a {@link CategoryPlot3D}) and
* {@code false} otherwise.
*
* @param plot the plot.
*
* @return A boolean.
*/
private boolean isGridlinesVisibleForZ(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlinesVisibleForRows();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.isGridlinesVisibleZ();
}
return false;
}
/**
* Returns the paint used to draw gridlines on the x-axis (or column axis
* in the case of {@link CategoryPlot3D}).
*
* @param plot the plot.
*
* @return The paint.
*/
private Paint fetchGridlinePaintX(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlinePaintForColumns();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getGridlinePaintX();
}
return null;
}
/**
* Returns the paint used to draw gridlines on the y-axis (or value axis
* in the case of {@link CategoryPlot3D}).
*
* @param plot the plot.
*
* @return The paint.
*/
private Paint fetchGridlinePaintY(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlinePaintForValues();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getGridlinePaintY();
}
return null;
}
/**
* Returns the paint used to draw gridlines on the z-axis (or row axis
* in the case of {@link CategoryPlot3D}).
*
* @param plot the plot.
*
* @return The paint.
*/
private Paint fetchGridlinePaintZ(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlinePaintForRows();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getGridlinePaintZ();
}
return null;
}
/**
* Returns the stroke used to draw gridlines on the x-axis (or column axis
* in the case of {@link CategoryPlot3D}).
*
* @param plot the plot.
*
* @return The stroke.
*/
private Stroke fetchGridlineStrokeX(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlineStrokeForColumns();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getGridlineStrokeX();
}
return null;
}
/**
* Returns the stroke used to draw gridlines on the y-axis (or value axis
* in the case of {@link CategoryPlot3D}).
*
* @param plot the plot.
*
* @return The stroke.
*/
private Stroke fetchGridlineStrokeY(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlineStrokeForValues();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getGridlineStrokeY();
}
return null;
}
/**
* Returns the stroke used to draw gridlines on the z-axis (or row axis
* in the case of {@link CategoryPlot3D}).
*
* @param plot the plot.
*
* @return The stroke.
*/
private Stroke fetchGridlineStrokeZ(Plot3D plot) {
if (plot instanceof CategoryPlot3D) {
CategoryPlot3D cp = (CategoryPlot3D) plot;
return cp.getGridlineStrokeForRows();
}
if (plot instanceof XYZPlot) {
XYZPlot xp = (XYZPlot) plot;
return xp.getGridlineStrokeZ();
}
return null;
}
/**
* Draws the pie labels for a {@link PiePlot3D} in 2D-space by creating a
* temporary world with vertices at anchor points for the labels, then
* projecting the points to 2D-space.
*
* @param g2 the graphics target.
* @param w the width.
* @param h the height.
* @param depth the depth.
* @param info the rendering info ({@code null} permitted).
*/
@SuppressWarnings("unchecked")
private void drawPieLabels(Graphics2D g2, double w, double h,
double depth, RenderingInfo info) {
PiePlot3D p = (PiePlot3D) this.plot;
World labelOverlay = new World();
List objs = p.getLabelFaces(-w / 2, -h / 2, -depth / 2);
for (Object3D obj : objs) {
labelOverlay.add(obj);
}
Point2D[] ppts = labelOverlay.calculateProjectedPoints(
this.viewPoint, this.projDist);
for (int i = 0; i < p.getDataset().getItemCount() * 2; i++) {
if (p.getDataset().getValue(i / 2) == null) {
continue;
}
Face f = labelOverlay.getFaces().get(i);
if (Utils2D.area2(ppts[f.getVertexIndex(0)],
ppts[f.getVertexIndex(1)],
ppts[f.getVertexIndex(2)]) > 0) {
Comparable key = p.getDataset().getKey(i / 2);
g2.setColor(p.getSectionLabelColorSource().getColor(key));
g2.setFont(p.getSectionLabelFontSource().getFont(key));
Point2D pt = Utils2D.centerPoint(ppts[f.getVertexIndex(0)],
ppts[f.getVertexIndex(1)], ppts[f.getVertexIndex(2)],
ppts[f.getVertexIndex(3)]);
String label = p.getSectionLabelGenerator().generateLabel(
p.getDataset(), key);
String ref = "{\"type\": \"sectionLabel\", \"key\": \""
+ key.toString() + "\"}";
beginElementWithRef(g2, ref);
Rectangle2D bounds = TextUtils.drawAlignedString(label, g2,
(float) pt.getX(), (float) pt.getY(),
TextAnchor.CENTER);
endElement(g2);
if (info != null) {
RenderedElement pieLabelRE = new RenderedElement(
InteractiveElementType.SECTION_LABEL, bounds);
pieLabelRE.setProperty("key", key);
info.addOffsetElement(pieLabelRE);
}
}
}
}
private void beginElementWithRef(Graphics2D g2, String ref) {
beginElement(g2, null, ref);
}
private void beginElement(Graphics2D g2, String id, String ref) {
if (this.elementHinting) {
Map m = new HashMap<>();
if (id != null) {
m.put("id", id);
}
m.put("ref", ref);
g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
}
}
private void endElement(Graphics2D g2) {
if (this.elementHinting) {
g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, Boolean.TRUE);
}
}
/**
* Determines appropriate tick units for the axes in the chart.
*
* @param g2 the graphics target.
* @param w the width.
* @param h the height.
* @param depth the depth.
*
* @return The tick sizes.
*/
private double[] findAxisTickUnits(Graphics2D g2, double w, double h,
double depth) {
World tempWorld = new World();
ChartBox3D chartBox = new ChartBox3D(w, h, depth, -w / 2.0, -h / 2.0,
-depth / 2.0, Color.WHITE);
tempWorld.add(chartBox.createObject3D());
Point2D[] axisPts2D = tempWorld.calculateProjectedPoints(
this.viewPoint, this.projDist);
// vertices
Point2D v0 = axisPts2D[0];
Point2D v1 = axisPts2D[1];
Point2D v2 = axisPts2D[2];
Point2D v3 = axisPts2D[3];
Point2D v4 = axisPts2D[4];
Point2D v5 = axisPts2D[5];
Point2D v6 = axisPts2D[6];
Point2D v7 = axisPts2D[7];
// faces
boolean a = chartBox.faceA().isFrontFacing(axisPts2D);
boolean b = chartBox.faceB().isFrontFacing(axisPts2D);
boolean c = chartBox.faceC().isFrontFacing(axisPts2D);
boolean d = chartBox.faceD().isFrontFacing(axisPts2D);
boolean e = chartBox.faceE().isFrontFacing(axisPts2D);
boolean f = chartBox.faceF().isFrontFacing(axisPts2D);
double xtick = 0, ytick = 0, ztick = 0;
Axis3D xAxis = null;
ValueAxis3D yAxis = null;
Axis3D zAxis = null;
if (this.plot instanceof XYZPlot) {
XYZPlot pp = (XYZPlot) this.plot;
xAxis = pp.getXAxis();
yAxis = pp.getYAxis();
zAxis = pp.getZAxis();
} else if (this.plot instanceof CategoryPlot3D) {
CategoryPlot3D pp = (CategoryPlot3D) this.plot;
xAxis = pp.getColumnAxis();
yAxis = pp.getValueAxis();
zAxis = pp.getRowAxis();
}
if (xAxis != null && yAxis != null && zAxis != null) {
double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0);
double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0);
double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0);
double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0);
double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0);
double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0);
double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0);
double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0);
double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0);
double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0);
double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0);
double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0);
if (count(a, b) == 1 && longest(ab, bc, cd, da)) {
if (xAxis instanceof ValueAxis3D) {
xtick = ((ValueAxis3D) xAxis).selectTick(g2, v0, v1, v7);
}
}
if (count(b, c) == 1 && longest(bc, ab, cd, da)) {
if (xAxis instanceof ValueAxis3D) {
xtick = ((ValueAxis3D) xAxis).selectTick(g2, v3, v2, v6);
}
}
if (count(c, d) == 1 && longest(cd, ab, bc, da)) {
if (xAxis instanceof ValueAxis3D) {
xtick = ((ValueAxis3D) xAxis).selectTick(g2, v4, v7, v1);
}
}
if (count(d, a) == 1 && longest(da, ab, bc, cd)) {
if (xAxis instanceof ValueAxis3D) {
xtick = ((ValueAxis3D) xAxis).selectTick(g2, v5, v6, v3);
}
}
if (count(b, e) == 1 && longest(be, bf, df, de)) {
ytick = yAxis.selectTick(g2, v0, v3, v7);
}
if (count(b, f) == 1 && longest(bf, be, df, de)) {
ytick = yAxis.selectTick(g2, v1, v2, v4);
}
if (count(d, f) == 1 && longest(df, be, bf, de)) {
ytick = yAxis.selectTick(g2, v6, v7, v0);
}
if (count(d, e) == 1 && longest(de, be, bf, df)) {
ytick = yAxis.selectTick(g2, v5, v4, v1);
}
if (count(a, e) == 1 && longest(ae, af, cf, ce)) {
if (zAxis instanceof ValueAxis3D) {
ztick = ((ValueAxis3D) zAxis).selectTick(g2, v0, v5, v2);
}
}
if (count(a, f) == 1 && longest(af, ae, cf, ce)) {
if (zAxis instanceof ValueAxis3D) {
ztick = ((ValueAxis3D) zAxis).selectTick(g2, v1, v6, v3);
}
}
if (count(c, f) == 1 && longest(cf, ae, af, ce)) {
if (zAxis instanceof ValueAxis3D) {
ztick = ((ValueAxis3D) zAxis).selectTick(g2, v2, v7, v5);
}
}
if (count(c, e) == 1 && longest(ce, ae, af, cf)) {
if (zAxis instanceof ValueAxis3D) {
ztick = ((ValueAxis3D) zAxis).selectTick(g2, v3, v4, v6);
}
}
}
return new double[] { xtick, ytick, ztick };
}
private void populateAnchorPoints(List tickData, Point2D[] pts) {
for (TickData t : tickData) {
t.setAnchorPt(pts[t.getVertexIndex()]);
}
}
/**
* Draws the axes for a chart.
*
* @param g2 the graphics target ({@code null} not permitted).
* @param chartBox the chart box (this contains projected points for
* the tick marks and labels)
* @param pts the projected points.
* @param info an object to be populated with rendering info, if it is
* non-{@code null}.
*/
private void drawAxes(Graphics2D g2, ChartBox3D chartBox, Point2D[] pts,
RenderingInfo info) {
// vertices
Point2D v0 = pts[0];
Point2D v1 = pts[1];
Point2D v2 = pts[2];
Point2D v3 = pts[3];
Point2D v4 = pts[4];
Point2D v5 = pts[5];
Point2D v6 = pts[6];
Point2D v7 = pts[7];
// faces
boolean a = chartBox.faceA().isFrontFacing(pts);
boolean b = chartBox.faceB().isFrontFacing(pts);
boolean c = chartBox.faceC().isFrontFacing(pts);
boolean d = chartBox.faceD().isFrontFacing(pts);
boolean e = chartBox.faceE().isFrontFacing(pts);
boolean f = chartBox.faceF().isFrontFacing(pts);
Axis3D xAxis = null, yAxis = null, zAxis = null;
if (this.plot instanceof XYZPlot) {
XYZPlot pp = (XYZPlot) this.plot;
xAxis = pp.getXAxis();
yAxis = pp.getYAxis();
zAxis = pp.getZAxis();
} else if (this.plot instanceof CategoryPlot3D) {
CategoryPlot3D pp = (CategoryPlot3D) this.plot;
xAxis = pp.getColumnAxis();
yAxis = pp.getValueAxis();
zAxis = pp.getRowAxis();
}
if (xAxis != null && yAxis != null && zAxis != null) {
double ab = (count(a, b) == 1 ? v0.distance(v1) : 0.0);
double bc = (count(b, c) == 1 ? v3.distance(v2) : 0.0);
double cd = (count(c, d) == 1 ? v4.distance(v7) : 0.0);
double da = (count(d, a) == 1 ? v5.distance(v6) : 0.0);
double be = (count(b, e) == 1 ? v0.distance(v3) : 0.0);
double bf = (count(b, f) == 1 ? v1.distance(v2) : 0.0);
double df = (count(d, f) == 1 ? v6.distance(v7) : 0.0);
double de = (count(d, e) == 1 ? v5.distance(v4) : 0.0);
double ae = (count(a, e) == 1 ? v0.distance(v5) : 0.0);
double af = (count(a, f) == 1 ? v1.distance(v6) : 0.0);
double cf = (count(c, f) == 1 ? v2.distance(v7) : 0.0);
double ce = (count(c, e) == 1 ? v3.distance(v4) : 0.0);
List ticks;
if (count(a, b) == 1 && longest(ab, bc, cd, da)) {
ticks = chartBox.faceA().getXTicksA();
populateAnchorPoints(ticks, pts);
xAxis.draw(g2, v0, v1, v7, ticks, info, this.elementHinting);
}
if (count(b, c) == 1 && longest(bc, ab, cd, da)) {
ticks = chartBox.faceB().getXTicksB();
populateAnchorPoints(ticks, pts);
xAxis.draw(g2, v3, v2, v6, ticks, info, this.elementHinting);
}
if (count(c, d) == 1 && longest(cd, ab, bc, da)) {
ticks = chartBox.faceC().getXTicksB();
populateAnchorPoints(ticks, pts);
xAxis.draw(g2, v4, v7, v1, ticks, info, this.elementHinting);
}
if (count(d, a) == 1 && longest(da, ab, bc, cd)) {
ticks = chartBox.faceA().getXTicksB();
populateAnchorPoints(ticks, pts);
xAxis.draw(g2, v5, v6, v3, ticks, info, this.elementHinting);
}
if (count(b, e) == 1 && longest(be, bf, df, de)) {
ticks = chartBox.faceB().getYTicksA();
populateAnchorPoints(ticks, pts);
yAxis.draw(g2, v0, v3, v7, ticks, info, this.elementHinting);
}
if (count(b, f) == 1 && longest(bf, be, df, de)) {
ticks = chartBox.faceB().getYTicksB();
populateAnchorPoints(ticks, pts);
yAxis.draw(g2, v1, v2, v4, ticks, info, this.elementHinting);
}
if (count(d, f) == 1 && longest(df, be, bf, de)) {
ticks = chartBox.faceD().getYTicksA();
populateAnchorPoints(ticks, pts);
yAxis.draw(g2, v6, v7, v0, ticks, info, this.elementHinting);
}
if (count(d, e) == 1 && longest(de, be, bf, df)) {
ticks = chartBox.faceD().getYTicksB();
populateAnchorPoints(ticks, pts);
yAxis.draw(g2, v5, v4, v1, ticks, info, this.elementHinting);
}
if (count(a, e) == 1 && longest(ae, af, cf, ce)) {
ticks = chartBox.faceA().getZTicksA();
populateAnchorPoints(ticks, pts);
zAxis.draw(g2, v0, v5, v2, ticks, info, this.elementHinting);
}
if (count(a, f) == 1 && longest(af, ae, cf, ce)) {
ticks = chartBox.faceA().getZTicksB();
populateAnchorPoints(ticks, pts);
zAxis.draw(g2, v1, v6, v3, ticks, info, this.elementHinting);
}
if (count(c, f) == 1 && longest(cf, ae, af, ce)) {
ticks = chartBox.faceC().getZTicksB();
populateAnchorPoints(ticks, pts);
zAxis.draw(g2, v2, v7, v5, ticks, info, this.elementHinting);
}
if (count(c, e) == 1 && longest(ce, ae, af, cf)) {
ticks = chartBox.faceC().getZTicksA();
populateAnchorPoints(ticks, pts);
zAxis.draw(g2, v3, v4, v6, ticks, info, this.elementHinting);
}
}
}
/**
* Draws the markers for one face on a chart box. The {@code pts}
* array contains all the projected points for all the vertices in the
* world...the chart box face references the required points by index.
*
* @param g2 the graphics target ({@code null} not permitted).
* @param face the face of the chart box ({@code null} not permitted).
* @param pts the projected points for the whole world.
*/
private void drawMarkers(Graphics2D g2, ChartBoxFace face, Point2D[] pts) {
// x markers
List xmarkers = face.getXMarkers();
for (MarkerData m : xmarkers) {
m.updateProjection(pts);
Marker marker = fetchXMarker(this.plot, m.getMarkerKey());
beginElementWithRef(g2, "{\"type\": \"xMarker\", \"key\": \""
+ m.getMarkerKey() + "\"}");
marker.draw(g2, m, true);
endElement(g2);
}
// y markers
List ymarkers = face.getYMarkers();
for (MarkerData m : ymarkers) {
m.updateProjection(pts);
Marker marker = fetchYMarker(this.plot, m.getMarkerKey());
beginElementWithRef(g2, "{\"type\": \"yMarker\", \"key\": \""
+ m.getMarkerKey() + "\"}");
marker.draw(g2, m, false);
endElement(g2);
}
// z markers
List zmarkers = face.getZMarkers();
for (MarkerData m : zmarkers) {
m.updateProjection(pts);
beginElementWithRef(g2, "{\"type\": \"zMarker\", \"key\": \""
+ m.getMarkerKey() + "\"}");
Marker marker = fetchZMarker(this.plot, m.getMarkerKey());
marker.draw(g2, m, false);
endElement(g2);
}
}
/**
* Returns the marker from the plot's x-axis that has the specified key,
* or {@code null} if there is no marker with that key.
*
* @param plot the plot ({@code null} not permitted).
* @param key the marker key ({@code null} not permitted).
*
* @return The marker (possibly {@code null}).
*/
private Marker fetchXMarker(Plot3D plot, String key) {
if (plot instanceof CategoryPlot3D) {
return ((CategoryPlot3D) plot).getColumnAxis().getMarker(key);
} else if (plot instanceof XYZPlot) {
return ((XYZPlot) plot).getXAxis().getMarker(key);
}
return null;
}
/**
* Returns the marker from the plot's y-axis that has the specified key,
* or {@code null} if there is no marker with that key.
*
* @param plot the plot ({@code null} not permitted).
* @param key the marker key ({@code null} not permitted).
*
* @return The marker (possibly {@code null}).
*/
private Marker fetchYMarker(Plot3D plot, String key) {
if (plot instanceof CategoryPlot3D) {
return ((CategoryPlot3D) plot).getValueAxis().getMarker(key);
} else if (plot instanceof XYZPlot) {
return ((XYZPlot) plot).getYAxis().getMarker(key);
}
return null;
}
/**
* Returns the marker from the plot's z-axis that has the specified key,
* or {@code null} if there is no marker with that key.
*
* @param plot the plot ({@code null} not permitted).
* @param key the marker key ({@code null} not permitted).
*
* @return The marker (possibly {@code null}).
*/
private Marker fetchZMarker(Plot3D plot, String key) {
if (plot instanceof CategoryPlot3D) {
return ((CategoryPlot3D) plot).getRowAxis().getMarker(key);
} else if (plot instanceof XYZPlot) {
return ((XYZPlot) plot).getZAxis().getMarker(key);
}
return null;
}
/**
* Receives a visitor. The visitor is first directed to the plot, then
* the visit is completed for the chart.
*
* @param visitor the visitor.
*
* @since 1.2
*/
@Override
public void receive(ChartElementVisitor visitor) {
this.plot.receive(visitor);
visitor.visit(this);
}
/**
* Tests this chart for equality with an arbitrary object.
*
* @param obj the object ({@code null} not permitted).
*
* @return A boolean.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Chart3D)) {
return false;
}
Chart3D that = (Chart3D) obj;
if (!ObjectUtils.equals(this.background, that.background)) {
return false;
}
if (!ObjectUtils.equals(this.title, that.title)) {
return false;
}
if (!this.titleAnchor.equals(that.titleAnchor)) {
return false;
}
if (!ObjectUtils.equalsPaint(this.chartBoxColor, that.chartBoxColor)) {
return false;
}
if (!ObjectUtils.equals(this.legendBuilder, that.legendBuilder)) {
return false;
}
if (!this.legendAnchor.equals(that.legendAnchor)) {
return false;
}
if (this.legendOrientation != that.legendOrientation) {
return false;
}
if (!this.renderingHints.equals(that.renderingHints)) {
return false;
}
if (this.projDist != that.projDist) {
return false;
}
return true;
}
/**
* A utility method that calculates a drawing area based on a bounding area
* and an anchor.
*
* @param dim the dimensions for the drawing area ({@code null} not
* permitted).
* @param anchor the anchor ({@code null} not permitted).
* @param bounds the bounds ({@code null} not permitted).
*
* @return A drawing area.
*/
private Rectangle2D calculateDrawArea(Dimension2D dim, Anchor2D anchor,
Rectangle2D bounds) {
Args.nullNotPermitted(dim, "dim");
Args.nullNotPermitted(anchor, "anchor");
Args.nullNotPermitted(bounds, "bounds");
double x, y;
double w = Math.min(dim.getWidth(), bounds.getWidth());
double h = Math.min(dim.getHeight(), bounds.getHeight());
if (anchor.getRefPt().equals(RefPt2D.CENTER)) {
x = bounds.getCenterX() - w / 2.0;
y = bounds.getCenterY() - h / 2.0;
} else if (anchor.getRefPt().equals(RefPt2D.CENTER_LEFT)) {
x = bounds.getX() + anchor.getOffset().getDX();
y = bounds.getCenterY() - h / 2.0;
} else if (anchor.getRefPt().equals(RefPt2D.CENTER_RIGHT)) {
x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
y = bounds.getCenterY() - h / 2.0;
} else if (anchor.getRefPt().equals(RefPt2D.TOP_CENTER)) {
x = bounds.getCenterX() - w / 2.0;
y = bounds.getY() + anchor.getOffset().getDY();
} else if (anchor.getRefPt().equals(RefPt2D.TOP_LEFT)) {
x = bounds.getX() + anchor.getOffset().getDX();
y = bounds.getY() + anchor.getOffset().getDY();
} else if (anchor.getRefPt().equals(RefPt2D.TOP_RIGHT)) {
x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
y = bounds.getY() + anchor.getOffset().getDY();
} else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_CENTER)) {
x = bounds.getCenterX() - w / 2.0;
y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
} else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_RIGHT)) {
x = bounds.getMaxX() - anchor.getOffset().getDX() - dim.getWidth();
y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
} else if (anchor.getRefPt().equals(RefPt2D.BOTTOM_LEFT)) {
x = bounds.getX() + anchor.getOffset().getDX();
y = bounds.getMaxY() - anchor.getOffset().getDY() - dim.getHeight();
} else {
x = 0.0;
y = 0.0;
}
return new Rectangle2D.Double(x, y, w, h);
}
/**
* Returns {@code true} if x is the longest of the four lengths,
* and {@code false} otherwise.
*
* @param x the x-length.
* @param a length 1.
* @param b length 2.
* @param c length 3.
*
* @return A boolean.
*/
private boolean longest(double x, double a, double b, double c) {
return x >= a && x >= b && x >= c;
}
/**
* Returns the number (0, 1 or 2) arguments that have the value
* {@code true}. We use this to examine the visibility of
* adjacent walls of the chart box...where only one wall is visible, there
* is an opportunity to display the axis along that edge.
*
* @param a boolean argument 1.
* @param b boolean argument 2.
*
* @return 0, 1, or 2.
*/
private int count(boolean a, boolean b) {
int result = 0;
if (a) {
result++;
}
if (b) {
result++;
}
return result;
}
/**
* Receives notification of a plot change event, refreshes the 3D model
* (world) and passes the event on, wrapped in a {@link Chart3DChangeEvent},
* to all registered listeners.
*
* @param event the plot change event.
*/
@Override
public void plotChanged(Plot3DChangeEvent event) {
if (event.requiresWorldUpdate()) {
this.world = null;
}
notifyListeners(new Chart3DChangeEvent(event, this));
}
@Override
public void styleChanged(ChartStyleChangeEvent event) {
ChartStyler styler = new ChartStyler(event.getChartStyle());
receive(styler);
// create a visitor that will visit all chart components and apply the
// style
notifyListeners(new Chart3DChangeEvent(event, this));
}
/**
* Registers a listener to receive notification of changes to the chart.
*
* @param listener the listener ({@code null} not permitted).
*/
public void addChangeListener(Chart3DChangeListener listener) {
this.listenerList.add(Chart3DChangeListener.class, listener);
}
/**
* Deregisters a listener so that it no longer receives notification of
* changes to the chart.
*
* @param listener the listener ({@code null} not permitted).
*/
public void removeChangeListener(Chart3DChangeListener listener) {
this.listenerList.remove(Chart3DChangeListener.class, listener);
}
/**
* Notifies all registered listeners that the chart has been modified.
*
* @param event information about the change event.
*/
public void notifyListeners(Chart3DChangeEvent event) {
// if the 'notify' flag has been switched to false, we don't notify
// the listeners
if (!this.notify) {
return;
}
Object[] listeners = this.listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == Chart3DChangeListener.class) {
((Chart3DChangeListener) listeners[i + 1]).chartChanged(event);
}
}
}
/**
* Returns a flag that controls whether or not change events are sent to
* registered listeners.
*
* @return A boolean.
*
* @see #setNotify(boolean)
*/
public boolean isNotify() {
return this.notify;
}
/**
* Sets a flag that controls whether or not listeners receive
* {@link Chart3DChangeEvent} notifications.
*
* @param notify a boolean.
*
* @see #isNotify()
*/
public void setNotify(boolean notify) {
this.notify = notify;
// if the flag is being set to true, there may be queued up changes...
if (notify) {
this.world = null;
fireChangeEvent();
}
}
/**
* Sends a {@link Chart3DChangeEvent} to all registered listeners.
*/
protected void fireChangeEvent() {
notifyListeners(new Chart3DChangeEvent(this, this));
}
/**
* Provides serialization support.
*
* @param stream the input stream.
*
* @throws IOException if there is an I/O error.
* @throws ClassNotFoundException if there is a classpath problem.
*/
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
// recreate an empty listener list
this.listenerList = new EventListenerList();
this.plot.addChangeListener(this);
// RenderingHints is not easily serialized, so we just put back the
// defaults...
this.renderingHints = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
this.renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
/**
* Returns a string representing the {@code element}, primarily for
* debugging purposes.
*
* @param element the element ({@code null} not permitted).
*
* @return A string (never {@code null}).
*/
public static String renderedElementToString(RenderedElement element) {
Object type = element.getProperty(RenderedElement.TYPE);
if (InteractiveElementType.SECTION_LABEL.equals(type)) {
StringBuilder sb = new StringBuilder();
sb.append("Section label with key '");
Object key = element.getProperty("key");
sb.append(key.toString());
sb.append("'");
return sb.toString();
} else if (InteractiveElementType.LEGEND_ITEM.equals(type)) {
StringBuilder sb = new StringBuilder();
sb.append("Legend item with section key '");
Object key = element.getProperty(Chart3D.SERIES_KEY);
sb.append(key);
sb.append("'");
return sb.toString();
} else if (InteractiveElementType.AXIS_LABEL.equals(type)) {
StringBuilder sb = new StringBuilder();
sb.append("Axis label with the label '");
sb.append(element.getProperty("label"));
sb.append("'");
return sb.toString();
} else if (InteractiveElementType.CATEGORY_AXIS_TICK_LABEL.equals(type)) {
StringBuilder sb = new StringBuilder();
sb.append("Axis tick label with the label '");
sb.append(element.getProperty("label"));
sb.append("'");
return sb.toString();
} else if (InteractiveElementType.VALUE_AXIS_TICK_LABEL.equals(type)) {
StringBuilder sb = new StringBuilder();
sb.append("Axis tick label with the value '");
sb.append(element.getProperty("value"));
sb.append("'");
return sb.toString();
} else if ("obj3d".equals(type)) {
StringBuilder sb = new StringBuilder();
sb.append("An object in the 3D model");
ItemKey itemKey = (ItemKey) element.getProperty(Object3D.ITEM_KEY);
if (itemKey != null) {
sb.append(" representing the data item [");
sb.append(itemKey.toString());
sb.append("]");
}
return sb.toString();
} else {
return element.toString();
}
}
}