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

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;