mistercrunch closed pull request #3202: [WiP][explore] metrics as popovers
URL: https://github.com/apache/incubator-superset/pull/3202
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/javascripts/components/ColumnOption.jsx
b/superset/assets/javascripts/components/ColumnOption.jsx
index c150937a0b..b00421dde1 100644
--- a/superset/assets/javascripts/components/ColumnOption.jsx
+++ b/superset/assets/javascripts/components/ColumnOption.jsx
@@ -5,11 +5,15 @@ import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
const propTypes = {
column: PropTypes.object.isRequired,
+ prefix: PropTypes.string,
};
-export default function ColumnOption({ column }) {
+export default function ColumnOption({ column, prefix }) {
return (
<span>
+ {prefix &&
+ <span className="m-r-5 text-muted">{prefix}</span>
+ }
<span className="m-r-5 option-label">
{column.verbose_name || column.column_name}
</span>
diff --git a/superset/assets/javascripts/explore/components/Control.jsx
b/superset/assets/javascripts/explore/components/Control.jsx
index 7d8fcc3dcd..e4a8375701 100644
--- a/superset/assets/javascripts/explore/components/Control.jsx
+++ b/superset/assets/javascripts/explore/components/Control.jsx
@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import controlMap from './controls';
-
const controlTypes = Object.keys(controlMap);
const propTypes = {
diff --git a/superset/assets/javascripts/explore/components/controls/Metric.jsx
b/superset/assets/javascripts/explore/components/controls/Metric.jsx
new file mode 100644
index 0000000000..6b7ea304c1
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/Metric.jsx
@@ -0,0 +1,305 @@
+import React, { PropTypes } from 'react';
+import Select from 'react-select';
+import {
+ Col,
+ FormControl,
+ FormGroup,
+ InputGroup,
+ Label,
+ OverlayTrigger,
+ Popover,
+ Radio,
+ Row,
+} from 'react-bootstrap';
+
+import MetricOption from '../../../components/MetricOption';
+import ColumnOption from '../../../components/ColumnOption';
+
+const NUMERIC_TYPES = ['INT', 'INTEGER', 'BIGINT', 'DOUBLE', 'FLOAT',
'NUMERIC'];
+function isNum(type) {
+ return NUMERIC_TYPES.some(s => type.startsWith(s));
+}
+const nonNumericAggFunctions = {
+ COUNT_DISTINCT: 'COUNT(DISTINCT {})',
+ COUNT: 'COUNT({})',
+};
+const numericAggFunctions = {
+ SUM: 'SUM({})',
+ AVG: 'AVG({})',
+ MIN: 'MIN({})',
+ MAX: 'MAX({})',
+ COUNT_DISTINCT: 'COUNT(DISTINCT {})',
+ COUNT: 'COUNT({})',
+};
+
+const propTypes = {
+ datasource: PropTypes.object,
+ onChange: PropTypes.func,
+ onDelete: PropTypes.func,
+ metric: PropTypes.object.isRequired,
+};
+
+export default class Metric extends React.Component {
+ constructor(props) {
+ super(props);
+ const datasource = props.datasource;
+ const metric = props.metric;
+ let column = datasource.columns[0];
+ let predefMetric = datasource.metrics[0];
+ if (metric.col) {
+ column = datasource.columns.find(c => c.column_name === metric.col);
+ }
+ if (metric.metricName) {
+ predefMetric = datasource.metrics.find(m => m.metric_name ===
metric.metricName);
+ }
+ this.state = {
+ column,
+ predefMetric,
+ agg: metric.agg,
+ label: metric.label,
+ metricType: metric.metricType,
+ expr: metric.expr,
+ };
+ this.onChange();
+ }
+ onChange() {
+ const metric = {
+ metricType: this.state.metricType,
+ label: this.state.label,
+ };
+ if (this.state.metricType === 'metric') {
+ metric.metricName = this.state.predefMetric.metric_name;
+ metric.expr = this.state.predefMetric.expression;
+ } else if (this.state.metricType === 'col') {
+ metric.col = this.state.column.column_name;
+ metric.agg = this.state.agg;
+ } else if (this.state.metricType === 'expr') {
+ metric.expr = this.state.expr;
+ }
+ this.props.onChange(metric);
+ }
+ onDelete() {
+ this.props.onDelete();
+ }
+ setMetricType(v) {
+ this.setState({ metricType: v }, this.onChange);
+ }
+ changeLabel(e) {
+ const label = e.target.value;
+ this.setState({ label }, this.onChange);
+ }
+ changeExpression(e) {
+ const expr = e.target.value;
+ this.setState({ expr, col: null, agg: null }, this.onChange);
+ }
+ optionify(arr) {
+ return arr.map(s => ({ value: s, label: s }));
+ }
+ changeColumnSection() {
+ let label;
+ if (this.state.agg && this.state.column) {
+ label = this.state.agg + '__' + this.state.column.column_name;
+ } else {
+ label = '';
+ }
+ this.setState({ label }, this.onChange);
+ }
+ changeagg(opt) {
+ const agg = opt ? opt.value : null;
+ this.setState({ agg }, this.changeColumnSection);
+ }
+ changeRadio(e) {
+ this.setState({ metricType: e.target.value }, this.onChange);
+ }
+ changeMetric(predefMetric) {
+ const label = predefMetric.verbose_name || predefMetric.metric_name;
+ this.setState({ label, predefMetric }, this.onChange);
+ }
+ changeColumn(column) {
+ let agg = this.state.agg;
+ if (column) {
+ if (!agg) {
+ if (isNum(column.type)) {
+ agg = 'SUM';
+ } else {
+ agg = 'COUNT_DISTINCT';
+ }
+ }
+ } else {
+ agg = null;
+ }
+ this.setState({ column, agg }, this.changeColumnSection);
+ }
+ renderColumnSelect() {
+ return (
+ <Select
+ name="select-schema"
+ placeholder="Column"
+ clearable={false}
+ onFocus={this.setMetricType.bind(this, 'col')}
+ options={this.props.datasource.columns}
+ onChange={this.changeColumn.bind(this)}
+ value={this.state.column}
+ autosize={false}
+ optionRenderer={c => <ColumnOption column={c} />}
+ valueRenderer={c => <ColumnOption prefix="Column: " column={c} />}
+ />);
+ }
+ renderMetricSelect() {
+ return (
+ <Select
+ name="select-schema"
+ placeholder="Predefined metric"
+ options={this.props.datasource.metrics}
+ onFocus={this.setMetricType.bind(this, 'metric')}
+ value={this.state.predefMetric}
+ autosize={false}
+ onChange={this.changeMetric.bind(this)}
+ optionRenderer={m => <MetricOption metric={m} />}
+ valueRenderer={m => <MetricOption prefix="Metric: " metric={m} />}
+ />);
+ }
+ renderAggSelect() {
+ let aggOptions = [];
+ const column = this.state.column;
+ if (column) {
+ if (isNum(column.type)) {
+ aggOptions = Object.keys(numericAggFunctions);
+ } else {
+ aggOptions = Object.keys(nonNumericAggFunctions);
+ }
+ }
+ return (
+ <Select
+ name="select-schema"
+ placeholder="agg function"
+ clearable={false}
+ onFocus={this.setMetricType.bind(this, 'col')}
+ disabled={aggOptions.length === 0}
+ options={this.optionify(aggOptions)}
+ value={this.state.agg}
+ autosize={false}
+ onChange={this.changeagg.bind(this)}
+ valueRenderer={o => (
+ <div>
+ <span className="text-muted">agg:</span> {o.label}
+ </div>
+ )}
+ />);
+ }
+ renderOverlay() {
+ const metricType = this.state.metricType;
+ return (
+ <Popover id="popover-positioned-right" title="Metric Definition">
+ <FormGroup bsSize="sm" controlId="label">
+ <InputGroup bsSize="sm">
+ <InputGroup.Addon>Label</InputGroup.Addon>
+ <FormControl
+ type="text"
+ value={this.state.label}
+ placeholder="Label"
+ onChange={this.changeLabel.bind(this)}
+ />
+ </InputGroup>
+ </FormGroup>
+ <hr />
+ <div className={metricType !== 'col' ? 'dimmed' : ''}>
+ <Row>
+ <Col md={1}>
+ <Radio
+ name="metricType"
+ inline
+ value="col"
+ onChange={this.changeRadio.bind(this)}
+ checked={this.state.metricType === 'col'}
+ />
+ </Col>
+ <Col md={11}>
+ <div className="m-b-5">
+ {this.renderColumnSelect()}
+ </div>
+ <div>
+ {this.renderAggSelect()}
+ </div>
+ </Col>
+ </Row>
+ </div>
+ <hr />
+ <div className={metricType !== 'metric' ? 'dimmed' : ''}>
+ <Row>
+ <Col md={1}>
+ <Radio
+ inline
+ name="metricType"
+ value="metric"
+ onChange={this.changeRadio.bind(this)}
+ checked={this.state.metricType === 'metric'}
+ />
+ </Col>
+ <Col md={11}>
+ {this.renderMetricSelect()}
+ </Col>
+ </Row>
+ </div>
+ <hr />
+ <div className={metricType !== 'expr' ? 'dimmed' : ''}>
+ <Row>
+ <Col md={1}>
+ <Radio
+ inline
+ name="metricType"
+ value="expr"
+ onChange={this.changeRadio.bind(this)}
+ checked={this.state.metricType === 'expr'}
+ />
+ </Col>
+ <Col md={11}>
+ <FormGroup bsSize="sm" controlId="expr">
+ <InputGroup bsSize="sm">
+ <InputGroup.Addon>expr</InputGroup.Addon>
+ <FormControl
+ type="text"
+ value={this.state.expr}
+ onFocus={this.setMetricType.bind(this, 'expr')}
+ placeholder="Free form expr"
+ onChange={this.changeExpression.bind(this)}
+ />
+ </InputGroup>
+ </FormGroup>
+ </Col>
+ </Row>
+ </div>
+ </Popover>
+ );
+ }
+
+ render() {
+ if (!this.props.datasource) {
+ return null;
+ }
+ let deleteButton;
+ if (this.props.onDelete) {
+ deleteButton = <i className="fa fa-times pointer"
onClick={this.onDelete.bind(this)} />;
+ }
+ const trigger = (
+ <OverlayTrigger
+ trigger="click"
+ placement="right"
+ rootClose
+ overlay={this.renderOverlay()}
+ >
+ <i className="fa fa-edit pointer" />
+ </OverlayTrigger>
+ );
+ return (
+ <Label
+ className="Metric lead"
+ style={{ display: 'inline-block', margin: '0 3 3 0', padding: '5px' }}
+ >
+ {this.state.label} {trigger} {deleteButton}
+ </Label>
+ );
+ }
+}
+
+Metric.propTypes = propTypes;
diff --git
a/superset/assets/javascripts/explore/components/controls/MetricControl.jsx
b/superset/assets/javascripts/explore/components/controls/MetricControl.jsx
new file mode 100644
index 0000000000..580a1e52f9
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/MetricControl.jsx
@@ -0,0 +1,34 @@
+import React, { PropTypes } from 'react';
+import ControlHeader from '../ControlHeader';
+import Metric from './Metric';
+
+const propTypes = {
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.object.isRequired,
+ datasource: PropTypes.object.isRequired,
+};
+
+const defaultProps = {
+ onChange: () => {},
+};
+
+export default class MetricControl extends React.Component {
+ onChange(value) {
+ this.props.onChange(value);
+ }
+ render() {
+ return (
+ <div>
+ <ControlHeader {...this.props} />
+ <Metric
+ metric={this.props.value}
+ datasource={this.props.datasource}
+ onChange={this.onChange.bind(this)}
+ />
+ </div>
+ );
+ }
+}
+
+MetricControl.propTypes = propTypes;
+MetricControl.defaultProps = defaultProps;
diff --git
a/superset/assets/javascripts/explore/components/controls/MetricListControl.jsx
b/superset/assets/javascripts/explore/components/controls/MetricListControl.jsx
new file mode 100644
index 0000000000..94ace436d4
--- /dev/null
+++
b/superset/assets/javascripts/explore/components/controls/MetricListControl.jsx
@@ -0,0 +1,89 @@
+import React, { PropTypes } from 'react';
+import shortid from 'shortid';
+
+import ControlHeader from '../ControlHeader';
+import Metric from './Metric';
+
+const propTypes = {
+ datasource: PropTypes.object.isRequired,
+ onChange: PropTypes.func,
+ value: PropTypes.array,
+};
+
+const defaultProps = {
+ onChange: () => {},
+ value: [],
+};
+
+export default class MetricListControl extends React.Component {
+ constructor(props) {
+ super(props);
+ let metrics = props.value || [];
+ /* eslint-disable no-param-reassign */
+ metrics = metrics.map((m) => {
+ m.uid = shortid.generate();
+ return m;
+ });
+ this.state = { metrics };
+ }
+ onChange() {
+ this.props.onChange(this.state.metrics, this.validate());
+ }
+ validate() {
+ const labels = {};
+ this.state.metrics.forEach((m) => {
+ labels[m.label] = null;
+ });
+ if (Object.keys(labels).length < this.state.metrics.length) {
+ return ['Provide a unique label for each metric'];
+ }
+ return null;
+ }
+ deleteMetric(uid) {
+ const metrics = this.state.metrics.filter(m => m.uid !== uid);
+ this.setState({ metrics }, this.onChange);
+ }
+ changeMetric(uid, metric) {
+ const metrics = this.state.metrics.map((m) => {
+ if (uid === m.uid) {
+ metric.uid = uid;
+ return metric;
+ }
+ return m;
+ });
+ this.setState({ metrics }, this.onChange);
+ }
+ addMetric() {
+ const metrics = this.state.metrics.slice();
+ const label = 'unlabeled metric ' + this.state.metrics.length;
+ metrics.push({
+ label,
+ uid: shortid.generate(),
+ expr: 'COUNT(*)',
+ metricType: 'expr',
+ });
+ this.setState({ metrics }, this.onChange);
+ }
+ render() {
+ return (
+ <div className="MetricListControl">
+ <ControlHeader {...this.props} />
+ {this.state.metrics.map(metric => (
+ <Metric
+ key={metric.uid}
+ metric={metric}
+ datasource={this.props.datasource}
+ onChange={this.changeMetric.bind(this, metric.uid)}
+ onDelete={this.deleteMetric.bind(this, metric.uid)}
+ />
+ ))}
+ <a onClick={this.addMetric.bind(this)} className="pointer">
+ <i className="fa fa-plus-circle" />
+ </a>
+ </div>
+ );
+ }
+}
+
+MetricListControl.propTypes = propTypes;
+MetricListControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/controls/index.js
b/superset/assets/javascripts/explore/components/controls/index.js
index 876bc4a1c6..56d50d62a4 100644
--- a/superset/assets/javascripts/explore/components/controls/index.js
+++ b/superset/assets/javascripts/explore/components/controls/index.js
@@ -7,6 +7,8 @@ import DatasourceControl from './DatasourceControl';
import DateFilterControl from './DateFilterControl';
import FilterControl from './FilterControl';
import HiddenControl from './HiddenControl';
+import MetricControl from './MetricControl';
+import MetricListControl from './MetricListControl';
import SelectAsyncControl from './SelectAsyncControl';
import SelectControl from './SelectControl';
import TextAreaControl from './TextAreaControl';
@@ -24,6 +26,8 @@ const controlMap = {
DateFilterControl,
FilterControl,
HiddenControl,
+ MetricControl,
+ MetricListControl,
SelectAsyncControl,
SelectControl,
TextAreaControl,
diff --git a/superset/assets/javascripts/explore/main.css
b/superset/assets/javascripts/explore/main.css
index a6afe5eba9..44671c0842 100644
--- a/superset/assets/javascripts/explore/main.css
+++ b/superset/assets/javascripts/explore/main.css
@@ -121,3 +121,9 @@
padding: 0;
background-color: transparent;
}
+#popover-positioned-right {
+ width: 400px;
+}
+.popover {
+ max-width: 100%;
+}
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx
b/superset/assets/javascripts/explore/stores/controls.jsx
index fa92cd5c66..90ec18c46c 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -85,21 +85,6 @@ export const controls = {
description: t('The type of visualization to display'),
},
- metrics: {
- type: 'SelectControl',
- multi: true,
- label: t('Metrics'),
- validators: [v.nonEmpty],
- valueKey: 'metric_name',
- optionRenderer: m => <MetricOption metric={m} />,
- valueRenderer: m => <MetricOption metric={m} />,
- default: c => c.options && c.options.length > 0 ?
[c.options[0].metric_name] : null,
- mapStateToProps: state => ({
- options: (state.datasource) ? state.datasource.metrics : [],
- }),
- description: t('One or many metrics to display'),
- },
-
percent_metrics: {
type: 'SelectControl',
multi: true,
@@ -153,32 +138,35 @@ export const controls = {
},
metric: {
- type: 'SelectControl',
- label: t('Metric'),
- clearable: false,
- description: t('Choose the metric'),
+ type: 'MetricControl',
+ label: 'Metric',
+ description: 'Choose a metric',
validators: [v.nonEmpty],
- optionRenderer: m => <MetricOption metric={m} />,
- valueRenderer: m => <MetricOption metric={m} />,
- default: c => c.options && c.options.length > 0 ? c.options[0].metric_name
: null,
- valueKey: 'metric_name',
mapStateToProps: state => ({
- options: (state.datasource) ? state.datasource.metrics : [],
+ datasource: state.datasource,
+ }),
+ },
+
+ metrics: {
+ type: 'MetricListControl',
+ multi: true,
+ label: 'Metrics',
+ validators: [v.nonEmpty],
+ mapStateToProps: state => ({
+ datasource: state.datasource,
}),
+ default: [],
+ description: 'One or many metrics to display',
},
metric_2: {
- type: 'SelectControl',
- label: t('Right Axis Metric'),
- default: null,
+ type: 'MetricControl',
+ label: 'Right Axis Metric',
+ description: 'Choose a metric for right axis',
validators: [v.nonEmpty],
- clearable: true,
- description: t('Choose a metric for right axis'),
- valueKey: 'metric_name',
- optionRenderer: m => <MetricOption metric={m} />,
- valueRenderer: m => <MetricOption metric={m} />,
+ clearable: false,
mapStateToProps: state => ({
- options: (state.datasource) ? state.datasource.metrics : [],
+ datasource: state.datasource,
}),
},
@@ -378,12 +366,12 @@ export const controls = {
},
secondary_metric: {
- type: 'SelectControl',
- label: t('Color Metric'),
- default: null,
- description: t('A metric to use for color'),
+ type: 'MetricControl',
+ label: 'Color Metric',
+ description: 'A metric to use for color',
+ validators: [v.nonEmpty],
mapStateToProps: state => ({
- choices: (state.datasource) ? state.datasource.metrics_combo : [],
+ datasource: state.datasource,
}),
},
select_country: {
@@ -767,38 +755,27 @@ export const controls = {
description: t('Metric assigned to the [X] axis'),
default: null,
validators: [v.nonEmpty],
- optionRenderer: m => <MetricOption metric={m} />,
- valueRenderer: m => <MetricOption metric={m} />,
- valueKey: 'metric_name',
mapStateToProps: state => ({
- options: (state.datasource) ? state.datasource.metrics : [],
+ datasource: state.datasource,
}),
},
y: {
- type: 'SelectControl',
+ type: 'MetricControl',
label: t('Y Axis'),
- default: null,
- validators: [v.nonEmpty],
description: t('Metric assigned to the [Y] axis'),
- optionRenderer: m => <MetricOption metric={m} />,
- valueRenderer: m => <MetricOption metric={m} />,
- valueKey: 'metric_name',
+ validators: [v.nonEmpty],
mapStateToProps: state => ({
- options: (state.datasource) ? state.datasource.metrics : [],
+ datasource: state.datasource,
}),
},
size: {
- type: 'SelectControl',
+ type: 'MetricControl',
label: t('Bubble Size'),
- default: null,
validators: [v.nonEmpty],
- optionRenderer: m => <MetricOption metric={m} />,
- valueRenderer: m => <MetricOption metric={m} />,
- valueKey: 'metric_name',
mapStateToProps: state => ({
- options: (state.datasource) ? state.datasource.metrics : [],
+ datasource: state.datasource,
}),
},
diff --git a/superset/assets/stylesheets/superset.less
b/superset/assets/stylesheets/superset.less
index fedb124c65..aedf7e184c 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -391,3 +391,6 @@ g.annotation-container {
.stroke-primary {
stroke: @brand-primary;
}
+.pointer {
+ cursor: pointer;
+}
diff --git a/superset/connectors/base/models.py
b/superset/connectors/base/models.py
index bf66c60684..e50fe5cf5c 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -252,7 +252,7 @@ def expression(self):
def data(self):
attrs = (
'column_name', 'verbose_name', 'description', 'expression',
- 'filterable', 'groupby')
+ 'filterable', 'groupby', 'type')
return {s: getattr(self, s) for s in attrs}
diff --git a/superset/connectors/sqla/models.py
b/superset/connectors/sqla/models.py
index 17c3fd5464..86a817ca24 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -277,12 +277,11 @@ def get_col(self, col_name):
@property
def data(self):
d = super(SqlaTable, self).data
- if self.type == 'table':
- grains = self.database.grains() or []
- if grains:
- grains = [(g.name, g.name) for g in grains]
- d['granularity_sqla'] = utils.choicify(self.dttm_cols)
- d['time_grain_sqla'] = grains
+ grains = self.database.grains() or []
+ if grains:
+ grains = [(g.name, g.name) for g in grains]
+ d['granularity_sqla'] = utils.choicify(self.dttm_cols)
+ d['time_grain_sqla'] = grains
return d
def values_for_column(self, column_name, limit=10000):
@@ -349,6 +348,48 @@ def get_from_clause(self, template_processor=None,
db_engine_spec=None):
return TextAsFrom(sa.text(from_sql), []).alias('expr_qry')
return self.get_sqla_table()
+ def get_sqla_metric(self, metric_conf):
+ metric_name = None
+ if isinstance(metric_conf, basestring):
+ metric_name = metric_conf
+ metric_type = 'string_ref'
+ else:
+ metric_type = metric_conf.get('metricType')
+ if metric_type == 'metric':
+ metric_name = metric_conf.get('metricName')
+ if metric_name:
+ metrics_dict = {m.metric_name: m for m in self.metrics}
+ if metric_name not in metrics_dict:
+ raise Exception(
+ _("Metric '{}' is not valid".format(metric_name)))
+ return metrics_dict.get(metric_name).sqla_col
+
+ sqla_metric = None
+ expr = metric_conf.get('expr')
+ if metric_type == 'expr':
+ sqla_metric = literal_column(expr)
+ elif metric_type == 'col':
+ agg = metric_conf.get('agg')
+ col = metric_conf.get('col')
+ sqla_col = column(col)
+ if agg == 'COUNT_DISTINCT':
+ sqla_metric = sa.func.COUNT(sa.distinct(sqla_col))
+ elif agg == 'COUNT':
+ sqla_metric = sa.func.COUNT(sqla_col)
+ elif agg == 'SUM':
+ sqla_metric = sa.func.SUM(sqla_col)
+ elif agg == 'AVG':
+ sqla_metric = sa.func.AVG(sqla_col)
+ elif agg == 'MIN':
+ sqla_metric = sa.func.MIN(sqla_col)
+ elif agg == 'MAX':
+ sqla_metric = sa.func.MAX(sqla_col)
+ else:
+ sqla_metric = literal_column(expr)
+ if sqla_metric is not None:
+ sqla_metric = sqla_metric.label(metric_conf.get('label'))
+ return sqla_metric
+
def get_sqla_query( # sqla
self,
groupby, metrics,
@@ -388,7 +429,6 @@ def get_sqla_query( # sqla
time_groupby_inline = db_engine_spec.time_groupby_inline
cols = {col.column_name: col for col in self.columns}
- metrics_dict = {m.metric_name: m for m in self.metrics}
if not granularity and is_timeseries:
raise Exception(_(
@@ -396,15 +436,17 @@ def get_sqla_query( # sqla
"and is required by this type of chart"))
if not groupby and not metrics and not columns:
raise Exception(_("Empty query?"))
+ metrics_exprs = []
for m in metrics:
- if m not in metrics_dict:
- raise Exception(_("Metric '{}' is not valid".format(m)))
- metrics_exprs = [metrics_dict.get(m).sqla_col for m in metrics]
+ metrics_exprs.append(self.get_sqla_metric(m))
+
+ metrics_dict = {m.metric_name: m for m in self.metrics}
+
+ timeseries_limit_metric = metrics_dict.get(timeseries_limit_metric)
if metrics_exprs:
main_metric_expr = metrics_exprs[0]
else:
main_metric_expr = literal_column("COUNT(*)").label("ccount")
-
select_exprs = []
groupby_exprs = []
diff --git a/superset/views/core.py b/superset/views/core.py
index bd4d4e52ac..48ebf34880 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -13,6 +13,7 @@
import time
import traceback
from urllib import parse
+from past.builtins import basestring
import sqlalchemy as sqla
@@ -901,6 +902,32 @@ def clean_fulfilled_requests(session):
session.commit()
return redirect('/accessrequestsmodelview/list/')
+ def handle_legacy_metric(self, conf):
+ if isinstance(conf, basestring):
+ return {
+ 'metricType': 'metric',
+ 'metricName': conf,
+ 'label': conf,
+ }
+ return conf
+
+ def handle_legacy_metric_list(self, conf):
+ if isinstance(conf, (tuple, list)):
+ return [self.handle_legacy_metric(metric) for metric in conf]
+ return self.handle_legacy_metric(conf)
+
+ def handle_legacy_metrics(self, form_data):
+ """Metrics used to be string references to predef metrics
+
+ Convert legacy notation to new one"""
+ metric_keys = (
+ 'metrics', 'x', 'y', 'metric', 'size',
+ 'metric_2', 'secondary_metric')
+ for mk in metric_keys:
+ if mk in form_data:
+ form_data[mk] = self.handle_legacy_metric_list(form_data[mk])
+ return form_data
+
def get_form_data(self):
# get form data from url
if request.args.get("form_data"):
@@ -912,6 +939,7 @@ def get_form_data(self):
form_data = '{}'
d = json.loads(form_data)
+ d = self.handle_legacy_metrics(d)
if request.args.get("viz_type"):
# Converting old URLs
@@ -1444,7 +1472,7 @@ def testconn(self):
# the password-masked uri was passed
# use the URI associated with this database
uri = database.sqlalchemy_uri_decrypted
-
+
url = make_url(uri)
db_engine =
models.Database.get_db_engine_spec_for_backend(url.get_backend_name())
db_engine.patch()
diff --git a/superset/viz.py b/superset/viz.py
index 025e9c52b0..cbacf8e9eb 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -58,7 +58,16 @@ def __init__(self, datasource, form_data):
self.query = ""
self.token = self.form_data.get(
'token', 'token_' + uuid.uuid4().hex[:8])
- self.metrics = self.form_data.get('metrics') or []
+
+ metrics = self.form_data.get('metrics') or []
+ self.metrics = []
+ for m in metrics:
+ if isinstance(m, dict):
+ metric_label = m.get('label')
+ else:
+ metric_label = m
+ self.metrics.append(metric_label)
+
self.groupby = self.form_data.get('groupby') or []
self.annotation_layers = []
@@ -511,7 +520,7 @@ def get_data(self, df):
df = df.pivot_table(
index=self.form_data.get('groupby'),
columns=self.form_data.get('columns'),
- values=self.form_data.get('metrics'),
+ values=self.metrics,
aggfunc=self.form_data.get('pandas_aggfunc'),
margins=self.form_data.get('pivot_margins'),
)
@@ -576,7 +585,10 @@ def query_obj(self):
def get_data(self, df):
# Ordering the columns
- df = df[[self.form_data.get('series'), self.form_data.get('metric')]]
+ df = df[[
+ self.form_data.get('series'),
+ self.form_data.get('metric').get('label')
+ ]]
# Labeling the columns for uniform json schema
df.columns = ['text', 'size']
return df.to_dict(orient="records")
@@ -771,9 +783,9 @@ def query_obj(self):
]
if form_data.get('series'):
d['groupby'].append(form_data.get('series'))
- self.x_metric = form_data.get('x')
- self.y_metric = form_data.get('y')
- self.z_metric = form_data.get('size')
+ self.x_metric = form_data.get('x').get('label')
+ self.y_metric = form_data.get('y').get('label')
+ self.z_metric = form_data.get('size').get('label')
self.entity = form_data.get('entity')
self.series = form_data.get('series') or self.entity
d['row_limit'] = form_data.get('limit')
@@ -969,12 +981,12 @@ def process_data(self, df, aggregate=False):
df = df.pivot_table(
index=DTTM_ALIAS,
columns=fd.get('groupby'),
- values=fd.get('metrics'))
+ values=self.metrics)
else:
df = df.pivot_table(
index=DTTM_ALIAS,
columns=fd.get('groupby'),
- values=fd.get('metrics'),
+ values=self.metrics,
fill_value=0,
aggfunc=sum)
@@ -1090,8 +1102,8 @@ def to_series(self, df, classed=''):
series = df.to_dict('series')
chart_data = []
metrics = [
- self.form_data.get('metric'),
- self.form_data.get('metric_2')
+ self.form_data.get('metric').get('label'),
+ self.form_data.get('metric_2').get('label')
]
for i, m in enumerate(metrics):
ys = series[m]
@@ -1118,8 +1130,8 @@ def get_data(self, df):
if self.form_data.get("granularity") == "all":
raise Exception(_("Pick a time granularity for your time series"))
- metric = fd.get('metric')
- metric_2 = fd.get('metric_2')
+ metric = fd.get('metric').get('label')
+ metric_2 = fd.get('metric_2').get('label')
df = df.pivot_table(
index=DTTM_ALIAS,
values=[metric, metric_2])
@@ -1280,21 +1292,20 @@ def get_data(self, df):
# if m1 == m2 duplicate the metric column
cols = self.form_data.get('groupby')
- metric = self.form_data.get('metric')
- secondary_metric = self.form_data.get('secondary_metric')
- if metric == secondary_metric:
+ if self.metric == self.secondary_metric:
ndf = df
ndf.columns = [cols + ['m1', 'm2']]
else:
- cols += [
- self.form_data['metric'], self.form_data['secondary_metric']]
+ cols += [self.metric, self.secondary_metric]
ndf = df[cols]
return json.loads(ndf.to_json(orient="values")) # TODO fix this
nonsense
def query_obj(self):
qry = super(SunburstViz, self).query_obj()
- qry['metrics'] = [
- self.form_data['metric'], self.form_data['secondary_metric']]
+ self.metric = self.form_data.get('metric').get('label')
+ self.secondary_metric = \
+ self.form_data.get('secondary_metric').get('label')
+ qry['metrics'] = [self.metric, self.secondary_metric]
return qry
@@ -1412,16 +1423,14 @@ class CountryMapViz(BaseViz):
def query_obj(self):
qry = super(CountryMapViz, self).query_obj()
- qry['metrics'] = [
- self.form_data['metric']]
+ qry['metrics'] = [self.form_data['metric']]
qry['groupby'] = [self.form_data['entity']]
return qry
def get_data(self, df):
fd = self.form_data
cols = [fd.get('entity')]
- metric = fd.get('metric')
- cols += [metric]
+ cols += [fd.get('metric').get('label')]
ndf = df[cols]
df = ndf
df.columns = ['country_id', 'metric']
@@ -1449,8 +1458,8 @@ def get_data(self, df):
from superset.data import countries
fd = self.form_data
cols = [fd.get('entity')]
- metric = fd.get('metric')
- secondary_metric = fd.get('secondary_metric')
+ metric = fd.get('metric').get('label')
+ secondary_metric = fd.get('secondary_metric').get('label')
if metric == secondary_metric:
ndf = df[cols]
# df[metric] will be a DataFrame
@@ -1578,7 +1587,7 @@ def get_data(self, df):
fd = self.form_data
x = fd.get('all_columns_x')
y = fd.get('all_columns_y')
- v = fd.get('metric')
+ v = fd.get('metric').get('label')
if x == y:
df.columns = ['x', 'y', 'v']
else:
----------------------------------------------------------------
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:
[email protected]
With regards,
Apache Git Services