/* =========================================================== * 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; 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 com.orsoncharts.ChartBox3D.ChartBoxFace; import com.orsoncharts.axis.Axis3D; import com.orsoncharts.axis.TickData; import com.orsoncharts.axis.ValueAxis3D; import com.orsoncharts.data.ItemKey; import com.orsoncharts.graphics3d.Dimension3D; import com.orsoncharts.graphics3d.DoubleSidedFace; import com.orsoncharts.graphics3d.Drawable3D; import com.orsoncharts.graphics3d.Face; import com.orsoncharts.graphics3d.FaceSorter; import com.orsoncharts.graphics3d.LabelFace; import com.orsoncharts.graphics3d.Object3D; import com.orsoncharts.graphics3d.Point3D; import com.orsoncharts.graphics3d.Utils2D; import com.orsoncharts.graphics3d.ViewPoint3D; import com.orsoncharts.graphics3d.World; import com.orsoncharts.legend.LegendAnchor; import com.orsoncharts.plot.CategoryPlot3D; import com.orsoncharts.plot.PiePlot3D; import com.orsoncharts.plot.Plot3DChangeEvent; import com.orsoncharts.plot.Plot3DChangeListener; import com.orsoncharts.plot.Plot3D; import com.orsoncharts.plot.XYZPlot; import com.orsoncharts.graphics3d.Offset2D; import com.orsoncharts.graphics3d.RenderingInfo; import com.orsoncharts.graphics3d.StandardFaceSorter; import com.orsoncharts.graphics3d.RenderedElement; import com.orsoncharts.interaction.InteractiveElementType; import com.orsoncharts.legend.LegendBuilder; import com.orsoncharts.legend.StandardLegendBuilder; import com.orsoncharts.marker.Marker; import com.orsoncharts.marker.MarkerData; import com.orsoncharts.style.ChartStyle; import com.orsoncharts.style.ChartStyleChangeEvent; import com.orsoncharts.style.ChartStyleChangeListener; import com.orsoncharts.style.ChartStyler; import com.orsoncharts.table.GradientRectanglePainter; import com.orsoncharts.table.GridElement; import com.orsoncharts.table.HAlign; import com.orsoncharts.table.TableElement; import com.orsoncharts.table.TextElement; import com.orsoncharts.table.StandardRectanglePainter; import com.orsoncharts.table.RectanglePainter; import com.orsoncharts.util.Anchor2D; import com.orsoncharts.util.ArgChecks; import com.orsoncharts.util.ObjectUtils; import com.orsoncharts.util.Orientation; import com.orsoncharts.util.RefPt2D; import com.orsoncharts.util.TextUtils; import com.orsoncharts.util.TextAnchor; /** * 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. * <br><br> * All rendering is done via the Java2D API, so this object is able to draw to * any implementation of the Graphics2D API (including * <a href="http://www.jfree.org/jfreesvg/" target="JFreeSVG">JFreeSVG</a> for * SVG output, and * <a href="http://www.object-refinery.com/pdf/" target="OrsonPDF">OrsonPDF</a> * for PDF output). * <br><br> * 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). * <br><br> * 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. * <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. * * @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) { ArgChecks.nullNotPermitted(plot, "plot"); ArgChecks.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(com.orsoncharts.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 ArgChecks.nullNotPermitted(font, "font"); ArgChecks.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(com.orsoncharts.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) { ArgChecks.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) { ArgChecks.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) { ArgChecks.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) { ArgChecks.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(com.orsoncharts.legend.LegendBuilder) * @see #setLegendAnchor(com.orsoncharts.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(com.orsoncharts.util.Anchor2D) */ public void setLegendBuilder(LegendBuilder legendBuilder) { this.legendBuilder = legendBuilder; fireChangeEvent(); } /** * Returns the legend anchor. * * @return The legend anchor (never {@code null}). * * @see #setLegendAnchor(com.orsoncharts.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) { ArgChecks.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) { ArgChecks.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(com.orsoncharts.util.Anchor2D)} and * {@link #setLegendOrientation(com.orsoncharts.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) { ArgChecks.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. * <P> * 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) { ArgChecks.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<Face> facesInPaintOrder = new ArrayList<Face>(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<String, String> m = new HashMap<String, String>(); 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<TickData> 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<TickData> 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<TickData> 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<MarkerData> fetchXMarkerData(Plot3D plot) { if (plot instanceof CategoryPlot3D) { return ((CategoryPlot3D) plot).getColumnAxis().generateMarkerData(); } if (plot instanceof XYZPlot) { return ((XYZPlot) plot).getXAxis().generateMarkerData(); } return new ArrayList<MarkerData>(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<MarkerData> fetchYMarkerData(Plot3D plot) { if (plot instanceof CategoryPlot3D) { return ((CategoryPlot3D) plot).getValueAxis().generateMarkerData(); } if (plot instanceof XYZPlot) { return ((XYZPlot) plot).getYAxis().generateMarkerData(); } return new ArrayList<MarkerData>(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<MarkerData> fetchZMarkerData(Plot3D plot) { if (plot instanceof CategoryPlot3D) { return ((CategoryPlot3D) plot).getRowAxis().generateMarkerData(); } if (plot instanceof XYZPlot) { return ((XYZPlot) plot).getZAxis().generateMarkerData(); } return new ArrayList<MarkerData>(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<TickData> xA = face.getXTicksA(); List<TickData> 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<TickData> yA = face.getYTicksA(); List<TickData> 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<TickData> zA = face.getZTicksA(); List<TickData> 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<Object3D> 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<String, String> m = new HashMap<String, String>(); 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> 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<TickData> 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<MarkerData> 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<MarkerData> 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<MarkerData> 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) { ArgChecks.nullNotPermitted(dim, "dim"); ArgChecks.nullNotPermitted(anchor, "anchor"); ArgChecks.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(); } } }