| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
| Plot2D |
|
| 2.4146341463414633;2,415 | ||||
| Plot2D$1 |
|
| 2.4146341463414633;2,415 | ||||
| Plot2D$DefaultSeries |
|
| 2.4146341463414633;2,415 | ||||
| Plot2D$Entry |
|
| 2.4146341463414633;2,415 | ||||
| Plot2D$Listeners |
|
| 2.4146341463414633;2,415 | ||||
| Plot2D$Series |
|
| 2.4146341463414633;2,415 |
| 1 | /* | |
| 2 | * Geotoolkit.org - An Open Source Java GIS Toolkit | |
| 3 | * http://www.geotoolkit.org | |
| 4 | * | |
| 5 | * (C) 1998-2012, Open Source Geospatial Foundation (OSGeo) | |
| 6 | * (C) 2009-2012, Geomatys | |
| 7 | * | |
| 8 | * This library is free software; you can redistribute it and/or | |
| 9 | * modify it under the terms of the GNU Lesser General Public | |
| 10 | * License as published by the Free Software Foundation; | |
| 11 | * version 2.1 of the License. | |
| 12 | * | |
| 13 | * This library is distributed in the hope that it will be useful, | |
| 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| 16 | * Lesser General Public License for more details. | |
| 17 | */ | |
| 18 | package org.geotoolkit.gui.swing; | |
| 19 | ||
| 20 | import java.awt.Font; | |
| 21 | import java.awt.Shape; | |
| 22 | import java.awt.Paint; | |
| 23 | import java.awt.Color; | |
| 24 | import java.awt.Stroke; | |
| 25 | import java.awt.Insets; | |
| 26 | import java.awt.Rectangle; | |
| 27 | import java.awt.Graphics2D; | |
| 28 | import java.awt.BasicStroke; | |
| 29 | import java.awt.RenderingHints; | |
| 30 | import java.awt.geom.Path2D; | |
| 31 | import java.awt.geom.Line2D; | |
| 32 | import java.awt.geom.Point2D; | |
| 33 | import java.awt.geom.Rectangle2D; | |
| 34 | import java.awt.geom.AffineTransform; | |
| 35 | import java.awt.geom.NoninvertibleTransformException; | |
| 36 | import java.awt.font.FontRenderContext; | |
| 37 | import java.awt.font.GlyphVector; | |
| 38 | import java.awt.event.ComponentEvent; | |
| 39 | import java.awt.event.ComponentAdapter; | |
| 40 | ||
| 41 | import java.util.Set; | |
| 42 | import java.util.Map; | |
| 43 | import java.util.List; | |
| 44 | import java.util.HashMap; | |
| 45 | import java.util.ArrayList; | |
| 46 | import java.util.Collections; | |
| 47 | import java.util.LinkedHashMap; | |
| 48 | import java.util.IdentityHashMap; | |
| 49 | import java.util.NoSuchElementException; | |
| 50 | ||
| 51 | import java.io.Serializable; | |
| 52 | import javax.vecmath.MismatchedSizeException; | |
| 53 | ||
| 54 | import org.geotoolkit.math.Vector; | |
| 55 | import org.geotoolkit.resources.Errors; | |
| 56 | import org.geotoolkit.display.axis.Axis2D; | |
| 57 | import org.geotoolkit.display.axis.AbstractGraduation; | |
| 58 | import org.geotoolkit.display.shape.TransformedShape; | |
| 59 | import org.geotoolkit.util.collection.UnmodifiableArrayList; | |
| 60 | import org.geotoolkit.util.converter.Numbers; | |
| 61 | import org.geotoolkit.util.logging.Logging; | |
| 62 | ||
| 63 | import static java.lang.Math.hypot; | |
| 64 | ||
| 65 | ||
| 66 | /** | |
| 67 | * Displays two axes and an arbitrary amount of series with zoom capability. | |
| 68 | * Axes may have arbitrary orientation (they don't need to be perpendicular). | |
| 69 | * It is possible for example to create a plot with a vertical <var>x</var> | |
| 70 | * axis increasing downward, like the ones used in oceanography for plotting | |
| 71 | * the data of <cite>Conductivity, Temperature, Depth</cite> (CTD) Sensors. | |
| 72 | * Axes can also be oblique for simulating 3D effects. | |
| 73 | * <p> | |
| 74 | * Axes color and font can bet set with call to {@link #setForeground} and {@link #setFont} | |
| 75 | * methods respectively. A scroll pane can be created with {@link #createScrollPane}. | |
| 76 | * The example below creates a plot with zoom capability restricted to the <var>x</var> axis: | |
| 77 | * | |
| 78 | * {@preformat java | |
| 79 | * float[] x = ...: | |
| 80 | * float[] y = ...: | |
| 81 | * Plot2D plot = new Plot2D(true, false); | |
| 82 | * plot.addXAxis("Some x values"); | |
| 83 | * plot.addYAxis("Some y values"); | |
| 84 | * plot.addSeries("Random values", Color.BLUE, x, y); | |
| 85 | * } | |
| 86 | * | |
| 87 | * <table cellspacing="24" cellpadding="12" align="center"><tr valign="top"><td> | |
| 88 | * <img src="doc-files/Plot2D.png"> | |
| 89 | * </td><td width="500" bgcolor="lightblue"> | |
| 90 | * {@section Demo} | |
| 91 | * The image on the left side gives an example of this widget appearance. | |
| 92 | * To try this component in your browser, see the | |
| 93 | * <a href="http://www.geotoolkit.org/demos/geotk-simples/applet/Plot2D.html">demonstration applet</a>. | |
| 94 | * </td></tr></table> | |
| 95 | * | |
| 96 | * @author Martin Desruisseaux (MPO, Geomatys) | |
| 97 | * @version 3.00 | |
| 98 | * | |
| 99 | * @since 1.1 | |
| 100 | * @module | |
| 101 | */ | |
| 102 | 1 | @SuppressWarnings("serial") |
| 103 | 0 | public class Plot2D extends ZoomPane { |
| 104 | /** | |
| 105 | * The axes for a given series. Instances of this class are used as values in the | |
| 106 | * {@link Plot2D#series} map. The <var>x</var> and <var>y</var> axes in this {@code Entry} | |
| 107 | * <strong>must</strong> be listed in {@link Plot2D#xAxes} and {@link Plot2D#yAxes} as well, | |
| 108 | * but the list order don't have to be the same than the {@link Plot2D#series} order. | |
| 109 | */ | |
| 110 | private static final class Entry implements Serializable { | |
| 111 | /** For cross-version compatibility. */ | |
| 112 | private static final long serialVersionUID = 1965783272889292496L; | |
| 113 | ||
| 114 | /** The <var>x</var> and <var>y</var> axis for a given series. */ | |
| 115 | public final Axis2D xAxis, yAxis; | |
| 116 | ||
| 117 | /** Constructs a new entry with the specified axis. */ | |
| 118 | 1 | public Entry(final Axis2D xAxis, final Axis2D yAxis) { |
| 119 | 1 | this.xAxis = xAxis; |
| 120 | 1 | this.yAxis = yAxis; |
| 121 | 1 | } |
| 122 | } | |
| 123 | ||
| 124 | /** | |
| 125 | * The set of <var>x</var> axes. There is usually only one axis, but more axes are allowed. | |
| 126 | * All {@code Entry.xAxis} instance <strong>must</strong> appears in this list as well, but | |
| 127 | * not necessarily in the same order. | |
| 128 | * | |
| 129 | * @see #addXAxis | |
| 130 | * @see #addSeries | |
| 131 | */ | |
| 132 | 1 | private final List<Axis2D> xAxes = new ArrayList<Axis2D>(3); |
| 133 | ||
| 134 | /** | |
| 135 | * The set of <var>y</var> axes. There is usually only one axis, but more axes are allowed. | |
| 136 | * All {@code Entry.yAxis} instance <strong>must</strong> appears in this list as well, but | |
| 137 | * not necessarily in the same order. | |
| 138 | * | |
| 139 | * @see #addYAxis | |
| 140 | * @see #addSeries | |
| 141 | */ | |
| 142 | 1 | private final List<Axis2D> yAxes = new ArrayList<Axis2D>(3); |
| 143 | ||
| 144 | /** | |
| 145 | * The set of series to plot. Keys are {@link Series} objects while values are {@code Entry} | |
| 146 | * objects with the <var>x</var> and <var>y</var> axis to use for the series. | |
| 147 | * | |
| 148 | * @see #addSeries | |
| 149 | */ | |
| 150 | 1 | private final Map<Series,Entry> series = new LinkedHashMap<Series,Entry>(); |
| 151 | ||
| 152 | /** | |
| 153 | * Immutable version of {@code series} to be returned by {@link #getSeries}. | |
| 154 | * | |
| 155 | * @see #getSeries | |
| 156 | */ | |
| 157 | 1 | private final Set<Series> unmodifiableSeries = Collections.unmodifiableSet(series.keySet()); |
| 158 | ||
| 159 | /** | |
| 160 | * The axes to use for the next series to be added to this plot, | |
| 161 | * or {@code null} if not yet created. | |
| 162 | */ | |
| 163 | private Entry currentAxes; | |
| 164 | ||
| 165 | /** | |
| 166 | * Title for next axis to be created, or {@code null} if the current axis should be used | |
| 167 | * instead. | |
| 168 | * | |
| 169 | * @see #addXAxis | |
| 170 | * @see #addYAxis | |
| 171 | */ | |
| 172 | 1 | private String nextXAxis="", nextYAxis=""; |
| 173 | ||
| 174 | /** | |
| 175 | * Bounding box of data in all series, or {@code null} if it must be recomputed. | |
| 176 | */ | |
| 177 | private transient Rectangle2D seriesBounds; | |
| 178 | ||
| 179 | /** | |
| 180 | * Margin between widget border and the drawing area. | |
| 181 | */ | |
| 182 | 1 | private int top=30, bottom=60, left=60, right=30; |
| 183 | ||
| 184 | /** | |
| 185 | * Horizontal (x) and vertival (y) offset to apply to any supplementary axis. | |
| 186 | */ | |
| 187 | 1 | private int xOffset=20, yOffset=-20; |
| 188 | ||
| 189 | /** | |
| 190 | * The widget's width and height when the graphics was rendered for the last time. | |
| 191 | */ | |
| 192 | private int lastWidth, lastHeight; | |
| 193 | ||
| 194 | /** | |
| 195 | * The plot title. | |
| 196 | */ | |
| 197 | private String title; | |
| 198 | ||
| 199 | /** | |
| 200 | * The title font. | |
| 201 | */ | |
| 202 | 1 | private Font titleFont = new Font("SansSerif", Font.BOLD, 16); |
| 203 | ||
| 204 | /** | |
| 205 | * The default cycle of colors. They are used only if the user added a series | |
| 206 | * without specifying explicitly the color to use for that series. | |
| 207 | * <p> | |
| 208 | * Those default colors may change in future Geotk versions. For safety, users are | |
| 209 | * encouraged to specify the desired color explicitly when adding a series to a plot. | |
| 210 | */ | |
| 211 | 1 | protected static final List<Color> DEFAULT_COLORS = UnmodifiableArrayList.wrap( |
| 212 | Color.RED, Color.BLUE, Color.GREEN, Color.ORANGE, Color.CYAN, Color.MAGENTA | |
| 213 | ); | |
| 214 | ||
| 215 | /** | |
| 216 | * The color to use for drawing grid lines, or {@code null} if the grid should not be drawn. | |
| 217 | */ | |
| 218 | 1 | private Color gridColor = Color.LIGHT_GRAY; |
| 219 | ||
| 220 | /** | |
| 221 | * Listener class for various events. | |
| 222 | */ | |
| 223 | 2 | private static final class Listeners extends ComponentAdapter { |
| 224 | /** | |
| 225 | * When resized, force the widget to layout its axis. | |
| 226 | */ | |
| 227 | @Override public void componentResized(final ComponentEvent event) { | |
| 228 | 0 | final Plot2D c = (Plot2D) event.getSource(); |
| 229 | 0 | c.layoutAxes(false); |
| 230 | 0 | } |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Crestes an initially empty {@code Plot2D} with | |
| 235 | * zoom capabilities on horizontal and vertical axis. | |
| 236 | */ | |
| 237 | public Plot2D() { | |
| 238 | 0 | this(SCALE_X | SCALE_Y | TRANSLATE_X | TRANSLATE_Y | RESET); |
| 239 | 0 | } |
| 240 | ||
| 241 | /** | |
| 242 | * Creates an initially empty {@code Plot2D} with | |
| 243 | * zoom capabilities on the specified axis. | |
| 244 | * | |
| 245 | * @param zoomX {@code true} for allowing zooming on the <var>x</var> axis. | |
| 246 | * @param zoomY {@code true} for allowing zooming on the <var>y</var> axis. | |
| 247 | */ | |
| 248 | public Plot2D(final boolean zoomX, final boolean zoomY) { | |
| 249 | 1 | this((zoomX ? SCALE_X | TRANSLATE_X : 0) | |
| 250 | (zoomY ? SCALE_Y | TRANSLATE_Y : 0) | RESET); | |
| 251 | 1 | } |
| 252 | ||
| 253 | /** | |
| 254 | * Construct an initially empty {@code Plot2D} with the specified zoom capacities. | |
| 255 | * | |
| 256 | * @param zoomCapacities Allowed zoom types. It can be a | |
| 257 | * bitwise combination of the following constants: | |
| 258 | * {@link #SCALE_X SCALE_X}, {@link #SCALE_Y SCALE_Y}, | |
| 259 | * {@link #TRANSLATE_X TRANSLATE_X}, {@link #TRANSLATE_Y TRANSLATE_Y}, | |
| 260 | * {@link #ROTATE ROTATE}, {@link #RESET RESET} and {@link #DEFAULT_ZOOM DEFAULT_ZOOM}. | |
| 261 | * @throws IllegalArgumentException If {@code zoomCapacities} is invalid. | |
| 262 | */ | |
| 263 | private Plot2D(final int zoomCapacities) { | |
| 264 | 1 | super(zoomCapacities); |
| 265 | 1 | super.setPaintingWhileAdjusting(true); |
| 266 | 1 | final Listeners listeners = new Listeners(); |
| 267 | 1 | super.addComponentListener(listeners); |
| 268 | 1 | } |
| 269 | ||
| 270 | /** | |
| 271 | * Adds a new <var>x</var> axis to be used for the next series to be added to this plot. | |
| 272 | * Special cases: | |
| 273 | * <p> | |
| 274 | * <ul> | |
| 275 | * <li>If this method is never invoked, then a single <var>x</var> axis with no label | |
| 276 | * will be used.</li> | |
| 277 | * <li>If this method is invoked only once before the first series is added, | |
| 278 | * then a single axis with the given label will be used.</li> | |
| 279 | * <li>If this method is invoked more than once, then many <var>x</var> axes | |
| 280 | * will be used. Additional axes will be drawn below the first axis.</li> | |
| 281 | * </ul> | |
| 282 | * | |
| 283 | * @param label The axis label, or {@code null} if the axis should not have any label. | |
| 284 | */ | |
| 285 | public void addXAxis(String label) { | |
| 286 | 1 | if (label == null) { |
| 287 | 0 | label = ""; |
| 288 | } | |
| 289 | 1 | nextXAxis = label.trim(); |
| 290 | 1 | } |
| 291 | ||
| 292 | /** | |
| 293 | * Adds a new <var>y</var> axis to be used for the next series to be added to this plot. | |
| 294 | * Special cases: | |
| 295 | * <p> | |
| 296 | * <ul> | |
| 297 | * <li>If this method is never invoked, then a single <var>y</var> axis with no label | |
| 298 | * will be used.</li> | |
| 299 | * <li>If this method is invoked only once before the first series is added, | |
| 300 | * then a single axis with the given label will be used.</li> | |
| 301 | * <li>If this method is invoked more than once, then many <var>y</var> axes | |
| 302 | * will be used. Additional axes will be drawn at the left of the first axis.</li> | |
| 303 | * </ul> | |
| 304 | * | |
| 305 | * @param label The axis label, or {@code null} if the axis should not have any label. | |
| 306 | */ | |
| 307 | public void addYAxis(String label) { | |
| 308 | 1 | if (label == null) { |
| 309 | 0 | label = ""; |
| 310 | } | |
| 311 | 1 | nextYAxis = label.trim(); |
| 312 | 1 | } |
| 313 | ||
| 314 | /** | |
| 315 | * Adds a new serie to the plot. This convenience method wraps the given arrays into {@link Vector} | |
| 316 | * objects and delegates to {@linkplain #addSeries(Map, Vector, Vector)}. | |
| 317 | * | |
| 318 | * @param name The series name, or {@code null} if none. | |
| 319 | * @param color The color to use for plotting the series, or {@code null} for a default color. | |
| 320 | * @param x The vector of <var>x</var> values. | |
| 321 | * @param y The vector of <var>y</var> values. | |
| 322 | * @return The series added. | |
| 323 | * @throws MismatchedSizeException if the arrays don't have the same length. | |
| 324 | */ | |
| 325 | public Series addSeries(final String name, final Paint color, final float[] x, final float[] y) | |
| 326 | throws MismatchedSizeException | |
| 327 | { | |
| 328 | 2 | return addSeries(properties(name, color), Vector.create(x), Vector.create(y)); |
| 329 | } | |
| 330 | ||
| 331 | /** | |
| 332 | * Adds a new serie to the plot. This convenience method wraps the given arrays into {@link Vector} | |
| 333 | * objects and delegates to {@link #addSeries(Map, Vector, Vector)}. | |
| 334 | * | |
| 335 | * @param name The series name, or {@code null} if none. | |
| 336 | * @param color The color to use for plotting the series, or {@code null} for a default color. | |
| 337 | * @param x The vector of <var>x</var> values. | |
| 338 | * @param y The vector of <var>y</var> values. | |
| 339 | * @return The series added. | |
| 340 | * @throws MismatchedSizeException if the arrays don't have the same length. | |
| 341 | */ | |
| 342 | public Series addSeries(final String name, final Paint color, final double[] x, final double[] y) | |
| 343 | throws MismatchedSizeException | |
| 344 | { | |
| 345 | 0 | return addSeries(properties(name, color), Vector.create(x), Vector.create(y)); |
| 346 | } | |
| 347 | ||
| 348 | /** | |
| 349 | * Creates a properties map for the given arguments. | |
| 350 | */ | |
| 351 | private static Map<String,Object> properties(final String name, final Paint color) { | |
| 352 | 2 | final Map<String,Object> properties = new HashMap<String,Object>(4); |
| 353 | 2 | properties.put("Name", name); |
| 354 | 2 | properties.put("Paint", color); |
| 355 | 2 | return properties; |
| 356 | } | |
| 357 | ||
| 358 | /** | |
| 359 | * Adds a new serie to the plot. This method creates a default {@link Series} implementation | |
| 360 | * for the given vectors and delegates to {@link #addSeries(Series)}. The series is configured | |
| 361 | * using the values given in the {@code properties} map. The following keys are recognized: | |
| 362 | * <p> | |
| 363 | * <ul> | |
| 364 | * <li>{@code "Name"} for a {@link String} value to be used as the {@linkplain Series#name series name}.</li> | |
| 365 | * <li>{@code "Paint"} for a {@link Paint} value to be used as the {@linkplain Series#paint series paint}.</li> | |
| 366 | * </ul> | |
| 367 | * <p> | |
| 368 | * Any keys not recognized by this method are ignored and can be used by subclasses for | |
| 369 | * their own additional information. Missing entries will be replaced by default values. | |
| 370 | * Future versions of the {@code Plot2D} class may add more keys - this method is using | |
| 371 | * a {@link Map} argument for allowing such extensibility. | |
| 372 | * | |
| 373 | * @param properties The properties to be given to the new series. | |
| 374 | * @param x The vector of <var>x</var> values. | |
| 375 | * @param y The vector of <var>y</var> values. | |
| 376 | * @return The series added. | |
| 377 | * @throws MismatchedSizeException if the arrays don't have the same length. | |
| 378 | */ | |
| 379 | public Series addSeries(Map<String,?> properties, final Vector x, final Vector y) | |
| 380 | throws MismatchedSizeException | |
| 381 | { | |
| 382 | 2 | if (properties == null) { |
| 383 | 0 | properties = Collections.emptyMap(); |
| 384 | } | |
| 385 | 2 | String name = (String) properties.get("Name"); |
| 386 | 2 | Paint color = (Paint) properties.get("Paint"); |
| 387 | 2 | if (color == null) { |
| 388 | 2 | color = DEFAULT_COLORS.get(series.size() % DEFAULT_COLORS.size()); |
| 389 | } | |
| 390 | 2 | boolean fill = Boolean.TRUE.equals(properties.get("Fill")); // Undocumented (for now) feature. |
| 391 | 2 | return addSeries(new DefaultSeries(name, color, x, y, fill)); |
| 392 | } | |
| 393 | ||
| 394 | /** | |
| 395 | * Adds a new serie to the plot. The new series will use the axes given by the last calls | |
| 396 | * to {@link #addXAxis addXAxis} and {@link #addYAxis addYAxis}. | |
| 397 | * | |
| 398 | * @param series The serie to add. | |
| 399 | * @return The added series, returned for convenience. | |
| 400 | */ | |
| 401 | public Series addSeries(final Series series) { | |
| 402 | /* | |
| 403 | * Computes the data extremums before to create axes because we need the zoom affine | |
| 404 | * transform, and the calculation of the zoom transform needs the data extremums. | |
| 405 | */ | |
| 406 | 2 | final Rectangle2D bounds = series.bounds(); |
| 407 | 2 | if (!bounds.isEmpty()) { |
| 408 | 2 | if (seriesBounds == null) { |
| 409 | 1 | seriesBounds = new Rectangle2D.Double(); |
| 410 | 1 | seriesBounds.setRect(bounds); |
| 411 | } else { | |
| 412 | 1 | seriesBounds.add(bounds); |
| 413 | } | |
| 414 | 2 | if (zoomIsReset()) { |
| 415 | 2 | reset(); // Needed for computing the zoom. |
| 416 | } | |
| 417 | } | |
| 418 | /* | |
| 419 | * Gets the axes, creating them if needed. | |
| 420 | */ | |
| 421 | final Axis2D xAxis; | |
| 422 | final Axis2D yAxis; | |
| 423 | 2 | boolean axisCreated = false; |
| 424 | try { | |
| 425 | 2 | if (nextXAxis != null) { |
| 426 | 1 | axisCreated = true; |
| 427 | 1 | xAxis = new Axis2D(); |
| 428 | 1 | layoutAxis(xAxis, xAxes.size(), true); |
| 429 | 1 | inferGraduation(xAxis, true); // Must be after layoutAxis. |
| 430 | 1 | final AbstractGraduation grad = (AbstractGraduation) xAxis.getGraduation(); |
| 431 | 1 | grad.setTitle(nextXAxis); |
| 432 | 1 | xAxes.add(xAxis); |
| 433 | 1 | nextXAxis = null; |
| 434 | 1 | } else { |
| 435 | 1 | xAxis = currentAxes.xAxis; |
| 436 | } | |
| 437 | 2 | if (nextYAxis != null) { |
| 438 | 1 | axisCreated = true; |
| 439 | 1 | yAxis = new Axis2D(); |
| 440 | 1 | layoutAxis(yAxis, yAxes.size(), false); |
| 441 | 1 | inferGraduation(yAxis, false); // Must be after layoutAxis. |
| 442 | 1 | final AbstractGraduation grad = (AbstractGraduation) yAxis.getGraduation(); |
| 443 | 1 | grad.setTitle(nextYAxis); |
| 444 | 1 | yAxes.add(yAxis); |
| 445 | 1 | nextYAxis = null; |
| 446 | 1 | } else { |
| 447 | 1 | yAxis = currentAxes.yAxis; |
| 448 | } | |
| 449 | 0 | } catch (NoninvertibleTransformException exception) { |
| 450 | 0 | throw new IllegalStateException(exception); |
| 451 | 2 | } |
| 452 | 2 | if (axisCreated) { |
| 453 | // At least one axis has been created. | |
| 454 | 1 | currentAxes = new Entry(xAxis, yAxis); |
| 455 | } | |
| 456 | 2 | this.series.put(series, currentAxes); |
| 457 | 2 | if (title == null) { |
| 458 | 1 | title = series.name(); |
| 459 | } | |
| 460 | 2 | repaint(); |
| 461 | 2 | return series; |
| 462 | } | |
| 463 | ||
| 464 | /** | |
| 465 | * Returns the set of series to be plotted. | |
| 466 | * Series are painted in the order they are returned. | |
| 467 | * | |
| 468 | * @return The series to be plotted. | |
| 469 | */ | |
| 470 | public Set<Series> getSeries() { | |
| 471 | 0 | return unmodifiableSeries; |
| 472 | } | |
| 473 | ||
| 474 | /** | |
| 475 | * Returns the color to use for drawing grid lines, or {@code null} if the grid should not | |
| 476 | * be drawn. | |
| 477 | * | |
| 478 | * @return The current grid color, or {@code null} if none. | |
| 479 | */ | |
| 480 | public Color getGridColor() { | |
| 481 | 0 | return gridColor; |
| 482 | } | |
| 483 | ||
| 484 | /** | |
| 485 | * Sets the color to use for drawing grid lines, or {@code null} if the grid should not | |
| 486 | * be drawn. | |
| 487 | * | |
| 488 | * @param color The new grid color to use, or {@code null} if none. | |
| 489 | */ | |
| 490 | public void setGridColor(final Color color) { | |
| 491 | 0 | gridColor = color; |
| 492 | 0 | } |
| 493 | ||
| 494 | /** | |
| 495 | * Returns the {<var>x</var>, <var>y</var>} axes for the specified series. | |
| 496 | * | |
| 497 | * @param series The series for which axis are wanted. | |
| 498 | * @return An array of length 2 containing <var>x</var> and <var>y</var> axis. | |
| 499 | * @throws NoSuchElementException if this widget doesn't contains the specified series. | |
| 500 | */ | |
| 501 | public Axis2D[] getAxes(final Series series) throws NoSuchElementException { | |
| 502 | 0 | final Entry entry = this.series.get(series); |
| 503 | 0 | if (entry != null) { |
| 504 | 0 | assert xAxes.indexOf(entry.xAxis) >= 0 : xAxes; |
| 505 | 0 | assert yAxes.indexOf(entry.yAxis) >= 0 : yAxes; |
| 506 | 0 | return new Axis2D[] { |
| 507 | entry.xAxis, | |
| 508 | entry.yAxis | |
| 509 | }; | |
| 510 | } | |
| 511 | 0 | throw new NoSuchElementException(series.name()); |
| 512 | } | |
| 513 | ||
| 514 | /** | |
| 515 | * Returns the minimal and maximal ordinate values of all (<var>x</var>,<var>y</var>) points | |
| 516 | * to be plotted. This is the union of the bounding boxes of {@linkplain #getSeries all series} | |
| 517 | * in this {@code Plot2D} component. | |
| 518 | * | |
| 519 | * @return The minimal and maximal ordinate values of (<var>x</var>,<var>y</var>) points. | |
| 520 | */ | |
| 521 | @Override | |
| 522 | public Rectangle2D getArea() { | |
| 523 | 4 | final Rectangle2D bounds = seriesBounds; |
| 524 | 4 | return (bounds != null) ? (Rectangle2D) bounds.clone() : null; |
| 525 | } | |
| 526 | ||
| 527 | /** | |
| 528 | * Returns the zoomable area in pixel coordinates. This area will not cover the | |
| 529 | * full widget area, since some room will be left for painting axis and titles. | |
| 530 | */ | |
| 531 | @Override | |
| 532 | protected Rectangle getZoomableBounds(Rectangle bounds) { | |
| 533 | 2 | bounds = super.getZoomableBounds(bounds); |
| 534 | 2 | bounds.x += left; |
| 535 | 2 | bounds.y += top; |
| 536 | 2 | bounds.width -= (left + right); |
| 537 | 2 | bounds.height -= (top + bottom); |
| 538 | 2 | return bounds; |
| 539 | } | |
| 540 | ||
| 541 | /** | |
| 542 | * Returns the margin between the {@linkplain #getBounds() widget bounds} and the | |
| 543 | * {@linkplain #getZoomableBounds zoomable bounds}. The zoomable bounds is the area | |
| 544 | * where the graph will be plotted. | |
| 545 | * | |
| 546 | * @return The margin between widget bounds and the area where the graph is plotted. | |
| 547 | * | |
| 548 | * @since 3.00 | |
| 549 | */ | |
| 550 | public Insets getMargin() { | |
| 551 | 0 | return new Insets(top, left, bottom, right); |
| 552 | } | |
| 553 | ||
| 554 | /** | |
| 555 | * Sets the margin between the {@linkplain #getBounds() widget bounds} and the | |
| 556 | * {@linkplain #getZoomableBounds zoomable bounds} to the given insets. | |
| 557 | * | |
| 558 | * @param margin The new margin between widget bounds and the area where the graph is plotted. | |
| 559 | * | |
| 560 | * @since 3.00 | |
| 561 | */ | |
| 562 | public void setMargin(final Insets margin) { | |
| 563 | 0 | top = margin.top; |
| 564 | 0 | left = margin.left; |
| 565 | 0 | bottom = margin.bottom; |
| 566 | 0 | right = margin.right; |
| 567 | 0 | } |
| 568 | ||
| 569 | /** | |
| 570 | * Adds the given bounds to a map of bounds. If no bounds were assigned to the given axis, | |
| 571 | * then the given bounds is copied and assigned to that axis. Otherwise - if a bounds | |
| 572 | * already exists for the given axis - then that bounds is expanded in order to contains | |
| 573 | * fully the given bounds. | |
| 574 | * | |
| 575 | * @param union The bounds computed up to date. | |
| 576 | * @param axis The axis for which the bounds is to be updated. | |
| 577 | * @param box The bounds to be added to the bounds associated to the given axis. | |
| 578 | */ | |
| 579 | private static void addAxisRange(final Map<Axis2D,Rectangle2D> unions, | |
| 580 | final Axis2D axis, final Rectangle2D bounds) | |
| 581 | { | |
| 582 | 2 | Rectangle2D union = unions.get(axis); |
| 583 | 2 | if (union != null) { |
| 584 | 0 | union.add(bounds); |
| 585 | } else { | |
| 586 | 2 | union = new Rectangle2D.Double(); |
| 587 | 2 | union.setRect(bounds); |
| 588 | 2 | unions.put(axis, union); |
| 589 | } | |
| 590 | 2 | } |
| 591 | ||
| 592 | /** | |
| 593 | * Reinitializes the affine transform {@link #zoom zoom} in order to cancel any zoom, rotation or | |
| 594 | * translation. The argument {@code yAxisUpward} indicates whether the <var>y</var> axis should | |
| 595 | * point upwards, which is usually {@code true} for a plot. | |
| 596 | */ | |
| 597 | @Override | |
| 598 | protected void reset(final Rectangle zoomableBounds, final boolean yAxisUpward) { | |
| 599 | 2 | layoutAxes(true); |
| 600 | /* | |
| 601 | * It is okay to use the same IdentityHashMap instance for both X and Y axes because the | |
| 602 | * same Axis2D instance should never be used for both axes. Note however that a plain HashMap | |
| 603 | * would not work because X and Y axis could be equal in the sense of Axis2D.equals(Object). | |
| 604 | */ | |
| 605 | 2 | final Map<Axis2D,Rectangle2D> unions = new IdentityHashMap<Axis2D,Rectangle2D>(); |
| 606 | 2 | for (final Map.Entry<Series,Entry> e : series.entrySet()) { |
| 607 | 1 | final Rectangle2D bounds = e.getKey().bounds(); |
| 608 | 1 | final Entry entry = e.getValue(); |
| 609 | 1 | addAxisRange(unions, entry.xAxis, bounds); |
| 610 | 1 | addAxisRange(unions, entry.yAxis, bounds); |
| 611 | 1 | } |
| 612 | 2 | for (final Axis2D axis : xAxes) { |
| 613 | 1 | final Rectangle2D bounds = unions.get(axis); |
| 614 | 1 | if (bounds != null) { |
| 615 | 1 | final AbstractGraduation grad = (AbstractGraduation) axis.getGraduation(); |
| 616 | 1 | grad.setMinimum(bounds.getMinX()); |
| 617 | 1 | grad.setMaximum(bounds.getMaxX()); |
| 618 | } | |
| 619 | 1 | } |
| 620 | 2 | for (final Axis2D axis : yAxes) { |
| 621 | 1 | final Rectangle2D bounds = unions.get(axis); |
| 622 | 1 | if (bounds != null) { |
| 623 | 1 | final AbstractGraduation grad = (AbstractGraduation) axis.getGraduation(); |
| 624 | 1 | grad.setMinimum(bounds.getMinY()); |
| 625 | 1 | grad.setMaximum(bounds.getMaxY()); |
| 626 | } | |
| 627 | 1 | } |
| 628 | 2 | super.reset(zoomableBounds, yAxisUpward); |
| 629 | 2 | } |
| 630 | ||
| 631 | /** | |
| 632 | * Sets axes location. This method is automatically invoked when the axes need to be layout. | |
| 633 | * This occurs for example when new axis are added, or when the component has been resized. | |
| 634 | * This method does not change the graduations. | |
| 635 | * | |
| 636 | * @param force If {@code true}, then axes orientation and position are reset to their default | |
| 637 | * value. If {@code false}, then this method tries to preserve axes orientation and | |
| 638 | * position relative to widget's border. | |
| 639 | */ | |
| 640 | private void layoutAxes(final boolean force) { | |
| 641 | 2 | final int width = getWidth(); |
| 642 | 2 | final int height = getHeight(); |
| 643 | 2 | final double tx = width - lastWidth; |
| 644 | 2 | final double ty = height - lastHeight; |
| 645 | 2 | int axisCount = 0; |
| 646 | 2 | for (final Axis2D axis : xAxes) { |
| 647 | 1 | if (force) { |
| 648 | 1 | layoutAxis(axis, axisCount, true); |
| 649 | } else { | |
| 650 | 0 | resize(axis, tx, ty); |
| 651 | } | |
| 652 | 1 | axisCount++; |
| 653 | } | |
| 654 | 2 | axisCount = 0; |
| 655 | 2 | for (final Axis2D axis : yAxes) { |
| 656 | 1 | if (force) { |
| 657 | 1 | layoutAxis(axis, axisCount, false); |
| 658 | } else { | |
| 659 | 0 | resize(axis, tx, ty); |
| 660 | } | |
| 661 | 1 | axisCount++; |
| 662 | } | |
| 663 | 2 | lastWidth = width; |
| 664 | 2 | lastHeight = height; |
| 665 | 2 | } |
| 666 | ||
| 667 | /** | |
| 668 | * Forces the layout of the given axis. This method changes only the axis position, | |
| 669 | * not the axis graduation. To change the graduation, invoke {@link #inferGraduation} | |
| 670 | * <strong>after</strong> the axis has been put at its proper location on the widget area. | |
| 671 | * | |
| 672 | * @param axis The axis to layout. | |
| 673 | * @param axisCount The index of the given axis. | |
| 674 | * @param isX {@code true} if the given axis is an X axis, or {@code false} for an Y axis. | |
| 675 | */ | |
| 676 | private void layoutAxis(final Axis2D axis, final int axisCount, final boolean isX) { | |
| 677 | 4 | final int width = super.getWidth(); |
| 678 | 4 | final int height = super.getHeight(); |
| 679 | final int x1, y1, x2, y2; | |
| 680 | 4 | x1 = left; |
| 681 | 4 | y1 = height - bottom; |
| 682 | 4 | if (isX) { |
| 683 | 2 | x2 = width - right; |
| 684 | 2 | y2 = y1; |
| 685 | } else { | |
| 686 | 2 | x2 = x1; |
| 687 | 2 | y2 = top; |
| 688 | } | |
| 689 | 4 | axis.setLabelClockwise(isX); |
| 690 | 4 | axis.setLine(x1, y1, x2, y2); |
| 691 | 4 | translatePerpendicularly(axis, xOffset*axisCount, yOffset*axisCount); |
| 692 | 4 | } |
| 693 | ||
| 694 | /** | |
| 695 | * Translates an axis in a perpendicular direction to its orientation. | |
| 696 | * The following rules applies: | |
| 697 | * <p> | |
| 698 | * <ul> | |
| 699 | * <li>If the axis is vertical, then the axis is translated horizontally | |
| 700 | * by {@code tx} only. The {@code ty} argument is ignored.</li> | |
| 701 | * <li>If the axis is horizontal, then the axis is translated vertically | |
| 702 | * by {@code ty} only. The {@code tx} argument is ignored.</li> | |
| 703 | * <li>If the axis is diagonal, then the axis is translated using the following | |
| 704 | * formula (<var>theta</var> is the axis orientation relative to the horizontal): | |
| 705 | * | |
| 706 | * {@preformat math | |
| 707 | * dx = tx*sin(theta) | |
| 708 | * dy = ty*cos(theta) | |
| 709 | * } | |
| 710 | * </li> | |
| 711 | * </ul> | |
| 712 | */ | |
| 713 | private static void translatePerpendicularly(final Axis2D axis, final double tx, final double ty) { | |
| 714 | 4 | final double x1 = axis.getX1(); |
| 715 | 4 | final double y1 = axis.getY1(); |
| 716 | 4 | final double x2 = axis.getX2(); |
| 717 | 4 | final double y2 = axis.getY2(); |
| 718 | 4 | double dy = x2 - x1; // Note: dx and dy are really swapped - this is not an error. |
| 719 | 4 | double dx = y1 - y2; |
| 720 | 4 | double length = hypot(dx, dy); |
| 721 | 4 | dx *= tx/length; |
| 722 | 4 | dy *= ty/length; |
| 723 | 4 | axis.setLine(x1+dx, y1+dy, x2+dx, y2+dy); |
| 724 | 4 | } |
| 725 | ||
| 726 | /** | |
| 727 | * Invoked when this component has been resized. This method adjust axis length while | |
| 728 | * preserving their orientation and position relative to border. | |
| 729 | * | |
| 730 | * @param axis The axis to adjust. | |
| 731 | * @param tx The change in component width. | |
| 732 | * @param ty The change in component height. | |
| 733 | */ | |
| 734 | private static void resize(final Axis2D axis, final double tx, final double ty) { | |
| 735 | 0 | final Point2D P1 = axis.getP1(); |
| 736 | 0 | final Point2D P2 = axis.getP2(); |
| 737 | final Point2D anchor, moveable; | |
| 738 | 0 | if (distance(P1) <= distance(P2)) { |
| 739 | 0 | anchor = P1; |
| 740 | 0 | moveable = P2; |
| 741 | } else { | |
| 742 | 0 | anchor = P2; |
| 743 | 0 | moveable = P1; |
| 744 | } | |
| 745 | 0 | final double x = moveable.getX(); |
| 746 | 0 | final double y = moveable.getY(); |
| 747 | 0 | final double dx = x-anchor.getX(); |
| 748 | 0 | final double dy = y-anchor.getY(); |
| 749 | 0 | final double length = hypot(dx, dy); |
| 750 | 0 | moveable.setLocation(x + tx*dx/length, |
| 751 | y + ty*dy/length); | |
| 752 | 0 | axis.setLine(P1, P2); |
| 753 | 0 | } |
| 754 | ||
| 755 | /** | |
| 756 | * Returns the distance from the origin (0,0) to the given point. We compute the distance | |
| 757 | * instead then the square of the distance because the later may overflow, while the Java | |
| 758 | * {@link Math#hypot} implementation is designated for avoiding such overflow. | |
| 759 | */ | |
| 760 | private static double distance(final Point2D point) { | |
| 761 | 0 | return hypot(point.getX(), point.getY()); |
| 762 | } | |
| 763 | ||
| 764 | /** | |
| 765 | * Changes the {@linkplain #zoom zoom} by applying an affine transform. The {@code change} | |
| 766 | * transform must express a change in the units of the {@linkplain #addSeries(Series) series | |
| 767 | * added} to this widget. The location of axes will <strong>not</strong> change as a result | |
| 768 | * of the given transform. Instead, the axis graduations will be updated with new minimal | |
| 769 | * and maximal values matching the new zoom. | |
| 770 | */ | |
| 771 | @Override | |
| 772 | public void transform(final AffineTransform change) { | |
| 773 | 0 | super.transform(change); |
| 774 | try { | |
| 775 | /* | |
| 776 | * The affine transform from "data" to "pixel" coordinates changed. If we assume that | |
| 777 | * the axes position don't change, then the graduations need to be updated in order | |
| 778 | * to reflect the affine transform change. We perform this update by converting the | |
| 779 | * axes coordinates from pixel units to data units. By definition, the (x,y) values | |
| 780 | * of axes end-points in "data" units are the extremums on the X and Y axes respectively. | |
| 781 | */ | |
| 782 | 0 | for (final Axis2D axis : xAxes) { |
| 783 | 0 | inferGraduation(axis, true); |
| 784 | } | |
| 785 | 0 | for (final Axis2D axis : yAxes) { |
| 786 | 0 | inferGraduation(axis, false); |
| 787 | } | |
| 788 | 0 | } catch (NoninvertibleTransformException exception) { |
| 789 | 0 | Logging.unexpectedException(Plot2D.class, "transform", exception); |
| 790 | 0 | } |
| 791 | 0 | repaint(); |
| 792 | 0 | } |
| 793 | ||
| 794 | /** | |
| 795 | * Sets the graduation of the given axis according its current position. The following | |
| 796 | * conditions must be hold before this method is invoked: | |
| 797 | * <p> | |
| 798 | * <ul> | |
| 799 | * <li>The {@link #zoom} transform must be set to the "data to pixels" transform.</li> | |
| 800 | * <li>The axis must be at its proper location in the widget area, typically through a | |
| 801 | * call to {@link #layoutAxis} before this method call}.</li> | |
| 802 | * </ul> | |
| 803 | * | |
| 804 | * @param axis The axis for which to set the graduation. | |
| 805 | * @param isX {@code true} if the given axis is a X axis. | |
| 806 | */ | |
| 807 | private void inferGraduation(final Axis2D axis, final boolean isX) throws NoninvertibleTransformException { | |
| 808 | 2 | Point2D P1 = axis.getP1(); |
| 809 | 2 | Point2D P2 = axis.getP2(); |
| 810 | 2 | P1 = zoom.inverseTransform(P1, P1); |
| 811 | 2 | P2 = zoom.inverseTransform(P2, P2); |
| 812 | double min, max; | |
| 813 | 2 | if (isX) { |
| 814 | 1 | min = P1.getX(); |
| 815 | 1 | max = P2.getX(); |
| 816 | } else { | |
| 817 | 1 | min = P1.getY(); |
| 818 | 1 | max = P2.getY(); |
| 819 | } | |
| 820 | 2 | if (min > max) { |
| 821 | 2 | final double tmp = max; |
| 822 | 2 | max = min; |
| 823 | 2 | min = tmp; |
| 824 | } | |
| 825 | 2 | final AbstractGraduation grad = (AbstractGraduation) axis.getGraduation(); |
| 826 | 2 | grad.setMinimum(min); |
| 827 | 2 | grad.setMaximum(max); |
| 828 | 2 | } |
| 829 | ||
| 830 | /** | |
| 831 | * Paints the axes and all series. At the opposite of typical {@link ZoomPane} subclasses, this | |
| 832 | * method does not use directly the {@linkplain #zoom zoom} transform. The zoom is honored only | |
| 833 | * indirectly since the axis graduations have been determined from the zoom by the | |
| 834 | * {@link #transform(AffineTransform)} method. | |
| 835 | */ | |
| 836 | @Override | |
| 837 | protected void paintComponent(final Graphics2D graphics) { | |
| 838 | 0 | if (xAxes.isEmpty() || yAxes.isEmpty()) { |
| 839 | 0 | return; |
| 840 | } | |
| 841 | 0 | final Rectangle bounds = getZoomableBounds(null); |
| 842 | 0 | final Stroke oldStroke = graphics.getStroke(); |
| 843 | 0 | final Paint oldPaint = graphics.getPaint(); |
| 844 | 0 | final Shape oldClip = graphics.getClip(); |
| 845 | 0 | final Font oldFont = graphics.getFont(); |
| 846 | 0 | final Object oldHint = graphics.getRenderingHint(RenderingHints.KEY_ANTIALIASING); |
| 847 | 0 | graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
| 848 | /* | |
| 849 | * Draws the grid lines before to paint the series. We use the first (x,y) | |
| 850 | * axes since they are the closest ones to the center of the graph area. | |
| 851 | */ | |
| 852 | 0 | final Axis2D xAxis = xAxes.get(0); |
| 853 | 0 | final Axis2D yAxis = yAxes.get(0); |
| 854 | 0 | final double xx1 = xAxis.getX1(); |
| 855 | 0 | final double xy1 = xAxis.getY1(); |
| 856 | 0 | final double xx2 = xAxis.getX2(); |
| 857 | 0 | final double xy2 = xAxis.getY2(); |
| 858 | 0 | final double yx1 = yAxis.getX1(); |
| 859 | 0 | final double yy1 = yAxis.getY1(); |
| 860 | 0 | final double yx2 = yAxis.getX2(); |
| 861 | 0 | final double yy2 = yAxis.getY2(); |
| 862 | 0 | final double xxD = (yx2 - yx1); |
| 863 | 0 | final double xyD = (yy2 - yy1); |
| 864 | 0 | final double yxD = (xx2 - xx1); |
| 865 | 0 | final double yyD = (xy2 - xy1); |
| 866 | 0 | final Line2D line = new Line2D.Double(); |
| 867 | 0 | if (gridColor != null) { |
| 868 | 0 | graphics.setPaint(gridColor); |
| 869 | 0 | final Point2D.Double point = new Point2D.Double(); |
| 870 | 0 | Axis2D.TickIterator tk = xAxis.new TickIterator(null); |
| 871 | 0 | for (tk.nextMajor(); !tk.isDone(); tk.nextMajor()) { |
| 872 | 0 | tk.currentPosition(point); |
| 873 | 0 | line.setLine(point.x, point.y, point.x + xxD, point.y + xyD); |
| 874 | 0 | graphics.draw(line); |
| 875 | } | |
| 876 | 0 | tk = yAxis.new TickIterator(null); |
| 877 | 0 | for (tk.nextMajor(); !tk.isDone(); tk.nextMajor()) { |
| 878 | 0 | tk.currentPosition(point); |
| 879 | 0 | line.setLine(point.x, point.y, point.x + yxD, point.y + yyD); |
| 880 | 0 | graphics.draw(line); |
| 881 | } | |
| 882 | } | |
| 883 | /* | |
| 884 | * Paints series first. | |
| 885 | */ | |
| 886 | 0 | graphics.clip(bounds); |
| 887 | 0 | graphics.setStroke(new BasicStroke((float) (1/getGraphicsScale()))); |
| 888 | 0 | final TransformedShape transformed = new TransformedShape(); |
| 889 | 0 | for (final Map.Entry<Series,Entry> e : series.entrySet()) { |
| 890 | 0 | final Series series = e.getKey(); |
| 891 | 0 | final Entry entry = e.getValue(); |
| 892 | 0 | final AffineTransform transform = Axis2D.createAffineTransform(entry.xAxis, entry.yAxis); |
| 893 | 0 | final Shape path = series.path(); |
| 894 | 0 | transformed.setTransform(transform); |
| 895 | 0 | transformed.setOriginalShape(path); |
| 896 | 0 | graphics.setPaint(series.paint()); |
| 897 | 0 | if (series instanceof DefaultSeries && ((DefaultSeries) series).fill) { |
| 898 | 0 | graphics.fill(transformed); |
| 899 | } else { | |
| 900 | 0 | graphics.draw(transformed); |
| 901 | } | |
| 902 | 0 | } |
| 903 | /* | |
| 904 | * Paints axes on top of series, then paint the remainder of the box | |
| 905 | * around the graph area. | |
| 906 | */ | |
| 907 | 0 | graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldHint); |
| 908 | 0 | graphics.setStroke(oldStroke); |
| 909 | 0 | graphics.setPaint(getForeground()); |
| 910 | 0 | graphics.setFont(getFont()); |
| 911 | 0 | graphics.setClip(oldClip); |
| 912 | 0 | for (final Axis2D axis : xAxes) { |
| 913 | 0 | axis.paint(graphics); |
| 914 | } | |
| 915 | 0 | for (final Axis2D axis : yAxes) { |
| 916 | 0 | axis.paint(graphics); |
| 917 | } | |
| 918 | 0 | line.setLine(xx2, xy2, xx2+xxD, xy2+xyD); graphics.draw(line); |
| 919 | 0 | line.setLine(yx2, yy2, yx2+yxD, yy2+yyD); graphics.draw(line); |
| 920 | /* | |
| 921 | * Paints the title. | |
| 922 | */ | |
| 923 | 0 | if (title != null) { |
| 924 | 0 | final FontRenderContext context = graphics.getFontRenderContext(); |
| 925 | 0 | final GlyphVector glyphs = titleFont.createGlyphVector(context, title); |
| 926 | 0 | final Rectangle2D titleBounds = glyphs.getVisualBounds(); |
| 927 | 0 | graphics.drawGlyphVector(glyphs, (float) ((getWidth() - titleBounds.getWidth()) / 2), 20); |
| 928 | } | |
| 929 | 0 | graphics.setPaint(oldPaint); |
| 930 | 0 | graphics.setFont(oldFont); |
| 931 | 0 | } |
| 932 | ||
| 933 | /** | |
| 934 | * Removes all axes and series from this plot. | |
| 935 | */ | |
| 936 | public void clear() { | |
| 937 | 0 | series.clear(); |
| 938 | 0 | xAxes .clear(); |
| 939 | 0 | yAxes .clear(); |
| 940 | 0 | nextXAxis = ""; |
| 941 | 0 | nextYAxis = ""; |
| 942 | 0 | seriesBounds = null; |
| 943 | 0 | currentAxes = null; |
| 944 | 0 | repaint(); |
| 945 | 0 | } |
| 946 | ||
| 947 | /** | |
| 948 | * A series to be displayed in a {@link Plot2D} widget. A {@code Series} contains the data | |
| 949 | * to plot as a {@link Shape} object and the {@link Paint} to use for drawing the lines. | |
| 950 | * | |
| 951 | * @author Martin Desruisseaux (MPO, Geomatys) | |
| 952 | * @version 3.00 | |
| 953 | * | |
| 954 | * @since 1.1 | |
| 955 | * @module | |
| 956 | */ | |
| 957 | public interface Series { | |
| 958 | /** | |
| 959 | * Returns the name of this series. If only one series is plotted, | |
| 960 | * then the name of that series will be used as the plot title. | |
| 961 | * | |
| 962 | * @return The name of this series, or {@code null} if none. | |
| 963 | */ | |
| 964 | String name(); | |
| 965 | ||
| 966 | /** | |
| 967 | * Returns the color to use for plotting this series. | |
| 968 | * | |
| 969 | * @return The color to use for plotting this series. | |
| 970 | */ | |
| 971 | Paint paint(); | |
| 972 | ||
| 973 | /** | |
| 974 | * Returns the bounding box of all <var>x</var> and <var>y</var> ordinates. | |
| 975 | * | |
| 976 | * @return The minimal and maximal (<var>x</var>, <var>y</var>) values. | |
| 977 | */ | |
| 978 | Rectangle2D bounds(); | |
| 979 | ||
| 980 | /** | |
| 981 | * Returns the series data as a path. | |
| 982 | * | |
| 983 | * @return The (<var>x</var>,<var>y</var>) coordinates as a Java2D {@linkplain Shape shape}. | |
| 984 | */ | |
| 985 | Shape path(); | |
| 986 | } | |
| 987 | ||
| 988 | /** | |
| 989 | * Default implementation of {@link Plot2D.Series}. | |
| 990 | * | |
| 991 | * @author Martin Desruisseaux (MPO, Geomatys) | |
| 992 | * @version 3.00 | |
| 993 | * | |
| 994 | * @since 1.1 | |
| 995 | * @module | |
| 996 | */ | |
| 997 | private static final class DefaultSeries implements Series { | |
| 998 | /** | |
| 999 | * The series name. | |
| 1000 | */ | |
| 1001 | private final String name; | |
| 1002 | ||
| 1003 | /** | |
| 1004 | * The color. | |
| 1005 | */ | |
| 1006 | private final Paint color; | |
| 1007 | ||
| 1008 | /** | |
| 1009 | * The path, which may be float or double precision. | |
| 1010 | */ | |
| 1011 | private final Path2D path; | |
| 1012 | ||
| 1013 | /** | |
| 1014 | * The minimal and maximum (<var>x</var>,<var>y</var>) values. | |
| 1015 | */ | |
| 1016 | private final Rectangle2D bounds; | |
| 1017 | ||
| 1018 | /** | |
| 1019 | * {@code true} if the {@linkplain #path path} is a closed polygon which should be painted | |
| 1020 | * using {@link Graphics2D#fill(Shape)} instead of {@link Graphics2D#draw(Shape)}. This is | |
| 1021 | * not a public API at this time. The usual value is {@code false}. | |
| 1022 | */ | |
| 1023 | final boolean fill; | |
| 1024 | ||
| 1025 | /** | |
| 1026 | * Constructs a series with the given name and (<var>x</var>,<var>y</var>) vectors. | |
| 1027 | * | |
| 1028 | * @throws MismatchedSizeException if the arrays don't have the same length. | |
| 1029 | */ | |
| 1030 | public DefaultSeries(final String name, final Paint color, final Vector x, final Vector y, boolean fill) | |
| 1031 | throws MismatchedSizeException | |
| 1032 | 2 | { |
| 1033 | 2 | this.name = name; |
| 1034 | 2 | this.color = color; |
| 1035 | 2 | final int length = x.size(); |
| 1036 | 2 | if (length != y.size()) { |
| 1037 | 0 | throw new MismatchedSizeException(Errors.format(Errors.Keys.MISMATCHED_ARRAY_LENGTH_$2, "x", "y")); |
| 1038 | } | |
| 1039 | /* | |
| 1040 | * Creates a Path2D of Float type if it is sufficient | |
| 1041 | * for the provided data, or of Double tpe otherwise. | |
| 1042 | */ | |
| 1043 | 2 | final Class<?> type = Numbers.widestClass( |
| 1044 | Numbers.primitiveToWrapper(x.getElementType()).asSubclass(Number.class), | |
| 1045 | Numbers.primitiveToWrapper(y.getElementType()).asSubclass(Number.class)); | |
| 1046 | 2 | if (type == Double.class || type == Long.class) { |
| 1047 | 0 | path = new Path2D.Double(); |
| 1048 | } else { | |
| 1049 | 2 | path = new Path2D.Float(); |
| 1050 | } | |
| 1051 | /* | |
| 1052 | * Creates the shape. | |
| 1053 | */ | |
| 1054 | 2 | boolean move = true; |
| 1055 | 1602 | for (int i=0; i<length; i++) { |
| 1056 | 1600 | double xi = x.doubleValue(i); |
| 1057 | 1600 | double yi = y.doubleValue(i); |
| 1058 | 1600 | if (Double.isNaN(yi) || Double.isNaN(xi)) { |
| 1059 | 0 | if (!move) { |
| 1060 | 0 | fill = false; // We will not be able to close the shape. |
| 1061 | 0 | move = true; |
| 1062 | } | |
| 1063 | continue; | |
| 1064 | } | |
| 1065 | 1600 | if (move) { |
| 1066 | 2 | move = false; |
| 1067 | 2 | path.moveTo(xi, yi); |
| 1068 | } else { | |
| 1069 | 1598 | path.lineTo(xi, yi); |
| 1070 | } | |
| 1071 | } | |
| 1072 | 2 | if (fill) { |
| 1073 | 0 | path.closePath(); |
| 1074 | } | |
| 1075 | 2 | this.fill = fill; |
| 1076 | 2 | bounds = path.getBounds2D(); |
| 1077 | 2 | } |
| 1078 | ||
| 1079 | /** | |
| 1080 | * Returns the series name. | |
| 1081 | */ | |
| 1082 | @Override | |
| 1083 | public String name() { | |
| 1084 | 1 | return name; |
| 1085 | } | |
| 1086 | ||
| 1087 | /** | |
| 1088 | * Returns the color for this series. | |
| 1089 | */ | |
| 1090 | @Override | |
| 1091 | public Paint paint() { | |
| 1092 | 0 | return color; |
| 1093 | } | |
| 1094 | ||
| 1095 | /** | |
| 1096 | * Returns the minimal and maximum (<var>x</var>,<var>y</var>) values. | |
| 1097 | */ | |
| 1098 | @Override | |
| 1099 | public Rectangle2D bounds() { | |
| 1100 | 3 | return (Rectangle2D) bounds.clone(); |
| 1101 | } | |
| 1102 | ||
| 1103 | /** | |
| 1104 | * Returns the series data as a path. This method does not clone the path for | |
| 1105 | * performance reason. However since the {@link Shape} interface doesn't provide | |
| 1106 | * setter methods, it should be reasonable. | |
| 1107 | */ | |
| 1108 | @Override | |
| 1109 | public Shape path() { | |
| 1110 | 0 | return path; |
| 1111 | } | |
| 1112 | ||
| 1113 | /** | |
| 1114 | * Returns a string representation for debugging purpose. | |
| 1115 | */ | |
| 1116 | @Override | |
| 1117 | public String toString() { | |
| 1118 | 0 | final Rectangle2D bounds = this.bounds; |
| 1119 | 0 | return "Series[\"" + name + "\", " + |
| 1120 | "x=[" + bounds.getMinX() + " \u2026 " + bounds.getMaxX() + "], " + | |
| 1121 | "y=[" + bounds.getMinY() + " \u2026 " + bounds.getMaxY() + "]]"; | |
| 1122 | } | |
| 1123 | } | |
| 1124 | } |