changeset 2455:998669414ab4

Align Byteman chart view with look and feel. Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-September/020887.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Wed, 14 Sep 2016 15:05:13 +0200
parents 69dee5f97497
children 8c3134195c82
files vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java
diffstat 5 files changed, 586 insertions(+), 468 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java	Wed Sep 14 15:05:13 2016 +0200
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2012-2016 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.vm.byteman.client.swing.internal;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jfree.data.DefaultKeyedValues;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.general.DefaultPieDataset;
+import org.jfree.data.general.PieDataset;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+import com.redhat.thermostat.common.Pair;
+import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
+
+class GraphDataset {
+    
+    enum CoordinateType {
+        INTEGRAL,
+        REAL,
+        TIME,
+        CATEGORY
+    };
+    
+    /**
+     * A special coordinate name used to identify the timestamp associated
+     * with any given Byteman metric. If it is entered as the x or y coordinate
+     * name in the graph dialogue then it will select the tiemstamp as the value
+     * to be graphed against the other chosen coordinate. Timestamp values
+     * are stored as longs but are displayed as time values.
+     *
+     * TODO: it really only makes sense to use timestamp as the X axis. maybe
+     * we should reject any attempt to use it for the y axis?
+     */
+    static final String TIMESTAMP_KEY = "timestamp";
+
+    /**
+     * A special coordinate name used to identify the frequency count
+     * of any given Byteman metric. If it is entered as the x or y coordinate
+     * name in the graph dialogue then it will count 1 for each occurence of
+     * other value. Frequency values are stored as longs.
+     */
+    static final String FREQUENCY_KEY = "frequency";
+
+    /**
+     * A special coordinate name used to identify the marker string
+     * of any given Byteman metric. If it is entered as the x or y coordinate
+     * name in the graph dialogue then it will select the marker as the value
+     * to be graphed against the other chosen coordinate.
+     */
+    static final String MARKER_KEY = "marker";
+    
+    private final List<Pair<Object, Object>> data;
+    String xkey;
+    String ykey;
+    CoordinateType xtype;
+    CoordinateType ytype;
+    private static CategoryDataset emptyCategoryDataset = new DefaultCategoryDataset();
+    private static PieDataset emptyPieDataset = new DefaultPieDataset();
+    private static XYDataset emptyXYDataset = new XYSeriesCollection();
+    private static Number frequencyUnit = Long.valueOf(1);
+
+    public GraphDataset(List<BytemanMetric> metrics, String xkey, String ykey, String filter, String value)
+    {
+        this.xkey = xkey;
+        this.ykey = ykey;
+        xtype = CoordinateType.INTEGRAL;
+        ytype = CoordinateType.INTEGRAL;
+        data = new ArrayList<Pair<Object,Object>>();
+        if (TIMESTAMP_KEY.equals(xkey)) {
+            xtype = CoordinateType.TIME;
+        } else if (FREQUENCY_KEY.equals(xkey)) {
+            xtype = CoordinateType.INTEGRAL;
+        } else if (MARKER_KEY.equals(xkey)) {
+            xtype = CoordinateType.CATEGORY;
+        }
+        if (TIMESTAMP_KEY.equals(ykey)) {
+            ytype = CoordinateType.TIME;
+        } else if (FREQUENCY_KEY.equals(ykey)) {
+            ytype = CoordinateType.INTEGRAL;
+        } else if (MARKER_KEY.equals(ykey)) {
+            ytype = CoordinateType.CATEGORY;
+        }
+        // if we have a filter value then convert it to a number if it is numeric
+        Object filterValue = value;
+        if (filter != null && value != null) {
+            // may need to convert String to Numeric
+            filterValue = maybeNumeric(value);
+        }
+        if (metrics != null) {
+            for (BytemanMetric m : metrics) {
+                Map<String, Object> map = m.getDataAsMap();
+                // ensure that lookups for the timestamp key always retrieve
+                // the Long timestamp value associated with the metric and
+                // that lookups for the frequency key always retrieve
+                // the Long value 1.
+                map.put(TIMESTAMP_KEY, m.getTimeStamp());
+                map.put(FREQUENCY_KEY, frequencyUnit);
+                map.put(MARKER_KEY, m.getMarker());
+                // if we have a filter then check for presence of filter key
+                if (filter != null && filter.length() > 0) {
+                    Object v = map.get(filter);
+                    if (v == null) {
+                        // skip this metric
+                        continue;
+                    }
+                    if (filterValue != null) {
+                        // may need to process String value as Numeric
+                        if (v instanceof String) {
+                            v = maybeNumeric((String)v);
+                        }
+                        if (!filterValue.equals(v)) {
+                            // skip this metric
+                            continue;
+                        }
+                    }
+                }
+                Object xval = map.get(xkey);
+                Object yval = map.get(ykey);
+                // only include records which contain values for both coordinates
+                if(xval != null && yval != null) {
+                    // maybe re-present retrieved values as Numeric
+                    // and/or downgrade coordinate type from INTEGRAL
+                    // to REAL or even CATEGORY
+                    xval = newCoordinate(xkey, xval, true);
+                    yval = newCoordinate(ykey, yval, false);
+                    data.add(new Pair<Object, Object>(xval, yval));
+                }
+            }
+        }
+    }
+
+    public int size() {
+        return data.size();
+    }
+
+    public XYDataset getXYDataset()
+    {
+        if (xtype == CoordinateType.CATEGORY ||
+                ytype == CoordinateType.CATEGORY) {
+            return emptyXYDataset;
+        }
+
+        XYSeries xyseries = new XYSeries(ykey + " against  " + xkey);
+
+        for (Pair<Object,Object> p : data) {
+            Number x = (Number)p.getFirst();
+            Number y = (Number)p.getSecond();
+            int idx = xyseries.indexOf(x);
+            if (idx >= 0) {
+                Number y1 = xyseries.getY(idx);
+                switch (ytype) {
+                case REAL:
+                    y = y.doubleValue() + y1.doubleValue();
+                default:
+                    y = y.longValue() + y1.longValue();
+                }
+            }
+            xyseries.add(x, y);
+        }
+        XYSeriesCollection xycollection = new  XYSeriesCollection();
+        xycollection.addSeries(xyseries);
+        return xycollection;
+    }
+
+    public CategoryDataset getCategoryDataset()
+    {
+        if (xtype == CoordinateType.TIME) {
+            return emptyCategoryDataset;
+        }
+        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+        // treat x values as category values by calling toString
+        // where they are numeric we ought to support binning them into ranges
+        switch (ytype) {
+        case CATEGORY:
+            // graph category against category by frequency
+            for (Pair<Object, Object> p : data) {
+                String first = p.getFirst().toString();
+                String second = p.getSecond().toString();
+                if(dataset.getRowKeys().contains(first) && dataset.getColumnKeys().contains(second)) {
+                    dataset.incrementValue(1.0, first, second);
+                } else {
+                    dataset.addValue(1.0, first, second);
+                }
+            }
+            break;
+        case TIME:
+            // bin time values into ranges and label range with start time
+            // for now just drop through to treat time value as numeric
+        default:
+            // graph category against numeric by summing numeric values
+            for (Pair<Object, Object> p : data) {
+                String first = p.getFirst().toString();
+                String second = "";
+                double increment = ((Number) p.getSecond()).doubleValue();
+                if(dataset.getRowKeys().contains(first)) {
+                    dataset.incrementValue(increment, first, second);
+                } else {
+                    dataset.addValue(increment, first, second);
+                }
+            }
+            break;
+        }
+        return dataset;
+    }
+
+    // alternative option for presenting category xkey with numeric ykey
+    public PieDataset getPieDataset()
+    {
+        if (xtype != CoordinateType.CATEGORY || ytype == CoordinateType.CATEGORY) {
+            return emptyPieDataset;
+        }
+
+        DefaultKeyedValues keyedValues = new DefaultKeyedValues();
+
+        for (Pair<Object,Object> p : data) {
+            String first = p.getFirst().toString();
+            double second = ((Number)p.getSecond()).doubleValue();
+            int index = keyedValues.getIndex(first);
+            if (index >= 0) {
+                Number existing = keyedValues.getValue(first);
+                keyedValues.addValue(first, existing.doubleValue() + second);
+            } else {
+                keyedValues.addValue(first, second);
+            }
+        }
+        PieDataset pieDataset = new DefaultPieDataset(keyedValues);
+        return pieDataset;
+    }
+
+    public XYDataset getCategoryTimePlot(String[][] symbolsReturn)
+    {
+        if (xtype != CoordinateType.TIME || ytype != CoordinateType.CATEGORY) {
+            return emptyXYDataset;
+        }
+
+        // we need to display changing category state over time
+        //
+        // we can create an XYDataSet substituting numeric Y values
+        // to encode the category data. then we provide the data
+        // set with a range axis which displays the numeric
+        // values symbolically.
+
+        XYSeries xyseries = new XYSeries(ykey + " against  " + xkey);
+        int count = 0;
+        HashMap<String, Number> tickmap = new HashMap<String, Number>();
+
+        for (Pair<Object,Object> p : data) {
+            Number x = (Number)p.getFirst();
+            String ysym = (String)p.getSecond();
+            Number y = tickmap.get(ysym);
+            if (y == null) {
+                y = Long.valueOf(count++);
+                tickmap.put(ysym, y);
+            }
+            xyseries.add(x, y);
+        }
+        // populate key array
+        String[] symbols = new String[count];
+        for (String key: tickmap.keySet()) {
+            int value = tickmap.get(key).intValue();
+            symbols[value] = key;
+        }
+
+        symbolsReturn[0] = symbols;
+
+        XYSeriesCollection xycollection = new  XYSeriesCollection();
+        xycollection.addSeries(xyseries);
+
+        return xycollection;
+    }
+
+    public String getXLabel() {
+        return xkey;
+    }
+
+    public String getYLabel() {
+        return ykey;
+    }
+
+    public CoordinateType getXType() {
+        return xtype;
+    }
+
+    public CoordinateType getYType() {
+        return ytype;
+    }
+
+    private Object maybeNumeric(String value) {
+        if (value == null || value.length() == 0)  {
+            return null;
+        }
+        try {
+            if(value.contains(".")) {
+                return Double.valueOf(value);
+            } else {
+                return Long.valueOf(value);
+            }
+        } catch (NumberFormatException nfe) {
+            return value;
+        }
+    }
+
+    /**
+     * process a newly read x or y coordinate value, which is either a Long timestanp or an unparsed
+     * numeric or category value String, returning a Long, parsed Numeric or String value. As a side
+     * effect of attempting to parse an input String the coordinate type for the relevant coordinate
+     * axis may be downgraded from INTEGRAL (assumed default) to DOUBLE or CATEGORY.
+     * @param key the label for the coordinate axis which may be the special value timestamp
+     * @param value the new found coordinate value which may be a Long timestamp or a String yet to be parsed
+     * @param isX  true if this is an x coordinate value false if it is a y coordinate value
+     * @return an Object repreenting
+     */
+    private Object newCoordinate(String key, Object value, boolean isX) {
+
+        CoordinateType ctype = (isX ? xtype : ytype);
+        if (ctype == CoordinateType.TIME) {
+            // guaranteed already to be a Long
+            return value;
+        }
+
+        boolean updateCType = false;
+
+        if (value instanceof String && ctype != CoordinateType.CATEGORY) {
+            String str = (String)value;
+            // see if we can parse this as a number
+            try {
+                if (str.contains(".")) {
+                    value = Double.valueOf(str);
+                    if (ctype != CoordinateType.REAL) {
+                        ctype = CoordinateType.REAL;
+                        updateCType = true;
+                    }
+                } else {
+                    value = Long.valueOf(str);
+                }
+            } catch (NumberFormatException nfe) {
+                ctype = CoordinateType.CATEGORY;
+                updateCType = true;
+            }
+        }
+        if (updateCType) {
+            if (isX) {
+                xtype = ctype;
+            } else {
+                ytype = ctype;
+            }
+        }
+        return value;
+    }
+}
\ No newline at end of file
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java	Fri Sep 16 10:25:53 2016 -0400
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java	Wed Sep 14 15:05:13 2016 +0200
@@ -55,6 +55,10 @@
     LABEL_LOCAL_RULE,
     LABEL_INJECTED_RULE,
     IMPORT_RULE,
