You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@superset.apache.org by GitBox <gi...@apache.org> on 2018/04/23 15:28:04 UTC

[GitHub] mistercrunch closed pull request #3006: [explore] new visualization that adds a paired t-test table to linechart

mistercrunch closed pull request #3006: [explore] new visualization that adds a paired t-test table to linechart
URL: https://github.com/apache/incubator-superset/pull/3006
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json
index 71e7130328..f57d8fae44 100644
--- a/superset/assets/backendSync.json
+++ b/superset/assets/backendSync.json
@@ -9,167 +9,9 @@
       "description": ""
     },
     "viz_type": {
-      "type": "SelectControl",
+      "type": "VizTypeControl",
       "label": "Visualization Type",
-      "clearable": false,
       "default": "table",
-      "choices": [
-        [
-          "dist_bar",
-          "Distribution - Bar Chart",
-          "/static/assets/images/viz_thumbnails/dist_bar.png"
-        ],
-        [
-          "pie",
-          "Pie Chart",
-          "/static/assets/images/viz_thumbnails/pie.png"
-        ],
-        [
-          "line",
-          "Time Series - Line Chart",
-          "/static/assets/images/viz_thumbnails/line.png"
-        ],
-        [
-          "dual_line",
-          "Time Series - Dual Axis Line Chart",
-          "/static/assets/images/viz_thumbnails/dual_line.png"
-        ],
-        [
-          "bar",
-          "Time Series - Bar Chart",
-          "/static/assets/images/viz_thumbnails/bar.png"
-        ],
-        [
-          "compare",
-          "Time Series - Percent Change",
-          "/static/assets/images/viz_thumbnails/compare.png"
-        ],
-        [
-          "area",
-          "Time Series - Stacked",
-          "/static/assets/images/viz_thumbnails/area.png"
-        ],
-        [
-          "table",
-          "Table View",
-          "/static/assets/images/viz_thumbnails/table.png"
-        ],
-        [
-          "markup",
-          "Markup",
-          "/static/assets/images/viz_thumbnails/markup.png"
-        ],
-        [
-          "pivot_table",
-          "Pivot Table",
-          "/static/assets/images/viz_thumbnails/pivot_table.png"
-        ],
-        [
-          "separator",
-          "Separator",
-          "/static/assets/images/viz_thumbnails/separator.png"
-        ],
-        [
-          "word_cloud",
-          "Word Cloud",
-          "/static/assets/images/viz_thumbnails/word_cloud.png"
-        ],
-        [
-          "treemap",
-          "Treemap",
-          "/static/assets/images/viz_thumbnails/treemap.png"
-        ],
-        [
-          "cal_heatmap",
-          "Calendar Heatmap",
-          "/static/assets/images/viz_thumbnails/cal_heatmap.png"
-        ],
-        [
-          "box_plot",
-          "Box Plot",
-          "/static/assets/images/viz_thumbnails/box_plot.png"
-        ],
-        [
-          "bubble",
-          "Bubble Chart",
-          "/static/assets/images/viz_thumbnails/bubble.png"
-        ],
-        [
-          "bullet",
-          "Bullet Chart",
-          "/static/assets/images/viz_thumbnails/bullet.png"
-        ],
-        [
-          "big_number",
-          "Big Number with Trendline",
-          "/static/assets/images/viz_thumbnails/big_number.png"
-        ],
-        [
-          "big_number_total",
-          "Big Number",
-          "/static/assets/images/viz_thumbnails/big_number_total.png"
-        ],
-        [
-          "histogram",
-          "Histogram",
-          "/static/assets/images/viz_thumbnails/histogram.png"
-        ],
-        [
-          "sunburst",
-          "Sunburst",
-          "/static/assets/images/viz_thumbnails/sunburst.png"
-        ],
-        [
-          "sankey",
-          "Sankey",
-          "/static/assets/images/viz_thumbnails/sankey.png"
-        ],
-        [
-          "directed_force",
-          "Directed Force Layout",
-          "/static/assets/images/viz_thumbnails/directed_force.png"
-        ],
-        [
-          "country_map",
-          "Country Map",
-          "/static/assets/images/viz_thumbnails/country_map.png"
-        ],
-        [
-          "world_map",
-          "World Map",
-          "/static/assets/images/viz_thumbnails/world_map.png"
-        ],
-        [
-          "filter_box",
-          "Filter Box",
-          "/static/assets/images/viz_thumbnails/filter_box.png"
-        ],
-        [
-          "iframe",
-          "iFrame",
-          "/static/assets/images/viz_thumbnails/iframe.png"
-        ],
-        [
-          "para",
-          "Parallel Coordinates",
-          "/static/assets/images/viz_thumbnails/para.png"
-        ],
-        [
-          "heatmap",
-          "Heatmap",
-          "/static/assets/images/viz_thumbnails/heatmap.png"
-        ],
-        [
-          "horizon",
-          "Horizon",
-          "/static/assets/images/viz_thumbnails/horizon.png"
-        ],
-        [
-          "mapbox",
-          "Mapbox",
-          "/static/assets/images/viz_thumbnails/mapbox.png"
-        ]
-      ],
       "description": "The type of visualization to display"
     },
     "metrics": {
@@ -179,8 +21,18 @@
       "validators": [
         null
       ],
+      "valueKey": "metric_name",
       "description": "One or many metrics to display"
     },
