mirror of
https://github.com/apache/superset.git
synced 2026-04-23 01:55:09 +00:00
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:
committed by
GitHub
parent
645de384e3
commit
bb0f69d074
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user