Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new visualization for custom X-Y axes #4185

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,14 @@ export const controls = {
description: t('Show data points as circle markers on the lines'),
},

hide_lines: {
type: 'CheckboxControl',
label: t('Hide Lines'),
renderTrigger: true,
default: false,
description: t('Hide lines between data points'),
},

show_bar_value: {
type: 'CheckboxControl',
label: t('Bar Values'),
Expand Down Expand Up @@ -644,6 +652,33 @@ export const controls = {
}),
},

columns_and_metrics_x: {
type: 'SelectControl',
label: t('X Axis'),
default: null,
description: t('Column or metric to display'),
mapStateToProps: (state) => {
let choices = [];
if (state.controls && state.datasource) {
const gbSet = new Set((state.controls.groupby || {}).value || []);
const gbSelectables = state.datasource.all_cols.filter(elem => gbSet.has(elem[0]));

const mSet = new Set((state.controls.metrics || {}).value || []);
const mSelectables = state.datasource.metrics_combo.filter(elem => mSet.has(elem[0]));

const cSet = new Set((state.controls.all_columns || {}).value || []);
const cSelectables = state.datasource.all_cols.filter(elem => cSet.has(elem[0]));
choices = [...gbSelectables, ...mSelectables, ...cSelectables];
} else {
const formValue = (state.form_data || {}).columns_and_metrics_x || [];
choices = [[formValue, formValue]];
}
return {
choices,
};
},
},

all_columns_y: {
type: 'SelectControl',
label: 'Y',
Expand All @@ -654,6 +689,59 @@ export const controls = {
}),
},

columns_and_metrics_y: {
type: 'SelectControl',
multi: true,
label: t('Y Axis'),
default: [],
description: t('Columns or metrics to display'),
mapStateToProps: (state) => {
let choices = [];
if (state.controls && state.datasource) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like the exact same function as in columns_and_metrics_x, let's make this DRY

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of being able to plot non grouped-by measures. In some cases I feel that this is a missing option (e.g., box plot).

The non-grouped xy-plot would be even more useful if there were an option to just plot markers without any lines. In other words, a plain scatter plot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I must have mussed that the scatter plot is implemented here, already.
So, I would highly appreciate if the non-grouped code could just stay...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to understand your NOT GROUPED BY use case, is it for optimization purposes in cases where the data is already aggregated? Note that you can still make columns that are used in metrics "groupbable" and group by them. Also note that the cost of grouping something that's already grouped should be fairly cheap.

I think having the two interfaces in one makes things confusing. For the case of the Table viz it was very necessary to allow NOT GROUPED BY, and I'm trying to understand what use case requires that in this context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We, for example, need it to show raw values of our sensor products. So each data point represents one serial number.
You might say that grouping by SN should just do the job. However, grouping by, e.g., the job number, is then more useful.

In other cases there exists real xy measurement data for a product, like e.g. a transfer characteristic, and this again is not useful to be aggregated but has to be investigated for each SN separately.

Limiting the amount of data may of course be challenging then, but this is the user's responsibility in my eyes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mistercrunch The 'NOT GROUPED BY' use case is the same one why it is available on the table visualization. Fundamentally, this general X-Y chart lets you select any column of a table-visualization as the X axis or the Y axis. In short, if you have two numeric columns in a table you should be able to display the data as a line chart regardless of what the 'query' looked like.
The reason for the 'NOT GROUPED BY' section to be included in the table viz is to be able to visualize (list) non-aggregated data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 not grouped by

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about as an alternative to the GROUPED BY / NOT GROUPED BY, if we allowed for a mix of metrics and dimensions in the dropdowns? If no metrics are selected, then we don't aggregate. Perhaps that's the way that the table view should work as well.

We're planning a fair amount of work to have a more column-centric approach in the explore view and supporting backwards compatibility on this new chart type may make that harder.

const gbSet = new Set((state.controls.groupby || {}).value || []);
const gbSelectables = state.datasource.all_cols.filter(elem => gbSet.has(elem[0]));

const mSet = new Set((state.controls.metrics || {}).value || []);
const mSelectables = state.datasource.metrics_combo.filter(elem => mSet.has(elem[0]));

const cSet = new Set((state.controls.all_columns || {}).value || []);
const cSelectables = state.datasource.all_cols.filter(elem => cSet.has(elem[0]));
choices = [...gbSelectables, ...mSelectables, ...cSelectables];
} else {
const formValues = (state.form_data || {}).columns_and_metrics_y || [];
choices = formValues.map(fv => [fv, fv]);
}
return {
choices,
};
},
},

slice_by: {
type: 'SelectControl',
multi: true,
label: t('Slice By'),
default: [],
description: t('Columns to slice by'),
mapStateToProps: (state) => {
let choices = [];
if (state.controls && state.datasource) {
const gbSet = new Set((state.controls.groupby || {}).value || []);
const gbSelectables = state.datasource.all_cols.filter(elem => gbSet.has(elem[0]));

const cSet = new Set((state.controls.all_columns || {}).value || []);
const cSelectables = state.datasource.all_cols.filter(elem => cSet.has(elem[0]));
choices = [...gbSelectables, ...cSelectables];
} else {
const formValues = (state.form_data || {}).slice_by || [];
choices = formValues.map(fv => [fv, fv]);
}
return {
choices,
};
},
},

druid_time_origin: {
type: 'SelectControl',
freeForm: true,
Expand Down
73 changes: 73 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,79 @@ export const visTypes = {
],
},

