Coverage Report - org.geotoolkit.gui.swing.Plot2D
 
Classes in this File Line Coverage Branch Coverage Complexity
Plot2D
56 %
171/305
42 %
39/92
2,415
Plot2D$1
N/A
N/A
2,415
Plot2D$DefaultSeries
69 %
23/33
55 %
10/18
2,415
Plot2D$Entry
100 %
4/4
N/A
2,415
Plot2D$Listeners
25 %
1/4
N/A
2,415
Plot2D$Series
N/A
N/A
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  
 }