1 /*
   2  * Copyright (c) 2004, 2008, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.tools.jconsole;
  27 
  28 import java.awt.*;
  29 import java.awt.event.*;
  30 import java.beans.*;
  31 import java.io.*;
  32 import java.lang.reflect.Array;
  33 import java.util.*;
  34 
  35 import javax.accessibility.*;
  36 import javax.swing.*;
  37 import javax.swing.border.*;
  38 import javax.swing.filechooser.*;
  39 import javax.swing.filechooser.FileFilter;
  40 
  41 import sun.tools.jconsole.resources.Messages;
  42 
  43 import com.sun.tools.jconsole.JConsoleContext;
  44 
  45 import static sun.tools.jconsole.Formatter.*;
  46 import static sun.tools.jconsole.ProxyClient.*;
  47 
  48 @SuppressWarnings("serial")
  49 public class Plotter extends JComponent
  50                      implements Accessible, ActionListener, PropertyChangeListener {
  51 
  52     public static enum Unit {
  53         NONE, BYTES, PERCENT
  54     }
  55 
  56     static final String[] rangeNames = {
  57         Messages.ONE_MIN,
  58         Messages.FIVE_MIN,
  59         Messages.TEN_MIN,
  60         Messages.THIRTY_MIN,
  61         Messages.ONE_HOUR,
  62         Messages.TWO_HOURS,
  63         Messages.THREE_HOURS,
  64         Messages.SIX_HOURS,
  65         Messages.TWELVE_HOURS,
  66         Messages.ONE_DAY,
  67         Messages.SEVEN_DAYS,
  68         Messages.ONE_MONTH,
  69         Messages.THREE_MONTHS,
  70         Messages.SIX_MONTHS,
  71         Messages.ONE_YEAR,
  72         Messages.ALL
  73     };
  74 
  75     static final int[] rangeValues = {
  76         1,
  77         5,
  78         10,
  79         30,
  80         1 * 60,
  81         2 * 60,
  82         3 * 60,
  83         6 * 60,
  84         12 * 60,
  85         1 * 24 * 60,
  86         7 * 24 * 60,
  87         1 * 31 * 24 * 60,
  88         3 * 31 * 24 * 60,
  89         6 * 31 * 24 * 60,
  90         366 * 24 * 60,
  91         -1
  92     };
  93 
  94 
  95     final static long SECOND = 1000;
  96     final static long MINUTE = 60 * SECOND;
  97     final static long HOUR   = 60 * MINUTE;
  98     final static long DAY    = 24 * HOUR;
  99 
 100     final static Color bgColor = new Color(250, 250, 250);
 101     final static Color defaultColor = Color.blue.darker();
 102 
 103     final static int ARRAY_SIZE_INCREMENT = 4000;
 104 
 105     private static Stroke dashedStroke;
 106 
 107     private TimeStamps times = new TimeStamps();
 108     private ArrayList<Sequence> seqs = new ArrayList<Sequence>();
 109     private JPopupMenu popupMenu;
 110     private JMenu timeRangeMenu;
 111     private JRadioButtonMenuItem[] menuRBs;
 112     private JMenuItem saveAsMI;
 113     private JFileChooser saveFC;
 114 
 115     private int viewRange = -1; // Minutes (value <= 0 means full range)
 116     private Unit unit;
 117     private int decimals;
 118     private double decimalsMultiplier;
 119     private Border border = null;
 120     private Rectangle r = new Rectangle(1, 1, 1, 1);
 121     private Font smallFont = null;
 122 
 123     // Initial margins, may be recalculated as needed
 124     private int topMargin = 10;
 125     private int bottomMargin = 45;
 126     private int leftMargin = 65;
 127     private int rightMargin = 70;
 128     private final boolean displayLegend;
 129 
 130     public Plotter() {
 131         this(Unit.NONE, 0);
 132     }
 133 
 134     public Plotter(Unit unit) {
 135         this(unit, 0);
 136     }
 137 
 138     public Plotter(Unit unit, int decimals) {
 139         this(unit,decimals,true);
 140     }
 141 
 142     // Note: If decimals > 0 then values must be decimally shifted left
 143     // that many places, i.e. multiplied by Math.pow(10.0, decimals).
 144     public Plotter(Unit unit, int decimals, boolean displayLegend) {
 145         this.displayLegend = displayLegend;
 146         setUnit(unit);
 147         setDecimals(decimals);
 148 
 149         enableEvents(AWTEvent.MOUSE_EVENT_MASK);
 150 
 151         addMouseListener(new MouseAdapter() {
 152             @Override
 153             public void mousePressed(MouseEvent e) {
 154                 if (getParent() instanceof PlotterPanel) {
 155                     getParent().requestFocusInWindow();
 156                 }
 157             }
 158         });
 159 
 160     }
 161 
 162     public void setUnit(Unit unit) {
 163         this.unit = unit;
 164     }
 165 
 166     public void setDecimals(int decimals) {
 167         this.decimals = decimals;
 168         this.decimalsMultiplier = Math.pow(10.0, decimals);
 169     }
 170 
 171     public void createSequence(String key, String name, Color color, boolean isPlotted) {
 172         Sequence seq = getSequence(key);
 173         if (seq == null) {
 174             seq = new Sequence(key);
 175         }
 176         seq.name = name;
 177         seq.color = (color != null) ? color : defaultColor;
 178         seq.isPlotted = isPlotted;
 179 
 180         seqs.add(seq);
 181     }
 182 
 183     public void setUseDashedTransitions(String key, boolean b) {
 184         Sequence seq = getSequence(key);
 185         if (seq != null) {
 186             seq.transitionStroke = b ? getDashedStroke() : null;
 187         }
 188     }
 189 
 190     public void setIsPlotted(String key, boolean isPlotted) {
 191         Sequence seq = getSequence(key);
 192         if (seq != null) {
 193             seq.isPlotted = isPlotted;
 194         }
 195     }
 196 
 197     // Note: If decimals > 0 then values must be decimally shifted left
 198     // that many places, i.e. multiplied by Math.pow(10.0, decimals).
 199     public synchronized void addValues(long time, long... values) {
 200         assert (values.length == seqs.size());
 201         times.add(time);
 202         for (int i = 0; i < values.length; i++) {
 203             seqs.get(i).add(values[i]);
 204         }
 205         repaint();
 206     }
 207 
 208     private Sequence getSequence(String key) {
 209         for (Sequence seq : seqs) {
 210             if (seq.key.equals(key)) {
 211                 return seq;
 212             }
 213         }
 214         return null;
 215     }
 216 
 217     /**
 218      * @return the displayed time range in minutes, or -1 for all data
 219      */
 220     public int getViewRange() {
 221         return viewRange;
 222     }
 223 
 224     /**
 225      * @param minutes the displayed time range in minutes, or -1 to diaplay all data
 226      */
 227     public void setViewRange(int minutes) {
 228         if (minutes != viewRange) {
 229             int oldValue = viewRange;
 230             viewRange = minutes;
 231             /* Do not i18n this string */
 232             firePropertyChange("viewRange", oldValue, viewRange);
 233             if (popupMenu != null) {
 234                 for (int i = 0; i < menuRBs.length; i++) {
 235                     if (rangeValues[i] == viewRange) {
 236                         menuRBs[i].setSelected(true);
 237                         break;
 238                     }
 239                 }
 240             }
 241             repaint();
 242         }
 243     }
 244 
 245     @Override
 246     public JPopupMenu getComponentPopupMenu() {
 247         if (popupMenu == null) {
 248             popupMenu = new JPopupMenu(Messages.CHART_COLON);
 249             timeRangeMenu = new JMenu(Messages.PLOTTER_TIME_RANGE_MENU);
 250             timeRangeMenu.setMnemonic(Resources.getMnemonicInt(Messages.PLOTTER_TIME_RANGE_MENU));
 251             popupMenu.add(timeRangeMenu);
 252             menuRBs = new JRadioButtonMenuItem[rangeNames.length];
 253             ButtonGroup rbGroup = new ButtonGroup();
 254             for (int i = 0; i < rangeNames.length; i++) {
 255                 menuRBs[i] = new JRadioButtonMenuItem(rangeNames[i]);
 256                 rbGroup.add(menuRBs[i]);
 257                 menuRBs[i].addActionListener(this);
 258                 if (viewRange == rangeValues[i]) {
 259                     menuRBs[i].setSelected(true);
 260                 }
 261                 timeRangeMenu.add(menuRBs[i]);
 262             }
 263 
 264             popupMenu.addSeparator();
 265 
 266             saveAsMI = new JMenuItem(Messages.PLOTTER_SAVE_AS_MENU_ITEM);
 267             saveAsMI.setMnemonic(Resources.getMnemonicInt(Messages.PLOTTER_SAVE_AS_MENU_ITEM));
 268             saveAsMI.addActionListener(this);
 269             popupMenu.add(saveAsMI);
 270         }
 271         return popupMenu;
 272     }
 273 
 274     public void actionPerformed(ActionEvent ev) {
 275         JComponent src = (JComponent)ev.getSource();
 276         if (src == saveAsMI) {
 277             saveAs();
 278         } else {
 279             int index = timeRangeMenu.getPopupMenu().getComponentIndex(src);
 280             setViewRange(rangeValues[index]);
 281         }
 282     }
 283 
 284     private void saveAs() {
 285         if (saveFC == null) {
 286             saveFC = new SaveDataFileChooser();
 287         }
 288         int ret = saveFC.showSaveDialog(this);
 289         if (ret == JFileChooser.APPROVE_OPTION) {
 290             saveDataToFile(saveFC.getSelectedFile());
 291         }
 292     }
 293 
 294     private void saveDataToFile(File file) {
 295         try {
 296             PrintStream out = new PrintStream(new FileOutputStream(file));
 297 
 298             // Print header line
 299             out.print("Time");
 300             for (Sequence seq : seqs) {
 301                 out.print(","+seq.name);
 302             }
 303             out.println();
 304 
 305             // Print data lines
 306             if (seqs.size() > 0 && seqs.get(0).size > 0) {
 307                 for (int i = 0; i < seqs.get(0).size; i++) {
 308                     double excelTime = toExcelTime(times.time(i));
 309                     out.print(String.format(Locale.ENGLISH, "%.6f", excelTime));
 310                     for (Sequence seq : seqs) {
 311                         out.print("," + getFormattedValue(seq.value(i), false));
 312                     }
 313                     out.println();
 314                 }
 315             }
 316 
 317             out.close();
 318             JOptionPane.showMessageDialog(this,
 319                                           Resources.format(Messages.FILE_CHOOSER_SAVED_FILE,
 320                                                   file.getAbsolutePath(),
 321                                                   file.length()));
 322         } catch (IOException ex) {
 323             String msg = ex.getLocalizedMessage();
 324             String path = file.getAbsolutePath();
 325             if (msg.startsWith(path)) {
 326                 msg = msg.substring(path.length()).trim();
 327             }
 328             JOptionPane.showMessageDialog(this,
 329                     Resources.format(Messages.FILE_CHOOSER_SAVE_FAILED_MESSAGE,
 330                                                   path, msg),
 331                                                   Messages.FILE_CHOOSER_SAVE_FAILED_TITLE,
 332                                           JOptionPane.ERROR_MESSAGE);
 333         }
 334     }
 335 
 336     @Override
 337     public void paintComponent(Graphics g) {
 338         super.paintComponent(g);
 339 
 340         Color oldColor = g.getColor();
 341         Font  oldFont  = g.getFont();
 342         Color fg = getForeground();
 343         Color bg = getBackground();
 344         boolean bgIsLight = (bg.getRed() > 200 &&
 345                              bg.getGreen() > 200 &&
 346                              bg.getBlue() > 200);
 347 
 348 
 349         ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 350                                          RenderingHints.VALUE_ANTIALIAS_ON);
 351 
 352         if (smallFont == null) {
 353             smallFont = oldFont.deriveFont(9.0F);
 354         }
 355 
 356         r.x = leftMargin - 5;
 357         r.y = topMargin  - 8;
 358         r.width  = getWidth()-leftMargin-rightMargin;
 359         r.height = getHeight()-topMargin-bottomMargin+16;
 360 
 361         if (border == null) {
 362             // By setting colors here, we avoid recalculating them
 363             // over and over.
 364             border = new BevelBorder(BevelBorder.LOWERED,
 365                                      getBackground().brighter().brighter(),
 366                                      getBackground().brighter(),
 367                                      getBackground().darker().darker(),
 368                                      getBackground().darker());
 369         }
 370 
 371         border.paintBorder(this, g, r.x, r.y, r.width, r.height);
 372 
 373         // Fill background color
 374         g.setColor(bgColor);
 375         g.fillRect(r.x+2, r.y+2, r.width-4, r.height-4);
 376         g.setColor(oldColor);
 377 
 378         long tMin = Long.MAX_VALUE;
 379         long tMax = Long.MIN_VALUE;
 380         long vMin = Long.MAX_VALUE;
 381         long vMax = 1;
 382 
 383         int w = getWidth()-rightMargin-leftMargin-10;
 384         int h = getHeight()-topMargin-bottomMargin;
 385 
 386         if (times.size > 1) {
 387             tMin = Math.min(tMin, times.time(0));
 388             tMax = Math.max(tMax, times.time(times.size-1));
 389         }
 390         long viewRangeMS;
 391         if (viewRange > 0) {
 392             viewRangeMS = viewRange * MINUTE;
 393         } else {
 394             // Display full time range, but no less than a minute
 395             viewRangeMS = Math.max(tMax - tMin, 1 * MINUTE);
 396         }
 397 
 398         // Calculate min/max values
 399         for (Sequence seq : seqs) {
 400             if (seq.size > 0) {
 401                 for (int i = 0; i < seq.size; i++) {
 402                     if (seq.size == 1 || times.time(i) >= tMax - viewRangeMS) {
 403                         long val = seq.value(i);
 404                         if (val > Long.MIN_VALUE) {
 405                             vMax = Math.max(vMax, val);
 406                             vMin = Math.min(vMin, val);
 407                         }
 408                     }
 409                 }
 410             } else {
 411                 vMin = 0L;
 412             }
 413             if (unit == Unit.BYTES || !seq.isPlotted) {
 414                 // We'll scale only to the first (main) value set.
 415                 // TODO: Use a separate property for this.
 416                 break;
 417             }
 418         }
 419 
 420         // Normalize scale
 421         vMax = normalizeMax(vMax);
 422         if (vMin > 0) {
 423             if (vMax / vMin > 4) {
 424                 vMin = 0;
 425             } else {
 426                 vMin = normalizeMin(vMin);
 427             }
 428         }
 429 
 430 
 431         g.setColor(fg);
 432 
 433         // Axes
 434         // Draw vertical axis
 435         int x = leftMargin - 18;
 436         int y = topMargin;
 437         FontMetrics fm = g.getFontMetrics();
 438 
 439         g.drawLine(x,   y,   x,   y+h);
 440 
 441         int n = 5;
 442         if ((""+vMax).startsWith("2")) {
 443             n = 4;
 444         } else if ((""+vMax).startsWith("3")) {
 445             n = 6;
 446         } else if ((""+vMax).startsWith("4")) {
 447             n = 4;
 448         } else if ((""+vMax).startsWith("6")) {
 449             n = 6;
 450         } else if ((""+vMax).startsWith("7")) {
 451             n = 7;
 452         } else if ((""+vMax).startsWith("8")) {
 453             n = 8;
 454         } else if ((""+vMax).startsWith("9")) {
 455             n = 3;
 456         }
 457 
 458         // Ticks
 459         ArrayList<Long> tickValues = new ArrayList<Long>();
 460         tickValues.add(vMin);
 461         for (int i = 0; i < n; i++) {
 462             long v = i * vMax / n;
 463             if (v > vMin) {
 464                 tickValues.add(v);
 465             }
 466         }
 467         tickValues.add(vMax);
 468         n = tickValues.size();
 469 
 470         String[] tickStrings = new String[n];
 471         for (int i = 0; i < n; i++) {
 472             long v = tickValues.get(i);
 473             tickStrings[i] = getSizeString(v, vMax);
 474         }
 475 
 476         // Trim trailing decimal zeroes.
 477         if (decimals > 0) {
 478             boolean trimLast = true;
 479             boolean removedDecimalPoint = false;
 480             do {
 481                 for (String str : tickStrings) {
 482                     if (!(str.endsWith("0") || str.endsWith("."))) {
 483                         trimLast = false;
 484                         break;
 485                     }
 486                 }
 487                 if (trimLast) {
 488                     if (tickStrings[0].endsWith(".")) {
 489                         removedDecimalPoint = true;
 490                     }
 491                     for (int i = 0; i < n; i++) {
 492                         String str = tickStrings[i];
 493                         tickStrings[i] = str.substring(0, str.length()-1);
 494                     }
 495                 }
 496             } while (trimLast && !removedDecimalPoint);
 497         }
 498 
 499         // Draw ticks
 500         int lastY = Integer.MAX_VALUE;
 501         for (int i = 0; i < n; i++) {
 502             long v = tickValues.get(i);
 503             y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin));
 504             g.drawLine(x-2, y, x+2, y);
 505             String s = tickStrings[i];
 506             if (unit == Unit.PERCENT) {
 507                 s += "%";
 508             }
 509             int sx = x-6-fm.stringWidth(s);
 510             if (y < lastY-13) {
 511                 if (checkLeftMargin(sx)) {
 512                     // Wait for next repaint
 513                     return;
 514                 }
 515                 g.drawString(s, sx, y+4);
 516             }
 517             // Draw horizontal grid line
 518             g.setColor(Color.lightGray);
 519             g.drawLine(r.x + 4, y, r.x + r.width - 4, y);
 520             g.setColor(fg);
 521             lastY = y;
 522         }
 523 
 524         // Draw horizontal axis
 525         x = leftMargin;
 526         y = topMargin + h + 15;
 527         g.drawLine(x,   y,   x+w, y);
 528 
 529         long t1 = tMax;
 530         if (t1 <= 0L) {
 531             // No data yet, so draw current time
 532             t1 = System.currentTimeMillis();
 533         }
 534         long tz = timeDF.getTimeZone().getOffset(t1);
 535         long tickInterval = calculateTickInterval(w, 40, viewRangeMS);
 536         if (tickInterval > 3 * HOUR) {
 537             tickInterval = calculateTickInterval(w, 80, viewRangeMS);
 538         }
 539         long t0 = tickInterval - (t1 - viewRangeMS + tz) % tickInterval;
 540         while (t0 < viewRangeMS) {
 541             x = leftMargin + (int)(w * t0 / viewRangeMS);
 542             g.drawLine(x, y-2, x, y+2);
 543 
 544             long t = t1 - viewRangeMS + t0;
 545             String str = formatClockTime(t);
 546             g.drawString(str, x, y+16);
 547             //if (tickInterval > (1 * HOUR) && t % (1 * DAY) == 0) {
 548             if ((t + tz) % (1 * DAY) == 0) {
 549                 str = formatDate(t);
 550                 g.drawString(str, x, y+27);
 551             }
 552             // Draw vertical grid line
 553             g.setColor(Color.lightGray);
 554             g.drawLine(x, topMargin, x, topMargin + h);
 555             g.setColor(fg);
 556             t0 += tickInterval;
 557         }
 558 
 559         // Plot values
 560         int start = 0;
 561         int nValues = 0;
 562         int nLists = seqs.size();
 563         if (nLists > 0) {
 564             nValues = seqs.get(0).size;
 565         }
 566         if (nValues == 0) {
 567             g.setColor(oldColor);
 568             return;
 569         } else {
 570             Sequence seq = seqs.get(0);
 571             // Find starting point
 572             for (int p = 0; p < seq.size; p++) {
 573                 if (times.time(p) >= tMax - viewRangeMS) {
 574                     start = p;
 575                     break;
 576                 }
 577             }
 578         }
 579 
 580         //Optimization: collapse plot of more than four values per pixel
 581         int pointsPerPixel = (nValues - start) / w;
 582         if (pointsPerPixel < 4) {
 583             pointsPerPixel = 1;
 584         }
 585 
 586         // Draw graphs
 587         // Loop backwards over sequences because the first needs to be painted on top
 588         for (int i = nLists-1; i >= 0; i--) {
 589             int x0 = leftMargin;
 590             int y0 = topMargin + h + 1;
 591 
 592             Sequence seq = seqs.get(i);
 593             if (seq.isPlotted && seq.size > 0) {
 594                 // Paint twice, with white and with color
 595                 for (int pass = 0; pass < 2; pass++) {
 596                     g.setColor((pass == 0) ? Color.white : seq.color);
 597                     int x1 = -1;
 598                     long v1 = -1;
 599                     for (int p = start; p < nValues; p += pointsPerPixel) {
 600                         // Make sure we get the last value
 601                         if (pointsPerPixel > 1 && p >= nValues - pointsPerPixel) {
 602                             p = nValues - 1;
 603                         }
 604                         int x2 = (int)(w * (times.time(p)-(t1-viewRangeMS)) / viewRangeMS);
 605                         long v2 = seq.value(p);
 606                         if (v2 >= vMin && v2 <= vMax) {
 607                             int y2  = (int)(h * (v2 -vMin) / (vMax-vMin));
 608                             if (x1 >= 0 && v1 >= vMin && v1 <= vMax) {
 609                                 int y1 = (int)(h * (v1-vMin) / (vMax-vMin));
 610 
 611                                 if (y1 == y2) {
 612                                     // fillrect is much faster
 613                                     g.fillRect(x0+x1, y0-y1-pass, x2-x1, 1);
 614                                 } else {
 615                                     Graphics2D g2d = (Graphics2D)g;
 616                                     Stroke oldStroke = null;
 617                                     if (seq.transitionStroke != null) {
 618                                         oldStroke = g2d.getStroke();
 619                                         g2d.setStroke(seq.transitionStroke);
 620                                     }
 621                                     g.drawLine(x0+x1, y0-y1-pass, x0+x2, y0-y2-pass);
 622                                     if (oldStroke != null) {
 623                                         g2d.setStroke(oldStroke);
 624                                     }
 625                                 }
 626                             }
 627                         }
 628                         x1 = x2;
 629                         v1 = v2;
 630                     }
 631                 }
 632 
 633                 // Current value
 634                 long v = seq.value(seq.size - 1);
 635                 if (v >= vMin && v <= vMax) {
 636                     if (bgIsLight) {
 637                         g.setColor(seq.color);
 638                     } else {
 639                         g.setColor(fg);
 640                     }
 641                     x = r.x + r.width + 2;
 642                     y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin));
 643                     // a small triangle/arrow
 644                     g.fillPolygon(new int[] { x+2, x+6, x+6 },
 645                                   new int[] { y,   y+3, y-3 },
 646                                   3);
 647                 }
 648                 g.setColor(fg);
 649             }
 650         }
 651 
 652         int[] valueStringSlots = new int[nLists];
 653         for (int i = 0; i < nLists; i++) valueStringSlots[i] = -1;
 654         for (int i = 0; i < nLists; i++) {
 655             Sequence seq = seqs.get(i);
 656             if (seq.isPlotted && seq.size > 0) {
 657                 // Draw current value
 658 
 659                 // TODO: collapse values if pointsPerPixel >= 4
 660 
 661                 long v = seq.value(seq.size - 1);
 662                 if (v >= vMin && v <= vMax) {
 663                     x = r.x + r.width + 2;
 664                     y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin));
 665                     int y2 = getValueStringSlot(valueStringSlots, y, 2*10, i);
 666                     g.setFont(smallFont);
 667                     if (bgIsLight) {
 668                         g.setColor(seq.color);
 669                     } else {
 670                         g.setColor(fg);
 671                     }
 672                     String curValue = getFormattedValue(v, true);
 673                     if (unit == Unit.PERCENT) {
 674                         curValue += "%";
 675                     }
 676                     int valWidth = fm.stringWidth(curValue);
 677                     String legend = (displayLegend?seq.name:"");
 678                     int legendWidth = fm.stringWidth(legend);
 679                     if (checkRightMargin(valWidth) || checkRightMargin(legendWidth)) {
 680                         // Wait for next repaint
 681                         return;
 682                     }
 683                     g.drawString(legend  , x + 17, Math.min(topMargin+h,      y2 + 3 - 10));
 684                     g.drawString(curValue, x + 17, Math.min(topMargin+h + 10, y2 + 3));
 685 
 686                     // Maybe draw a short line to value
 687                     if (y2 > y + 3) {
 688                         g.drawLine(x + 9, y + 2, x + 14, y2);
 689                     } else if (y2 < y - 3) {
 690                         g.drawLine(x + 9, y - 2, x + 14, y2);
 691                     }
 692                 }
 693                 g.setFont(oldFont);
 694                 g.setColor(fg);
 695 
 696             }
 697         }
 698         g.setColor(oldColor);
 699     }
 700 
 701     private boolean checkLeftMargin(int x) {
 702         // Make sure leftMargin has at least 2 pixels over
 703         if (x < 2) {
 704             leftMargin += (2 - x);
 705             // Repaint from top (above any cell renderers)
 706             SwingUtilities.getWindowAncestor(this).repaint();
 707             return true;
 708         }
 709         return false;
 710     }
 711 
 712     private boolean checkRightMargin(int w) {
 713         // Make sure rightMargin has at least 2 pixels over
 714         if (w + 2 > rightMargin) {
 715             rightMargin = (w + 2);
 716             // Repaint from top (above any cell renderers)
 717             SwingUtilities.getWindowAncestor(this).repaint();
 718             return true;
 719         }
 720         return false;
 721     }
 722 
 723     private int getValueStringSlot(int[] slots, int y, int h, int i) {
 724         for (int s = 0; s < slots.length; s++) {
 725             if (slots[s] >= y && slots[s] < y + h) {
 726                 // collide below us
 727                 if (slots[s] > h) {
 728                     return getValueStringSlot(slots, slots[s]-h, h, i);
 729                 } else {
 730                     return getValueStringSlot(slots, slots[s]+h, h, i);
 731                 }
 732             } else if (y >= h && slots[s] > y - h && slots[s] < y) {
 733                 // collide above us
 734                 return getValueStringSlot(slots, slots[s]+h, h, i);
 735             }
 736         }
 737         slots[i] = y;
 738         return y;
 739     }
 740 
 741     private long calculateTickInterval(int w, int hGap, long viewRangeMS) {
 742         long tickInterval = viewRangeMS * hGap / w;
 743         if (tickInterval < 1 * MINUTE) {
 744             tickInterval = 1 * MINUTE;
 745         } else if (tickInterval < 5 * MINUTE) {
 746             tickInterval = 5 * MINUTE;
 747         } else if (tickInterval < 10 * MINUTE) {
 748             tickInterval = 10 * MINUTE;
 749         } else if (tickInterval < 30 * MINUTE) {
 750             tickInterval = 30 * MINUTE;
 751         } else if (tickInterval < 1 * HOUR) {
 752             tickInterval = 1 * HOUR;
 753         } else if (tickInterval < 3 * HOUR) {
 754             tickInterval = 3 * HOUR;
 755         } else if (tickInterval < 6 * HOUR) {
 756             tickInterval = 6 * HOUR;
 757         } else if (tickInterval < 12 * HOUR) {
 758             tickInterval = 12 * HOUR;
 759         } else if (tickInterval < 1 * DAY) {
 760             tickInterval = 1 * DAY;
 761         } else {
 762             tickInterval = normalizeMax(tickInterval / DAY) * DAY;
 763         }
 764         return tickInterval;
 765     }
 766 
 767     private long normalizeMin(long l) {
 768         int exp = (int)Math.log10((double)l);
 769         long multiple = (long)Math.pow(10.0, exp);
 770         int i = (int)(l / multiple);
 771         return i * multiple;
 772     }
 773 
 774     private long normalizeMax(long l) {
 775         int exp = (int)Math.log10((double)l);
 776         long multiple = (long)Math.pow(10.0, exp);
 777         int i = (int)(l / multiple);
 778         l = (i+1)*multiple;
 779         return l;
 780     }
 781 
 782     private String getFormattedValue(long v, boolean groupDigits) {
 783         String str;
 784         String fmt = "%";
 785         if (groupDigits) {
 786             fmt += ",";
 787         }
 788         if (decimals > 0) {
 789             fmt += "." + decimals + "f";
 790             str = String.format(fmt, v / decimalsMultiplier);
 791         } else {
 792             fmt += "d";
 793             str = String.format(fmt, v);
 794         }
 795         return str;
 796     }
 797 
 798     private String getSizeString(long v, long vMax) {
 799         String s;
 800 
 801         if (unit == Unit.BYTES && decimals == 0) {
 802             s = formatBytes(v, vMax);
 803         } else {
 804             s = getFormattedValue(v, true);
 805         }
 806         return s;
 807     }
 808 
 809     private static synchronized Stroke getDashedStroke() {
 810         if (dashedStroke == null) {
 811             dashedStroke = new BasicStroke(1.0f,
 812                                            BasicStroke.CAP_BUTT,
 813                                            BasicStroke.JOIN_MITER,
 814                                            10.0f,
 815                                            new float[] { 2.0f, 3.0f },
 816                                            0.0f);
 817         }
 818         return dashedStroke;
 819     }
 820 
 821     private static Object extendArray(Object a1) {
 822         int n = Array.getLength(a1);
 823         Object a2 =
 824             Array.newInstance(a1.getClass().getComponentType(),
 825                               n + ARRAY_SIZE_INCREMENT);
 826         System.arraycopy(a1, 0, a2, 0, n);
 827         return a2;
 828     }
 829 
 830 
 831     private static class TimeStamps {
 832         // Time stamps (long) are split into offsets (long) and a
 833         // series of times from the offsets (int). A new offset is
 834         // stored when the the time value doesn't fit in an int
 835         // (approx every 24 days).  An array of indices is used to
 836         // define the starting point for each offset in the times
 837         // array.
 838         long[] offsets = new long[0];
 839         int[] indices = new int[0];
 840         int[] rtimes = new int[ARRAY_SIZE_INCREMENT];
 841 
 842         // Number of stored timestamps
 843         int size = 0;
 844 
 845         /**
 846          * Returns the time stamp for index i
 847          */
 848         public long time(int i) {
 849             long offset = 0;
 850             for (int j = indices.length - 1; j >= 0; j--) {
 851                 if (i >= indices[j]) {
 852                     offset = offsets[j];
 853                     break;
 854                 }
 855             }
 856             return offset + rtimes[i];
 857         }
 858 
 859         public void add(long time) {
 860             // May need to store a new time offset
 861             int n = offsets.length;
 862             if (n == 0 || time - offsets[n - 1] > Integer.MAX_VALUE) {
 863                 // Grow offset and indices arrays and store new offset
 864                 offsets = Arrays.copyOf(offsets, n + 1);
 865                 offsets[n] = time;
 866                 indices = Arrays.copyOf(indices, n + 1);
 867                 indices[n] = size;
 868             }
 869 
 870             // May need to extend the array size
 871             if (rtimes.length == size) {
 872                 rtimes = (int[])extendArray(rtimes);
 873             }
 874 
 875             // Store the time
 876             rtimes[size]  = (int)(time - offsets[offsets.length - 1]);
 877             size++;
 878         }
 879     }
 880 
 881     private static class Sequence {
 882         String key;
 883         String name;
 884         Color color;
 885         boolean isPlotted;
 886         Stroke transitionStroke = null;
 887 
 888         // Values are stored in an int[] if all values will fit,
 889         // otherwise in a long[]. An int can represent up to 2 GB.
 890         // Use a random start size, so all arrays won't need to
 891         // be grown during the same update interval
 892         Object values =
 893             new byte[ARRAY_SIZE_INCREMENT + (int)(Math.random() * 100)];
 894 
 895         // Number of stored values
 896         int size = 0;
 897 
 898         public Sequence(String key) {
 899             this.key = key;
 900         }
 901 
 902         /**
 903          * Returns the value at index i
 904          */
 905         public long value(int i) {
 906             return Array.getLong(values, i);
 907         }
 908 
 909         public void add(long value) {
 910             // May need to switch to a larger array type
 911             if ((values instanceof byte[] ||
 912                  values instanceof short[] ||
 913                  values instanceof int[]) &&
 914                        value > Integer.MAX_VALUE) {
 915                 long[] la = new long[Array.getLength(values)];
 916                 for (int i = 0; i < size; i++) {
 917                     la[i] = Array.getLong(values, i);
 918                 }
 919                 values = la;
 920             } else if ((values instanceof byte[] ||
 921                         values instanceof short[]) &&
 922                        value > Short.MAX_VALUE) {
 923                 int[] ia = new int[Array.getLength(values)];
 924                 for (int i = 0; i < size; i++) {
 925                     ia[i] = Array.getInt(values, i);
 926                 }
 927                 values = ia;
 928             } else if (values instanceof byte[] &&
 929                        value > Byte.MAX_VALUE) {
 930                 short[] sa = new short[Array.getLength(values)];
 931                 for (int i = 0; i < size; i++) {
 932                     sa[i] = Array.getShort(values, i);
 933                 }
 934                 values = sa;
 935             }
 936 
 937             // May need to extend the array size
 938             if (Array.getLength(values) == size) {
 939                 values = extendArray(values);
 940             }
 941 
 942             // Store the value
 943             if (values instanceof long[]) {
 944                 ((long[])values)[size] = value;
 945             } else if (values instanceof int[]) {
 946                 ((int[])values)[size] = (int)value;
 947             } else if (values instanceof short[]) {
 948                 ((short[])values)[size] = (short)value;
 949             } else {
 950                 ((byte[])values)[size] = (byte)value;
 951             }
 952             size++;
 953         }
 954     }
 955 
 956     // Can be overridden by subclasses
 957     long getValue() {
 958         return 0;
 959     }
 960 
 961     long getLastTimeStamp() {
 962         return times.time(times.size - 1);
 963     }
 964 
 965     long getLastValue(String key) {
 966         Sequence seq = getSequence(key);
 967         return (seq != null && seq.size > 0) ? seq.value(seq.size - 1) : 0L;
 968     }
 969 
 970 
 971     // Called on EDT
 972     public void propertyChange(PropertyChangeEvent ev) {
 973         String prop = ev.getPropertyName();
 974 
 975         if (prop == JConsoleContext.CONNECTION_STATE_PROPERTY) {
 976             ConnectionState newState = (ConnectionState)ev.getNewValue();
 977 
 978             switch (newState) {
 979               case DISCONNECTED:
 980                 synchronized(this) {
 981                     long time = System.currentTimeMillis();
 982                     times.add(time);
 983                     for (Sequence seq : seqs) {
 984                         seq.add(Long.MIN_VALUE);
 985                     }
 986                 }
 987                 break;
 988             }
 989         }
 990     }
 991 
 992     private static class SaveDataFileChooser extends JFileChooser {
 993         private static final long serialVersionUID = -5182890922369369669L;
 994         SaveDataFileChooser() {
 995             setFileFilter(new FileNameExtensionFilter("CSV file", "csv"));
 996         }
 997 
 998         @Override
 999         public void approveSelection() {
1000             File file = getSelectedFile();
1001             if (file != null) {
1002                 FileFilter filter = getFileFilter();
1003                 if (filter != null && filter instanceof FileNameExtensionFilter) {
1004                     String[] extensions =
1005                         ((FileNameExtensionFilter)filter).getExtensions();
1006 
1007                     boolean goodExt = false;
1008                     for (String ext : extensions) {
1009                         if (file.getName().toLowerCase().endsWith("." + ext.toLowerCase())) {
1010                             goodExt = true;
1011                             break;
1012                         }
1013                     }
1014                     if (!goodExt) {
1015                         file = new File(file.getParent(),
1016                                         file.getName() + "." + extensions[0]);
1017                     }
1018                 }
1019 
1020                 if (file.exists()) {
1021                     String okStr = Messages.FILE_CHOOSER_FILE_EXISTS_OK_OPTION;
1022                     String cancelStr = Messages.FILE_CHOOSER_FILE_EXISTS_CANCEL_OPTION;
1023                     int ret =
1024                         JOptionPane.showOptionDialog(this,
1025                                                      Resources.format(Messages.FILE_CHOOSER_FILE_EXISTS_MESSAGE,
1026                                                              file.getName()),
1027                                                              Messages.FILE_CHOOSER_FILE_EXISTS_TITLE,
1028                                                      JOptionPane.OK_CANCEL_OPTION,
1029                                                      JOptionPane.WARNING_MESSAGE,
1030                                                      null,
1031                                                      new Object[] { okStr, cancelStr },
1032                                                      okStr);
1033                     if (ret != JOptionPane.OK_OPTION) {
1034                         return;
1035                     }
1036                 }
1037                 setSelectedFile(file);
1038             }
1039             super.approveSelection();
1040         }
1041     }
1042 
1043     @Override
1044     public AccessibleContext getAccessibleContext() {
1045         if (accessibleContext == null) {
1046             accessibleContext = new AccessiblePlotter();
1047         }
1048         return accessibleContext;
1049     }
1050 
1051     protected class AccessiblePlotter extends AccessibleJComponent {
1052         private static final long serialVersionUID = -3847205410473510922L;
1053         protected AccessiblePlotter() {
1054             setAccessibleName(Messages.PLOTTER_ACCESSIBLE_NAME);
1055         }
1056 
1057         @Override
1058         public String getAccessibleName() {
1059             String name = super.getAccessibleName();
1060 
1061             if (seqs.size() > 0 && seqs.get(0).size > 0) {
1062                 String keyValueList = "";
1063                 for (Sequence seq : seqs) {
1064                     if (seq.isPlotted) {
1065                         String value = "null";
1066                         if (seq.size > 0) {
1067                             if (unit == Unit.BYTES) {
1068                                 value = Resources.format(Messages.SIZE_BYTES, seq.value(seq.size - 1));
1069                             } else {
1070                                 value =
1071                                     getFormattedValue(seq.value(seq.size - 1), false) +
1072                                     ((unit == Unit.PERCENT) ? "%" : "");
1073                             }
1074                         }
1075                         // Assume format string ends with newline
1076                         keyValueList +=
1077                             Resources.format(Messages.PLOTTER_ACCESSIBLE_NAME_KEY_AND_VALUE,
1078                                     seq.key, value);
1079                     }
1080                 }
1081                 name += "\n" + keyValueList + ".";
1082             } else {
1083                 name += "\n" + Messages.PLOTTER_ACCESSIBLE_NAME_NO_DATA;
1084             }
1085             return name;
1086         }
1087 
1088         @Override
1089         public AccessibleRole getAccessibleRole() {
1090             return AccessibleRole.CANVAS;
1091         }
1092     }
1093 }