line_xy: {
label: t('XY - Line Chart'),
showOnExplore: true,
controlPanelSections: [
{
label: t('GROUP BY'),
description: t('Use this section if you want a query that aggregates'),
controlSetRows: [
['groupby'],
['metrics'],
],
},
{
label: t('NOT GROUPED BY'),
description: t('Use this section if you want to query atomic rows'),
controlSetRows: [
['all_columns'],
],
},
{
label: t('Row Limit'),
controlSetRows: [
['row_limit'],
],
},
{
label: t('Axis Options'),
description: t('Select which columns or metrics from the query to plot'),
expanded: true,
controlSetRows: [
['columns_and_metrics_x'],
['columns_and_metrics_y'],
['slice_by'],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
['show_brush', 'show_legend'],
['hide_lines', 'show_markers'],
['line_interpolation'],
],
},
{
label: t('X Axis'),
expanded: true,
controlSetRows: [
['x_axis_label', 'bottom_margin'],
['x_axis_showminmax', 'x_axis_format'],
],
},
{
label: t('Y Axis'),
expanded: true,
controlSetRows: [
['y_axis_label', 'left_margin'],
['y_axis_showminmax', 'y_log_scale'],
['y_axis_format', 'y_axis_bounds'],
],
},
],
controlOverrides: {
metrics: {
validators: [],
},
time_grain_sqla: {
default: null,
},
},
},

line: {
label: t('Time Series - Line Chart'),
showOnExplore: true,
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/visualizations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const VIZ_TYPES = {
horizon: 'horizon',
iframe: 'iframe',
line: 'line',
line_xy: 'line_xy',
mapbox: 'mapbox',
markup: 'markup',
para: 'para',
Expand Down Expand Up @@ -70,6 +71,7 @@ const vizMap = {
[VIZ_TYPES.horizon]: require('./horizon.js'),
[VIZ_TYPES.iframe]: require('./iframe.js'),
[VIZ_TYPES.line]: require('./nvd3_vis.js'),
[VIZ_TYPES.line_xy]: require('./nvd3_vis.js'),
[VIZ_TYPES.time_pivot]: require('./nvd3_vis.js'),
[VIZ_TYPES.mapbox]: require('./mapbox.jsx'),
[VIZ_TYPES.markup]: require('./markup.js'),
Expand Down
19 changes: 19 additions & 0 deletions superset/assets/visualizations/nvd3_vis.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ function nvd3Vis(slice, payload) {
}
let height = slice.height();
switch (vizType) {
case 'line_xy':
fd.rich_tooltip = false;
if (fd.show_brush) {
chart = nv.models.lineWithFocusChart();
chart.focus.xScale(d3.scale.linear());
chart.x2Axis.staggerLabels(false);
} else {
chart = nv.models.lineChart();
}
chart.xScale(d3.scale.linear());
chart.interpolate(fd.line_interpolation);
chart.xAxis.staggerLabels(false);
break;

case 'line':
if (
fd.show_brush === true ||
Expand Down Expand Up @@ -469,6 +483,11 @@ function nvd3Vis(slice, payload) {
.style('fill-opacity', 1);
}

if (fd.hide_lines) {
svg.selectAll('.nv-line')
.style('stroke-width', 0);
}

if (chart.yAxis !== undefined || chart.yAxis2 !== undefined) {
// Hack to adjust y axis left margin to accommodate long numbers
const containerWidth = slice.container.width();
Expand Down
75 changes: 75 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,81 @@ def get_data(self, df):
}


class NVD3LineXYViz(NVD3Viz):

"""A rich line chart component with customizable X & Y axes"""

viz_type = 'line_xy'
verbose_name = _('XY Line Chart')
is_timeseries = False

def query_obj(self):
d = super(NVD3LineXYViz, self).query_obj()
fd = self.form_data

if fd.get('all_columns') and (fd.get('groupby') or fd.get('metrics')):
raise Exception(_(
'Choose either fields to [Group By] and [Metrics] or '
'[Columns], not both'))

x_field = fd.get('columns_and_metrics_x')
y_fields = fd.get('columns_and_metrics_y')

if x_field and y_fields:
if fd.get('all_columns'):
d['columns'] = fd.get('all_columns')
d['orderby'] = [(x_field, True)]
else:
raise Exception(_('X and Y columns must be defined'))
return d

def get_data(self, df):
fd = self.form_data

x_field = fd.get('columns_and_metrics_x')
y_fields = fd.get('columns_and_metrics_y')
sliceby_keys = fd.get('slice_by')

if self.datasource.type == 'druid':
df = df.sort_values(x_field)

result_list = []

if len(sliceby_keys) > 0:
df = df.groupby(sliceby_keys)

for y_field in y_fields:
if len(sliceby_keys) > 0:
for key, rows in df:
key_list = [key, y_field]
if len(sliceby_keys) > 1:
key_list = key.append(y_field)
vals_as_strings = map(lambda val: str(val), key_list)
key_string = ', '.join(vals_as_strings)
series = self.convert_rows(x_field, y_field, key_string, rows)
result_list.append(series)
else:
series = self.convert_rows(x_field, y_field, y_field, df)
result_list.append(series)
return result_list

@staticmethod
def convert_rows(x_field, y_field, key, rows):
values = []
for row in rows.to_dict(orient='records'):
value = {
'x': row[x_field],
'y': row[y_field],
}
values.append(value)

series = {
'key': key,
'values': values,
}
return series


class NVD3TimeSeriesViz(NVD3Viz):

"""A rich line chart component with tons of options"""
Expand Down