New "Time Series - Table" visualization (#3543)

* [WiP] adding a new "Time Series - Table" viz

* Adding drag-n-drop to collection

* Using keys in arrays

* tests
This commit is contained in:
Maxime Beauchemin 2017-10-04 10:17:33 -07:00 committed by GitHub
parent 645de384e3
commit bb0f69d074
20 changed files with 710 additions and 90 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -5,7 +5,7 @@ import { slugify } from '../modules/utils';
const propTypes = {
label: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
tooltip: PropTypes.string,
icon: PropTypes.string,
className: PropTypes.string,
onClick: PropTypes.func,
@ -17,11 +17,21 @@ const defaultProps = {
className: 'text-muted',
placement: 'right',
};
const tooltipStyle = { wordWrap: 'break-word' };
export default function InfoTooltipWithTrigger({
label, tooltip, icon, className, onClick, placement, bsStyle }) {
const iconClass = `fa fa-${icon} ${className} ${bsStyle ? 'text-' + bsStyle : ''}`;
const tooltipStyle = { wordWrap: 'break-word' };
const iconEl = (
<i
className={iconClass}
onClick={onClick}
style={{ cursor: onClick ? 'pointer' : null }}
/>
);
if (!tooltip) {
return iconEl;
}
return (
<OverlayTrigger
placement={placement}
@ -31,11 +41,7 @@ export default function InfoTooltipWithTrigger({
</Tooltip>
}
>
<i
className={iconClass}
onClick={onClick}
style={{ cursor: onClick ? 'pointer' : null }}
/>
{iconEl}
</OverlayTrigger>
);
}

View File

@ -5,9 +5,13 @@ import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
const propTypes = {
metric: PropTypes.object.isRequired,
showFormula: PropTypes.bool,
};
const defaultProps = {
showFormula: true,
};
export default function MetricOption({ metric }) {
export default function MetricOption({ metric, showFormula }) {
return (
<div>
<span className="m-r-5 option-label">
@ -21,12 +25,14 @@ export default function MetricOption({ metric }) {
label={`descr-${metric.metric_name}`}
/>
}
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="question-circle-o"
tooltip={metric.expression}
label={`expr-${metric.metric_name}`}
/>
{showFormula &&
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="question-circle-o"
tooltip={metric.expression}
label={`expr-${metric.metric_name}`}
/>
}
{metric.warning_text &&
<InfoTooltipWithTrigger
className="m-r-5 text-danger"
@ -38,3 +44,4 @@ export default function MetricOption({ metric }) {
</div>);
}
MetricOption.propTypes = propTypes;
MetricOption.defaultProps = defaultProps;

View File

@ -1,33 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import BoundsControl from './controls/BoundsControl';
import CheckboxControl from './controls/CheckboxControl';
import ColorSchemeControl from './controls/ColorSchemeControl';
import DatasourceControl from './controls/DatasourceControl';
import DateFilterControl from './controls/DateFilterControl';
import FilterControl from './controls/FilterControl';
import HiddenControl from './controls/HiddenControl';
import SelectAsyncControl from './controls/SelectAsyncControl';
import SelectControl from './controls/SelectControl';
import TextAreaControl from './controls/TextAreaControl';
import TextControl from './controls/TextControl';
import VizTypeControl from './controls/VizTypeControl';
import controlMap from './controls';
const controlMap = {
BoundsControl,
CheckboxControl,
DatasourceControl,
DateFilterControl,
FilterControl,
HiddenControl,
SelectControl,
TextAreaControl,
TextControl,
VizTypeControl,
ColorSchemeControl,
SelectAsyncControl,
};
const controlTypes = Object.keys(controlMap);
const propTypes = {

View File

@ -5,16 +5,11 @@ import ControlHeader from '../ControlHeader';
import { t } from '../../../locales';
const propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
description: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.array,
};
const defaultProps = {
label: null,
description: null,
onChange: () => {},
value: [null, null],
};

View File

@ -0,0 +1,119 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import shortid from 'shortid';
import {
SortableContainer, SortableHandle, SortableElement, arrayMove,
} from 'react-sortable-hoc';
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
import ControlHeader from '../ControlHeader';
const propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
description: PropTypes.string,
placeholder: PropTypes.string,
addTooltip: PropTypes.string,
itemGenerator: PropTypes.func,
keyAccessor: PropTypes.func,
onChange: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.array,
]),
isFloat: PropTypes.bool,
isInt: PropTypes.bool,
control: PropTypes.func,
};
const defaultProps = {
label: null,
description: null,
onChange: () => {},
placeholder: 'Empty collection',
itemGenerator: () => ({ key: shortid.generate() }),
keyAccessor: o => o.key,
value: [],
addTooltip: 'Add an item',
};
const SortableListGroupItem = SortableElement(ListGroupItem);
const SortableListGroup = SortableContainer(ListGroup);
const SortableDragger = SortableHandle(() => (
<i className="fa fa-bars text-primary" style={{ cursor: 'ns-resize' }} />));
export default class CollectionControl extends React.Component {
constructor(props) {
super(props);
this.onAdd = this.onAdd.bind(this);
}
onChange(i, value) {
Object.assign(this.props.value[i], value);
this.props.onChange(this.props.value);
}
onAdd() {
this.props.onChange(this.props.value.concat([this.props.itemGenerator()]));
}
onSortEnd({ oldIndex, newIndex }) {
this.props.onChange(arrayMove(this.props.value, oldIndex, newIndex));
}
removeItem(i) {
this.props.onChange(this.props.value.filter((o, ix) => i !== ix));
}
renderList() {
if (this.props.value.length === 0) {
return <div className="text-muted">{this.props.placeholder}</div>;
}
return (
<SortableListGroup
useDragHandle
lockAxis="y"
onSortEnd={this.onSortEnd.bind(this)}
>
{this.props.value.map((o, i) => (
<SortableListGroupItem
className="clearfix"
key={this.props.keyAccessor(o)}
index={i}
>
<div className="pull-left m-r-5">
<SortableDragger />
</div>
<div className="pull-left">
<this.props.control
{...o}
onChange={this.onChange.bind(this, i)}
/>
</div>
<div className="pull-right">
<InfoTooltipWithTrigger
icon="times"
label="remove-item"
tooltip="remove item"
bsStyle="primary"
onClick={this.removeItem.bind(this, i)}
/>
</div>
</SortableListGroupItem>))}
</SortableListGroup>
);
}
render() {
return (
<div>
<ControlHeader {...this.props} />
{this.renderList()}
<InfoTooltipWithTrigger
icon="plus-circle"
label="add-item"
tooltip={this.props.addTooltip}
bsStyle="primary"
className="fa-lg"
onClick={this.onAdd}
/>
</div>
);
}
}
CollectionControl.propTypes = propTypes;
CollectionControl.defaultProps = defaultProps;

View File

@ -0,0 +1,223 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Row, Col, FormControl, OverlayTrigger, Popover,
} from 'react-bootstrap';
import Select from 'react-select';
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
import BoundsControl from './BoundsControl';
const propTypes = {
onChange: PropTypes.func,
};
const defaultProps = {
onChange: () => {},
};
const comparisonTypeOptions = [
{ value: 'value', label: 'Actual value' },
{ value: 'diff', label: 'Difference' },
{ value: 'perc', label: 'Percentage' },
{ value: 'perc_change', label: 'Percentage Change' },
];
const colTypeOptions = [
{ value: 'time', label: 'Time Comparison' },
{ value: 'contrib', label: 'Contribution' },
{ value: 'spark', label: 'Sparkline' },
{ value: 'avg', label: 'Period Average' },
];
export default class TimeSeriesColumnControl extends React.Component {
constructor(props) {
super(props);
const state = Object.assign({}, props);
delete state.onChange;
this.state = state;
this.onChange = this.onChange.bind(this);
}
onChange() {
this.props.onChange(this.state);
}
onSelectChange(attr, opt) {
this.setState({ [attr]: opt.value }, this.onChange);
}
onTextInputChange(attr, event) {
this.setState({ [attr]: event.target.value }, this.onChange);
}
onBoundsChange(bounds) {
this.setState({ bounds }, this.onChange);
}
setType() {
}
textSummary() {
return `${this.state.label}`;
}
edit() {
}
formRow(label, tooltip, ttLabel, control) {
return (
<Row style={{ marginTop: '5px' }}>
<Col md={5}>
{label}{' '}
<InfoTooltipWithTrigger
placement="top"
tooltip={tooltip}
label={ttLabel}
/>
</Col>
<Col md={7}>{control}</Col>
</Row>
);
}
renderPopover() {
return (
<Popover id="ts-col-popo" title="Column Configuration">
<div style={{ width: '280px' }}>
{this.formRow(
'Label',
'The column header label',
'time-lag',
<FormControl
value={this.state.label}
onChange={this.onTextInputChange.bind(this, 'label')}
bsSize="small"
placeholder="Label"
/>,
)}
{this.formRow(
'Tooltip',
'Column header tooltip',
'col-tooltip',
<FormControl
value={this.state.tooltip}
onChange={this.onTextInputChange.bind(this, 'tooltip')}
bsSize="small"
placeholder="Tooltip"
/>,
)}
{this.formRow(
'Type',
'Type of comparison, value difference or percentage',
'col-type',
<Select
value={this.state.colType}
clearable={false}
onChange={this.onSelectChange.bind(this, 'colType')}
options={colTypeOptions}
/>,
)}
<hr />
{this.state.colType === 'spark' && this.formRow(
'Width',
'Width of the sparkline',
'spark-width',
<FormControl
value={this.state.width}
onChange={this.onTextInputChange.bind(this, 'width')}
bsSize="small"
placeholder="Width"
/>,
)}
{this.state.colType === 'spark' && this.formRow(
'Height',
'Height of the sparkline',
'spark-width',
<FormControl
value={this.state.height}
onChange={this.onTextInputChange.bind(this, 'height')}
bsSize="small"
placeholder="height"
/>,
)}
{['time', 'avg'].indexOf(this.state.colType) >= 0 && this.formRow(
'Time Lag',
'Number of periods to compare against',
'time-lag',
<FormControl
value={this.state.timeLag}
onChange={this.onTextInputChange.bind(this, 'timeLag')}
bsSize="small"
placeholder="Time Lag"
/>,
)}
{['spark'].indexOf(this.state.colType) >= 0 && this.formRow(
'Time Ratio',
'Number of periods to ratio against',
'time-ratio',
<FormControl
value={this.state.timeRatio}
onChange={this.onTextInputChange.bind(this, 'timeRatio')}
bsSize="small"
placeholder="Time Lag"
/>,
)}
{this.state.colType === 'time' && this.formRow(
'Type',
'Type of comparison, value difference or percentage',
'comp-type',
<Select
value={this.state.comparisonType}
clearable={false}
onChange={this.onSelectChange.bind(this, 'comparisonType')}
options={comparisonTypeOptions}
/>,
)}
{this.state.colType !== 'spark' && this.formRow(
'Bounds',
(
'Number bounds used for color coding from red to green. ' +
'Reverse the number for green to red. To get boolean ' +
'red or green without spectrum, you can use either only ' +
'min, or max, depending on whether small or big should be ' +
'green or red.'
),
'bounds',
<BoundsControl
value={this.state.bounds}
onChange={this.onBoundsChange.bind(this)}
/>,
)}
{this.formRow(
'D3 format',
'D3 format string',
'd3-format',
<FormControl
value={this.state.d3format}
onChange={this.onTextInputChange.bind(this, 'd3format')}
bsSize="small"
placeholder="D3 format string"
/>,
)}
</div>
</Popover>
);
}
render() {
return (
<span>
{this.textSummary()}{' '}
<OverlayTrigger
container={document.body}
trigger="click"
rootClose
ref="trigger"
placement="right"
overlay={this.renderPopover()}
>
<InfoTooltipWithTrigger
icon="edit"
className="text-primary"
onClick={this.edit.bind(this)}
label="edit-ts-column"
/>
</OverlayTrigger>
</span>
);
}
}
TimeSeriesColumnControl.propTypes = propTypes;
TimeSeriesColumnControl.defaultProps = defaultProps;

View File

@ -0,0 +1,33 @@
import BoundsControl from './BoundsControl';
import CheckboxControl from './CheckboxControl';
import CollectionControl from './CollectionControl';
import ColorSchemeControl from './ColorSchemeControl';
import DatasourceControl from './DatasourceControl';
import DateFilterControl from './DateFilterControl';
import FilterControl from './FilterControl';
import HiddenControl from './HiddenControl';
import SelectAsyncControl from './SelectAsyncControl';
import SelectControl from './SelectControl';
import TextAreaControl from './TextAreaControl';
import TextControl from './TextControl';
import TimeSeriesColumnControl from './TimeSeriesColumnControl';
import VizTypeControl from './VizTypeControl';
const controlMap = {
BoundsControl,
CheckboxControl,
CollectionControl,
ColorSchemeControl,
DatasourceControl,
DateFilterControl,
FilterControl,
HiddenControl,
SelectAsyncControl,
SelectControl,
TextAreaControl,
TextControl,
TimeSeriesColumnControl,
VizTypeControl,
};
export default controlMap;

View File

@ -109,4 +109,7 @@
}
.save-modal-selector {
margin: 10px 0;
}
}
.list-group {
margin-bottom: 10px;
}

