diff --git a/superset/assets/images/viz_thumbnails/time_table.png b/superset/assets/images/viz_thumbnails/time_table.png new file mode 100644 index 0000000000000..5eba0c2f0b33d Binary files /dev/null and b/superset/assets/images/viz_thumbnails/time_table.png differ diff --git a/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx b/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx index 09632cdf538cf..d86d0515e718c 100644 --- a/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx +++ b/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx @@ -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 = ( + + ); + if (!tooltip) { + return iconEl; + } return ( } > - + {iconEl} ); } diff --git a/superset/assets/javascripts/components/MetricOption.jsx b/superset/assets/javascripts/components/MetricOption.jsx index b19043479d647..f0994142569b8 100644 --- a/superset/assets/javascripts/components/MetricOption.jsx +++ b/superset/assets/javascripts/components/MetricOption.jsx @@ -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 (
@@ -21,12 +25,14 @@ export default function MetricOption({ metric }) { label={`descr-${metric.metric_name}`} /> } - + {showFormula && + + } {metric.warning_text && ); } MetricOption.propTypes = propTypes; +MetricOption.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/explore/components/Control.jsx b/superset/assets/javascripts/explore/components/Control.jsx index 972ff0d3c19d4..ed7ea97ad31d6 100644 --- a/superset/assets/javascripts/explore/components/Control.jsx +++ b/superset/assets/javascripts/explore/components/Control.jsx @@ -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 = { diff --git a/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx b/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx index 776f7a499bde0..803a539619560 100644 --- a/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx +++ b/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx @@ -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], }; diff --git a/superset/assets/javascripts/explore/components/controls/CollectionControl.jsx b/superset/assets/javascripts/explore/components/controls/CollectionControl.jsx new file mode 100644 index 0000000000000..74f0d9b45cd6f --- /dev/null +++ b/superset/assets/javascripts/explore/components/controls/CollectionControl.jsx @@ -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(() => ( + )); + +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
{this.props.placeholder}
; + } + return ( + + {this.props.value.map((o, i) => ( + +
+ +
+
+ +
+
+ +
+
))} +
+ ); + } + render() { + return ( +
+ + {this.renderList()} + +
+ ); + } +} + +CollectionControl.propTypes = propTypes; +CollectionControl.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/explore/components/controls/TimeSeriesColumnControl.jsx b/superset/assets/javascripts/explore/components/controls/TimeSeriesColumnControl.jsx new file mode 100644 index 0000000000000..cd8ec98c22912 --- /dev/null +++ b/superset/assets/javascripts/explore/components/controls/TimeSeriesColumnControl.jsx @@ -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 ( + + + {label}{' '} + + + {control} + + ); + } + renderPopover() { + return ( + +
+ {this.formRow( + 'Label', + 'The column header label', + 'time-lag', + , + )} + {this.formRow( + 'Tooltip', + 'Column header tooltip', + 'col-tooltip', + , + )} + {this.formRow( + 'Type', + 'Type of comparison, value difference or percentage', + 'col-type', + , + )} + {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', + , + )} + {this.formRow( + 'D3 format', + 'D3 format string', + 'd3-format', + , + )} +
+
+ ); + } + render() { + return ( + + {this.textSummary()}{' '} + + + + + ); + } +} + +TimeSeriesColumnControl.propTypes = propTypes; +TimeSeriesColumnControl.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/explore/components/controls/index.jsx b/superset/assets/javascripts/explore/components/controls/index.jsx new file mode 100644 index 0000000000000..499e6055f6b13 --- /dev/null +++ b/superset/assets/javascripts/explore/components/controls/index.jsx @@ -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; diff --git a/superset/assets/javascripts/explore/main.css b/superset/assets/javascripts/explore/main.css index 684fdf0e8b011..bc67249e75bd7 100644 --- a/superset/assets/javascripts/explore/main.css +++ b/superset/assets/javascripts/explore/main.css @@ -109,4 +109,7 @@ } .save-modal-selector { margin: 10px 0; -} \ No newline at end of file +} +.list-group { + margin-bottom: 10px; +} diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx index 92faad17393da..57222a6efcfd6 100644 --- a/superset/assets/javascripts/explore/stores/controls.jsx +++ b/superset/assets/javascripts/explore/stores/controls.jsx @@ -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; diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index 5648f4bbfc0b6..1d4d79b5b47dd 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -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: [ diff --git a/superset/assets/javascripts/modules/colors.js b/superset/assets/javascripts/modules/colors.js index 7fd585f370030..8e3e521665464 100644 --- a/superset/assets/javascripts/modules/colors.js +++ b/superset/assets/javascripts/modules/colors.js @@ -1,5 +1,7 @@ import d3 from 'd3'; +export const brandColor = '#00A699'; + // Color related utility functions go in this object const bnbColors = [ '#ff5a5f', // rausch diff --git a/superset/assets/js_build.sh b/superset/assets/js_build.sh index 7e48caa126d7d..c71739888e983 100755 --- a/superset/assets/js_build.sh +++ b/superset/assets/js_build.sh @@ -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 diff --git a/superset/assets/package.json b/superset/assets/package.json index c74f66d139d4f..15cfff2a50bcd 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -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", diff --git a/superset/assets/spec/javascripts/explore/components/TimeSeriesColumnControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/TimeSeriesColumnControl_spec.jsx new file mode 100644 index 0000000000000..2f8cd6b913d50 --- /dev/null +++ b/superset/assets/spec/javascripts/explore/components/TimeSeriesColumnControl_spec.jsx @@ -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(); + 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); + }); +}); diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less index a4bb70a163fed..78f26bb68c13c 100644 --- a/superset/assets/stylesheets/superset.less +++ b/superset/assets/stylesheets/superset.less @@ -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; +} diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index d5c3abb1a7e68..dc5ee30516270 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -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'), diff --git a/superset/assets/visualizations/time_table.css b/superset/assets/visualizations/time_table.css new file mode 100644 index 0000000000000..5f8a41bdd9e76 --- /dev/null +++ b/superset/assets/visualizations/time_table.css @@ -0,0 +1,3 @@ +.time-table { + overflow: auto; +} diff --git a/superset/assets/visualizations/time_table.jsx b/superset/assets/visualizations/time_table.jsx new file mode 100644 index 0000000000000..928352cfc7b10 --- /dev/null +++ b/superset/assets/visualizations/time_table.jsx @@ -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 ( + {d3format(format, num)} + ); + } + return {num}; +} +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 = ; + } 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] = ( + +
+ + + + +
+
); + } 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] = ( + + + ); + } + }); + return row; + }); + ReactDOM.render( + + + + {fd.column_collection.map((c, i) => ( + ))} + +
Metric + {c.label} {c.tooltip && ( + + )} +
, + document.getElementById(slice.containerId), + ); +} + +module.exports = viz; diff --git a/superset/viz.py b/superset/viz.py index 22d2ea9ec3776..2283e8daaccb1 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -10,12 +10,13 @@ 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 @@ def json_dumps(self, obj): 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 Superset 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 @@ def get_data(self, df): "color": fd.get("mapbox_color"), } + class EventFlowViz(BaseViz): """A visualization to explore patterns in event sequences""" @@ -1684,7 +1728,8 @@ def query_obj(self): 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 @@ def get_data(self, df): 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'))}