Group panel with alphabetized group and method names.
diff --git a/src/main/java/org/testng/collections/Maps.java b/src/main/java/org/testng/collections/Maps.java
index fa5da96..a5ec025 100755
--- a/src/main/java/org/testng/collections/Maps.java
+++ b/src/main/java/org/testng/collections/Maps.java
@@ -18,4 +18,8 @@
   public static <K, V> ListMultiMap<K, V> newListMultiMap() {
     return new ListMultiMap<K, V>();
   }
+
+  public static <K, V> SetMultiMap<K, V> newSetMultiMap() {
+    return new SetMultiMap<K, V>();
+  }
 }
diff --git a/src/main/java/org/testng/collections/SetMultiMap.java b/src/main/java/org/testng/collections/SetMultiMap.java
new file mode 100644
index 0000000..ade4759
--- /dev/null
+++ b/src/main/java/org/testng/collections/SetMultiMap.java
@@ -0,0 +1,81 @@
+package org.testng.collections;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * A container to hold sets indexed by a key.
+ */
+public class SetMultiMap<K, V> {
+  private Map<K, Set<V>> m_objects = Maps.newHashMap();
+
+  public void put(K key, V method) {
+    Set<V> l = m_objects.get(key);
+    if (l == null) {
+      l = Sets.newHashSet();
+      m_objects.put(key, l);
+    }
+    l.add(method);
+  }
+
+  public Set<V> get(K key) {
+    return m_objects.get(key);
+  }
+
+  public Set<K> getKeys() {
+    return new HashSet(m_objects.keySet());
+//    Set<K> result = new ArraySet<K>();
+//    for (K k : m_objects.keySet()) {
+//      result.add(k);
+//    }
+//    Collections.sort(result);
+//    return result;
+  }
+
+  public boolean containsKey(K k) {
+    return m_objects.containsKey(k);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder result = new StringBuilder();
+    Set<K> indices = getKeys();
+//    Collections.sort(indices);
+    for (K i : indices) {
+      result.append("\n    ").append(i).append(" <-- ");
+      for (Object o : m_objects.get(i)) {
+        result.append(o).append(" ");
+      }
+    }
+    return result.toString();
+  }
+
+  public boolean isEmpty() {
+    return m_objects.size() == 0;
+  }
+
+  public int getSize() {
+    return m_objects.size();
+  }
+
+  public Set<V> remove(K key) {
+    return m_objects.remove(key);
+  }
+
+  public Set<Entry<K, Set<V>>> getEntrySet() {
+    return m_objects.entrySet();
+  }
+
+  public Collection<Set<V>> getValues() {
+    return m_objects.values();
+  }
+
+  public void putAll(K k, Collection<V> values) {
+    for (V v : values) {
+      put(k, v);
+    }
+  }
+}
diff --git a/src/main/java/org/testng/reporters/jq/GroupPanel.java b/src/main/java/org/testng/reporters/jq/GroupPanel.java
new file mode 100644
index 0000000..ac70c87
--- /dev/null
+++ b/src/main/java/org/testng/reporters/jq/GroupPanel.java
@@ -0,0 +1,48 @@
+package org.testng.reporters.jq;
+
+import org.testng.ISuite;
+import org.testng.reporters.XMLStringBuffer;
+
+import java.util.Collections;
+import java.util.List;
+
+public class GroupPanel extends BaseMultiSuitePanel {
+  public GroupPanel(Model model) {
+    super(model);
+  }
+
+  private static String getTag(ISuite suite) {
+    return "group-" + suiteToTag(suite);
+  }
+
+  @Override
+  public String getHeader(ISuite suite) {
+    return "Groups for " + suite.getName();
+  }
+
+  @Override
+  public String getPanelName(ISuite suite) {
+    return getTag(suite);
+  }
+
+  @Override
+  public String getContent(ISuite suite, XMLStringBuffer main) {
+    XMLStringBuffer xsb = new XMLStringBuffer(main.getCurrentIndent());
+    List<String> sortedGroups = getModel().getGroups(suite.getName());
+    Collections.sort(sortedGroups);
+    for (String group : sortedGroups) {
+      xsb.push(D, C, "test-group");
+      xsb.addRequired(S, group, C, "test-group-name");
+      xsb.addEmptyElement("br");
+      List<String> sortedMethods = getModel().getMethodsInGroup(group);
+      for (String method : sortedMethods) {
+        xsb.push(D, C, "method-in-group");
+        xsb.addRequired(S, method, C, "method-in-group-name");
+        xsb.addEmptyElement("br");
+        xsb.pop(D);
+      }
+      xsb.pop(D);
+    }
+    return xsb.toXML();
+  }
+}
diff --git a/src/main/java/org/testng/reporters/jq/Main.java b/src/main/java/org/testng/reporters/jq/Main.java
index ffa9a87..2c815fc 100644
--- a/src/main/java/org/testng/reporters/jq/Main.java
+++ b/src/main/java/org/testng/reporters/jq/Main.java
@@ -38,7 +38,8 @@
     // Navigator on the left hand side
     TestNgXmlPanel testNgPanel = new TestNgXmlPanel(m_model);
     TestPanel testPanel = new TestPanel(m_model);
