Skip to content

Commit

Permalink
Add event-flow visualization (#3102)
Browse files Browse the repository at this point in the history
* [event-flow] add event flow visualizaton type from @data-ui/event-flow.

* [event-flow] update vis thumbnail

* [event-flow] update row limit label, remove duplicate chart controls

* [dependencies] add @data-ui/event-flow 0.0.2

* [linting] fix multiple imports

* [deps] bump mapbox-gl and react-map-gl to fix build

* [event-flow] bump to 0.0.3 for es2015 + stage-0 babel presets

* [deps] revert mapbox version bumps

* [event-flow] update png, bump to newest version, address reviewer comments, add min event count form.

* [event-flow] pin version

* [event-flow][spec] add test for coveralls

* [event-flow] revert spec
  • Loading branch information
williaster authored and mistercrunch committed Jul 21, 2017
1 parent a141695 commit 40d9e15
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 14 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 : [],
}),
Expand Down Expand Up @@ -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;
46 changes: 45 additions & 1 deletion superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { D3_TIME_FORMAT_OPTIONS } from './controls';

import * as v from '../validators';

export const sections = {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions superset/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions superset/assets/visualizations/EventFlow.jsx
Original file line number Diff line number Diff line change
@@ -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 <EventFlow /> 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
}) => (
<App
width={parentWidth}
height={parentHeight - (isExplorer ? 80 : 0)}
{...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 <EventFlow />'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 = <ResponsiveVis data={cleanData} initialMinEventCount={minEventCount} />;
} else {
Component = <div>Sorry, there appears to be no data</div>;
}

ReactDOM.render(Component, container);
}

module.exports = renderEventFlow;
1 change: 1 addition & 0 deletions superset/assets/visualizations/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
24 changes: 12 additions & 12 deletions superset/assets/visualizations/treemap.css
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions superset/assets/visualizations/treemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function treemap(slice, payload) {
.round(false);

const svg = div.append('svg')
.attr('class', 'treemap')
.attr('width', eltWidth)
.attr('height', eltHeight);

Expand Down
30 changes: 30 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,35 @@ def get_data(self, df):
"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 <a href="https://github.com/williaster/data-ui">@data-ui</a>'
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,
Expand Down Expand Up @@ -1621,6 +1650,7 @@ def get_data(self, df):
MapboxViz,
HistogramViz,
SeparatorViz,
EventFlowViz,
]

viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list
Expand Down

0 comments on commit 40d9e15

Please sign in to comment.