mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
Add event-flow visualization (#3102)
* [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
This commit is contained in:
parent
a141695b2b
commit
40d9e15126
BIN
superset/assets/images/viz_thumbnails/event_flow.png
Normal file
BIN
superset/assets/images/viz_thumbnails/event_flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
@ -659,7 +659,7 @@ export const controls = {
|
|||||||
label: 'Entity',
|
label: 'Entity',
|
||||||
default: null,
|
default: null,
|
||||||
validators: [v.nonEmpty],
|
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 => ({
|
mapStateToProps: state => ({
|
||||||
choices: (state.datasource) ? state.datasource.gb_cols : [],
|
choices: (state.datasource) ? state.datasource.gb_cols : [],
|
||||||
}),
|
}),
|
||||||
@ -1273,5 +1273,23 @@ export const controls = {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
description: 'The number of seconds before expiring the cache',
|
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;
|
export default controls;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { D3_TIME_FORMAT_OPTIONS } from './controls';
|
import { D3_TIME_FORMAT_OPTIONS } from './controls';
|
||||||
|
|
||||||
import * as v from '../validators';
|
import * as v from '../validators';
|
||||||
|
|
||||||
export const sections = {
|
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;
|
export default visTypes;
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/airbnb/superset#readme",
|
"homepage": "https://github.com/airbnb/superset#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@data-ui/event-flow": "0.0.4",
|
||||||
"babel-register": "^6.24.1",
|
"babel-register": "^6.24.1",
|
||||||
"bootstrap": "^3.3.6",
|
"bootstrap": "^3.3.6",
|
||||||
"brace": "^0.10.0",
|
"brace": "^0.10.0",
|
||||||
|
61
superset/assets/visualizations/EventFlow.jsx
Normal file
61
superset/assets/visualizations/EventFlow.jsx
Normal file
@ -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;
|
@ -32,5 +32,6 @@ const vizMap = {
|
|||||||
word_cloud: require('./word_cloud.js'),
|
word_cloud: require('./word_cloud.js'),
|
||||||
world_map: require('./world_map.js'),
|
world_map: require('./world_map.js'),
|
||||||
dual_line: require('./nvd3_vis.js'),
|
dual_line: require('./nvd3_vis.js'),
|
||||||
|
event_flow: require('./EventFlow.jsx'),
|
||||||
};
|
};
|
||||||
export default vizMap;
|
export default vizMap;
|
||||||
|
@ -1,43 +1,43 @@
|
|||||||
text {
|
.treemap text {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grandparent text {
|
.treemap .grandparent text {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
rect {
|
.treemap rect {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: #fff;
|
stroke: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
rect.parent,
|
.treemap rect.parent,
|
||||||
.grandparent rect {
|
.treemap .grandparent rect {
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
rect.parent {
|
.treemap rect.parent {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grandparent rect {
|
.treemap .grandparent rect {
|
||||||
fill: #eee;
|
fill: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grandparent:hover rect {
|
.treemap .grandparent:hover rect {
|
||||||
fill: #aaa;
|
fill: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.children rect.parent,
|
.treemap .children rect.parent,
|
||||||
.grandparent rect {
|
.treemap .grandparent rect {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.children rect.parent {
|
.treemap .children rect.parent {
|
||||||
fill: #bbb;
|
fill: #bbb;
|
||||||
fill-opacity: .5;
|
fill-opacity: .5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.children:hover rect.child {
|
.treemap .children:hover rect.child {
|
||||||
fill: #bbb;
|
fill: #bbb;
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ function treemap(slice, payload) {
|
|||||||
.round(false);
|
.round(false);
|
||||||
|
|
||||||
const svg = div.append('svg')
|
const svg = div.append('svg')
|
||||||
|
.attr('class', 'treemap')
|
||||||
.attr('width', eltWidth)
|
.attr('width', eltWidth)
|
||||||
.attr('height', eltHeight);
|
.attr('height', eltHeight);
|
||||||
|
|
||||||
|
@ -1587,6 +1587,35 @@ class MapboxViz(BaseViz):
|
|||||||
"color": fd.get("mapbox_color"),
|
"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 = [
|
viz_types_list = [
|
||||||
TableViz,
|
TableViz,
|
||||||
@ -1621,6 +1650,7 @@ viz_types_list = [
|
|||||||
MapboxViz,
|
MapboxViz,
|
||||||
HistogramViz,
|
HistogramViz,
|
||||||
SeparatorViz,
|
SeparatorViz,
|
||||||
|
EventFlowViz,
|
||||||
]
|
]
|
||||||
|
|
||||||
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list
|
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list
|
||||||
|
Loading…
Reference in New Issue
Block a user