changeset 1234:ba8a0e50ddee

Timeline for JMX Notifications Reviewed-by: jerboaa, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-August/007919.html
author Omair Majid <omajid@redhat.com>
date Wed, 28 Aug 2013 12:03:09 -0400
parents 4279c0994a38
children 2b4427689a30
files client/swing/src/main/java/com/redhat/thermostat/client/swing/GraphicsUtils.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimeline.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineDataChangeListener.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineModel.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineRangeChangeListener.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineUI.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/LocalizedLabel.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/Timeline.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/LocaleResources.java client/swing/src/main/resources/com/redhat/thermostat/client/swing/internal/strings.properties client/swing/src/test/java/com/redhat/thermostat/client/swing/components/EventTimelineModelTest.java common/core/src/main/java/com/redhat/thermostat/common/model/LongRangeNormalizer.java common/core/src/test/java/com/redhat/thermostat/common/model/LongRangeNormalizerTest.java thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapChartPanelLayout.java vm-jmx/agent/src/main/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackend.java vm-jmx/agent/src/test/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackendTest.java vm-jmx/client-swing/src/main/java/com/redhat/thermostat/vm/jmx/client/swing/internal/JmxNotificationsSwingView.java vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/JmxNotification.java vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java
diffstat 23 files changed, 1480 insertions(+), 86 deletions(-) [+]
line wrap: on
line diff
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/GraphicsUtils.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/GraphicsUtils.java	Wed Aug 28 12:03:09 2013 -0400
@@ -62,6 +62,7 @@
     public Graphics2D createAAGraphics(Graphics g) {
         Graphics2D graphics = (Graphics2D) g.create();
         graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
         return graphics;
     }
     
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Cursor;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Paint;
+import java.awt.RenderingHints;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.event.HierarchyBoundsListener;
+import java.awt.event.HierarchyEvent;
+import java.awt.event.HierarchyListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+
+import com.redhat.thermostat.client.swing.components.EventTimelineModel.Event;
+import com.redhat.thermostat.client.swing.components.timeline.Timeline;
+import com.redhat.thermostat.client.swing.internal.LocaleResources;
+import com.redhat.thermostat.common.model.LongRangeNormalizer;
+import com.redhat.thermostat.common.model.Range;
+import com.redhat.thermostat.shared.locale.Translate;
+
+public class BasicEventTimelineUI extends EventTimelineUI {
+
+    private static final Translate<LocaleResources> translate = LocaleResources.createLocalizer();
+
+    private static final Color DEFAULT_FILL_COLOR = new Color(0,0,0,255);
+    private static final Color DEFAULT_EDGE_COLOR = Color.BLACK;
+    private static final Color DEFAULT_MARKER_COLOR = Color.BLACK;
+
+    private EventTimeline eventTimeline;
+
+    private OverviewPanel overviewPanel = new OverviewPanel();
+    private Timeline overviewRuler;
+
+    private Refresher refresher = new Refresher(overviewPanel);
+    private JButton moveLeftButton;
+    private JButton moveRightButton;
+    private JButton zoomInButton;
+    private JButton zoomOutButton;
+    private JButton resetZoomButton;
+
+    @Override
+    protected void installComponents(EventTimeline component) {
+        eventTimeline = component;
+
+        overviewRuler = new Timeline(new Range<Long>(1l, 2l));
+
+        moveLeftButton = new JButton("<");
+        moveLeftButton.setMargin(new Insets(0, 0, 0, 0));
+        moveRightButton = new JButton(">");
+        moveRightButton.setMargin(new Insets(0, 0, 0, 0));
+
+        JPanel buttonPanel = new JPanel();
+        buttonPanel.setLayout(new GridLayout());
+
+        zoomInButton = new JButton("+");
+        zoomInButton.setToolTipText(translate.localize(LocaleResources.ZOOM_IN).getContents());
+        zoomInButton.setMargin(new Insets(2, 2, 2, 2));
+
+        buttonPanel.add(zoomInButton);
+
+        zoomOutButton = new JButton("-");
+        zoomOutButton.setToolTipText(translate.localize(LocaleResources.ZOOM_OUT).getContents());
+        zoomOutButton.setMargin(new Insets(2, 2, 2, 2));
+        buttonPanel.add(zoomOutButton);
+
+        resetZoomButton = new JButton("R");
+        resetZoomButton.setToolTipText(translate.localize(LocaleResources.RESET_ZOOM).getContents());
+        resetZoomButton.setMargin(new Insets(2, 2, 2, 2));
+        buttonPanel.add(resetZoomButton);
+
+        GridBagLayout layout = new GridBagLayout();
+        component.setLayout(layout);
+        overviewPanel.setLayout(layout);
+
+        GridBagConstraints c = new GridBagConstraints();
+
+        c.gridx = 0;
+        c.gridy = 0;
+        c.fill = GridBagConstraints.VERTICAL;
+        c.weightx = 0;
+        c.weighty = 1;
+
+        component.add(moveLeftButton, c);
+
+        c.gridx = 1;
+        c.gridy = 0;
+        c.fill = GridBagConstraints.BOTH;
+        c.weighty = 1.0;
+        c.weightx = 1.0;
+
+        component.add(overviewPanel, c);
+
+        c.gridy++;
+        c.fill = GridBagConstraints.HORIZONTAL;
+        c.weighty = 0;
+        component.add(overviewRuler, c);
+
+        c.gridx = 2;
+        c.gridy = 0;
+        c.fill = GridBagConstraints.VERTICAL;
+        c.weighty = 1;
+        c.weightx = 0;
+        component.add(moveRightButton, c);
+
+        c.gridx = 0;
+        c.gridy = 3;
+        c.fill = GridBagConstraints.VERTICAL;
+        c.anchor = GridBagConstraints.LINE_START;
+        c.weightx = 1;
+        c.weighty = 0;
+        c.gridwidth = 4;
+        component.add(buttonPanel, c);
+    }
+
+    @Override
+    protected void installDefaults(EventTimeline c) {
+        c.setSelectionEdgePaint(DEFAULT_EDGE_COLOR);
+        c.setSelectionFillPaint(DEFAULT_FILL_COLOR);
+        c.setEventPaint(DEFAULT_MARKER_COLOR);
+    }
+
+    @Override
+    protected void installListeners(EventTimeline c) {
+        c.addHierarchyBoundsListener(refresher);
+        c.addHierarchyListener(refresher);
+        c.getModel().addDataChangeListener(refresher);
+        c.getModel().addRangeChangeListener(new EventTimelineRangeChangeListener() {
+            @Override
+            public void rangeChanged(Range<Long> overview, Range<Long> detail) {
+                overviewRuler.setRange(overview);
+                overviewRuler.repaint();
+
+            }
+        });
+        moveRightButton.addActionListener(new DetailChangeListener() {
+            @Override
+            protected Range<Long> computeNewDetailRange(long min, long max) {
+                long diff = (long) ((max - min) * 0.1);
+                return new Range<>(min + diff, max + diff);
+            }
+        });
+        moveLeftButton.addActionListener(new DetailChangeListener() {
+            protected Range<Long> computeNewDetailRange(long min, long max) {
+                long diff = (long) ((max - min) * 0.1);
+                return new Range<>(min - diff, max - diff);
+            };
+        });
+        zoomOutButton.addActionListener(new DetailChangeListener() {
+           protected Range<Long> computeNewDetailRange(long min, long max) {
+               long diff = max - min;
+               return new Range<>(min - diff / 2, max + diff / 2);
+           };
+        });
+
+        zoomInButton.addActionListener(new DetailChangeListener() {
+            protected Range<Long> computeNewDetailRange(long min, long max) {
+                long diff = max - min;
+                return new Range<>(min + diff / 4, max - diff / 4);
+            };
+        });
+
+        resetZoomButton.addActionListener(new DetailChangeListener() {
+            @Override
+            protected Range<Long> computeNewDetailRange(long min, long max) {
+                long timeDelta = max - min;
+
+                long tenMinutesInMillis = TimeUnit.MINUTES.toMillis(10);
+
+                if (timeDelta <= tenMinutesInMillis) {
+                    return new Range<>(min, max);
+                } else {
+                    return new Range<>(max - tenMinutesInMillis, max);
+                }
+            }
+        });
+    }
+
+    private abstract class DetailChangeListener implements ActionListener {
+        @Override
+        public void actionPerformed(ActionEvent arg0) {
+            Range<Long> range = eventTimeline.getModel().getDetailRange();
+
+            long min = range.getMin();
+            long max = range.getMax();
+
+            eventTimeline.getModel().setDetailRange(computeNewDetailRange(min, max));
+
+            overviewPanel.refresh();
+        }
+
+        protected abstract Range<Long> computeNewDetailRange(long min, long max);
+    }
+
+    protected void uninstallListeners(EventTimeline c) {
+        c.removeHierarchyBoundsListener(refresher);
+        c.removeHierarchyListener(refresher);
+    }
+
+    @Override
+    protected void uninstallComponents(EventTimeline c) {
+        c.remove(overviewPanel);
+
+        eventTimeline = null;
+    }
+
+    private long positionToTimeStamp(int position) {
+        Range<Long> range = eventTimeline.getModel().getTotalRange();
+        LongRangeNormalizer normalizer = new LongRangeNormalizer(new Range<>(0l, (long)overviewPanel.getWidth()), range);
+        long result = normalizer.getValueNormalized(position);
+        return result;
+    }
+
+    private int timeStampToPosition(long timeStamp) {
+        Range<Long> range = eventTimeline.getModel().getTotalRange();
+        LongRangeNormalizer normalizer = new LongRangeNormalizer(range, new Range<>(0l, (long)overviewPanel.getWidth()));
+        int result = (int) normalizer.getValueNormalized(timeStamp);
+        return result;
+    }
+
+    private static class Refresher implements HierarchyBoundsListener, HierarchyListener, AdjustmentListener, EventTimelineDataChangeListener {
+
+        private OverviewPanel toRefresh;
+
+        public Refresher(OverviewPanel toRefresh) {
+            this.toRefresh = toRefresh;
+        }
+
+        @Override
+        public void dataChanged() {
+            refresh();
+        }
+
+        @Override
+        public void adjustmentValueChanged(AdjustmentEvent e) {
+            refresh();
+        }
+
+        @Override
+        public void ancestorMoved(HierarchyEvent e) {
+            refresh();
+        }
+
+        @Override
+        public void ancestorResized(HierarchyEvent e) {
+            refresh();
+        }
+
+        @Override
+        public void hierarchyChanged(HierarchyEvent e) {
+            refresh();
+        }
+
+        private void refresh() {
+            toRefresh.refresh();
+        }
+    }
+
+    private class OverviewPanel extends JPanel {
+
+        private int MOUSE_MARGIN = 10;
+
+        private int left;
+        private int right;
+
+        private boolean moving = false;
+        private boolean movingLeft = false;
+        private boolean movingRight = false;
+
+        private int oldX = -1;
+        private int oldLeft = -1;
+        private int oldRight = -1;
+
+        public OverviewPanel() {
+            OverviewMotionListener chartMotionListener = new OverviewMotionListener();
+            addMouseMotionListener(chartMotionListener);
+            addMouseListener(chartMotionListener);
+        }
+
+        public void refresh() {
+            recomputeBars();
+            repaint();
+        }
+
+        private void recomputeBars() {
+            Range<Long> range = eventTimeline.getModel().getDetailRange();
+            if (range != null) {
+                left = timeStampToPosition(range.getMin());
+                right = timeStampToPosition(range.getMax());
+            }
+        }
+
+        @Override
+        protected void paintComponent(Graphics g) {
+            super.paintComponent(g);
+
+            int width = (right - left);
+            g.clearRect(0, 0, getWidth(), getHeight());
+
+            Graphics2D g2 = (Graphics2D) g;
+            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+            EventTimelineModel model = eventTimeline.getModel();
+
+            int i = 0;
+            for (Event event : model.getEvents()) {
+                paintEvent(g2, event, i);
+                i++;
+            }
+
+            Paint fillColor = eventTimeline.getSelectionFillPaint();
+            g2.setPaint(fillColor);
+            g2.fillRect(left, 1, width, getHeight() - 2);
+
+            g2.setStroke(new BasicStroke(2));
+            Paint edgeColor = eventTimeline.getSelectionEdgePaint();
+            g2.setPaint(edgeColor);
+            g2.drawRect(left, 1, width, getHeight() - 2);
+
+            g2.dispose();
+        }
+
+        private void paintEvent(Graphics2D g, Event event, int count) {
+            int y = getYBandPosition(count);
+            int x = timeStampToPosition(event.getTimeStamp());
+            paintEvent(g, event.getDescription(), x, y);
+        }
+
+        private int getYBandPosition(int step) {
+            // TODO can we do better in determining the number of 'bands' ?
+            int TOTAL_STEPS = 10;
+            step = step % TOTAL_STEPS + 1;
+
+            return Math.round(1.0f * step * getHeight() / TOTAL_STEPS);
+        }
+
+        private void paintEvent(Graphics2D g, String text, int x, int y) {
+            g = (Graphics2D) g.create();
+
+            FontMetrics metrics = g.getFontMetrics();
+            int descent = metrics.getDescent();
+            int stringWidth = (int) Math.round(metrics.getStringBounds(text, g).getWidth());
+
+            g.setPaint(eventTimeline.getEventPaint());
+            g.drawLine(x, getHeight(), x, y + descent);
+            g.drawLine(x, y + descent, x + stringWidth, y + descent);
+            g.drawString(text, x, y);
+
+            g.dispose();
+        }
+
+        class OverviewMotionListener extends MouseAdapter {
+            @Override
+            public void mouseMoved(MouseEvent e) {
+                if (Math.abs(e.getX() - left) < MOUSE_MARGIN) {
+                    setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
+                } else if (Math.abs(e.getX() - right) < MOUSE_MARGIN) {
+                    setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
+                } else if ((e.getX() > MOUSE_MARGIN + left) && (e.getX() < right - MOUSE_MARGIN)) {
+                    setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+                } else {
+                    setCursor(Cursor.getDefaultCursor());
+                }
+
+            }
+
+            @Override
+            public void mouseDragged(MouseEvent e) {
+                if (moving || movingLeft || movingRight) {
+
+                    int newLeft = oldLeft;
+                    int newRight = oldRight;
+                    if (movingLeft) {
+                        newLeft = e.getX();
+                    } else if (movingRight) {
+                        newRight = e.getX();
+                    } else if (moving) {
+                        long delta = e.getX() - oldX;
+                        newLeft += delta;
+                        newRight += delta;
+                    }
+
+                    Range<Long> range = new Range<Long>(positionToTimeStamp(newLeft), positionToTimeStamp(newRight));
+
+                    eventTimeline.getModel().setDetailRange(range);
+
+                    refresh();
+                }
+            }
+
+            @Override
+            public void mouseReleased(MouseEvent e) {
+                moving = movingLeft = movingRight = false;
+                oldLeft = oldRight = oldX = -1;
+            }
+
+            @Override
+            public void mousePressed(MouseEvent e) {
+                if (Math.abs(e.getX() - left) < MOUSE_MARGIN) {
+                    movingLeft = true;
+                } else if (Math.abs(e.getX() - right) < MOUSE_MARGIN) {
+                    movingRight = true;
+                } else if ((e.getX() > left + MOUSE_MARGIN) && (e.getX() < right - MOUSE_MARGIN)) {
+                    moving = true;
+                }
+                Range<Long> range = eventTimeline.getModel().getDetailRange();
+                oldLeft = timeStampToPosition(range.getMin());
+                oldRight = timeStampToPosition(range.getMax());
+                oldX = e.getX();
+            }
+
+            // TODO implement wheel scrolling
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimeline.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import java.awt.Paint;
+
+import javax.swing.JComponent;
+import javax.swing.UIManager;
+
+public class EventTimeline extends JComponent {
+
+    private static final String uiClassID = "EventTimelineUI";
+
+    private EventTimelineModel model = new EventTimelineModel();
+
+    private Paint selectionEdgePaint = null;
+    private Paint selectionFillPaint = null;
+    private Paint eventPaint;
+
+    public EventTimeline() {
+        updateUI();
+    }
+
+    public void setUI(EventTimelineUI newUI) {
+        super.setUI(newUI);
+    }
+
+    @Override
+    public void updateUI() {
+        if (UIManager.get(getUIClassID()) != null) {
+            setUI((EventTimelineUI) UIManager.getUI(this));
+        } else {
+            setUI(new BasicEventTimelineUI());
+        }
+    }
+
+    @Override
+    public String getUIClassID() {
+        return uiClassID;
+    }
+
+    public EventTimelineModel getModel() {
+        return model;
+    }
+
+    public Paint getSelectionEdgePaint() {
+        return selectionEdgePaint;
+    }
+
+    public void setSelectionEdgePaint(Paint edgePaint) {
+        this.selectionEdgePaint = edgePaint;
+    }
+
+    public Paint getSelectionFillPaint() {
+        return selectionFillPaint;
+    }
+
+    public void setSelectionFillPaint(Paint fillPaint) {
+        this.selectionFillPaint = fillPaint;
+    }
+
+    public void setEventPaint(Paint eventPaint) {
+        this.eventPaint = eventPaint;
+    }
+
+    public Paint getEventPaint() {
+        return eventPaint;
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineDataChangeListener.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+public interface EventTimelineDataChangeListener {
+
+    void dataChanged();
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineModel.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+import com.redhat.thermostat.common.model.Range;
+
+/**
+ * A time line with an total and a detail range.
+ */
+public class EventTimelineModel {
+
+    private final List<EventTimelineRangeChangeListener> rangeListeners = new CopyOnWriteArrayList<>();
+    private final List<EventTimelineDataChangeListener> dataListeners = new CopyOnWriteArrayList<>();
+
+    private Range<Long> totalRange;
+    private Range<Long> detailRange;;
+    private List<Event> events = new ArrayList<>();
+
+    private boolean isAdjusting = false;
+
+    public Range<Long> getTotalRange() {
+        return totalRange;
+    }
+
+    private void setTotalRange(Range<Long> newRange) {
+        if (totalRange != null && totalRange.equals(newRange)) {
+            return;
+        }
+
+        this.totalRange = newRange;
+        fireRangeChanged();
+    }
+
+    public void addEvent(long eventTimeStamp, String description) {
+        addEvent(new Event(eventTimeStamp, description));
+    }
+
+    public void addEvent(Event event) {
+        long eventTimeStamp = event.getTimeStamp();
+
+        if (totalRange == null) {
+            // some heuristics to get sane initial ranges automagically
+            setTotalRange(new Range<>(eventTimeStamp - TimeUnit.MINUTES.toMillis(1), eventTimeStamp + TimeUnit.MINUTES.toMillis(1)));
+            setDetailRange(new Range<>(eventTimeStamp - TimeUnit.MINUTES.toMillis(1), eventTimeStamp + TimeUnit.MINUTES.toMillis(1)));
+        } else {
+            long delta = (long) ((totalRange.getMax() - totalRange.getMin()) * 0.1);
+
+            if (totalRange.getMax() < eventTimeStamp + delta) {
+                setTotalRange(new Range<>(totalRange.getMin(), eventTimeStamp + delta));
+            } else if (totalRange.getMin() + delta > eventTimeStamp) {
+                setTotalRange(new Range<>(eventTimeStamp - delta, totalRange.getMax()));
+            }
+        }
+
+        events.add(event);
+        fireEventDataChanged();
+    }
+
+    public List<Event> getEvents() {
+        return events;
+    }
+
+    public void clearEvents() {
+        events.clear();
+    }
+
+    public void setDetailRange(Range<Long> range) {
+        if (detailRange != null && detailRange.equals(range)) {
+            return;
+        }
+
+        this.detailRange = range;
+
+        fireRangeChanged();
+    }
+
+    public Range<Long> getDetailRange() {
+        return detailRange;
+    }
+
+    public void addRangeChangeListener(EventTimelineRangeChangeListener listener) {
+        rangeListeners.add(listener);
+    }
+
+    public void removeRangeChangeListener(EventTimelineRangeChangeListener listener) {
+        rangeListeners.remove(listener);
+    }
+
+    private void fireRangeChanged() {
+        for (EventTimelineRangeChangeListener listener : rangeListeners) {
+            listener.rangeChanged(this.totalRange, this.detailRange);
+        }
+    }
+
+    public void addDataChangeListener(EventTimelineDataChangeListener listener) {
+        dataListeners.add(listener);
+    }
+
+    public void removeEventDataChangeListener(EventTimelineDataChangeListener listener) {
+        dataListeners.remove(listener);
+    }
+
+    private void fireEventDataChanged() {
+        for (EventTimelineDataChangeListener listener : dataListeners) {
+            listener.dataChanged();
+        }
+    }
+
+    public static class Event {
+
+        private final long timeStamp;
+        private final String description;
+
+        public Event(long timeStamp, String description) {
+            this.timeStamp = timeStamp;
+            this.description = description;
+        }
+
+        public long getTimeStamp() {
+            return timeStamp;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineRangeChangeListener.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import com.redhat.thermostat.common.model.Range;
+
+public interface EventTimelineRangeChangeListener {
+
+    void rangeChanged(Range<Long> overview, Range<Long> detail);
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineUI.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import javax.swing.JComponent;
+import javax.swing.plaf.ComponentUI;
+
+public abstract class EventTimelineUI extends ComponentUI {
+
+    public void installUI(JComponent c) {
+        EventTimeline zc = (EventTimeline) c;
+        installComponents(zc);
+        installDefaults(zc);
+        installListeners(zc);
+    }
+
+    protected void installComponents(EventTimeline c) {}
+
+    protected void installDefaults(EventTimeline c) {}
+
+    protected void installListeners(EventTimeline c) {}
+
+    public void uninstallUI(JComponent c) {
+        EventTimeline zc = (EventTimeline) c;
+
+        uninstallListeners(zc);
+        uninstallDefaults(zc);
+        uninstallComponents(zc);
+    }
+
+    protected void uninstallListeners(EventTimeline c) {}
+
+    protected void uninstallDefaults(EventTimeline c) {}
+
+    protected void uninstallComponents(EventTimeline c) {}
+
+}
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/LocalizedLabel.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/LocalizedLabel.java	Wed Aug 28 12:03:09 2013 -0400
@@ -43,7 +43,8 @@
 public class LocalizedLabel extends JLabel {
 
     public LocalizedLabel(LocalizedString text) {
-        super(text.getContents());
+        // enable word-wrapping
+        super("<html>" + text.getContents() + "</html>");
     }
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/Timeline.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components.timeline;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Rectangle;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import com.redhat.thermostat.client.swing.GraphicsUtils;
+import com.redhat.thermostat.client.swing.components.GradientPanel;
+import com.redhat.thermostat.client.ui.Palette;
+import com.redhat.thermostat.common.model.LongRangeNormalizer;
+import com.redhat.thermostat.common.model.Range;
+
+/**
+ * Displays a timeline between the specified start and stop ranges, dynamically
+ * adjusting itself as needed. The values of the ranges are interpreted as
+ * timestamps in milliseconds.
+ */
+@SuppressWarnings("serial")
+public class Timeline extends GradientPanel {
+
+    /** Default height of this component. Subclasses may use different values */
+    public static final int DEFAULT_HEIGHT = 25;
+
+    private Range<Long> range;
+
+    public Timeline(Range<Long> range) {
+
+        super(Palette.LIGHT_GRAY.getColor(), Palette.WHITE.getColor());
+        setFont(TimelineUtils.FONT);
+
+        this.range = range;
+    }
+
+    public Range<Long> getRange() {
+        return range;
+    }
+
+    public void setRange(Range<Long> newRange) {
+        this.range = newRange;
+        repaint();
+    }
+
+    @Override
+    public int getHeight() {
+        return DEFAULT_HEIGHT;
+    }
+
+    @Override
+    public Dimension getMaximumSize() {
+        Dimension dim = super.getMaximumSize();
+        dim.height = getHeight();
+        return dim;
+    }
+
+    @Override
+    public Dimension getMinimumSize() {
+        Dimension dim = super.getMinimumSize();
+        dim.height = getHeight();
+        return dim;
+    }
+
+    @Override
+    public Dimension getPreferredSize() {
+        Dimension dim = super.getPreferredSize();
+        dim.height = getHeight();
+        return dim;
+    }
+
+    @Override
+    public Dimension getSize() {
+        return getPreferredSize();
+    }
+
+    @Override
+    protected void paintComponent(Graphics g) {
+        super.paintComponent(g);
+
+        if (range == null) {
+            return;
+        }
+
+        Graphics2D graphics = GraphicsUtils.getInstance().createAAGraphics(g);
+
+        Rectangle bounds = g.getClipBounds();
+
+        TimeUnit timeUnitForTicks = getBestTimeUnit();
+        
+        drawTicks(graphics, bounds, timeUnitForTicks);
+
+        graphics.setColor(Palette.THERMOSTAT_BLU.getColor());
+        graphics.drawLine(bounds.x, bounds.height - 1, bounds.width, bounds.height - 1);
+
+        graphics.dispose();
+    }
+
+    /**
+     * Returns a TimeUnit and the number of subticks needed for that TimeUnit
+     * need to display the current range
+     */
+    private TimeUnit getBestTimeUnit() {
+        long min = range.getMin();
+        long max = range.getMax();
+
+        List<TimeUnit> units = new ArrayList<>();
+        units.add(TimeUnit.DAYS);
+        units.add(TimeUnit.HOURS);
+        units.add(TimeUnit.MINUTES);
+        units.add(TimeUnit.SECONDS);
+        units.add(TimeUnit.MILLISECONDS);
+
+        /* Find the largest unit of time suitable for the range */
+        for (TimeUnit unit: units) {
+            long millis = unit.toMillis(1);
+            if (Math.abs(max - min) >= millis) {
+                return unit;
+            }
+        }
+
+        return null;
+    }
+
+    private void drawTicks(Graphics2D graphics, Rectangle bounds, TimeUnit tickUnit) {
+        Font font = graphics.getFont();
+
+        int widthOfOneCharacter = (int) font.getStringBounds("0", graphics.getFontRenderContext()).getWidth();
+
+        long deltaInMilliseconds = Math.max(1, tickUnit.toMillis(1) / 10);
+
+        // some heuristics for spacing
+        int targetNumberOfIntervals = (int) (getWidth() / (10 * widthOfOneCharacter) / 1.3);
+
+        while ((range.getMax() - range.getMin()) / deltaInMilliseconds > targetNumberOfIntervals) {
+            deltaInMilliseconds *= 2;
+
+        }
+
+        long start = (range.getMin() / deltaInMilliseconds) * deltaInMilliseconds;
+        long end = range.getMax();
+
+        DateFormat df = null;
+
+        switch (tickUnit) {
+        case DAYS:
+            df = new SimpleDateFormat("YY-MM-dd");
+            break;
+        case HOURS:
+            df = new SimpleDateFormat("hh:mm a");
+            break;
+        case MINUTES:
+            df = new SimpleDateFormat("hh:mm a");
+            break;
+        case SECONDS:
+            df = new SimpleDateFormat("mm.ss");
+            break;
+        default:
+            df = new SimpleDateFormat("hh:mm:ss a");
+        }
+
+        Paint gradient = new GradientPaint(0, 0, Palette.WHITE.getColor(), 0,
+                getHeight(), Palette.GRAY.getColor());
+
+        LongRangeNormalizer normalizer = new LongRangeNormalizer(range, new Range<Long>(0l, (long)bounds.width));
+
+        for (long i = start; i < end; i += deltaInMilliseconds) {
+            int x = (int) normalizer.getValueNormalized(i);
+
+            graphics.setColor(Palette.THERMOSTAT_BLU.getColor());
+            graphics.drawLine(x, 0, x, bounds.height);
+
+            graphics.setPaint(gradient);
+
+            String value = df.format(new Date(i));
+
+            int stringWidth = (int) font.getStringBounds(value,
+                    graphics.getFontRenderContext()).getWidth() - 1;
+            int stringHeight = (int) font.getStringBounds(value,
+                    graphics.getFontRenderContext()).getHeight();
+            graphics.fillRect(x + 1, bounds.y + 5, stringWidth + 4, stringHeight +
+                    4);
+
+            graphics.setColor(Color.BLACK /* Palette.THERMOSTAT_BLU.getColor() */);
+            graphics.drawString(value, x + 1, bounds.y + stringHeight + 5);
+        }
+    }
+
+}
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java	Wed Aug 28 12:03:09 2013 -0400
@@ -88,7 +88,7 @@
     public Range<Long> getRange() {
         return range;
     }
-    
+
     @Override
     public int getHeight() {
         return DEFAULT_HEIGHT;
@@ -136,7 +136,7 @@
         
         int unitIncrement = getUnitIncrementInPixels();
         
-        TimelineUtils.drawMarks(range, graphics, bounds, currentValue, false, unitIncrement);
+        TimelineUtils.drawMarks(graphics, bounds, currentValue, false, unitIncrement);
         drawTimelineStrings(graphics, currentValue, bounds, unitIncrement);
         
         graphics.setColor(Palette.THERMOSTAT_BLU.getColor());
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java	Wed Aug 28 12:03:09 2013 -0400
@@ -41,12 +41,11 @@
 import java.awt.Rectangle;
 
 import com.redhat.thermostat.client.ui.Palette;
-import com.redhat.thermostat.common.model.Range;
 
 public class TimelineUtils {
     public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10);
  
-    public static void drawMarks(Range<Long> range, Graphics2D graphics, Rectangle bounds,
+    public static void drawMarks(Graphics2D graphics, Rectangle bounds,
                                  int currentValue, boolean darkerTop, int increment)
     {
         int inc = currentValue % increment;
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/LocaleResources.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/LocaleResources.java	Wed Aug 28 12:03:09 2013 -0400
@@ -42,6 +42,10 @@
 
     HOST_PRIMARY_STATUS,
     VM_PRIMARY_STATUS,
+
+    ZOOM_IN,
+    ZOOM_OUT,
+    RESET_ZOOM,
     ;
 
     static final String RESOURCE_BUNDLE =
--- a/client/swing/src/main/resources/com/redhat/thermostat/client/swing/internal/strings.properties	Mon Aug 26 18:33:02 2013 +0200
+++ b/client/swing/src/main/resources/com/redhat/thermostat/client/swing/internal/strings.properties	Wed Aug 28 12:03:09 2013 -0400
@@ -1,2 +1,6 @@
 HOST_PRIMARY_STATUS = host: {0}, id: {1}
 VM_PRIMARY_STATUS = vm: {0}, pid: {1}, host: {2}
+
+ZOOM_IN = Zoom In
+ZOOM_OUT = Zoom Out
+RESET_ZOOM = Reset Zoom
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/components/EventTimelineModelTest.java	Wed Aug 28 12:03:09 2013 -0400
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.client.swing.components.EventTimelineModel.Event;
+import com.redhat.thermostat.common.model.Range;
+
+public class EventTimelineModelTest {
+
+    @Test
+    public void testEvent() {
+        EventTimelineModel model = new EventTimelineModel();
+        assertEquals(0, model.getEvents().size());
+
+        model.addEvent(0, "event1");
+
+        assertEquals(1, model.getEvents().size());
+    }
+
+    @Test
+    public void testAddEventObjects() {
+        EventTimelineModel model = new EventTimelineModel();
+        assertEquals(0, model.getEvents().size());
+
+        Event event1 = new EventTimelineModel.Event(0, "event1");
+        Event event2 = new EventTimelineModel.Event(2, "event2");
+        model.addEvent(event1);
+        model.addEvent(event2);
+
+        assertEquals(Arrays.asList(event1, event2), model.getEvents());
+    }
+
+    @Test
+    public void testGetTotalRange() {
+        long START = 0;
+        long END = 2;
+
+        EventTimelineModel model = new EventTimelineModel();
+
+        Event event1 = new EventTimelineModel.Event(START, "event1");
+        Event event2 = new EventTimelineModel.Event(END, "event2");
+        model.addEvent(event1);
+        model.addEvent(event2);
+
+        assertTrue(model.getTotalRange().getMin() <= START);
+        assertTrue(model.getTotalRange().getMax() >= END);
+    }
+
+    @Test
+    public void testDataListeners() {
+        long START = 0;
+        long END = 2;
+
+        EventTimelineDataChangeListener listener = mock(EventTimelineDataChangeListener.class);
+
+        EventTimelineModel model = new EventTimelineModel();
+        model.addDataChangeListener(listener);
+
+        Event event1 = new EventTimelineModel.Event(START, "event1");
+        Event event2 = new EventTimelineModel.Event(END, "event2");
+        model.addEvent(event1);
+        model.addEvent(event2);
+
+        verify(listener, times(2)).dataChanged();
+    }
+
+    @Test
+    public void testRangeListeners() {
+        long START = 0;
+        long END = 20000000;
+
+        EventTimelineRangeChangeListener listener = mock(EventTimelineRangeChangeListener.class);
+
+        EventTimelineModel model = new EventTimelineModel();
+        model.addRangeChangeListener(listener);
+
+        Event event1 = new EventTimelineModel.Event(START, "event1");
+        Event event2 = new EventTimelineModel.Event(END, "event2");
+
+        model.addEvent(event1);
+        model.addEvent(event2);
+
+        verify(listener, times(2)).rangeChanged(isA(Range.class), isA(Range.class));
+    }
+}
--- a/common/core/src/main/java/com/redhat/thermostat/common/model/LongRangeNormalizer.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/common/core/src/main/java/com/redhat/thermostat/common/model/LongRangeNormalizer.java	Wed Aug 28 12:03:09 2013 -0400
@@ -49,8 +49,6 @@
     
     private long maxNormalized;
  
-    private long value;
-
     private Range<Long> range;
     
     public LongRangeNormalizer(Range<Long> range) {
@@ -77,13 +75,6 @@
         this.minNormalized = minNormalized;
     }
     
-    public long getValue() {
-        return value;
-    }
-
-    public void setValue(long newValue) {
-        this.value = newValue;
-    }
     
     public long getMaxNormalized() {
         return maxNormalized;
@@ -93,7 +84,7 @@
         return minNormalized;
     }
     
-    public long getValueNormalized() {
+    public long getValueNormalized(long value) {
         double normalized = ((value - range.min) * (double)(maxNormalized - minNormalized)/(range.max - range.min)) + minNormalized;
         return Math.round(normalized);
     }
--- a/common/core/src/test/java/com/redhat/thermostat/common/model/LongRangeNormalizerTest.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/common/core/src/test/java/com/redhat/thermostat/common/model/LongRangeNormalizerTest.java	Wed Aug 28 12:03:09 2013 -0400
@@ -46,35 +46,17 @@
 public class LongRangeNormalizerTest {
 
     @Test
-    public void testSameRange() {
-        
-        Range<Long> range = new Range<>(0l, 10l);
-        
-        LongRangeNormalizer model = new LongRangeNormalizer(range);
-        
-        model.setValue(5);
-        
-        model.setMaxNormalized(10);
-        model.setMinNormalized(0);
-        
-        
-        Assert.assertEquals((int) model.getValue(), model.getValueNormalized());
-    }
-
-    @Test
     public void testDoubleRange() {
         
         Range<Long> range = new Range<>(0l, 10l);
         
         LongRangeNormalizer model = new LongRangeNormalizer(range);
         
-        model.setValue(5);
-        
         model.setMaxNormalized(20);
         model.setMinNormalized(0);
         
         
-        Assert.assertEquals(10, model.getValueNormalized());
+        Assert.assertEquals(10, model.getValueNormalized(5));
     }
     
     @Test
@@ -84,34 +66,30 @@
         
         LongRangeNormalizer model = new LongRangeNormalizer(range);
         
-        model.setValue(5);
-        
         model.setMaxNormalized(40);
         model.setMinNormalized(0);
                 
-        Assert.assertEquals(20, model.getValueNormalized());
+        Assert.assertEquals(20, model.getValueNormalized(5));
         
         model.setMaxNormalized(60);
         model.setMinNormalized(0);
                 
-        Assert.assertEquals(30, model.getValueNormalized());
+        Assert.assertEquals(30, model.getValueNormalized(5));
                 
         model.setMaxNormalized(200);
         model.setMinNormalized(100);
                 
-        Assert.assertEquals(150, model.getValueNormalized());
+        Assert.assertEquals(150, model.getValueNormalized(5));
         
         range.setMax(100l);
         range.setMin(0l);
-        model.setValue(50);
         
         model.setMaxNormalized(1);
         model.setMinNormalized(0);
                 
-        Assert.assertEquals(1, model.getValueNormalized());
+        Assert.assertEquals(1, model.getValueNormalized(50));
         
-        model.setValue(49);
-        Assert.assertEquals(0, model.getValueNormalized());
+        Assert.assertEquals(0, model.getValueNormalized(49));
     }
 }
 
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java	Wed Aug 28 12:03:09 2013 -0400
@@ -107,7 +107,7 @@
         
         int currentValue = scrollPane.getHorizontalScrollBar().getValue();
         int totalInc = pixelUnitIncrement;
-        TimelineUtils.drawMarks(range, graphics, bounds, currentValue, false, totalInc);
+        TimelineUtils.drawMarks(graphics, bounds, currentValue, false, totalInc);
 
         drawBoldMarks(graphics, currentValue, bounds, totalInc);
         Color lastColor = drawTimeline(graphics, currentValue, bounds);
@@ -157,21 +157,17 @@
             TimelineInfo info1 = infos[i];
             TimelineInfo info2 = infos[i + 1];
             
-            normalizer.setValue(info1.getTimeStamp());
-            int x0 = (int) normalizer.getValueNormalized();
+            int x0 = (int) normalizer.getValueNormalized(info1.getTimeStamp());
 
-            normalizer.setValue(info2.getTimeStamp());
-            int x1 = (int) normalizer.getValueNormalized();
+            int x1 = (int) normalizer.getValueNormalized(info2.getTimeStamp());
             
             graphics.setColor(info1.getColor().getColor());
             graphics.fillRect(x0, 5, x1 - x0 + 1, 5);
         }
         
-        normalizer.setValue(infos[infos.length - 1].getTimeStamp());
-        int x0 = (int) normalizer.getValueNormalized();
+        int x0 = (int) normalizer.getValueNormalized(infos[infos.length - 1].getTimeStamp());
 
-        normalizer.setValue(infos[infos.length - 1].getTimeStamp() + 250);
-        int x1 = (int) normalizer.getValueNormalized();
+        int x1 = (int) normalizer.getValueNormalized(infos[infos.length - 1].getTimeStamp() + 250);
 
         graphics.setColor(lastColor);        
         graphics.fillRect(x0, 5, x1 - x0 + 1, 5);
--- a/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapChartPanelLayout.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapChartPanelLayout.java	Wed Aug 28 12:03:09 2013 -0400
@@ -95,8 +95,7 @@
             
             Dimension preferredSize = child.getIconCenter();
 
-            normaliser.setValue(child.getTimestamp());
-            x = (int) normaliser.getValueNormalized() - preferredSize.width;
+            x = (int) normaliser.getValueNormalized(child.getTimestamp()) - preferredSize.width;
 
             preferredSize = child.getPreferredSize();
             Rectangle bounds = new Rectangle(x, y, preferredSize.width, preferredSize.height);
--- a/vm-jmx/agent/src/main/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackend.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/vm-jmx/agent/src/main/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackend.java	Wed Aug 28 12:03:09 2013 -0400
@@ -220,11 +220,10 @@
 
             JmxNotification data = new JmxNotification();
             data.setVmId(idAndPid.vmId);
-            data.setImportance("unknown");
             data.setTimeStamp(notification.getTimeStamp());
             data.setSourceBackend(JmxBackend.class.getName());
-            data.setSourceDetails("dunno");
-            data.setContents(notification.toString());
+            data.setSourceDetails(((ObjectName) notification.getSource()).getCanonicalName());
+            data.setContents(notification.getMessage());
             dao.addNotification(data);
 
         }
--- a/vm-jmx/agent/src/test/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackendTest.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/vm-jmx/agent/src/test/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackendTest.java	Wed Aug 28 12:03:09 2013 -0400
@@ -180,6 +180,7 @@
 
         Notification notification = mock(Notification.class);
         when(notification.toString()).thenReturn("testing");
+        when(notification.getSource()).thenReturn(name1);
 
         listener.handleNotification(notification, handbackCaptor.getValue());
 
--- a/vm-jmx/client-swing/src/main/java/com/redhat/thermostat/vm/jmx/client/swing/internal/JmxNotificationsSwingView.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/vm-jmx/client-swing/src/main/java/com/redhat/thermostat/vm/jmx/client/swing/internal/JmxNotificationsSwingView.java	Wed Aug 28 12:03:09 2013 -0400
@@ -36,20 +36,30 @@
 
 package com.redhat.thermostat.vm.jmx.client.swing.internal;
 
+import java.awt.Color;
 import java.awt.Component;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
+import java.awt.RenderingHints;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.swing.AbstractButton;
-import javax.swing.BorderFactory;
 import javax.swing.ButtonModel;
-import javax.swing.DefaultListModel;
-import javax.swing.JList;
+import javax.swing.JFrame;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
-import javax.swing.JScrollPane;
 import javax.swing.SwingUtilities;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
@@ -58,10 +68,16 @@
 import com.redhat.thermostat.client.swing.IconResource;
 import com.redhat.thermostat.client.swing.SwingComponent;
 import com.redhat.thermostat.client.swing.components.ActionToggleButton;
+import com.redhat.thermostat.client.swing.components.EventTimeline;
+import com.redhat.thermostat.client.swing.components.EventTimelineRangeChangeListener;
+import com.redhat.thermostat.client.swing.components.HeaderPanel;
 import com.redhat.thermostat.client.swing.components.LocalizedLabel;
-import com.redhat.thermostat.client.swing.components.HeaderPanel;
+import com.redhat.thermostat.client.swing.components.timeline.Timeline;
+import com.redhat.thermostat.client.ui.Palette;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.model.LongRangeNormalizer;
+import com.redhat.thermostat.common.model.Range;
 import com.redhat.thermostat.shared.locale.LocalizedString;
 import com.redhat.thermostat.shared.locale.Translate;
 import com.redhat.thermostat.vm.jmx.client.core.JmxNotificationsView;
@@ -74,15 +90,18 @@
     private List<ActionListener<NotificationAction>> listeners = new CopyOnWriteArrayList<>();
 
     private final HeaderPanel visiblePanel;
-    private final DefaultListModel<String> listModel = new DefaultListModel<>();
 
     private ActionToggleButton toolbarButton;
 
+    private List<JmxNotification> notifications = new ArrayList<>();
+
+    private DetailPanel timelineDetails;
+    private Timeline ruler;
+    private EventTimeline timeline;
+
     public JmxNotificationsSwingView() {
 
         LocalizedLabel description = new LocalizedLabel(translate.localize(LocaleResources.NOTIFICATIONS_DESCRIPTION));
-        description.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
-        JList<String> issuesList = new JList<>(listModel);
 
         JPanel contents = new JPanel();
         contents.setLayout(new GridBagLayout());
@@ -94,11 +113,45 @@
         contents.add(description, c);
 
         c.gridy++;
+        c.fill = GridBagConstraints.BOTH;
+        c.anchor = GridBagConstraints.FIRST_LINE_START;
+
         c.weightx = 1;
         c.weighty = 1;
-        c.fill = GridBagConstraints.BOTH;
-        c.anchor = GridBagConstraints.FIRST_LINE_START;
-        contents.add(new JScrollPane(issuesList), c);
+
+        timelineDetails = new DetailPanel();
+        contents.add(timelineDetails, c);
+
+        c.gridy++;
+        c.weightx = 0;
+        c.weighty = 0;
+
+        ruler = new Timeline(new Range<Long>(1l, 2l));
+
+        contents.add(ruler, c);
+
+        c.gridy++;
+        c.weightx = 0.25;
+        c.weighty = 0.25;
+
+        timeline = new EventTimeline();
+        Color edgeColor = Palette.THERMOSTAT_BLU.getColor();
+        Color fillColor = new Color(edgeColor.getRed(), edgeColor.getGreen(), edgeColor.getBlue(), 25);
+        Color eventColor = Palette.THERMOSTAT_RED.getColor();
+        timeline.setSelectionEdgePaint(edgeColor);
+        timeline.setSelectionFillPaint(fillColor);
+        timeline.setEventPaint(eventColor);
+
+        timeline.getModel().addRangeChangeListener(new EventTimelineRangeChangeListener() {
+            @Override
+            public void rangeChanged(Range<Long> overview, Range<Long> detail) {
+                if (detail != null) {
+                    plotDetails(detail.getMin(), detail.getMax());
+                }
+            }
+        });
+
+        contents.add(timeline, c);
 
         contents.addHierarchyListener(new ComponentVisibleListener() {
             @Override
@@ -141,6 +194,11 @@
         visiblePanel.setContent(contents);
     }
 
+    protected void plotDetails(long start, long end) {
+        ruler.setRange(new Range<Long>(start, end));
+        timelineDetails.setDisplayRange(start, end);
+    }
+
     @Override
     public void addNotificationActionListener(ActionListener<NotificationAction> listener) {
         listeners.add(listener);
@@ -172,7 +230,8 @@
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                listModel.clear();
+                notifications.clear();
+                timeline.getModel().clearEvents();
             }
         });
     }
@@ -182,7 +241,8 @@
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                listModel.add(listModel.size(), data.getContents());
+                notifications.add(data);
+                timeline.getModel().addEvent(data.getTimeStamp(), data.getSourceDetails());
             }
         });
     }
@@ -202,4 +262,120 @@
         });
     }
 
+    private class DetailPanel extends JPanel {
+
+        private long start = 0;
+        private long end = 0;
+
+        public void setDisplayRange(long start, long end) {
+            this.start = start;
+            this.end = end;
+
+            repaint();
+        }
+
+        @Override
+        protected void paintComponent(Graphics g) {
+            super.paintComponent(g);
+
+            long startTimeStamp = start;
+            long endTimeStamp = end;
+
+            int step = 0;
+
+            for (JmxNotification not : notifications) {
+                step++;
+                if ((not.getTimeStamp() >= startTimeStamp) && (not.getTimeStamp() <= endTimeStamp)) {
+                    paintNotification((Graphics2D) g, not, step);
+                }
+            }
+        }
+
+        private void paintNotification(Graphics2D g, JmxNotification not, int step) {
+            long startTimeStamp = start;
+            long endTimeStamp = end;
+            LongRangeNormalizer normalizer = new LongRangeNormalizer(new Range<>(startTimeStamp, endTimeStamp), 0, getWidth());
+            int xPos = (int) normalizer.getValueNormalized(not.getTimeStamp());
+
+            paintNotificationDetails(not, g, xPos, getYBandPosition(step));
+        }
+
+        private int getYBandPosition(int step) {
+            // TODO can we do better in determining the number of 'bands' ?
+            int TOTAL_STEPS = 10;
+            step = step % TOTAL_STEPS;
+
+            return Math.round(1.0f * step * getHeight() / TOTAL_STEPS);
+        }
+
+        private void paintNotificationDetails(JmxNotification notification, Graphics2D g, int x, int y) {
+            g = (Graphics2D) g.create();
+
+            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+            FontMetrics metrics = g.getFontMetrics();
+            double textWidth = 0;
+
+            textWidth = Math.max(textWidth, metrics.getStringBounds(notification.getSourceDetails(), g).getWidth());
+            textWidth = Math.max(textWidth, metrics.getStringBounds(notification.getContents(), g).getWidth());
+
+            int lines = 3;
+            int lineHeight = metrics.getHeight();
+
+            int textHeight = lineHeight * lines;
+
+            final int TEXT_PADDING = 10;
+
+            g.setColor(Palette.PALE_RED.getColor());
+
+            g.drawLine(x, getHeight(), x, y);
+            g.fillRect(x, y, (int) textWidth + (2 * TEXT_PADDING), textHeight + (lines * TEXT_PADDING));
+
+            g.setColor(Color.WHITE);
+            DateFormat df = new SimpleDateFormat("HH:mm:ss");
+            g.drawString(df.format(new Date(notification.getTimeStamp())), x + TEXT_PADDING, y + TEXT_PADDING + (lineHeight * 1));
+            g.drawString(notification.getSourceDetails(), x + TEXT_PADDING, y + TEXT_PADDING + (lineHeight * 2));
+            g.drawString(notification.getContents(), x + TEXT_PADDING, y + TEXT_PADDING + (lineHeight * 3));
+
+            g.dispose();
+        }
+
+    }
+
+    public static void main(String[] args) {
+        final JmxNotificationsSwingView view = new JmxNotificationsSwingView();
+
+        SwingUtilities.invokeLater(new Runnable() {
+
+            @Override
+            public void run() {
+                JFrame frame = new JFrame();
+                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+                frame.add(view.getUiComponent());
+
+                frame.setVisible(true);
+            }
+        });
+
+        final long time = System.currentTimeMillis();
+
+        final AtomicInteger i = new AtomicInteger(0);
+        Timer t = new Timer();
+        TimerTask task = new TimerTask() {
+            @Override
+            public void run() {
+                int c = i.incrementAndGet();
+                JmxNotification data = new JmxNotification();
+                data.setTimeStamp(time + (c * TimeUnit.MINUTES.toMillis(1)));
+                data.setSourceBackend("foo");
+                data.setSourceDetails("GarbageCollection " + c);
+                data.setContents(c + ": PS Scavenge on 'old' gen. 10 s");
+                view.addNotification(data);
+            }
+        };
+
+        t.schedule(task, 0, TimeUnit.SECONDS.toMillis(1));
+    }
+
 }
--- a/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/JmxNotification.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/JmxNotification.java	Wed Aug 28 12:03:09 2013 -0400
@@ -46,7 +46,6 @@
 
     private long timeStamp;
     private String vmId;
-    private String importance;
     private String sourceBackend;
     private String sourceDetails;
     private String contents;
@@ -62,16 +61,6 @@
     }
 
     @Persist
-    public String getImportance() {
-        return importance;
-    }
-
-    @Persist
-    public void setImportance(String importance) {
-        this.importance = importance;
-    }
-
-    @Persist
     public String getSourceBackend() {
         return sourceBackend;
     }
--- a/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java	Mon Aug 26 18:33:02 2013 +0200
+++ b/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java	Wed Aug 28 12:03:09 2013 -0400
@@ -66,17 +66,14 @@
             new Category<>("vm-jmx-notification-status", JmxNotificationStatus.class,
                     Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP, NOTIFICATIONS_ENABLED);
 
-    // TODO: private static final Key IMPORTANCE = new Key<>("importance",
-    // false);
-
     private static final Key<String> SOURCE_BACKEND = new Key<>("sourceBackend");
-    private static final Key<String> SOURCE_DESCRPTION = new Key<>("sourceDescription");
+    private static final Key<String> SOURCE_DETAILS = new Key<>("sourceDetails");
     private static final Key<String> CONTENTS = new Key<>("contents");
 
     static final Category<JmxNotification> NOTIFICATIONS =
             new Category<>("vm-jmx-notification", JmxNotification.class,
                     Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP,
-                    SOURCE_BACKEND, SOURCE_DESCRPTION, CONTENTS);
+                    SOURCE_BACKEND, SOURCE_DETAILS, CONTENTS);
 
     static final String QUERY_LATEST_NOTIFICATION_STATUS = "QUERY "
             + NOTIFICATION_STATUS.getName() + " WHERE '"