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 event-flow visualization #3102

Merged
merged 12 commits into from Jul 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
@@ -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
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
@@ -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';
Copy link
Member

Choose a reason for hiding this comment

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

what does '@%' do? where does the event-flow code live?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's just a name-spaced package on npm, under the "@data-ui" user but mainly so all the @data-ui packages are in the same repo.

it will live at https://github.com/williaster/data-ui/packages/event-flow but I haven't quite merged the chart into the master @data-ui repo, adding one more feature. this is the (massive 🙈) PR


/*
* 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);
Copy link
Member

Choose a reason for hiding this comment

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

not sure if I understand why, but I'm wondering if using slice.width() and slice.height() would address the 80px issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried this to begin with but it doesn't seem to work/couldn't come up with any fix for the css. The panel dom looks like:

<div class="panel">
  <div class="panel-heading">...</div>
  <div class="panel-body">...</div>
</div>

panel-body is what the slice is in and it seems to have the same height as the parent panel. that means it overflows by the height of panel-heading :/


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
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
@@ -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
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
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