+    FILTER,
+    FILTER_VALUE_LABEL,
+    X_COORD,
+    Y_COORD
     ;
     
     static final String RESOURCE_BUNDLE = LocaleResources.class.getPackage().getName() + ".strings";
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java	Fri Sep 16 10:25:53 2016 -0400
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java	Wed Sep 14 15:05:13 2016 +0200
@@ -54,13 +54,9 @@
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.text.DateFormat;
-import java.util.ArrayList;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -94,29 +90,26 @@
 import com.redhat.thermostat.client.swing.components.ThermostatTabbedPane;
 import com.redhat.thermostat.client.swing.components.ThermostatTextArea;
 import com.redhat.thermostat.client.swing.components.experimental.RecentTimeControlPanel;
+import com.redhat.thermostat.client.swing.components.experimental.RecentTimeControlPanel.UnitRange;
+import com.redhat.thermostat.client.swing.components.experimental.ThermostatChartPanel;
+import com.redhat.thermostat.client.swing.components.experimental.ThermostatChartPanelBuilder;
 import com.redhat.thermostat.client.swing.experimental.ComponentVisibilityNotifier;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.Duration;
-import com.redhat.thermostat.common.Pair;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.shared.locale.LocalizedString;
 import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.vm.byteman.client.swing.internal.GraphDataset.CoordinateType;
 import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
 
 import org.jfree.chart.ChartFactory;