-    new NavigatorPanel(m_model, testNgPanel, testPanel).generate(xsb);
+    GroupPanel groupPanel = new GroupPanel(m_model);
+    new NavigatorPanel(m_model, testNgPanel, testPanel, groupPanel).generate(xsb);
 
     xsb.push(D, C, "wrapper");
     xsb.push(D, "class", "main-panel-root");
@@ -49,6 +50,10 @@
     new SuitePanel(m_model).generate(xsb);
 
     //
+    // Group panel
+    groupPanel.generate(xsb);
+
+    //
     // Panel that displays the list of test names
     //
     testPanel.generate(xsb);
diff --git a/src/main/java/org/testng/reporters/jq/Model.java b/src/main/java/org/testng/reporters/jq/Model.java
index 796e471..1e66ad1 100644
--- a/src/main/java/org/testng/reporters/jq/Model.java
+++ b/src/main/java/org/testng/reporters/jq/Model.java
@@ -8,9 +8,13 @@
 import org.testng.collections.ListMultiMap;
 import org.testng.collections.Lists;
 import org.testng.collections.Maps;
+import org.testng.collections.SetMultiMap;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class Model {
   private ListMultiMap<ISuite, ITestResult> m_model = Maps.newListMultiMap();
@@ -24,6 +28,8 @@
   private List<ITestResult> m_allFailedResults = Lists.newArrayList();
   // Each suite is mapped to failed.png, skipped.png or nothing (which means passed.png)
   private Map<String, String> m_imageBySuiteName = Maps.newHashMap();
+  private SetMultiMap<String, String> m_groupsBySuiteName = Maps.newSetMultiMap();
+  private SetMultiMap<String, String> m_methodsByGroup = Maps.newSetMultiMap();
 
   public Model(List<ISuite> suites) {
     m_suites = suites;
@@ -68,6 +74,7 @@
         ResultsByClass rbc = new ResultsByClass();
         for (ITestResult tr : passed) {
           rbc.addResult(tr.getTestClass().getRealClass(), tr);
+          updateGroups(suite, tr);
         }
         m_passedResultsByClass.put(suite, rbc);
       }
@@ -78,6 +85,7 @@
         for (ITestResult tr : skipped) {
           m_imageBySuiteName.put(suite.getName(), getImage("skipped"));
           rbc.addResult(tr.getTestClass().getRealClass(), tr);
+          updateGroups(suite, tr);
         }
         m_skippedResultsByClass.put(suite, rbc);
       }
@@ -89,6 +97,7 @@
           m_imageBySuiteName.put(suite.getName(), getImage("failed"));
           rbc.addResult(tr.getTestClass().getRealClass(), tr);
           m_allFailedResults.add(tr);
+          updateGroups(suite, tr);
         }
         m_failedResultsByClass.put(suite, rbc);
       }
@@ -99,6 +108,15 @@
     }
   }
 
