commit c240cba806b45f55ae14ed77873602674e8cdc49
Author: Nathan Haug <nate@quicksketch.org>
Date:   Thu Aug 22 12:32:04 2013 -0600

    Issue #1459372: Patch to provide usefully formatted data structures for 3rd party webform analysis (e.g. charts).

diff --git a/components/date.inc b/components/date.inc
index 953f860..e379c04 100644
--- a/components/date.inc
+++ b/components/date.inc
@@ -25,6 +25,7 @@ function _webform_defaults_date() {
       'title_display' => 0,
       'description' => '',
       'private' => FALSE,
+      'analysis' => FALSE,
     ),
   );
 }
@@ -392,7 +393,10 @@ function _webform_analysis_date($component, $sids = array()) {
   $nonblanks = count($dates);
   $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks));
   $rows[1] = array(t('User entered value'), $nonblanks);
-  return $rows;
+
+  return array(
+    'table_rows' => $rows,
+  );
 }
 
 /**
diff --git a/components/email.inc b/components/email.inc
index fbfa5a5..3143ad0 100644
--- a/components/email.inc
+++ b/components/email.inc
@@ -25,6 +25,7 @@ function _webform_defaults_email() {
       'placeholder' => '',
       'attributes' => array(),
       'private' => FALSE,
+      'analysis' => FALSE,
     ),
   );
 }
@@ -266,8 +267,14 @@ function _webform_analysis_email($component, $sids = array()) {
 
   $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks));
   $rows[1] = array(t('User entered value'), $nonblanks);
-  $rows[2] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
-  return $rows;
+
+  $other[0] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
+
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
+
 }
 
 /**
diff --git a/components/file.inc b/components/file.inc
index 1113175..c8c3ffa 100644
--- a/components/file.inc
+++ b/components/file.inc
@@ -28,6 +28,7 @@ function _webform_defaults_file() {
       'description' => '',
       'attributes' => array(),
       'private' => FALSE,
+      'analysis' => FALSE,
     ),
   );
 }
@@ -443,8 +444,11 @@ function _webform_analysis_file($component, $sids = array()) {
 
   $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks));
   $rows[1] = array(t('User uploaded file'), $nonblanks);
-  $rows[2] = array(t('Average uploaded file size'), ($sizetotal != 0 ? (int) (($sizetotal/$nonblanks)/1024) . ' KB' : '0'));
-  return $rows;
+  $other[0] = array(t('Average uploaded file size'), ($sizetotal != 0 ? (int) (($sizetotal/$nonblanks)/1024) . ' KB' : '0'));
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/components/grid.inc b/components/grid.inc
index 11cb3c7..aebb49d 100644
--- a/components/grid.inc
+++ b/components/grid.inc
@@ -29,6 +29,7 @@ function _webform_defaults_grid() {
       'custom_question_keys' => 0,
       'description' => '',
       'private' => FALSE,
+      'analysis' => TRUE,
     ),
   );
 }
@@ -352,10 +353,11 @@ function _webform_analysis_grid($component, $sids = array()) {
     }
     $rows[] = $row;
   }
-  $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('webform-grid'))));
 
-
-  return array(array(array('data' => $output, 'colspan' => 2)));
+  return array(
+    'table_header' => $header,
+    'table_rows' => $rows,
+  );
 }
 
 /**
diff --git a/components/hidden.inc b/components/hidden.inc
index 0b6ceaa..9be04e2 100644
--- a/components/hidden.inc
+++ b/components/hidden.inc
@@ -18,6 +18,7 @@ function _webform_defaults_hidden() {
     'extra' => array(
       'private' => FALSE,
       'hidden_type' => 'value',
+      'analysis' => FALSE,
     ),
   );
 }
@@ -142,11 +143,14 @@ function _webform_analysis_hidden($component, $sids = array()) {
     $submissions++;
   }
 
-  $rows[0] = array( t('Empty'), ($submissions - $nonblanks));
-  $rows[1] = array( t('Non-empty'), $nonblanks);
-  $rows[2] = array( t('Average submission length in words (ex blanks)'),
-                    ($nonblanks !=0 ? number_format($wordcount/$nonblanks, 2) : '0'));
-  return $rows;
+  $rows[0] = array(t('Empty'), ($submissions - $nonblanks));
+  $rows[1] = array(t('Non-empty'), $nonblanks);
+  $other[0] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
+
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/components/number.inc b/components/number.inc
index 9c470a9..907d524 100644
--- a/components/number.inc
+++ b/components/number.inc
@@ -26,6 +26,7 @@ function _webform_defaults_number() {
       'description' => '',
       'attributes' => array(),
       'private' => FALSE,
+      'analysis' => FALSE,
       'min' => '',
       'max' => '',
       'step' => '',
@@ -434,13 +435,14 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE)
   $average = _webform_number_format($component, $average);
   $sum = _webform_number_format($component, $sum);
 
-  $rows[0] = array(t('Zero/blank'), ($submissions - $nonzero));
-  $rows[1] = array(t('User entered value'), $not_empty);
-  $rows[2] = array(t('Sum') . ($advanced_stats ? ' (&Sigma;)' : ''), $sum);
-  $rows[3] = array($average_title, $average);
+  $rows[] = array(t('Zero/blank'), ($submissions - $nonzero));
+  $rows[] = array(t('User entered value'), $not_empty);
+
+  $other[] = array(t('Sum') . ($advanced_stats ? ' (&Sigma;)' : ''), $sum);
+  $other[] = array($average_title, $average);
 
   if (!$advanced_stats && $sum != 0) {
-    $rows[4] = array('', l(t('More stats »'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']));
+    $other[] = l(t('More stats »'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']);
   }
 
   // Normal distribution information.
@@ -460,7 +462,10 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE)
 
     // Skip the rest of the distribution rows if standard deviation is 0.
     if (empty($stddev)) {
-      return $rows;
+      return array(
+        'table_rows' => $rows,
+        'other_data' => $other,
+      );
     }
 
     // Build normal distribution table rows.
@@ -511,14 +516,17 @@ function _webform_analysis_number($component, $sids = array(), $single = FALSE)
     array_unshift($count, t('Count'));
     array_unshift($percent, t('% of !description', array('!description' => $description)));
 
-    $output = theme('table', array('header' => $header, 'rows' => array($limit, $count, $percent)));
+    $normal_distribution = theme('table', array('header' => $header, 'rows' => array($limit, $count, $percent)));
 
-    $rows[4] = array(t('Range'), t('!low to !high', array('!low' => $low, '!high' => $high)));
-    $rows[5] = array(t('Standard deviation (!sigma)', array('!sigma' => $sigma)), $stddev);
-    $rows[6] = array(array('data' => $output, 'colspan' => 2));
+    $other[] = array(t('Range'), t('!low to !high', array('!low' => $low, '!high' => $high)));
+    $other[] = array(t('Standard deviation (!sigma)', array('!sigma' => $sigma)), $stddev);
+    $other[] = $normal_distribution;
   }
 
-  return $rows;
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/components/select.inc b/components/select.inc
index 7910036..7c74470 100644
--- a/components/select.inc
+++ b/components/select.inc
@@ -28,6 +28,7 @@ function _webform_defaults_select() {
       'custom_keys' => FALSE,
       'options_source' => '',
       'private' => FALSE,
+      'analysis' => TRUE,
     ),
   );
 }
@@ -633,16 +634,12 @@ function _webform_analysis_select($component, $sids = array(), $single = FALSE)
   $options = _webform_select_options($component, TRUE);
   $show_other_results = $single;
 
-  $sid_placeholders = count($sids) ? array_fill(0, count($sids), "'%s'") : array();
-  $sid_filter = count($sids) ? " AND sid IN (" . implode(",", $sid_placeholders) . ")" : "";
-
-  $option_operator = $show_other_results ? 'NOT IN' : 'IN';
+  // Gather the normal results first (not "other" options).
   $query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC))
     ->fields('wsd', array('data'))
     ->condition('nid', $component['nid'])
     ->condition('cid', $component['cid'])
     ->condition('data', '', '<>')
-    ->condition('data', array_keys($options), $option_operator)
     ->groupBy('data');
   $query->addExpression('COUNT(data)', 'datacount');
 
@@ -650,44 +647,64 @@ function _webform_analysis_select($component, $sids = array(), $single = FALSE)
     $query->condition('sid', $sids, 'IN');
   }
 
-  $count_query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC))
-    ->condition('nid', $component['nid'])
-    ->condition('cid', $component['cid'])
-    ->condition('data', '', '<>');
-  $count_query->addExpression('COUNT(*)', 'datacount');
-  if (count($sids)) {
-    $count_query->condition('sid', $sids, 'IN');
+  // Duplicate the query for other options.
+  if ($show_other_results) {
+    $other_query = clone($query);
+    $other_query->condition('data', array_keys($options), 'NOT IN');
+    $other_query->orderBy('datacount', 'DESC');
   }
 
+  $query->condition('data', array_keys($options), 'IN');
+
   $result = $query->execute();
   $rows = array();
+  $other = array();
   $normal_count = 0;
   foreach ($result as $data) {
-    $display_option = $single ? $data['data'] : $options[$data['data']];
+    $display_option = isset($options[$data['data']]) ? $options[$data['data']] : $data['data'];
     $rows[$data['data']] = array(webform_filter_xss($display_option), $data['datacount']);
     $normal_count += $data['datacount'];
   }
 
-  if (!$show_other_results) {
-    // Order the results according to the normal options array.
-    $ordered_rows = array();
-    foreach (array_intersect_key($options, $rows) as $key => $label) {
-      $ordered_rows[] = $rows[$key];
+  // Order the results according to the normal options array.
+  $ordered_rows = array();
+  foreach (array_intersect_key($options, $rows) as $key => $label) {
+    $ordered_rows[] = $rows[$key];
+  }
+  $rows = $ordered_rows;
+
+  // Add a row for displaying the total unknown or user-entered values.
+  if ($component['extra']['other_option']) {
+    $count_query = db_select('webform_submitted_data', 'wsd', array('fetch' => PDO::FETCH_ASSOC))
+      ->condition('nid', $component['nid'])
+      ->condition('cid', $component['cid'])
+      ->condition('data', '', '<>');
+    $count_query->addExpression('COUNT(*)', 'datacount');
+    if (count($sids)) {
+      $count_query->condition('sid', $sids, 'IN');
     }
-
-    // Add a row for any unknown or user-entered values.
-    if ($component['extra']['other_option']) {
-      $full_count = $count_query->execute()->fetchField();
-      $other_count = $full_count - $normal_count;
-      $display_option = !empty($component['extra']['other_text']) ? check_plain($component['extra']['other_text']) : t('Other...');
-      $other_text = $other_count ? $other_count . ' (' . l(t('view'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']) . ')' : $other_count;
-      $ordered_rows[] = array($display_option, $other_text);
+    $full_count = $count_query->execute()->fetchField();
+    $other_count = $full_count - $normal_count;
+    $display_option = !empty($component['extra']['other_text']) ? check_plain($component['extra']['other_text']) : t('Other...');
+    $other_text = ($other_count && !$show_other_results) ? $other_count . ' (' . l(t('view'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']) . ')' : $other_count;
+    $rows[] = array($display_option, $other_text);
+
+    // If showing all results, execute the "other" query and append their rows.
+    if ($show_other_results) {
+      $other_result = $other_query->execute();
+      foreach ($other_result as $data) {
+        $other[] = array(check_plain($data['data']), $data['datacount']);
+      }
+      if ($other) {
+        array_unshift($other, '<strong>' . t('Other responses') . '</strong>');
+      }
     }
-
-    $rows = $ordered_rows;
   }
 
-  return $rows;
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/components/textarea.inc b/components/textarea.inc
index d4bd236..b877a01 100644
--- a/components/textarea.inc
+++ b/components/textarea.inc
@@ -26,6 +26,7 @@ function _webform_defaults_textarea() {
       'placeholder' => '',
       'attributes' => array(),
       'private' => FALSE,
+      'analysis' => FALSE,
     ),
   );
 }
@@ -199,8 +200,13 @@ function _webform_analysis_textarea($component, $sids = array()) {
 
   $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks));
   $rows[1] = array(t('User entered value'), $nonblanks);
-  $rows[2] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
-  return $rows;
+
+  $other[] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
+
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/components/textfield.inc b/components/textfield.inc
index 8eb2975..ecb83bd 100644
--- a/components/textfield.inc
+++ b/components/textfield.inc
@@ -28,6 +28,7 @@ function _webform_defaults_textfield() {
       'placeholder' => '',
       'attributes' => array(),
       'private' => FALSE,
+      'analysis' => FALSE,
     ),
   );
 }
@@ -237,8 +238,13 @@ function _webform_analysis_textfield($component, $sids = array()) {
 
   $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks));
   $rows[1] = array(t('User entered value'), $nonblanks);
-  $rows[2] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
-  return $rows;
+
+  $other[] = array(t('Average submission length in words (ex blanks)'), ($nonblanks != 0 ? number_format($wordcount/$nonblanks, 2) : '0'));
+
+  return array(
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/components/time.inc b/components/time.inc
index b99abc4..111cdc5 100644
--- a/components/time.inc
+++ b/components/time.inc
@@ -26,6 +26,7 @@ function _webform_defaults_time() {
       'title_display' => 0,
       'description' => '',
       'private' => FALSE,
+      'analysis' => FALSE,
     ),
   );
 }
@@ -352,7 +353,10 @@ function _webform_analysis_time($component, $sids = array()) {
   $nonblanks = count($times);
   $rows[0] = array(t('Left Blank'), ($submissions - $nonblanks));
   $rows[1] = array(t('User entered value'), $nonblanks);
-  return $rows;
+
+  return array(
+    'table_rows' => $rows,
+  );
 }
 
 /**
diff --git a/css/webform-admin.css b/css/webform-admin.css
index 671df4e..e6a42da 100644
--- a/css/webform-admin.css
+++ b/css/webform-admin.css
@@ -24,6 +24,39 @@
   margin-left: 20px;
 }
 
+/* Analysis pages */
+.webform-analysis-component {
+  width: 33.33%;
+  margin: 0 -2px;
+  padding: 0;
+  display: inline-block;
+  vertical-align: top;
+}
+@media all and (max-width: 1024px) {
+  .webform-analysis-component {
+    width: 50%;
+  }
+}
+@media all and (max-width: 600px) {
+  .webform-analysis-component {
+    width: 100%;
+  }
+}
+.webform-analysis-component-grid {
+  width: 100%;
+  clear: both;
+}
+.webform-analysis-component-inner {
+  padding: 12px;
+  overflow: auto;
+}
+.webform-analysis-component table {
+  width: 100%;
+}
+.webform-analysis-component table td {
+  white-space: normal;
+}
+
 /* Element for selecting components, i.e. included components for e-mails. */
 .webform-component-select-wrapper {
   max-height: 300px;
diff --git a/includes/webform.components.inc b/includes/webform.components.inc
index 0e3e5fe..0c2cc5c 100644
--- a/includes/webform.components.inc
+++ b/includes/webform.components.inc
@@ -906,6 +906,7 @@ function webform_component_clone(&$node, &$component) {
 function webform_component_feature($type, $feature) {
   $components = webform_components();
   $defaults = array(
+    'analysis' => TRUE,
     'csv' => TRUE,
     'default_value' => TRUE,
     'description' => TRUE,
@@ -1025,6 +1026,7 @@ function webform_component_select($element) {
   $element['#attached'] = array(
     'library' => array(
       array('webform', 'admin'),
+      array('system', 'drupal.collapse'),
     ),
     'js' => array(
       'misc/tableselect.js' => array(),
@@ -1065,7 +1067,7 @@ function theme_webform_component_select($variables) {
     $element['#children'] = t('No available components.');
   }
   else {
-    $element['#children'] = '<div class="webform-component-select-wrapper">' . theme('table', array('header' => $header, 'rows' => $rows)) . '</div>';
+    $element['#children'] = '<div class="webform-component-select-wrapper">' . theme('table', array('header' => $header, 'rows' => $rows, 'sticky' => FALSE)) . '</div>';
   }
 
   return theme('fieldset', array('element' => $element));
diff --git a/includes/webform.report.inc b/includes/webform.report.inc
index 5129f9b..75188a9 100644
--- a/includes/webform.report.inc
+++ b/includes/webform.report.inc
@@ -1235,22 +1235,196 @@ function webform_results_analysis($node, $sids = array(), $analysis_component =
     $sids = array();
   }
 
-  // If showing a component's details, we don't want to loose the menu tabs.
-  if ($analysis_component) {
-    $item = menu_get_item('node/' . $node->nid . '/webform-results/analysis');
-    menu_set_item(NULL, $item);
+  // Build a renderable for the content of this page.
+  $analysis = array(
+    '#theme' => array('webform_analysis__' . $node->nid, 'webform_analysis'),
+    '#node' => $node,
+    '#component' => $analysis_component,
+  );
+
+  // If showing all components, display selection form.
+  if (!$analysis_component) {
+    $analysis['form'] = drupal_get_form('webform_analysis_components_form', $node);
   }
 
-  $components = isset($analysis_component) ? array($analysis_component['cid'] => $analysis_component) : $node->webform['components'];
-  $data = array();
-  foreach ($components as $cid => $component) {
+  // Add all the components to the analysis renderable array.
+  $components = isset($analysis_component) ? array($analysis_component['cid']) : webform_analysis_enabled_components($node);
+  foreach ($components as $cid) {
     // Do component specific call.
-    if ($row_data = webform_component_invoke($component['type'], 'analysis', $component, $sids, isset($analysis_component))) {
-      $data[$cid] = $row_data;
+    $component = $node->webform['components'][$cid];
+    if ($data = webform_component_invoke($component['type'], 'analysis', $component, $sids, isset($analysis_component))) {
+      drupal_alter('webform_analysis_component_data', $data, $node, $component);
+      $analysis['data'][$cid] = array(
+        '#theme' => array('webform_analysis_component__' . $node->nid . '__' . $cid, 'webform_analysis_component__' . $node->nid, 'webform_analysis_component'),
+        '#node' => $node,
+        '#component' => $component,
+        '#data' => $data,
+      );
+      $analysis['data'][$cid]['basic'] = array(
+        '#theme' => array('webform_analysis_component_basic__' . $node->nid . '__' . $cid, 'webform_analysis_component_basic__' . $node->nid, 'webform_analysis_component_basic'),
+        '#component' => $component,
+        '#data' => $data,
+      );
+    }
+  }
+
+  drupal_alter('webform_analysis', $analysis);
+  return drupal_render($analysis);
+}
+
+/**
+ * Prerender function for webform-analysis.tpl.php.
+ */
+function template_preprocess_webform_analysis(&$variables) {
+  $analysis = $variables['analysis'];
+  $variables['node'] = $analysis['#node'];
+  $variables['component'] = $analysis['#component'];
+}
+
+/**
+ * Prerender function for webform-analysis-component.tpl.php.
+ */
+function template_preprocess_webform_analysis_component(&$variables) {
+  $component_analysis = $variables['component_analysis'];
+  $variables['node'] = $component_analysis['#node'];
+  $variables['component'] = $component_analysis['#component'];
+
+  // Ensure defaults.
+  $variables['component_analysis']['#data'] += array(
+    'table_header' => NULL,
+    'table_rows' => array(),
+    'other_data' => array(),
+  );
+  $variables['classes_array'][] = 'webform-analysis-component-' . $variables['component']['type'];
+  $variables['classes_array'][] = 'webform-analysis-component--' . str_replace('_', '-', implode('--', webform_component_parent_keys($variables['node'], $variables['component'])));
+}
+
+/**
+ * Render an individual component's analysis data in a table.
+ *
+ * @param $variables
+ *   An array of theming variables for this theme function. Included keys:
+ *   - $component: The component whose analysis is being rendered.
+ *   - $data: An array of array containing the analysis data. Contains the keys:
+ *     - table_header: If this table has more than a single column, an array
+ *       of header labels.
+ *     - table_rows: If this component has a table that should be rendered, an
+ *       array of values
+ */
+function theme_webform_analysis_component_basic($variables) {
+  $data = $variables['data'];
+
+  // Ensure defaults.
+  $data += array(
+    'table_header' => NULL,
+    'table_rows' => array(),
+    'other_data' => array(),
+  );
+
+  // Combine the "other data" into the table rows by default.
+  if (is_array($data['other_data'])) {
+    foreach ($data['other_data'] as $other_data) {
+      if (is_array($other_data)) {
+        $data['table_rows'][] = $other_data;
+      }
+      else {
+        $data['table_rows'][] = array(array(
+          'colspan' => 2,
+          'data' => $other_data,
+        ));
+      }
+    }
+  }
+  elseif (strlen($data['other_data'])) {
+    $data['table_rows'][] = array(
+      'colspan' => 2,
+      'data' => $data['other_data'],
+    );
+  }
+
+  return theme('table', array(
+    'header' => $data['table_header'],
+    'rows' => $data['table_rows'],
+    'class' => array('webform-analysis-table'),
+  ));
+}
+
+/**
+ * Return a list of components that should be displayed for analysis.
+ *
+ * @param $node
+ *   The node whose components' data is being analyzed.
+ * @return array
+ *   An array of component IDs.
+ */
+function webform_analysis_enabled_components($node) {
+  $cids = array();
+  foreach ($node->webform['components'] as $cid => $component) {
+    if (!empty($component['extra']['analysis'])) {
+      $cids[] = $cid;
     }
   }
+  return $cids;
+}
 
-  return theme('webform_results_analysis', array('node' => $node, 'data' => $data, 'sids' => $sids, 'component' => $analysis_component));
+/**
+ * Form for selecting which components should be shown on the analysis page.
+ */
+function webform_analysis_components_form($form, &$form_state, $node) {
+  form_load_include($form_state, 'inc', 'webform', 'includes/webform.components');
+  $form['#node'] = $node;
+
+  $component_list = webform_component_list($node, 'analysis', TRUE);
+  $enabled_components = webform_analysis_enabled_components($node);
+  if (empty($component_list)) {
+    $help = t('No components have added that support analysis. <a href="!url">Add components to your form</a> to see calculated data.', array('!url' => url('node/' . $node->nid . '/webform')));
+  }
+  elseif (empty($enabled_components)) {
+    $help = t('No components have analysis enabled in this form. Enable analysis under the "Add analysis components" fieldset.');
+  }
+  else {
+    $help = t('This page shows analysis of submitted data, such as the number of submissions per component value, calculations, and averages. Additional components may be added under the "Add analysis components" fieldset.');
+  }
+
+  $form['help'] = array(
+    '#markup' => '<p>' . $help . '</p>',
+    '#access' => !empty($help),
+    '#weight' => -100,
+  );
+
+  $form['components'] = array(
+    '#type' => 'select',
+    '#title' => t('Add analysis components'),
+    '#options' => $component_list,
+    '#default_value' => $enabled_components,
+    '#multiple' => TRUE,
+    '#size' => 10,
+    '#description' => t('The selected components will be included on the analysis page.'),
+    '#process' => array('webform_component_select'),
+    '#access' => count($component_list),
+  );
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+    '#access' => count($component_list),
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update analysis display'),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler for webform_analysis_components_form().
+ */
+function webform_analysis_components_form_submit($form, $form_state) {
+  $node = $form['#node'];
+  foreach ($form_state['values']['components'] as $cid => $enabled) {
+    $node->webform['components'][$cid]['extra']['analysis'] = (bool) $enabled;
+  }
+  node_save($node);
 }
 
 /**
diff --git a/js/webform-admin.js b/js/webform-admin.js
index b5681bd..ec724aa 100644
--- a/js/webform-admin.js
+++ b/js/webform-admin.js
@@ -8,8 +8,8 @@ Drupal.behaviors.webformAdmin = {};
 Drupal.behaviors.webformAdmin.attach = function(context) {
   // On click or change, make a parent radio button selected.
   Drupal.webform.setActive(context);
-  // Update the template select list upon changing a template.
   Drupal.webform.updateTemplate(context);
+  // Update the template select list upon changing a template.
   // Select all link for file extensions.
   Drupal.webform.selectCheckboxesLink(context);
   // Enhance the normal tableselect.js file to support indentations.
diff --git a/templates/webform-analysis-component.tpl.php b/templates/webform-analysis-component.tpl.php
new file mode 100644
index 0000000..660f05a
--- /dev/null
+++ b/templates/webform-analysis-component.tpl.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @file
+ * Template for rendering an individual component's analysis data.
+ *
+ * Available variables:
+ * - $component: The component whose analysis is being rendered.
+ * - $component_analysis: A renderable containing this components analysis.
+ * - $data: An array of array containing the analysis data. Contains the keys:
+ *   - table_header: If this table has more than a single column, an array
+ *     of header labels.
+ *   - table_rows: If this component has a table that should be rendered, an
+ *     array of values
+ */
+?>
+<div class="<?php print $classes; ?>">
+  <div class="webform-analysis-component-inner">
+    <h3><?php print check_plain($component['name']); ?></h3>
+    <?php print drupal_render_children($component_analysis); ?>
+  </div>
+</div>
diff --git a/templates/webform-analysis.tpl.php b/templates/webform-analysis.tpl.php
new file mode 100644
index 0000000..734fab8
--- /dev/null
+++ b/templates/webform-analysis.tpl.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * @file
+ * Template for printing out the contents of the "Analysis" tab on a Webform.
+ *
+ * Available variables:
+ * - $node: The node object for this webform.
+ * - $component: If a single components analysis is being printed, this will
+ *   contain a Webform component. Otherwise all components are having their
+ *   analysis printed on the same page.
+ * - $analysis: A renderable object containing the following children:
+ *   - 'form': A form for selecting which components should be included in the
+ *     analysis.
+ *   - 'data': An render array of analysis results for each component enabled.
+ */
+?>
+<div class="webform-analysis">
+  <?php print drupal_render($analysis['form']['help']); ?>
+
+  <div class="webform-analysis-data">
+    <?php print drupal_render($analysis['data']); ?>
+  </div>
+  <?php print drupal_render($analysis['form']); ?>
+  <?php /* Print out any remaining part of the renderable. */ ?>
+  <?php print drupal_render_children($analysis); ?>
+</div>
diff --git a/webform.api.php b/webform.api.php
index 09ef6ad..333333f 100644
--- a/webform.api.php
+++ b/webform.api.php
@@ -302,6 +302,64 @@ function hook_webform_component_delete($component) {
 }
 
 /**
+ * Alter the entire analysis before rendering to the page on the Analysis tab.
+ *
+ * This alter hook allows modification of the entire analysis of a node's
+ * Webform results. The resulting analysis is displayed on the Results ->
+ * Analysis tab on the Webform.
+ *
+ * @param array $analysis
+ *   A Drupal renderable array, passed by reference, containing the entire
+ *   contents of the analysis page. This typically will contain the following
+ *   two major keys:
+ *   - form: The form for configuring the shown analysis.
+ *   - components: The list of analyses for each analysis-enabled component
+ *     for the node. Each keyed by its component ID.
+ */
+function hook_webform_analysis_alter(&$analysis) {
+  $node = $analysis['#node'];
+
+  // Add an additional piece of information to every component's analysis:
+  foreach (element_children($analysis['components']) as $cid) {
+    $component = $node->components[$cid];
+    $analysis['components'][$cid]['chart'] = array(
+      '#markup' => t('Chart for the @name component', array('@name' => $component['name']);
+    );
+  }
+}
+/**
+ * Alter data when displaying an analysis on that component.
+ *
+ * This hook modifies the data from an individual component's analysis results.
+ * It can be used to add additional analysis, or to modify the existing results.
+ * If needing to alter the entire set of analyses rather than an individual
+ * component, hook_webform_analysis_alter() may be used instead.
+ *
+ * @param array $data
+ *   An array containing the result of a components analysis hook, passed by
+ *   reference. This is passed directly from a component's
+ *   _webform_analysis_component() function. See that hook for more information
+ *   on this value.
+ * @param object $node
+ *   The node object that contains the component being analyzed.
+ * @param array $component
+ *   The Webform component array whose analysis results are being displayed.
+ *
+ * @see _webform_analysis_component()
+ * @see hook_webform_analysis_alter()
+ */
+function hook_webform_analysis_component_data_alter(&$data, $node, $component) {
+  if ($component['type'] === 'textfield') {
+    // Do not display rows that contain a zero value.
+    foreach ($data as $row_number => $row_data) {
+      if ($row_data[1] === 0) {
+        unset($data[$row_number]);
+      }
+    }
+  }
+}
+
+/**
  * Alter a Webform submission's header when exported.
  */
 function hook_webform_csv_header_alter(&$header, $component) {
@@ -383,6 +441,9 @@ function hook_webform_component_info() {
     'label' => t('Textfield'),
     'description' => t('Basic textfield type.'),
     'features' => array(
+      // This component includes an analysis callback. Defaults to TRUE.
+      'analysis' => TRUE,
+
       // Add content to CSV downloads. Defaults to TRUE.
       'csv' => TRUE,
 
@@ -629,6 +690,8 @@ function _webform_defaults_component() {
       'optrand' => 0,
       'qrand' => 0,
       'description' => '',
+      'private' => FALSE,
+      'analysis' => TRUE,
     ),
   );
 }
@@ -885,8 +948,24 @@ function _webform_theme_component() {
  *   shown. May be used to provided detailed information about a single
  *   component's analysis, such as showing "Other" options within a select list.
  * @return
- *   An array of data rows, each containing a statistic for this component's
- *   submissions.
+ *   An array containing one or more of the following keys:
+ *   - table_rows: If this component has numeric data that can be represented in
+ *     a grid, return the values here. This array assumes a 2-dimensional
+ *     structure, with the first value being a label and subsequent values
+ *     containing a decimal or integer.
+ *   - table_header: If this component has more than a single set of values,
+ *     include a table header so each column can be labeled.
+ *   - other_data: If your component has non-numeric data to include, such as
+ *     a description or link, include that in the other_data array. Each item
+ *     may be a string or an array of values that matches the number of columns
+ *     in the table_header property.
+ *   At the very least, either table_rows or other_data should be provided.
+ *   Note that if you want your component's analysis to be available by default
+ *   without the user specifically enabling it, you must set
+ *   $component['extra']['analysis'] = TRUE in your
+ *   _webform_defaults_component() callback.
+ *
+ * @see _webform_defaults_component()
  */
 function _webform_analysis_component($component, $sids = array(), $single = FALSE) {
   // Generate the list of options and questions.
@@ -930,9 +1009,15 @@ function _webform_analysis_component($component, $sids = array(), $single = FALS
     }
     $rows[] = $row;
   }
-  $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('webform-grid'))));
 
-  return array(array(array('data' => $output, 'colspan' => 2)));
+  $other = array();
+  $other[] = l(t('More information'), 'node/' . $component['nid'] . '/webform-results/analysis/' . $component['cid']);
+
+  array(
+    'table_header' => $header,
+    'table_rows' => $rows,
+    'other_data' => $other,
+  );
 }
 
 /**
diff --git a/webform.module b/webform.module
index 9a2f2fd..ca5a35e 100644
--- a/webform.module
+++ b/webform.module
@@ -276,7 +276,11 @@ function webform_menu() {
     'access callback' => 'webform_results_access',
     'access arguments' => array(1),
     'file' => 'includes/webform.report.inc',
-    'type' => MENU_CALLBACK,
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['node/%webform_menu/webform-results/analysis/%webform_menu_component/more'] = array(
+    'title' => 'In-depth analysis',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
   );
   $items['node/%webform_menu/webform-results/table'] = array(
     'title' => 'Table',
@@ -788,8 +792,18 @@ function webform_theme() {
       'render element' => 'element',
       'file' => 'includes/webform.report.inc',
     ),
-    'webform_results_analysis' => array(
-      'variables' => array('node' => NULL, 'data' => NULL, 'sids' => array(), 'component' => NULL),
+    'webform_analysis' => array(
+      'render element' => 'analysis',
+      'template' => 'templates/webform-analysis',
+      'file' => 'includes/webform.report.inc',
+    ),
+    'webform_analysis_component' => array(
+      'render element' => 'component_analysis',
+      'template' => 'templates/webform-analysis-component',
+      'file' => 'includes/webform.report.inc',
+    ),
+    'webform_analysis_component_basic' => array(
+      'variables' => array('component' => NULL, 'data' => NULL),
       'file' => 'includes/webform.report.inc',
     ),
     // webform.submissions.inc
@@ -954,6 +968,7 @@ function webform_webform_component_info() {
       'label' => t('Markup'),
       'description' => t('Displays text as HTML in the form; does not render a field.'),
       'features' => array(
+        'analysis' => FALSE,
         'csv' => FALSE,
         'default_value' => FALSE,
         'description' => FALSE,
@@ -979,6 +994,7 @@ function webform_webform_component_info() {
       'label' => t('Page break'),
       'description' => t('Organize forms into multiple pages.'),
       'features' => array(
+        'analysis' => FALSE,
         'conditional' => FALSE,
         'csv' => FALSE,
         'default_value' => FALSE,
