diff --git a/superset/assets/images/viz_thumbnails/event_flow.png b/superset/assets/images/viz_thumbnails/event_flow.png new file mode 100644 index 0000000000..45765295be Binary files /dev/null and b/superset/assets/images/viz_thumbnails/event_flow.png differ diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 794c9ee0cd..13306932ce 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -659,7 +659,7 @@ export const controls = { label: 'Entity', default: null, validators: [v.nonEmpty], - description: 'This define the element to be plotted on the chart', + description: 'This defines the element to be plotted on the chart', mapStateToProps: state => ({ choices: (state.datasource) ? state.datasource.gb_cols : [], }), @@ -1273,5 +1273,23 @@ export const controls = { hidden: true, description: 'The number of seconds before expiring the cache', }, + + order_by_entity: { + type: 'CheckboxControl', + label: 'Order by entity id', + description: 'Important! Select this if the table is not already sorted by entity id, ' + + 'else there is no guarantee that all events for each entity are returned.', + default: true, + }, + + min_leaf_node_event_count: { + type: 'SelectControl', + freeForm: false, + label: 'Minimum leaf node event count', + default: 1, + choices: formatSelectOptionsForRange(1, 10), + description: 'Leaf nodes that represent fewer than this number of events will be initially ' + + 'hidden in the visualization', + }, }; export default controls; diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index 68991e35ba..bdebf07642 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -1,5 +1,4 @@ import { D3_TIME_FORMAT_OPTIONS } from './controls'; - import * as v from '../validators'; export const sections = { @@ -890,6 +889,51 @@ const visTypes = { }, }, }, + + event_flow: { + label: 'Event flow', + requiresTime: true, + controlPanelSections: [ + { + label: 'Event definition', + controlSetRows: [ + ['entity'], + ['all_columns_x'], + ['row_limit'], + ['order_by_entity'], + ['min_leaf_node_event_count'], + ], + }, + { + label: 'Additional meta data', + controlSetRows: [ + ['all_columns'], + ], + }, + ], + controlOverrides: { + entity: { + label: 'Column containing entity ids', + description: 'e.g., a "user id" column', + }, + all_columns_x: { + label: 'Column containing event names', + validators: [v.nonEmpty], + default: control => ( + control.choices && control.choices.length > 0 ? + control.choices[0][0] : null + ), + }, + row_limit: { + label: 'Event count limit', + description: 'The maximum number of events to return, equivalent to number of rows', + }, + all_columns: { + label: 'Meta data', + description: 'Select any columns for meta data inspection', + }, + }, + }, }; export default visTypes; diff --git a/superset/assets/package.json b/superset/assets/package.json index 5d5022def1..ff4c161ad6 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -38,6 +38,7 @@ }, "homepage": "https://github.com/airbnb/superset#readme", "dependencies": { + "@data-ui/event-flow": "0.0.4", "babel-register": "^6.24.1", "bootstrap": "^3.3.6", "brace": "^0.10.0", diff --git a/superset/assets/visualizations/EventFlow.jsx b/superset/assets/visualizations/EventFlow.jsx new file mode 100644 index 0000000000..110f4a7648 --- /dev/null +++ b/superset/assets/visualizations/EventFlow.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { + App, + withParentSize, + cleanEvents, + TS, + EVENT_NAME, + ENTITY_ID, +} from '@data-ui/event-flow'; + +/* + * This function takes the slice object and json payload as input and renders a + * responsive component using the json data. + */ +function renderEventFlow(slice, json) { + const container = document.querySelector(slice.selector); + const hasData = json.data && json.data.length > 0; + + // the slice container overflows ~80px in explorer, so we have to correct for this + const isExplorer = (/explore/).test(window.location.pathname); + + const ResponsiveVis = withParentSize(({ + parentWidth, + parentHeight, + ...rest + }) => ( + + )); + + // render the component if we have data, otherwise render a no-data message + let Component; + if (hasData) { + const userKey = json.form_data.entity; + const eventNameKey = json.form_data.all_columns_x; + + // map from the Superset form fields to 's expected data keys + const accessorFunctions = { + [TS]: datum => new Date(datum.__timestamp), // eslint-disable-line no-underscore-dangle + [EVENT_NAME]: datum => datum[eventNameKey], + [ENTITY_ID]: datum => String(datum[userKey]), + }; + + const dirtyData = json.data; + const cleanData = cleanEvents(dirtyData, accessorFunctions); + const minEventCount = slice.formData.min_leaf_node_event_count; + + Component = ; + } else { + Component =
Sorry, there appears to be no data
; + } + + ReactDOM.render(Component, container); +} + +module.exports = renderEventFlow; diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index 68abddf5e3..a02f508c33 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -32,5 +32,6 @@ const vizMap = { word_cloud: require('./word_cloud.js'), world_map: require('./world_map.js'), dual_line: require('./nvd3_vis.js'), + event_flow: require('./EventFlow.jsx'), }; export default vizMap; diff --git a/superset/assets/visualizations/treemap.css b/superset/assets/visualizations/treemap.css index c385780c82..2fdcdc76d7 100644 --- a/superset/assets/visualizations/treemap.css +++ b/superset/assets/visualizations/treemap.css @@ -1,43 +1,43 @@ -text { +.treemap text { pointer-events: none; } -.grandparent text { +.treemap .grandparent text { font-weight: bold; } -rect { +.treemap rect { fill: none; stroke: #fff; } -rect.parent, -.grandparent rect { +.treemap rect.parent, +.treemap .grandparent rect { stroke-width: 2px; } -rect.parent { +.treemap rect.parent { pointer-events: none; } -.grandparent rect { +.treemap .grandparent rect { fill: #eee; } -.grandparent:hover rect { +.treemap .grandparent:hover rect { fill: #aaa; } -.children rect.parent, -.grandparent rect { +.treemap .children rect.parent, +.treemap .grandparent rect { cursor: pointer; } -.children rect.parent { +.treemap .children rect.parent { fill: #bbb; fill-opacity: .5; } -.children:hover rect.child { +.treemap .children:hover rect.child { fill: #bbb; } diff --git a/superset/assets/visualizations/treemap.js b/superset/assets/visualizations/treemap.js index 1e025935e6..f728985dba 100644 --- a/superset/assets/visualizations/treemap.js +++ b/superset/assets/visualizations/treemap.js @@ -34,6 +34,7 @@ function treemap(slice, payload) { .round(false); const svg = div.append('svg') + .attr('class', 'treemap') .attr('width', eltWidth) .attr('height', eltHeight); diff --git a/superset/viz.py b/superset/viz.py index a8cf3bfe5b..1ae42b369a 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -1587,6 +1587,35 @@ class MapboxViz(BaseViz): "color": fd.get("mapbox_color"), } +class EventFlowViz(BaseViz): + """A visualization to explore patterns in event sequences""" + + viz_type = "event_flow" + verbose_name = _("Event flow") + credits = 'from @data-ui' + is_timeseries = True + + def query_obj(self): + query = super(EventFlowViz, self).query_obj() + form_data = self.form_data + + event_key = form_data.get('all_columns_x') + entity_key = form_data.get('entity') + meta_keys = [ + col for col in form_data.get('all_columns') if col != event_key and col != entity_key + ] + + query['columns'] = [event_key, entity_key] + meta_keys + + if form_data['order_by_entity']: + query['orderby'] = [(entity_key, True)] + + return query + + def get_data(self, df): + return df.to_dict(orient="records") + + viz_types_list = [ TableViz, @@ -1621,6 +1650,7 @@ viz_types_list = [ MapboxViz, HistogramViz, SeparatorViz, + EventFlowViz, ] viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list