-import org.jfree.chart.ChartPanel;
 import org.jfree.chart.JFreeChart;
 import org.jfree.chart.axis.SymbolAxis;
 import org.jfree.chart.plot.PlotOrientation;
-import org.jfree.data.DefaultKeyedValues;
 import org.jfree.data.category.CategoryDataset;
 import org.jfree.data.category.DefaultCategoryDataset;
-import org.jfree.data.general.DefaultPieDataset;
-import org.jfree.data.general.PieDataset;
 import org.jfree.data.xy.XYDataset;
-import org.jfree.data.xy.XYSeries;
-import org.jfree.data.xy.XYSeriesCollection;
 
 public class SwingVmBytemanView extends VmBytemanView implements SwingComponent {
 
@@ -127,6 +120,7 @@
     private static final Icon ARROW_LEFT = IconResource.ARROW_LEFT.getIcon();
     private static final Icon ARROW_RIGHT = IconResource.ARROW_RIGHT.getIcon();
     private static final String EMPTY_STR = "";
+    private static final String BYTEMAN_CHART_LABEL = EMPTY_STR;
     
     static final String NO_METRICS_AVAILABLE = t.localize(LocaleResources.NO_METRICS_AVAILABLE).getContents();
     
@@ -138,15 +132,16 @@
     
     private String injectedRuleContent;
     private String unloadedRuleContent;
+    private ThermostatChartPanel graphPanel;
+    private RecentTimeControlPanel graphTimeControlPanel;
     private boolean generateRuleToggle;
     private final JTextArea metricsText;
     private final JTextArea unloadedRulesText;
     private final JTextArea injectedRulesText;
     private final JButton injectRuleButton;
     private final JButton unloadRuleButton;
-    private JPanel graphMainPanel;
-    private ChartPanel graphPanel;
-    private JFreeChart graph;
+    private final JPanel graphMainPanel;
+    private final JPanel metricsPanel;
     private final JTabbedPane tabbedPane;
     private final HeaderPanel mainContainer;
     private final ActionToggleButton toggleButton;
@@ -158,15 +153,14 @@
     // ideally these ought to be stored as
     // fields of a separate model instance
 
-    String xkey = null;
-    String ykey = null;
-    String filter = null;
-    String value = null;
-    String graphtype = null;
+    private String xkey = null;
+    private String ykey = null;
+    private String filter = null;
+    private String value = null;
+    private String graphtype = null;
 
     // duration over which to search for metrics
-
-    Duration duration = new Duration(5, TimeUnit.MINUTES);
+    private Duration duration = ThermostatChartPanel.DEFAULT_DATA_DISPLAY;
 
     // Mutable state
     private boolean viewControlsEnabled;
@@ -264,6 +258,7 @@
         splitPane.setRightComponent(injectedPane);
         splitPane.setResizeWeight(halfWeight);
         BasicSplitPaneUI ui = new BasicSplitPaneUI() {
+            @SuppressWarnings("serial")
             public BasicSplitPaneDivider createDefaultDivider() {
                 return new BasicSplitPaneDivider(this) {
                     @Override
@@ -357,7 +352,7 @@
         rulesPanel.add(buttonHolder, cRules);
         
         // Metrics tab
-        final JPanel metricsPanel = new JPanel();
+        metricsPanel = new JPanel();
         metricsPanel.setLayout(new GridBagLayout());
         metricsText = new ThermostatTextArea(EMPTY_STR);
         metricsText.setName(METRICS_TEXT_NAME);
@@ -373,40 +368,18 @@
         c.insets = paddingInsets;
         JScrollPane metricsScroll = new ThermostatScrollPane(metricsText);
         metricsPanel.add(metricsScroll, c);
-        c.fill = GridBagConstraints.BOTH;
-        c.gridx = 0;
-        c.gridy = 1;
-        c.weighty = yWeightRow2;
-        c.weightx = xWeightFullWidth;
         // add a panel to control selection of metrics time interval
-        RecentTimeControlPanel graphTimeControlPanel = new RecentTimeControlPanel(duration, RecentTimeControlPanel.UnitRange.MEDIUM);
-        graphTimeControlPanel.addPropertyChangeListener(RecentTimeControlPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() {
-            @Override
-            public void propertyChange(final PropertyChangeEvent evt) {
-                duration = (Duration) evt.getNewValue();
-                fireTabSelectedEvent(TabbedPaneAction.METRICS_TAB_SELECTED);
-            }
-        });
-
-        graphTimeControlPanel.setPreferredSize(buttonHolder.getPreferredSize());
-        metricsPanel.add(graphTimeControlPanel, c);
+        updateGraphControlPanel(xWeightFullWidth, yWeightRow2);
 
         // graph tab
+        Insets spacerLeftInsets = new Insets(0, 5, 0, 0);
         graphMainPanel = new JPanel();
         // add a panel to control display of the graph
         JPanel graphControlHolder =  new JPanel();
-        JPanel subHolder1 = new JPanel();
-        JPanel subHolder2 = new JPanel();
-        layout = new FlowLayout();
-        layout.setAlignment(FlowLayout.RIGHT);
-        layout.setHgap(5);
-        layout.setVgap(0);
-        subHolder1.setLayout(layout);
-        subHolder1.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
 
         // insert two labelled text fields to allow axis selection
-        JLabel xlabel = new JLabel("x:");
-        JLabel ylabel = new JLabel("y:");
+        JLabel xlabel = new JLabel(t.localize(LocaleResources.X_COORD).getContents());
+        JLabel ylabel = new JLabel(t.localize(LocaleResources.Y_COORD).getContents());
         final JTextField xtext = new JTextField(30);
         final JTextField ytext = new JTextField(30);
 
@@ -454,15 +427,9 @@
             }
 
         });
-        subHolder1.add(generateGraphButton);
-        subHolder1.add(ytext);
-        subHolder1.add(ylabel);
-        subHolder1.add(xtext);
-        subHolder1.add(xlabel);
-        subHolder1.setAlignmentX(Component.RIGHT_ALIGNMENT);
 
-        JLabel filterlabel = new JLabel("filter:");
-        JLabel valuelabel = new JLabel("==");
+        JLabel filterlabel = new JLabel(t.localize(LocaleResources.FILTER).getContents());
+        JLabel valuelabel = new JLabel(t.localize(LocaleResources.FILTER_VALUE_LABEL).getContents());
         final JTextField filterText = new JTextField(30);
         final JTextField valueText = new JTextField(30);
 
@@ -499,32 +466,74 @@
             }
         });
 