+  private void updateGroups(ISuite suite, ITestResult tr) {
+    String[] groups = tr.getMethod().getGroups();
+    m_groupsBySuiteName.putAll(suite.getName(),
+        Arrays.asList(groups));
+    for (String group : groups) {
+      m_methodsByGroup.put(group, tr.getMethod().getMethodName());
+    }
+  }
+
   public ResultsByClass getFailedResultsByClass(ISuite suite) {
     return m_failedResultsByClass.get(suite);
   }
@@ -152,4 +170,24 @@
     }
     return result;
   }
+
+  public <T> Set<T> nonnullSet(Set<T> l) {
+    return l != null ? l : Collections.<T>emptySet();
+  }
+
+  public <T> List<T> nonnullList(List<T> l) {
+    return l != null ? l : Collections.<T>emptyList();
+  }
+
+  public List<String> getGroups(String name) {
+    List<String> result = Lists.newArrayList(nonnullSet(m_groupsBySuiteName.get(name)));
+    Collections.sort(result);
+    return result;
+  }
+
+  public List<String> getMethodsInGroup(String groupName) {
+    List<String> result = Lists.newArrayList(nonnullSet(m_methodsByGroup.get(groupName)));
+    Collections.sort(result);
+    return result;
+  }
 }
diff --git a/src/main/java/org/testng/reporters/jq/NavigatorPanel.java b/src/main/java/org/testng/reporters/jq/NavigatorPanel.java
index 161bf89..ddcfb0e 100644
--- a/src/main/java/org/testng/reporters/jq/NavigatorPanel.java
+++ b/src/main/java/org/testng/reporters/jq/NavigatorPanel.java
@@ -14,11 +14,14 @@
 
   private TestNgXmlPanel m_testNgPanel;
   private TestPanel m_testPanel;
+  private GroupPanel m_groupPanel;
 
-  public NavigatorPanel(Model model, TestNgXmlPanel testNgPanel, TestPanel testPanel) {
+  public NavigatorPanel(Model model, TestNgXmlPanel testNgPanel, TestPanel testPanel,
+      GroupPanel groupPanel) {
     super(model);
     m_testNgPanel = testNgPanel;
     m_testPanel = testPanel;
+    m_groupPanel = groupPanel;
   }
 
   @Override
@@ -69,7 +72,9 @@
       header.addRequired(S, "Info");
       header.pop(D);
 
-      // Info content
+      //
+      // Info
+      //
       header.push(D, C, "suite-section-content");
       int total = failed + skipped + passed;
       String stats = String.format("%s, %s %s %s",
@@ -80,7 +85,7 @@
 
       header.push("ul");
 
-      // Tests
+      // "59 Tests"
       header.push("li");
       header.push("a", "href", "#",
           "panel-name", m_testPanel.getPanelName(suite),
@@ -90,7 +95,17 @@
       header.pop("a");
       header.pop("li");
 
-      // testng.xml
+      // "12 groups"
+      header.push("li");
+      header.push("a", "href", "#",
+          "panel-name", m_groupPanel.getPanelName(suite),
+          C, "navigator-link ");
+      header.addOptional(S,
+          String.format("%s ", pluralize(getModel().getGroups(suite.getName()).size(), "group")));
+      header.pop("a");
+      header.pop("li");
+
+      // "testng.xml"
       header.push("li");
       header.push("a", "href", "#",
           "panel-name", m_testNgPanel.getPanelName(suite),
@@ -106,7 +121,7 @@
       header.pop(D); // suite-section-content
 
       //
-      // Methods
+      // Results
       //
       header.push(D, C, "result-section");
 
diff --git a/src/main/resources/testng-reports.css b/src/main/resources/testng-reports.css
index 9165dee..1bc7698 100644
--- a/src/main/resources/testng-reports.css
+++ b/src/main/resources/testng-reports.css
@@ -150,3 +150,20 @@
     float: right;
     height: 20;
 }
+
+.test-group {
+    font: 20px 'Lucida Grande';
+    margin: 5px 5px 10px 5px;
+    border-width: 0px 0px 1px 0px;
+    border-style: solid;
+    padding: 5px;
+}
+
+.test-group-name {
+    font-weight: bold;
+}
+
+.method-in-group {
+    font-size: 16px;
+    margin-left: 80px;
+}