+    "y_axis_bounds": {
+      "type": "BoundsControl",
+      "label": "Y Axis Bounds",
+      "default": [
+        null,
+        null
+      ],
+      "description": "Bounds for the Y axis. When left empty, the bounds are dynamically defined based on the min/max of the data. Note that this feature will only expand the axis range. It won't narrow the data's extent."
+    },
     "order_by_cols": {
       "type": "SelectControl",
       "multi": true,
@@ -192,14 +44,22 @@
       "type": "SelectControl",
       "label": "Metric",
       "clearable": false,
-      "description": "Choose the metric"
+      "description": "Choose the metric",
+      "validators": [
+        null
+      ],
+      "valueKey": "metric_name"
     },
     "metric_2": {
       "type": "SelectControl",
       "label": "Right Axis Metric",
-      "choices": [],
-      "default": [],
-      "description": "Choose a metric for right axis"
+      "default": null,
+      "validators": [
+        null
+      ],
+      "clearable": true,
+      "description": "Choose a metric for right axis",
+      "valueKey": "metric_name"
     },
     "stacked_style": {
       "type": "SelectControl",
@@ -785,29 +645,21 @@
     },
     "select_country": {
       "type": "SelectControl",
-      "label": "Country Name Type",
+      "label": "Country Name",
       "default": "France",
       "choices": [
-        [
-          "Algeria",
-          "Algeria"
-        ],
         [
           "Belgium",
           "Belgium"
         ],
         [
-          "Brasil",
-          "Brasil"
+          "Brazil",
+          "Brazil"
         ],
         [
           "China",
           "China"
         ],
-        [
-          "Germany",
-          "Germany"
-        ],
         [
           "Egypt",
           "Egypt"
@@ -816,6 +668,10 @@
           "France",
           "France"
         ],
+        [
+          "Germany",
+          "Germany"
+        ],
         [
           "Italy",
           "Italy"
@@ -825,8 +681,8 @@
           "Morocco"
         ],
         [
-          "Nederlanden",
-          "Nederlanden"
+          "Netherlands",
+          "Netherlands"
         ],
         [
           "Russia",
@@ -844,6 +700,10 @@
           "Uk",
           "Uk"
         ],
+        [
+          "Ukraine",
+          "Ukraine"
+        ],
         [
           "Usa",
           "Usa"
@@ -880,7 +740,8 @@
       "multi": true,
       "label": "Group by",
       "default": [],
-      "description": "One or many controls to group by"
+      "description": "One or many controls to group by",
+      "valueKey": "column_name"
     },
     "columns": {
       "type": "SelectControl",
@@ -1573,19 +1434,31 @@
     "x": {
       "type": "SelectControl",
       "label": "X Axis",
+      "description": "Metric assigned to the [X] axis",
       "default": null,
-      "description": "Metric assigned to the [X] axis"
+      "validators": [
+        null
+      ],
+      "valueKey": "metric_name"
     },
     "y": {
       "type": "SelectControl",
       "label": "Y Axis",
       "default": null,
-      "description": "Metric assigned to the [Y] axis"
+      "validators": [
+        null
+      ],
+      "description": "Metric assigned to the [Y] axis",
+      "valueKey": "metric_name"
     },
     "size": {
       "type": "SelectControl",
       "label": "Bubble Size",
-      "default": null
+      "default": null,
+      "validators": [
+        null
+      ],
+      "valueKey": "metric_name"
     },
     "url": {
       "type": "TextControl",
@@ -1746,7 +1619,41 @@
     "x_axis_format": {
       "type": "SelectControl",
       "freeForm": true,
-      "label": "X axis format",
+      "label": "X Axis Format",
+      "renderTrigger": true,
+      "default": ".3s",
+      "choices": [
+        [
+          ".3s",
+          ".3s | 12.3k"
+        ],
+        [
+          ".3%",
+          ".3% | 1234543.210%"
+        ],
+        [
+          ".4r",
+          ".4r | 12350"
+        ],
+        [
+          ".3f",
+          ".3f | 12345.432"
+        ],
+        [
+          "+,",
+          "+, | +12,345.4321"
+        ],
+        [
+          "$,.2f",
+          "$,.2f | $12,345.43"
+        ]
+      ],
+      "description": "D3 format syntax: https://github.com/d3/d3-format"
+    },
+    "x_axis_time_format": {
+      "type": "SelectControl",
+      "freeForm": true,
+      "label": "X Axis Format",
       "renderTrigger": true,
       "default": "smart_date",
       "choices": [
@@ -1776,7 +1683,7 @@
     "y_axis_format": {
       "type": "SelectControl",
       "freeForm": true,
-      "label": "Y axis format",
+      "label": "Y Axis Format",
       "renderTrigger": true,
       "default": ".3s",
       "choices": [
@@ -1810,7 +1717,7 @@
     "y_axis_2_format": {
       "type": "SelectControl",
       "freeForm": true,
-      "label": "Right axis format",
+      "label": "Right Axis Format",
       "default": ".3s",
       "choices": [
         [
@@ -1843,6 +1750,7 @@
     "markup_type": {
       "type": "SelectControl",
       "label": "Markup Type",
+      "clearable": false,
       "choices": [
         [
           "markdown",
@@ -1854,6 +1762,9 @@
         ]
       ],
       "default": "markdown",
+      "validators": [
+        null
+      ],
       "description": "Pick your favorite markup language"
     },
     "rotation": {
@@ -2053,13 +1964,6 @@
       "default": true,
       "description": "The rich tooltip shows a list of all series for that point in time"
     },
-    "y_axis_zero": {
-      "type": "CheckboxControl",
-      "label": "Y Axis Zero",
-      "default": false,
-      "renderTrigger": true,
-      "description": "Force the Y axis to start at 0 instead of the minimum value"
-    },
     "y_log_scale": {
       "type": "CheckboxControl",
       "label": "Y Log Scale",
@@ -2078,12 +1982,14 @@
       "type": "CheckboxControl",
       "label": "Donut",
       "default": false,
+      "renderTrigger": true,
       "description": "Do you want a donut or a pie?"
     },
     "labels_outside": {
       "type": "CheckboxControl",
       "label": "Put labels outside",
       "default": true,
+      "renderTrigger": true,
       "description": "Put the labels outside the pie?"
     },
     "contribution": {
diff --git a/superset/assets/images/viz_thumbnails/line_ttest.png b/superset/assets/images/viz_thumbnails/line_ttest.png
new file mode 100644
index 0000000000..8be9136f1c
Binary files /dev/null and b/superset/assets/images/viz_thumbnails/line_ttest.png differ
diff --git a/superset/assets/javascripts/explore/components/ChartContainer.jsx b/superset/assets/javascripts/explore/components/ChartContainer.jsx
index fb0a342840..e9678778fe 100644
--- a/superset/assets/javascripts/explore/components/ChartContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ChartContainer.jsx
@@ -14,6 +14,7 @@ import Timer from '../../components/Timer';
 import { getExploreUrl } from '../exploreUtils';
 import { getFormDataFromControls } from '../stores/store';
 import CachedLabel from '../../components/CachedLabel';
+import PairedTTestTableContainer from './PairedTTestTableContainer';
 
 const CHART_STATUS_MAP = {
   failed: 'danger',
@@ -248,71 +249,72 @@ class ChartContainer extends React.PureComponent {
     }
     const queryResponse = this.props.queryResponse;
     return (
-      <div className="chart-container">
-        <Panel
-          style={{ height: this.props.height }}
-          header={
-            <div
-              id="slice-header"
-              className="clearfix panel-title-large"
-            >
-              <EditableTitle
-                title={this.renderChartTitle()}
-                canEdit={this.props.can_overwrite}
-                onSaveTitle={this.updateChartTitle.bind(this)}
-              />
-
-              {this.props.slice &&
-                <span>
-                  <FaveStar
-                    sliceId={this.props.slice.slice_id}
-                    actions={this.props.actions}
-                    isStarred={this.props.isStarred}
-                  />
-
-                  <TooltipWrapper
-                    label="edit-desc"
-                    tooltip="Edit slice properties"
-                  >
-                    <a
-                      className="edit-desc-icon"
-                      href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
+      <div>
+        <div className="chart-container">
+          <Panel
+            style={{ height: this.props.height }}
+            header={
+              <div
+                id="slice-header"
+                className="clearfix panel-title-large"
+              >
+                <EditableTitle
+                  title={this.renderChartTitle()}
+                  canEdit={this.props.can_overwrite}
+                  onSaveTitle={this.updateChartTitle.bind(this)}
+                />
+                {this.props.slice &&
+                  <span>
+                    <FaveStar
+                      sliceId={this.props.slice.slice_id}
+                      actions={this.props.actions}
+                      isStarred={this.props.isStarred}
+                    />
+                    <TooltipWrapper
+                      label="edit-desc"
+                      tooltip="Edit Description"
                     >
-                      <i className="fa fa-edit" />
-                    </a>
-                  </TooltipWrapper>
-                </span>
-              }
-
-              <div className="pull-right">
-                {this.props.chartStatus === 'success' &&
-                this.props.queryResponse &&
-                this.props.queryResponse.is_cached &&
-                  <CachedLabel
-                    onClick={this.runQuery.bind(this)}
-                    cachedTimestamp={queryResponse.cached_dttm}
-                  />
+                      <a
+                        className="edit-desc-icon"
+                        href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
+                      >
+                        <i className="fa fa-edit" />
+                      </a>
+                    </TooltipWrapper>
+                  </span>
                 }
-                <Timer
-                  startTime={this.props.chartUpdateStartTime}
-                  endTime={this.props.chartUpdateEndTime}
-                  isRunning={this.props.chartStatus === 'loading'}
-                  status={CHART_STATUS_MAP[this.props.chartStatus]}
-                  style={{ fontSize: '10px', marginRight: '5px' }}
-                />
-                <ExploreActionButtons
-                  slice={this.state.mockSlice}
-                  canDownload={this.props.can_download}
-                  chartStatus={this.props.chartStatus}
-                  queryResponse={queryResponse}
-                  queryEndpoint={getExploreUrl(this.props.latestQueryFormData, 'query')}
-                />
+                <div className="pull-right">
+                  {this.props.chartStatus === 'success' &&
+                  this.props.queryResponse &&
+                  this.props.queryResponse.is_cached &&
+                    <CachedLabel
+                      onClick={this.runQuery.bind(this)}
+                      cachedTimestamp={queryResponse.cached_dttm}
+                    />
+                  }
+                  <Timer
+                    startTime={this.props.chartUpdateStartTime}
+                    endTime={this.props.chartUpdateEndTime}
+                    isRunning={this.props.chartStatus === 'loading'}
+                    status={CHART_STATUS_MAP[this.props.chartStatus]}
+                    style={{ fontSize: '10px', marginRight: '5px' }}
+                  />
+                  <ExploreActionButtons
+                    slice={this.state.mockSlice}
+                    canDownload={this.props.can_download}
+                    chartStatus={this.props.chartStatus}
+                    queryResponse={queryResponse}
+                    queryEndpoint={getExploreUrl(this.props.latestQueryFormData, 'query')}
+                  />
+                </div>
               </div>
-            </div>
-          }
-        >
-          {this.renderChart()}
-        </Panel>
+            }
+          >
+            {this.renderChart()}
+          </Panel>
+        </div>
+        {this.props.viz_type === 'line_ttest' &&
+        <PairedTTestTableContainer />}
       </div>
     );
   }
@@ -340,6 +342,7 @@ function mapStateToProps(state) {
     standalone: state.standalone,
     table_name: formData.datasource_name,
     viz_type: formData.viz_type,
+    metrics: formData.metrics ? formData.metrics.length : 0,
     triggerRender: state.triggerRender,
     datasourceType: state.datasource.type,
     datasourceId: state.datasource_id,
diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
index e8c904263b..bb0e6b08cc 100644
--- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
@@ -18,12 +18,15 @@ const propTypes = {
   controls: PropTypes.object.isRequired,
   forcedHeight: PropTypes.string,
   form_data: PropTypes.object.isRequired,
+  metrics: PropTypes.number.isRequired,
   standalone: PropTypes.bool.isRequired,
   triggerQuery: PropTypes.bool.isRequired,
+  queryResponse: PropTypes.object,
   queryRequest: PropTypes.object,
 };
 
 class ExploreViewContainer extends React.Component {
+
   constructor(props) {
     super(props);
     this.state = {
@@ -72,6 +75,18 @@ class ExploreViewContainer extends React.Component {
     this.props.actions.chartUpdateStopped(this.props.queryRequest);
   }
 
+  getChartOffset() {
+    if (this.props.form_data.viz_type === 'line_ttest') {
+      // placed inside a try-catch block because queryResponse will be null initially
+      try {
+        return this.props.queryResponse.data.length * 40;
+      } catch (err) {
+        return 0;
+      }
+    }
+    return 0;
+  }
+
   getHeight() {
     if (this.props.forcedHeight) {
       return this.props.forcedHeight + 'px';
@@ -80,13 +95,25 @@ class ExploreViewContainer extends React.Component {
     return `${window.innerHeight - navHeight}px`;
   }
 
+  getChartHeight() {
+    const offset = this.getChartOffset();
+    const navHeight = this.props.standalone ? 0 : 90;
+    if (this.props.form_data.viz_type !== 'line_ttest') {
+      return this.getHeight();
+    } else if (this.props.metrics > 1) {
+      return parseInt(this.getHeight().slice(0, -2), 10) - 100 + 'px';
+    } else if (this.props.forcedHeight) {
+      return this.getHeight();
+    }
+    const chartHeight = Math.max(500, window.innerHeight - offset - navHeight);
+    return `${chartHeight}px`;
+  }
 
   triggerQueryIfNeeded() {
     if (this.props.triggerQuery && !this.hasErrors()) {
       this.props.actions.runQuery(this.props.form_data);
     }
   }
-
   handleResize() {
     clearTimeout(this.resizeTimer);
     this.resizeTimer = setTimeout(() => {
@@ -128,10 +155,12 @@ class ExploreViewContainer extends React.Component {
     return (
       <ChartContainer
         actions={this.props.actions}
-        height={this.state.height}
-      />);
+        height={this.getChartHeight()}
+      />
+    );
   }
 
+
   render() {
     if (this.props.standalone) {
       return this.renderChartContainer();
@@ -170,7 +199,11 @@ class ExploreViewContainer extends React.Component {
             />
           </div>
           <div className="col-sm-8">
-            {this.renderChartContainer()}
+            <div className="scrollbar-container">
+              <div className="scrollbar-content">
+                {this.renderChartContainer()}
+              </div>
+            </div>
           </div>
         </div>
       </div>
@@ -189,7 +222,9 @@ function mapStateToProps(state) {
     form_data,
     standalone: state.standalone,
     triggerQuery: state.triggerQuery,
+    metrics: state.latestQueryFormData.metrics ? state.latestQueryFormData.metrics.length : 0,
     forcedHeight: state.forced_height,
+    queryResponse: state.queryResponse,
     queryRequest: state.queryRequest,
   };
 }
diff --git a/superset/assets/javascripts/explore/components/PairedTTestTableContainer.jsx b/superset/assets/javascripts/explore/components/PairedTTestTableContainer.jsx
new file mode 100644
index 0000000000..17641aef60
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/PairedTTestTableContainer.jsx
@@ -0,0 +1,161 @@
+/* eslint camelcase: 0 */
+import React from 'react';
+import PropTypes from 'prop-types';
+import dist from 'distributions';
+
+import { connect } from 'react-redux';
+import { Table } from 'react-bootstrap';
+
+
+const propTypes = {
+  queryResponse: PropTypes.object,
+  viz_type: PropTypes.string.isRequired,
+  metrics: PropTypes.number.isRequired,
+};
+
+class PairedTTestTableContainer extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      table: Array(0),
+    };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (JSON.stringify(nextProps.queryResponse) !== JSON.stringify(this.props.queryResponse)) {
+      this.pairedTTestQuery();
+    }
+  }
+
+  componentDidUpdate(prevProps) {
+    if (JSON.stringify(prevProps.queryResponse) !== JSON.stringify(this.props.queryResponse)) {
+      this.pairedTTestQuery();
+    }
+  }
+
+  getTTestValueClass(row) {
+    if (row === 'control') {
+      return row;
+    }
+    return row > 0.05 ? 'color-red' : 'color-green';
+  }
+
+  getLiftClass(row) {
+    if (row === 'control') {
+      return row;
+    }
+    return row >= 0 ? 'color-green' : 'color-red';
+  }
+
+  computePairedTTest(data, control) {
+    // compute the paired t-test values and update state
+    const dataLength = data ? data.length : 0;
+    const pValueTable = Array(dataLength);
+    for (let i = 0; i < dataLength; i += 1) {
+      if (i === control) {
+        pValueTable[i] = {
+          pval: 'control', lift: 'control', stream: data[i].key, highlight: true,
+        };
+        continue;
+      }
+      const currentPairedTTest = this.statsPairedTTest(data[i].values, data[control].values);
+      const currentLift = this.statsLift(data[i].values, data[control].values);
+      pValueTable[i] = {
+        pval: currentPairedTTest, lift: currentLift, stream: data[i].key, highlight: false,
+      };
+    }
+    this.setState({ table: pValueTable });
+  }
+
+  pairedTTestQuery(control = 0) {
+    // check the viz_type and compute the paired t-test values
+    if (this.props.viz_type === 'line_ttest' && this.props.queryResponse) {
+      this.computePairedTTest(this.props.queryResponse.data, control);
+    }
+  }
+
+  statsLift(aa, bb) {
+    // compute the lift between two arrays
+    let a_sum = 0;
+    let b_sum = 0;
+    for (let i = 0; i < aa.length; ++i) {
+      a_sum += aa[i].y;
+      b_sum += bb[i].y;
+    }
+    return (((a_sum - b_sum) / b_sum) * 100).toFixed(4);
+  }
+
+  statsPairedTTest(aa, bb) {
+    // calculate the paired t-test values between two arrays
+    let ii;
+    let sum = 0;
+    let nn = 0;
+    let ss = 0;
+    for (ii = 0; ii < aa.length; ii += 1) {
+      const diff = bb[ii].y - aa[ii].y;
+      if (global.isFinite(diff)) {
+        nn += 1;
+        sum += diff;
+        ss += diff * diff;
+      }
+    }
+    const tvalue = -Math.abs(sum * Math.sqrt((nn - 1) / (nn * ss - sum * sum)));
+    try {
+      return (2 * new dist.Studentt(nn - 1).cdf(tvalue)).toFixed(6);
+    } catch (error) {
+      return NaN;
+    }
+  }
+
+  render() {
+    if (this.props.metrics > 1) {
+      return (
+        <h4 className="nvd3-paired-ttest-table">
+          <div className="message-outline">
+            Paired t-test values do not apply across multiple metrics
+          </div>
+        </h4>);
+    }
+
+    return (
+      <div className="nvd3-paired-ttest-table">
+        <Table className="table-container" bordered condensed hover>
+          <thead>
+            <tr>
+              <th>Dimensions</th>
+              <th>Paired t-test value</th>
+              <th>Lift %</th>
+            </tr>
+          </thead>
+          <tbody>
+            {this.state.table.map((row, i) => (
+              <tr onClick={() => { this.pairedTTestQuery(i); }} className={row.highlight ? 'highlight' : ''} key={i}>
+                <td>
+                  {row.stream}
+                </td>
+                <td className={this.getTTestValueClass(row.pval)}>
+                  {row.pval} {this.getTTestValueClass(row.pval) === 'color-red' ? '(not significant)' : ''}
+                </td>
+                <td className={this.getLiftClass(row.lift)}>
+                  {row.lift}
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </Table>
+      </div>
+
+    );
+  }
+}
+PairedTTestTableContainer.propTypes = propTypes;
+
+function mapStateToProps(state) {
+  return {
+    viz_type: state.latestQueryFormData.viz_type,
+    metrics: state.latestQueryFormData.metrics.length,
+    queryResponse: state.queryResponse,
+  };
+}
+
+export default connect(mapStateToProps, () => ({}))(PairedTTestTableContainer);
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 4dd2700c5e..0f8c255593 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -154,6 +154,11 @@ export const visTypes = {
     },
   },
 
+  line_ttest: {
+    label: 'Time Series - Line Chart with Paired t-test',
+    requiresTime: true,
+  },
+
   dual_line: {
     label: 'Dual Axis Line Chart',
     requiresTime: true,
@@ -940,6 +945,8 @@ export const visTypes = {
     },
   },
 };
+visTypes.line_ttest.controlPanelSections = visTypes.line.controlPanelSections;
+visTypes.line_ttest.controlOverrides = visTypes.line.controlOverrides;
 
 export default visTypes;
 
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 3a37efe8c1..6b3f486644 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -53,6 +53,7 @@
     "datatables-bootstrap3-plugin": "^0.5.0",
     "datatables.net": "^1.10.13",
     "datatables.net-bs": "^1.10.12",
+    "distributions": "^1.0.0",
     "immutable": "^3.8.1",
     "jquery": "^3.2.1",
     "lodash.throttle": "^4.1.1",
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index a02f508c33..909b8d60b3 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -18,6 +18,7 @@ const vizMap = {
   horizon: require('./horizon.js'),
   iframe: require('./iframe.js'),
   line: require('./nvd3_vis.js'),
+  line_ttest: require('./nvd3_vis.js'),
   mapbox: require('./mapbox.jsx'),
   markup: require('./markup.js'),
   para: require('./parallel_coordinates.js'),
diff --git a/superset/assets/visualizations/nvd3_vis.css b/superset/assets/visualizations/nvd3_vis.css
index cc33870dee..32569d6be8 100644
--- a/superset/assets/visualizations/nvd3_vis.css
+++ b/superset/assets/visualizations/nvd3_vis.css
@@ -11,6 +11,51 @@ text.nv-axislabel {
   font-size: 14px;
 }
 
+.nvd3-paired-ttest-table .table-container {
+  position: relative;
+  top: 0px;
+  bottom: 0px;
+  left: 0px;
+  right: 0px;
+  width: 100%;
+  padding: 0;
+  margin-right: 0px;
+  font-size: 1em;
+  background-color: white;
+}
+
+h4.nvd3-paired-ttest-table .message-outline {
+  border: 1px solid lightgrey;
+  height: 40px;
+  text-align: center;
+  vertical-align: middle;
+  line-height: 40px;
+}
+
+h4.nvd3-paired-ttest-table {
+  padding: 15px;
+  height: 70px;
+  background-color: white;
+}
+
+.nvd3-paired-ttest-table .highlight {
+  font-weight: bold;
+  font-size: 110%;
+  background-color: lightgrey;
+}
+
+.nvd3-paired-ttest-table .color-green {
+  color: green;
+}
+
+.nvd3-paired-ttest-table .color-red {
+  color: red;
+}
+
+.nvd3-paired-ttest-table .control {
+  color: blue;
+}
+
 .dist_bar {
   overflow-x: auto !important;
 }
diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js
index 21342942cf..c7a42c7b79 100644
--- a/superset/assets/visualizations/nvd3_vis.js
+++ b/superset/assets/visualizations/nvd3_vis.js
@@ -122,6 +122,7 @@ function nvd3Vis(slice, payload) {
       svg = d3.select(slice.selector).append('svg');
     }
     switch (vizType) {
+      case 'line_ttest':
       case 'line':
         if (fd.show_brush) {
           chart = nv.models.lineWithFocusChart();
@@ -310,7 +311,7 @@ function nvd3Vis(slice, payload) {
       chart.xScale(d3.scale.log());
     }
     const isTimeSeries = [
-      'line', 'dual_line', 'area', 'compare', 'bar'].indexOf(vizType) >= 0;
+      'line', 'line_ttest', 'dual_line', 'area', 'compare', 'bar'].indexOf(vizType) >= 0;
     // if x axis format is a date format, rotate label 90 degrees
     if (isTimeSeries) {
       chart.xAxis.rotateLabels(45);
@@ -339,9 +340,9 @@ function nvd3Vis(slice, payload) {
     if (vizType !== 'bullet') {
       chart.color(d => category21(d[colorKey]));
     }
-    if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
+    if ((['line', 'area', 'line_ttest'].indexOf(vizType) >= 0) && fd.rich_tooltip) {
       chart.useInteractiveGuideline(true);
-      if (vizType === 'line') {
+      if (vizType === 'line' || vizType === 'line_ttest') {
         // Custom sorted tooltip
         chart.interactiveLayer.tooltip.contentGenerator((d) => {
           let tooltip = '';
diff --git a/superset/viz.py b/superset/viz.py
index de1f635ec5..acbba92825 100755
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -928,6 +928,14 @@ def get_data(self, df):
         return chart_data
 
 
+class NVD3TimeSeriesPairedTTestViz(NVD3TimeSeriesViz):
+
+    """A standard line chart with a table displaying paired t-test values"""
+
+    viz_type = "line_ttest"
+    verbose_name = _("Time Series - Line Chart with Paired t-test")
+
+
 class NVD3DualLineViz(NVD3Viz):
 
     """A rich line chart with dual axis"""
@@ -1630,6 +1638,7 @@ def get_data(self, df):
     TableViz,
     PivotTableViz,
     NVD3TimeSeriesViz,
+    NVD3TimeSeriesPairedTTestViz,
     NVD3DualLineViz,
     NVD3CompareTimeSeriesViz,
     NVD3TimeSeriesStackedViz,


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services