-        layout = new FlowLayout();
-        layout.setAlignment(FlowLayout.RIGHT);
-        layout.setHgap(5);
-        layout.setVgap(0);
-        subHolder2.setLayout(layout);
-        subHolder2.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
-
-        subHolder2.add(valueText);
-        subHolder2.add(valuelabel);
-        subHolder2.add(filterText);
-        subHolder2.add(filterlabel);
-        subHolder2.setAlignmentX(Component.RIGHT_ALIGNMENT);
-
         graphControlHolder.setLayout(new GridBagLayout());
-        c.fill = GridBagConstraints.BOTH;
-        c.gridx = 0;
-        c.gridy = 0;
-        c.weighty = 0.5;
-        c.weightx = xWeightFullWidth;
-        graphControlHolder.add(subHolder1, c);
-
-        c.gridx = 0;
-        c.gridy = 1;
-        c.weighty = 0.5;
-        c.weightx = xWeightFullWidth;
-        graphControlHolder.add(subHolder2, c);
+        
+        double weightShortLabel = 0.05;
+        double weightTextBox = 0.3;
+        GridBagConstraints graphConstraints = new GridBagConstraints();
+        graphConstraints.fill = GridBagConstraints.BOTH;
+        graphConstraints.gridx = 0;
+        graphConstraints.gridy = 0;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightShortLabel;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(xlabel, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 1;
+        graphConstraints.gridy = 0;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightTextBox;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(xtext, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 2;
+        graphConstraints.gridy = 0;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightShortLabel;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(ylabel, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 3;
+        graphConstraints.gridy = 0;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightTextBox;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(ytext, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 0;
+        graphConstraints.gridy = 1;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightShortLabel;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(filterlabel, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 1;
+        graphConstraints.gridy = 1;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightTextBox;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(filterText, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 2;
+        graphConstraints.gridy = 1;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightShortLabel;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(valuelabel, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 3;
+        graphConstraints.gridy = 1;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightTextBox;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(valueText, graphConstraints);
+        graphConstraints.fill = GridBagConstraints.HORIZONTAL;
+        graphConstraints.gridx = 4;
+        graphConstraints.gridy = 1;
+        graphConstraints.weighty = 0.5;
+        graphConstraints.weightx = weightTextBox;
+        graphConstraints.insets = spacerLeftInsets;
+        graphControlHolder.add(generateGraphButton, graphConstraints);
 
         // add controls and empty graph to main panel but don't add graph panel yet
         graphMainPanel.setLayout(new GridBagLayout());
@@ -536,18 +545,11 @@
         graphMainPanel.add(graphControlHolder, c);
 
         CategoryDataset categoryDataset = new DefaultCategoryDataset();
-        graph = ChartFactory.createBarChart("empty", "", "",
+        JFreeChart graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, EMPTY_STR, EMPTY_STR,
                                             categoryDataset, PlotOrientation.VERTICAL,
                                             true, true, false);
-        graphPanel = new ChartPanel(graph, true);
-        c.fill = GridBagConstraints.BOTH;
-        c.gridx = 0;
-        c.gridy = 1;
-        c.weighty = yWeightRow1 + yWeightRow2;
-        c.weightx = xWeightFullWidth;
-        graphMainPanel.add(graphPanel, c);
-
-        graphMainPanel.revalidate();
+        double weighty = yWeightRow1 + yWeightRow2;
+        updateGraphPanel(weighty, xWeightFullWidth, graph);
 
         tabbedPane = new ThermostatTabbedPane();
         tabbedPane.addTab(t.localize(LocaleResources.TAB_RULES).getContents(), rulesPanel);
@@ -576,6 +578,54 @@
         mainContainer.setContent(tabbedPane);
         mainContainer.addToolBarButton(toggleButton);
     }
+
+    private void updateGraphControlPanel(double weightx, double weighty) {
+        GridBagConstraints c = new GridBagConstraints();
+        c.fill = GridBagConstraints.BOTH;
+        c.gridx = 0;
+        c.gridy = 1;
+        c.weighty = weighty;
+        c.weightx = weightx;
+        
+        graphTimeControlPanel = new RecentTimeControlPanel(duration, RecentTimeControlPanel.UnitRange.MEDIUM);
+        graphTimeControlPanel.addPropertyChangeListener(RecentTimeControlPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() {
+            @Override
+            public void propertyChange(final PropertyChangeEvent evt) {
+                duration = (Duration) evt.getNewValue();
+                fireTabSelectedEvent(TabbedPaneAction.METRICS_TAB_SELECTED);
+            }
+        });
+        
+        metricsPanel.add(graphTimeControlPanel, c);
+        metricsPanel.revalidate();
+    }
+
+    private void updateGraphPanel(final double weighty,
+                                  final double weightx,
+                                  JFreeChart graph) {
+        ThermostatChartPanelBuilder chartBuilder = new ThermostatChartPanelBuilder();
+        graphPanel = chartBuilder
+            .duration(duration)
+            .chart(graph)
+            .xyPlotFixedAutoRange(false)
+            .unitRange(UnitRange.MEDIUM)
+            .build();
+        graphPanel.addPropertyChangeListener(RecentTimeControlPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() {
+            @Override
+            public void propertyChange(PropertyChangeEvent evt) {
+                duration = (Duration) evt.getNewValue();
+                fireGenerateEvent(GenerateAction.GENERATE_GRAPH);
+            }
+        });
+        GridBagConstraints c = new GridBagConstraints();
+        c.fill = GridBagConstraints.BOTH;
+        c.gridx = 0;
+        c.gridy = 1;
+        c.weighty = weighty;
+        c.weightx = weightx;
+        graphMainPanel.add(graphPanel, c);
+        graphMainPanel.revalidate();
+    }
     
     private void fireGenerateEvent(final GenerateAction action) {
         fireAction(action, generateListeners);
@@ -684,6 +734,7 @@
             @SuppressWarnings("unchecked")
             List<BytemanMetric> graphMetrics = (List<BytemanMetric>)event.getPayload();
             updateGraphInView(graphMetrics, xkey, ykey, filter, value, graphtype);
+            updateMetricsRangeInView();
             break;
         default:
             throw new AssertionError("Unknown event: " + action);
@@ -691,6 +742,25 @@
         
     }
 
+    // time range might have changed in graph view. update metrics
+    // accordingly
+    private void updateMetricsRangeInView() {
+        SwingUtilities.invokeLater(new Runnable() {
+
+            @Override
+            public void run() {
+                double weightx = 1.0;
+                double weighty = 0.05;
+                if (graphTimeControlPanel != null) {
+                    graphTimeControlPanel.setVisible(false);
+                    metricsPanel.remove(graphMainPanel);
+                }
+                updateGraphControlPanel(weightx, weighty);
+            }
+            
+        });
+    }
+
     private void updateRuleInView(final String rule) {
         SwingUtilities.invokeLater(new Runnable() {
             @Override
@@ -786,7 +856,6 @@
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                System.out.println("updateGraphInView:");
                 GraphDataset dataset = makeGraphDataset(ms, xk, yk, f, v);
                 if (dataset != null) {
                     switchGraph(dataset, t);
@@ -809,20 +878,21 @@
         String ylabel = dataset.getYLabel();
         CoordinateType xtype = dataset.getXType();
         CoordinateType ytype = dataset.getYType();
+        JFreeChart graph = null;
         switch (xtype) {
             case CATEGORY:
                 if(ytype == CoordinateType.CATEGORY) {
                     // use a bar chart with multiple bars per category 1 value
                     // where each bar counts the frequency for the second category
                     CategoryDataset categoryDataset = dataset.getCategoryDataset();
-                    graph = ChartFactory.createBarChart("Byteman Metrics", xlabel, ylabel,
+                    graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, xlabel, ylabel,
                                                         categoryDataset, PlotOrientation.VERTICAL,
                                                         true, true, false);
                 } else {
                     // draw as a bar chart with one bar per category where
                     // each bar sums the associated second coordinate values
                     CategoryDataset categoryDataset = dataset.getCategoryDataset();
-                    graph = ChartFactory.createBarChart("Byteman Metrics", xlabel, ylabel,
+                    graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, xlabel, ylabel,
                                                         categoryDataset, PlotOrientation.VERTICAL,
                                                         true, true, false);
                     /*
@@ -842,7 +912,7 @@
                     // from numeric values to symbolic keys
                     String[][] symbolsReturn = new String[1][];
                     XYDataset xydataset = dataset.getCategoryTimePlot(symbolsReturn);
-                    graph = ChartFactory.createXYStepChart("Byteman Metrics", xlabel, ylabel,
+                    graph = ChartFactory.createXYStepChart(BYTEMAN_CHART_LABEL, xlabel, ylabel,
                                                            xydataset, PlotOrientation.VERTICAL,
                                                            true, true, false);
                     // now we change the range axis of the xyplot to draw symbols in
@@ -851,7 +921,7 @@
                 } else {
                     // draw a graph of numeric value against time
                     XYDataset xydataset = dataset.getXYDataset();
-                    graph = ChartFactory.createTimeSeriesChart("Byteman Metrics", xlabel, ylabel,
+                    graph = ChartFactory.createTimeSeriesChart(BYTEMAN_CHART_LABEL, xlabel, ylabel,
                                                                xydataset, true, true, false);
                 }
                 break;
@@ -861,7 +931,7 @@
                     // we could treat the numeric values as category values (or ranges?)
                     // and draw this as a bar chart
                     CategoryDataset categoryDataset = dataset.getCategoryDataset();
-                    graph = ChartFactory.createBarChart("Byteman Metrics", xlabel, ylabel,
+                    graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, xlabel, ylabel,
                                                         categoryDataset, PlotOrientation.VERTICAL,
                                                         true, true, false);
                     // for now draw an empty graph
@@ -878,26 +948,21 @@
                 } else {
                     // draw an xy line plot of numeric value against numeric value
                     XYDataset xydataset = dataset.getXYDataset();
-                    graph = ChartFactory.createXYLineChart("Byteman Metrics", xlabel, ylabel,
+                    graph = ChartFactory.createXYLineChart(BYTEMAN_CHART_LABEL, xlabel, ylabel,
                                                            xydataset, PlotOrientation.VERTICAL,
                                                            true, true, false);
                 }
                 break;
         }
-        if(graphPanel != null) {
-            graphPanel.setVisible(false);
+        if (graph == null) {
+            throw new AssertionError("Graph must not be null");
+        }
+        if (graphPanel != null) {
             graphMainPanel.remove(graphPanel);
         }
-        graphPanel = new ChartPanel(graph, true);
-        GridBagConstraints c = new GridBagConstraints();
-        c.fill = GridBagConstraints.BOTH;
-        c.gridx = 0;
-        c.gridy = 1;
-        c.weighty = 0.90;
-        c.weightx = 1.0;
-        graphMainPanel.add(graphPanel, c);
-        graphPanel.setVisible(true);
-        graphMainPanel.revalidate();
+        double weighty = 0.95;
+        double weightx = 1.0;
+        updateGraphPanel(weighty, weightx, graph);
     }
 
     @Override
@@ -934,352 +999,9 @@
 
         });
     }
+    
     @Override
-    public long getDurationMillisecs()
-    {
+    public long getDurationMillisecs() {
         return duration.asMilliseconds();
     }
-
-    public enum CoordinateType {
-        INTEGRAL,
-        REAL,
-        TIME,
-        CATEGORY
-    };
-
-    /**
-     * a special coordinate name used to identify the timestamp associated
-     * with any given Byteman metric. if it is entered as the x or y coordinate
-     * name in the graph dialogue then it will select the tiemstamp as the value
-     * to be graphed against tehthe other chosen coordinate. timestamp values
-     * are stored as longs but are displayed as time values.
-     *
-     * n.b. this text string really needs to be localised.
-     *
-     * n.b.b. it really only makes sense to use timestamp as the X axis. maybe
-     * we should reject any attempt to use it for the y axis?
-     */
-    final public static String TIMESTAMP_KEY = "timestamp";
-
-    /**
-     * a special coordinate name used to identify the frequency count
-     * of any given Byteman metric. if it is entered as the x or y coordinate
-     * name in the graph dialogue then it will count 1 for each occurence of
-     * other value. frequency values are stored as longs.
-     *
-     * n.b. this text string really needs to be localised.
-     */
-    final public static String FREQUENCY_KEY = "frequency";
-
-    /**
-     * a special coordinate name used to identify the marker string
-     * of any given Byteman metric. if it is entered as the x or y coordinate
-     * name in the graph dialogue then it will select the marker as the value
-     * to be graphed against the other chosen coordinate.
-     *
-     * n.b. this text string really needs to be localised.
-     */
-    final public static String MARKER_KEY = "marker";
-
-    public static class GraphDataset
-    {
-        private List<Pair<Object, Object>> data;
-        String xkey;
-        String ykey;
-        CoordinateType xtype;
-        CoordinateType ytype;
-        private static CategoryDataset emptyCategoryDataset = new DefaultCategoryDataset();
-        private static PieDataset emptyPieDataset = new DefaultPieDataset();
-        private static XYDataset emptyXYDataset = new XYSeriesCollection();
-        private static Number frequencyUnit = Long.valueOf(1);
-
-        private Object maybeNumeric(String value) {
-            if (value == null || value.length() == 0)  {
-                return null;
-            }
-            try {
-                if(value.contains(".")) {
-                    return Double.valueOf(value);
-                } else {
-                    return Long.valueOf(value);
-                }
-            } catch (NumberFormatException nfe) {
-                return value;
-            }
-        }
-
-        public GraphDataset(List<BytemanMetric> metrics, String xkey, String ykey, String filter, String value)
-        {
-            this.xkey = xkey;
-            this.ykey = ykey;
-            xtype = CoordinateType.INTEGRAL;
-            ytype = CoordinateType.INTEGRAL;
-            data = new ArrayList<Pair<Object,Object>>();
-            if (TIMESTAMP_KEY.equals(xkey)) {
-                xtype = CoordinateType.TIME;
-            } else if (FREQUENCY_KEY.equals(xkey)) {
-                xtype = CoordinateType.INTEGRAL;
-            } else if (MARKER_KEY.equals(xkey)) {
-                xtype = CoordinateType.CATEGORY;
-            }
-            if (TIMESTAMP_KEY.equals(ykey)) {
-                ytype = CoordinateType.TIME;
-            } else if (FREQUENCY_KEY.equals(ykey)) {
-                ytype = CoordinateType.INTEGRAL;
-            } else if (MARKER_KEY.equals(ykey)) {
-                ytype = CoordinateType.CATEGORY;
-            }
-            // if we have a filter value then convert it to a number if it is numeric
-            Object filterValue = value;
-            if (filter != null && value != null) {
-                // may need to convert String to Numeric
-                filterValue = maybeNumeric(value);
-            }
-            if (metrics != null) {
-                for (BytemanMetric m : metrics) {
-                    Map<String, Object> map = m.getDataAsMap();
-                    // ensure that lookups for the timestamp key always retrieve
-                    // the Long timestamp value associated with the metric and
-                    // that lookups for the frequency key always retrieve
-                    // the Long value 1.
-                    map.put(TIMESTAMP_KEY, m.getTimeStamp());
-                    map.put(FREQUENCY_KEY, frequencyUnit);
-                    map.put(MARKER_KEY, m.getMarker());
-                    // if we have a filter then check for presence of filter key
-                    if (filter != null && filter.length() > 0) {
-                        Object v = map.get(filter);
-                        if (v == null) {
-                            // skip this metric
-                            continue;
-                        }
-                        if (filterValue != null) {
-                            // may need to process String value as Numeric
-                            if (v instanceof String) {
-                                v = maybeNumeric((String)v);
-                            }
-                            if (!filterValue.equals(v)) {
-                                // skip this metric
-                                continue;
-                            }
-                        }
-                    }
-                    Object xval = map.get(xkey);
-                    Object yval = map.get(ykey);
-                    // only include records which contain values for both coordinates
-                    if(xval != null && yval != null) {
-                        // maybe re-present retrieved values as Numeric
-                        // and/or downgrade coordinate type from INTEGRAL
-                        // to REAL or even CATEGORY
-                        xval = newCoordinate(xkey, xval, true);
-                        yval = newCoordinate(ykey, yval, false);
-                        data.add(new Pair<Object, Object>(xval, yval));
-                    }
-                }
-            }
-        }
-
-        public int size() {
-            return data.size();
-        }
-
-        public XYDataset getXYDataset()
-        {
-            if (xtype == CoordinateType.CATEGORY ||
-                    ytype == CoordinateType.CATEGORY) {
-                return emptyXYDataset;
-            }
-
-            XYSeries xyseries = new XYSeries(ykey + " against  " + xkey);
-
-            for (Pair<Object,Object> p : data) {
-                Number x = (Number)p.getFirst();
-                Number y = (Number)p.getSecond();
-                int idx = xyseries.indexOf(x);
-                if (idx >= 0) {
-                    Number y1 = xyseries.getY(idx);
-                    switch (ytype) {
-                    case REAL:
-                        y = y.doubleValue() + y1.doubleValue();
-                    default:
-                        y = y.longValue() + y1.longValue();
-                    }
-                }
-                xyseries.add(x, y);
-            }
-            XYSeriesCollection xycollection = new  XYSeriesCollection();
-            xycollection.addSeries(xyseries);
-            return xycollection;
-        }
-
-        public CategoryDataset getCategoryDataset()
-        {
-            if (xtype == CoordinateType.TIME) {
-                return emptyCategoryDataset;
-            }
-            DefaultCategoryDataset dataset = new DefaultCategoryDataset();
-            // treat x values as category values by calling toString
-            // where they are numeric we ought to support binning them into ranges
-            switch (ytype) {
-            case CATEGORY:
-                // graph category against category by frequency
-                for (Pair<Object, Object> p : data) {
-                    String first = p.getFirst().toString();
-                    String second = p.getSecond().toString();
-                    if(dataset.getRowKeys().contains(first) && dataset.getColumnKeys().contains(second)) {
-                        dataset.incrementValue(1.0, first, second);
-                    } else {
-                        dataset.addValue(1.0, first, second);
-                    }
-                }
-                break;
-            case TIME:
-                // bin time values into ranges and label range with start time
-                // for now just drop through to treat time value as numeric
-            default:
-                // graph category against numeric by summing numeric values
-                for (Pair<Object, Object> p : data) {
-                    String first = p.getFirst().toString();
-                    String second = "";
-                    double increment = ((Number) p.getSecond()).doubleValue();
-                    if(dataset.getRowKeys().contains(first)) {
-                        dataset.incrementValue(increment, first, second);
-                    } else {
-                        dataset.addValue(increment, first, second);
-                    }
-                }
-                break;
-            }
-            return dataset;
-        }
-
-        // alternative option for presenting category xkey with numeric ykey
-        public PieDataset getPieDataset()
-        {
-            if (xtype != CoordinateType.CATEGORY || ytype == CoordinateType.CATEGORY) {
-                return emptyPieDataset;
-            }
-
-            DefaultKeyedValues keyedValues = new DefaultKeyedValues();
-
-            for (Pair<Object,Object> p : data) {
-                String first = p.getFirst().toString();
-                double second = ((Number)p.getSecond()).doubleValue();
-                int index = keyedValues.getIndex(first);
-                if (index >= 0) {
-                    Number existing = keyedValues.getValue(first);
-                    keyedValues.addValue(first, existing.doubleValue() + second);
-                } else {
-                    keyedValues.addValue(first, second);
-                }
-            }
-            PieDataset pieDataset = new DefaultPieDataset(keyedValues);
-            return pieDataset;
-        }
-
-        public XYDataset getCategoryTimePlot(String[][] symbolsReturn)
-        {
-            if (xtype != CoordinateType.TIME || ytype != CoordinateType.CATEGORY) {
-                return emptyXYDataset;
-            }
-
-            // we need to display changing category state over time
-            //
-            // we can create an XYDataSet substituting numeric Y values
-            // to encode the category data. then we provide the data
-            // set with a range axis which displays the numeric
-            // values symbolically.
-
-            XYSeries xyseries = new XYSeries(ykey + " against  " + xkey);
-            int count = 0;
-            HashMap<String, Number> tickmap = new HashMap<String, Number>();
-
-            for (Pair<Object,Object> p : data) {
-                Number x = (Number)p.getFirst();
-                String ysym = (String)p.getSecond();
-                Number y = tickmap.get(ysym);
-                if (y == null) {
-                    y = Long.valueOf(count++);
-                    tickmap.put(ysym, y);
-                }
-                xyseries.add(x, y);
-            }
-            // populate key array
-            String[] symbols = new String[count];
-            for (String key: tickmap.keySet()) {
-                int value = tickmap.get(key).intValue();
-                symbols[value] = key;
-            }
-
-            symbolsReturn[0] = symbols;
-
-            XYSeriesCollection xycollection = new  XYSeriesCollection();
-            xycollection.addSeries(xyseries);
-
-            return xycollection;
-        }
-
-        public String getXLabel() {
-            return xkey;
-        }
-
-        public String getYLabel() {
-            return ykey;
-        }
-
-        public CoordinateType getXType() {
-            return xtype;
-        }
-
-        public CoordinateType getYType() {
-            return ytype;
-        }
-
-        /**
-         * process a newly read x or y coordinate value, which is either a Long timestanp or an unparsed
-         * numeric or category value String, returning a Long, parsed Numeric or String value. As a side
-         * effect of attempting to parse an input String the coordinate type for the relevant coordinate
-         * axis may be downgraded from INTEGRAL (assumed default) to DOUBLE or CATEGORY.
-         * @param key the label for the coordinate axis which may be the special value timestamp
-         * @param value the new found coordinate value which may be a Long timestamp or a String yet to be parsed
-         * @param isX  true if this is an x coordinate value false if it is a y coordinate value
-         * @return an Object repreenting
-         */
-        private Object newCoordinate(String key, Object value, boolean isX) {
-
-            CoordinateType ctype = (isX ? xtype : ytype);
-            if (ctype == CoordinateType.TIME) {
-                // guaranteed already to be a Long
-                return value;
-            }
-
-            boolean updateCType = false;
-
-            if (value instanceof String && ctype != CoordinateType.CATEGORY) {
-                String str = (String)value;
-                // see if we can parse this as a number
-                try {
-                    if (str.contains(".")) {
-                        value = Double.valueOf(str);
-                        if (ctype != CoordinateType.REAL) {
-                            ctype = CoordinateType.REAL;
-                            updateCType = true;
-                        }
-                    } else {
-                        value = Long.valueOf(str);
-                    }
-                } catch (NumberFormatException nfe) {
-                    ctype = CoordinateType.CATEGORY;
-                    updateCType = true;
-                }
-            }
-            if (updateCType) {
-                if (isX) {
-                    xtype = ctype;
-                } else {
-                    ytype = ctype;
-                }
-            }
-            return value;
-        }
-    }
 }
--- a/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties	Fri Sep 16 10:25:53 2016 -0400
+++ b/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties	Wed Sep 14 15:05:13 2016 +0200
@@ -13,3 +13,7 @@
 LABEL_LOCAL_RULE = Local Rule
 LABEL_INJECTED_RULE = Injected Rule
 IMPORT_RULE = Import Rule from File
+FILTER = Filter:
+FILTER_VALUE_LABEL = ==
+X_COORD = x:
+Y_COORD = y:
--- a/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java	Fri Sep 16 10:25:53 2016 -0400
+++ b/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java	Wed Sep 14 15:05:13 2016 +0200
@@ -55,19 +55,6 @@
 import javax.swing.SwingUtilities;
 import javax.swing.text.JTextComponent;
 
-import com.redhat.thermostat.client.command.RequestQueue;
-import com.redhat.thermostat.common.ActionListener;
-import com.redhat.thermostat.common.Clock;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.dao.AgentInfoDAO;
-import com.redhat.thermostat.storage.dao.VmInfoDAO;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.storage.model.VmInfo;
-import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.GenerateAction;
-import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO;
 import org.fest.swing.annotation.GUITest;
 import org.fest.swing.core.NameMatcher;
 import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
@@ -85,12 +72,22 @@
 import org.junit.runner.RunWith;
 
 import com.redhat.thermostat.annotations.internal.CacioTest;
+import com.redhat.thermostat.client.command.RequestQueue;
 import com.redhat.thermostat.client.swing.components.ActionToggleButton;
 import com.redhat.thermostat.client.swing.components.Icon;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.VmInfo;
 import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.BytemanInjectState;
 import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
+import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO;
 
 import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;