View File

@ -5,6 +5,7 @@ import { ALL_COLOR_SCHEMES, spectrums } from '../../modules/colors';
import MetricOption from '../../components/MetricOption';
import ColumnOption from '../../components/ColumnOption';
import { t } from '../../locales';
import controlMap from '../components/controls';
const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format';
@ -1410,5 +1411,12 @@ export const controls = {
default: 4,
description: 'Number of decimal places with which to display lift values',
},
column_collection: {
type: 'CollectionControl',
label: t('Time Series Columns'),
validators: [v.nonEmpty],
control: controlMap.TimeSeriesColumnControl,
},
};
export default controls;

View File

@ -369,6 +369,25 @@ export const visTypes = {
},
},
time_table: {
label: t('Time Series Table'),
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['groupby', 'metrics'],
['column_collection'],
],
},
],
controlOverrides: {
groupby: {
multiple: false,
},
},
},
markup: {
label: t('Markup'),
controlPanelSections: [

View File

@ -1,5 +1,7 @@
import d3 from 'd3';
export const brandColor = '#00A699';
// Color related utility functions go in this object
const bnbColors = [
'#ff5a5f', // rausch

View File

@ -5,7 +5,6 @@ npm --version
node --version
npm install -g yarn
yarn
npm run sync-backend
npm run lint
npm run test
npm run build

View File

@ -40,6 +40,7 @@
"homepage": "http://superset.apache.org/",
"dependencies": {
"@data-ui/event-flow": "0.0.8",
"@data-ui/sparkline": "0.0.1",
"babel-register": "^6.24.1",
"bootstrap": "^3.3.6",
"brace": "^0.10.0",
@ -56,12 +57,12 @@
"distributions": "^1.0.0",
"immutable": "^3.8.2",
"jed": "^1.1.1",
"po2json": "^0.4.5",
"jquery": "3.1.1",
"lodash.throttle": "^4.1.1",
"moment": "^2.14.1",
"mustache": "^2.2.1",
"nvd3": "1.8.6",
"po2json": "^0.4.5",
"prop-types": "^15.6.0",
"react": "^15.6.2",
"react-ace": "^5.0.1",
@ -70,8 +71,8 @@
"react-alert": "^1.0.14",
"react-bootstrap": "^0.31.2",
"react-bootstrap-table": "^4.0.2",
"react-datetime": "^2.9.0",
"react-dom": "^15.6.2",
"react-datetime": "2.9.0",
"react-gravatar": "^2.6.1",
"react-grid-layout": "^0.14.4",
"react-map-gl": "^3.0.4",
@ -79,6 +80,7 @@
"react-resizable": "^1.3.3",
"react-select": "1.0.0-rc.3",
"react-select-fast-filter-options": "^0.2.1",
"react-sortable-hoc": "^0.6.7",
"react-split-pane": "^0.1.66",
"react-syntax-highlighter": "^5.7.0",
"react-virtualized": "^9.3.0",

View File

@ -0,0 +1,33 @@
/* eslint-disable no-unused-expressions */
import React from 'react';
import { FormControl, OverlayTrigger } from 'react-bootstrap';
import sinon from 'sinon';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { shallow } from 'enzyme';
import TimeSeriesColumnControl from '../../../../javascripts/explore/components/controls/TimeSeriesColumnControl';
const defaultProps = {
name: 'x_axis_label',
label: 'X Axis Label',
onChange: sinon.spy(),
};
describe('SelectControl', () => {
let wrapper;
let inst;
beforeEach(() => {
wrapper = shallow(<TimeSeriesColumnControl {...defaultProps} />);
inst = wrapper.instance();
});
it('renders an OverlayTrigger', () => {
expect(wrapper.find(OverlayTrigger)).to.have.lengthOf(1);
});
it('renders an Popover', () => {
const popOver = shallow(inst.renderPopover());
expect(popOver.find(FormControl)).to.have.lengthOf(3);
});
});

View File

@ -1,4 +1,5 @@
@import './less/index.less';
@import "./less/cosmo/variables.less";
body {
margin: 0px !important;
@ -364,6 +365,9 @@ iframe {
.PopoverSection {
padding-bottom: 10px;
}
.popover {
max-width: 500px !important;
}
.float-left {
float: left;
}
@ -382,3 +386,6 @@ g.annotation-container {
stroke-width: 1;
}
}
.stroke-primary {
stroke: @brand-primary;
}

View File

@ -27,6 +27,7 @@ const vizMap = {
separator: require('./markup.js'),
sunburst: require('./sunburst.js'),
table: require('./table.js'),
time_table: require('./time_table.jsx'),
treemap: require('./treemap.js'),
country_map: require('./country_map.js'),
word_cloud: require('./word_cloud.js'),

View File

@ -0,0 +1,3 @@
.time-table {
overflow: auto;
}

View File

@ -0,0 +1,173 @@
import ReactDOM from 'react-dom';
import React from 'react';
import propTypes from 'prop-types';
import { Table, Thead, Th } from 'reactable';
import d3 from 'd3';
import { Sparkline, LineSeries, PointSeries } from '@data-ui/sparkline';
import MetricOption from '../javascripts/components/MetricOption';
import TooltipWrapper from '../javascripts/components/TooltipWrapper';
import { d3format, brandColor } from '../javascripts/modules/utils';
import InfoTooltipWithTrigger from '../javascripts/components/InfoTooltipWithTrigger';
import './time_table.css';
const SPARK_MARGIN = 3;
const ACCESSIBLE_COLOR_BOUNDS = ['#ca0020', '#0571b0'];
function FormattedNumber({ num, format }) {
if (format) {
return (
<span title={num}>{d3format(format, num)}</span>
);
}
return <span>{num}</span>;
}
FormattedNumber.propTypes = {
num: propTypes.number.isRequired,
format: propTypes.string.isRequired,
};
function viz(slice, payload) {
slice.container.css('overflow', 'auto');
slice.container.css('height', slice.height());
const recs = payload.data.records;
const fd = payload.form_data;
const data = Object.keys(recs).sort().map((iso) => {
const o = recs[iso];
return o;
});
const reversedData = data.slice();
reversedData.reverse();
const metricMap = {};
slice.datasource.metrics.forEach((m) => {
metricMap[m.metric_name] = m;
});
let metrics;
if (payload.data.is_group_by) {
// Sorting by first column desc
metrics = payload.data.columns.sort((m1, m2) => (
reversedData[0][m1] > reversedData[0][m2] ? -1 : 1
));
} else {
// Using ordering specified in Metrics dropdown
metrics = payload.data.columns;
}
const tableData = metrics.map((metric) => {
let leftCell;
if (!payload.data.is_group_by) {
leftCell = <MetricOption metric={metricMap[metric]} showFormula={false} />;
} else {
leftCell = metric;
}
const row = { metric: leftCell };
fd.column_collection.forEach((c) => {
if (c.colType === 'spark') {
let sparkData;
if (!c.timeRatio) {
sparkData = data.map(d => d[metric]);
} else {
// Period ratio sparkline
sparkData = [];
for (let i = c.timeRatio; i < data.length; i++) {
sparkData.push(data[i][metric] / data[i - c.timeRatio][metric]);
}
}
const extent = d3.extent(data, d => d[metric]);
const tooltip = `min: ${extent[0]}, max: ${extent[1]}`;
row[c.key] = (
<TooltipWrapper label="tt-spark" tooltip={tooltip}>
<div>
<Sparkline
ariaLabel={`spark-${metric}`}
width={parseInt(c.width, 10) || 300}
height={parseInt(c.height, 10) || 50}
margin={{
top: SPARK_MARGIN,
bottom: SPARK_MARGIN,
left: SPARK_MARGIN,
right: SPARK_MARGIN,
}}
data={sparkData}
>
<LineSeries
showArea={false}
stroke={brandColor}
/>
<PointSeries
points={['min', 'max', 'last']}
fill={brandColor}
/>
</Sparkline>
</div>
</TooltipWrapper>);
} else {
const recent = reversedData[0][metric];
let v;
if (c.colType === 'time') {
// Time lag ratio
v = reversedData[parseInt(c.timeLag, 10)][metric];
if (c.comparisonType === 'diff') {
v -= recent;
} else if (c.comparisonType === 'perc') {
v /= recent;
} else if (c.comparisonType === 'perc_change') {
v = (v / recent) - 1;
}
} else if (c.colType === 'contrib') {
// contribution to column total
v = recent / Object.keys(reversedData[0])
.map(k => reversedData[0][k])
.reduce((a, b) => a + b);
} else if (c.colType === 'avg') {
// Average over the last {timeLag}
v = reversedData
.map((k, i) => i < c.timeLag ? k[metric] : 0)
.reduce((a, b) => a + b) / c.timeLag;
}
let color;
if (c.bounds && c.bounds[0] !== null && c.bounds[1] !== null) {
const scaler = d3.scale.linear()
.domain([
c.bounds[0],
c.bounds[0] + ((c.bounds[1] - c.bounds[0]) / 2),
c.bounds[1]])
.range([ACCESSIBLE_COLOR_BOUNDS[0], 'grey', ACCESSIBLE_COLOR_BOUNDS[1]]);
color = scaler(v);
} else if (c.bounds && c.bounds[0] !== null) {
color = v >= c.bounds[0] ? ACCESSIBLE_COLOR_BOUNDS[1] : ACCESSIBLE_COLOR_BOUNDS[0];
} else if (c.bounds && c.bounds[1] !== null) {
color = v < c.bounds[1] ? ACCESSIBLE_COLOR_BOUNDS[1] : ACCESSIBLE_COLOR_BOUNDS[0];
}
row[c.key] = (
<span style={{ color }}>
<FormattedNumber num={v} format={c.d3format} />
</span>);
}
});
return row;
});
ReactDOM.render(
<Table
className="table table-condensed"
data={tableData}
>
<Thead>
<Th column="metric">Metric</Th>
{fd.column_collection.map((c, i) => (
<Th column={c.key} key={c.key} width={c.colType === 'spark' ? '1%' : null}>
{c.label} {c.tooltip && (
<InfoTooltipWithTrigger
tooltip={c.tooltip}
label={`tt-col-${i}`}
placement="top"
/>
)}
</Th>))}
</Thead>
</Table>,
document.getElementById(slice.containerId),
);
}
module.exports = viz;

View File

@ -10,12 +10,13 @@ from __future__ import unicode_literals
import copy
import hashlib
import inspect
import logging
import traceback
import uuid
import zlib
from collections import OrderedDict, defaultdict
from collections import defaultdict
from itertools import product
from datetime import datetime, timedelta
@ -422,6 +423,48 @@ class TableViz(BaseViz):
return super(TableViz, self).json_dumps(obj)
class TimeTableViz(BaseViz):
"""A data table with rich time-series related columns"""
viz_type = "time_table"
verbose_name = _("Time Table View")
credits = 'a <a href="https://github.com/airbnb/superset">Superset</a> original'
is_timeseries = True
def query_obj(self):
d = super(TimeTableViz, self).query_obj()
fd = self.form_data
if not fd.get('metrics'):
raise Exception(_("Pick at least one metric"))
if fd.get('groupby') and len(fd.get('metrics')) > 1:
raise Exception(_(
"When using 'Group By' you are limited to use "
"a single metric"))
return d
def get_data(self, df):
fd = self.form_data
values = self.metrics
columns = None
if fd.get('groupby'):
values = self.metrics[0]
columns = fd.get('groupby')
pt = df.pivot_table(
index=DTTM_ALIAS,
columns=columns,
values=values)
pt.index = pt.index.map(str)
pt = pt.sort_index()
return dict(
records=pt.to_dict(orient='index'),
columns=list(pt.columns),
is_group_by=len(fd.get('groupby')) > 0,
)
class PivotTableViz(BaseViz):
"""A pivot table view, define your rows, columns and metrics"""
@ -1669,6 +1712,7 @@ class MapboxViz(BaseViz):
"color": fd.get("mapbox_color"),
}
class EventFlowViz(BaseViz):
"""A visualization to explore patterns in event sequences"""
@ -1684,7 +1728,8 @@ class EventFlowViz(BaseViz):
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
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
@ -1758,42 +1803,9 @@ class PairedTTestViz(BaseViz):
return data
viz_types_list = [
TableViz,
PivotTableViz,
NVD3TimeSeriesViz,
NVD3DualLineViz,
NVD3CompareTimeSeriesViz,
NVD3TimeSeriesStackedViz,
NVD3TimeSeriesBarViz,
DistributionBarViz,
DistributionPieViz,
BubbleViz,
BulletViz,
MarkupViz,
WordCloudViz,
BigNumberViz,
BigNumberTotalViz,
SunburstViz,
DirectedForceViz,
SankeyViz,
CountryMapViz,
ChordViz,
WorldMapViz,
FilterBoxViz,
IFrameViz,
ParallelCoordinatesViz,
HeatmapViz,
BoxPlotViz,
TreemapViz,
CalHeatmapViz,
HorizonViz,
MapboxViz,
HistogramViz,
SeparatorViz,
EventFlowViz,
PairedTTestViz,
]
viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list
if v.viz_type not in config.get('VIZ_TYPE_BLACKLIST')])
viz_types = {
o.viz_type: o for o in globals().values()
if (
inspect.isclass(o) and
issubclass(o, BaseViz) and
o.viz_type not in config.get('VIZ_TYPE_BLACKLIST'))}