fabianmenges commented on a change in pull request #3518: Full Annotation
Framework
URL:
https://github.com/apache/incubator-superset/pull/3518#discussion_r156262321
##########
File path:
superset/assets/javascripts/explore/components/controls/AnnotationLayer.jsx
##########
@@ -0,0 +1,558 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { CirclePicker } from 'react-color';
+import { Button } from 'react-bootstrap';
+
+import $ from 'jquery';
+import mathjs from 'mathjs';
+
+import SelectControl from './SelectControl';
+import TextControl from './TextControl';
+import CheckboxControl from './CheckboxControl';
+
+import AnnotationTypes, { isQueryAnnotationType, supportedSliceTypes }
+ from '../../../modules/AnnotationTypes';
+import { ALL_COLOR_SCHEMES } from '../../../modules/colors';
+import PopoverSection from '../../../components/PopoverSection';
+import ControlHeader from '../ControlHeader';
+import { nonEmpty } from '../../validators';
+import vizTypes from '../../stores/visTypes';
+
+const AUTOMATIC_COLOR = '';
+
+const propTypes = {
+ name: PropTypes.string,
+ annotationType: PropTypes.string,
+ color: PropTypes.string,
+ opacity: PropTypes.string,
+ style: PropTypes.string,
+ width: PropTypes.number,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ overrides: PropTypes.object,
+ show: PropTypes.bool,
+ titleColumn: PropTypes.string,
+ descriptionColumns: PropTypes.arrayOf(PropTypes.string),
+ timeColumn: PropTypes.string,
+ intervalEndColumn: PropTypes.string,
+
+ error: PropTypes.string,
+ colorScheme: PropTypes.string,
+
+ addAnnotationLayer: PropTypes.func,
+ removeAnnotationLayer: PropTypes.func,
+ close: PropTypes.func,
+};
+
+const defaultProps = {
+ name: '',
+ annotationType: AnnotationTypes.FORMULA,
+ color: AUTOMATIC_COLOR,
+ opacity: '',
+ style: 'solid',
+ width: 1,
+ overrides: {},
+ colorScheme: 'd3Category10',
+ show: true,
+ titleColumn: '',
+ descriptionColumns: [],
+ timeColumn: '',
+ intervalEndColumn: '',
+
+ addAnnotationLayer: () => {},
+ removeAnnotationLayer: () => {},
+ close: () => {},
+};
+
+export default class AnnotationLayer extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ let isNew;
+ const isLoadingOptions = true;
+ if (!this.props.name) {
+ isNew = true;
+ } else {
+ isNew = false;
+ }
+ const { name, annotationType,
+ color, opacity, style, width, value,
+ overrides, show, titleColumn, descriptionColumns,
+ timeColumn, intervalEndColumn } = props;
+ this.state = {
+ // base
+ name,
+ oldName: isNew ? null : name,
+ annotationType,
+ value,
+ overrides,
+ show,
+ // slice
+ titleColumn,
+ descriptionColumns,
+ timeColumn,
+ intervalEndColumn,
+ // display
+ color: color || AUTOMATIC_COLOR,
+ opacity,
+ style,
+ width,
+ // refData
+ isNew,
+ isLoadingOptions,
+ valueOptions: [],
+ };
+ this.submitAnnotation = this.submitAnnotation.bind(this);
+ this.deleteAnnotation = this.deleteAnnotation.bind(this);
+ this.applyAnnotation = this.applyAnnotation.bind(this);
+ this.fetchOptions = this.fetchOptions.bind(this);
+ this.handleAnnotationType = this.handleAnnotationType.bind(this);
+ this.handleValue = this.handleValue.bind(this);
+ this.validate = this.validate.bind(this);
+ }
+
+ componentDidMount() {
+ const { annotationType, isLoadingOptions } = this.state;
+ this.fetchOptions(annotationType, isLoadingOptions);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.annotationType !== this.state.annotationType) {
+ this.fetchOptions(this.state.annotationType, true);
+ }
+ }
+
+ validateFormula(value, annotationType) {
+ if (annotationType === AnnotationTypes.FORMULA) {
+ try {
+ mathjs.parse(value).compile().eval({ x: 0 });
+ } catch (err) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ validate() {
+ const { name, annotationType, value, timeColumn, intervalEndColumn } =
this.state;
+ const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)];
+ if (annotationType === annotationType.EVENT) {
+ errors.push(nonEmpty(timeColumn));
+ }
+ if (annotationType === AnnotationTypes.INTERVAL) {
+ errors.push(nonEmpty(timeColumn));
+ errors.push(nonEmpty(intervalEndColumn));
+ }
+ errors.push(this.validateFormula(value, annotationType));
+ return errors.filter(x => x).length;
+ }
+
+ handleAnnotationType(annotationType) {
+ this.setState({
+ annotationType,
+ isLoadingOptions: true,
+ validationErrors: {},
+ value: null,
+ });
+ }
+
+ handleValue(value) {
+ this.setState({
+ value,
+ descriptionColumns: null,
+ intervalEndColumn: null,
+ timeColumn: null,
+ titleColumn: null,
+ overrides: {},
+ });
+ }
+
+ fetchOptions(annotationType, isLoadingOptions) {
+ if (isLoadingOptions === true) {
+ if (annotationType === AnnotationTypes.NATIVE) {
+ $.ajax({
+ type: 'GET',
+ url: '/annotationlayermodelview/api/read?',
+ }).then((data) => {
+ const layers = data ? data.result.map(layer => ({
+ value: layer.id,
+ label: layer.name,
+ })) : [];
+ this.setState({
+ isLoadingOptions: false,
+ valueOptions: layers,
+ });
+ });
+ } else if (isQueryAnnotationType(annotationType)) {
+ $.ajax({
+ type: 'GET',
+ url: '/superset/user_slices',
+ }).then(data =>
+ this.setState({
+ isLoadingOptions: false,
+ valueOptions: data.filter(x => supportedSliceTypes(annotationType)
+ .find(v => v === x.viz_type))
+ .map(x => ({ value: x.id, label: x.title, slice: x })),
+ }),
+ );
+ }
+ }
+ }
+
+ deleteAnnotation() {
+ this.props.close();
+ if (!this.state.isNew) {
+ this.props.removeAnnotationLayer(this.state);
+ }
+ }
+
+ applyAnnotation() {
+ if (this.state.name.length) {
+ const annotation = { ...this.state };
+ annotation.color = annotation.color === AUTOMATIC_COLOR ? null :
annotation.color;
+ delete annotation.isNew;
+ delete annotation.valueOptions;
+ delete annotation.isLoadingOptions;
+ this.props.addAnnotationLayer(annotation);
+ this.setState({ isNew: false, oldName: this.state.name });
+ }
+ }
+
+ submitAnnotation() {
+ this.applyAnnotation();
+ this.props.close();
+ }
+
+ renderValueConfiguration() {
+ const { annotationType, value, valueOptions, isLoadingOptions } =
this.state;
+ let label = '';
+ let description = '';
+ if (annotationType === AnnotationTypes.NATIVE) {
+ label = 'Annotation Layer';
+ description = 'Select the Annotation Layer you would like to use.';
+ } else if (isQueryAnnotationType(annotationType)) {
+ label = 'Slice';
+ description = 'Use a pre defined Superset Slice as a source for
annotations and overlays. ' +
+ 'your Slice must be one of these visualization types: ' +
+ `[${supportedSliceTypes(annotationType).map(x =>
vizTypes[x].label).join(', ')}]`;
+ } else if (annotationType === AnnotationTypes.FORMULA) {
+ label = 'Formula';
+ description = 'Expects a formula with depending time parameter `x`' +
+ ' in milliseconds since epoch. mathjs is used to evaluate the
formulas.';
+ }
+ if (isQueryAnnotationType(annotationType) || annotationType ===
AnnotationTypes.NATIVE) {
+ return (
+ <SelectControl
+ name="annotation-layer-value"
+ showHeader
+ hovered
+ description={description}
+ label={label}
+ placeholder=""
+ options={valueOptions}
+ isLoading={isLoadingOptions}
+ value={value}
+ onChange={this.handleValue}
+ validationErrors={!value ? ['Mandatory'] : []}
+ />
+ );
+ } if (annotationType === AnnotationTypes.FORMULA) {
+ return (
+ <TextControl
+ name="annotation-layer-value"
+ hovered
+ showHeader
+ description={description}
+ label={label}
+ placeholder=""
+ value={value}
+ onChange={this.handleValue}
+ validationErrors={this.validateFormula(value, annotationType) ?
['Bad formula.'] : []}
+ />
+ );
+ }
+ return '';
+ }
+
+ renderSliceConfiguration() {
+ const { annotationType, value, valueOptions, overrides, titleColumn,
+ timeColumn, intervalEndColumn, descriptionColumns } = this.state;
+ const slice = (valueOptions.find(x => x.value === value) || {}).slice;
+ if (isQueryAnnotationType(annotationType) && slice) {
+ const columns = (slice.form_data.groupby || []).concat(
+ (slice.form_data.all_columns || [])).map(x => ({ value: x, label: x
}));
+ const timeColumnOptions = slice.form_data.include_time ?
+ [{ value: '__timestamp', label: '__timestamp' }].concat(columns) :
columns;
+ return (
+ <div style={{ marginRight: '2rem' }}>
+ <PopoverSection
+ isSelected
+ onSelect={() => {
+ }}
+ title="Annotation Slice Configuration"
+ info={
+ 'This section allows you to configure how to use the slice ' +
+ 'to generate annotations.'
+ }
+ >
+ {
+ (
+ annotationType === AnnotationTypes.EVENT ||
+ annotationType === AnnotationTypes.INTERVAL
+ ) &&
+ <SelectControl
+ hovered
+ name="annotation-layer-time-column"
+ label={
+ annotationType === AnnotationTypes.INTERVAL ?
+ 'Interval Start column' : 'Event Time Column'
+ }
+ description={'This column must contain date/time information.'}
+ validationErrors={!timeColumn ? ['Mandatory'] : []}
+ clearable={false}
+ options={timeColumnOptions}
+ value={timeColumn}
+ onChange={v => this.setState({ timeColumn: v })}
+ />
+ }
+ {
+ annotationType === AnnotationTypes.INTERVAL &&
+ <SelectControl
+ hovered
+ name="annotation-layer-intervalEnd"
+ label="Interval End column"
+ description={'This column must contain date/time information.'}
+ validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
+ options={columns}
+ value={intervalEndColumn}
+ onChange={v => this.setState({ intervalEndColumn: v })}
+ />
+ }
+ <SelectControl
+ hovered
+ name="annotation-layer-title"
+ label="Title Column"
+ description={'Pick a title for you annotation.'}
+ options={
+ [{ value: '', label: 'None' }].concat(columns)
+ }
+ value={titleColumn}
+ onChange={v => this.setState({ titleColumn: v })}
+ />
+ {
+ annotationType !== AnnotationTypes.TIME_SERIES &&
+ <SelectControl
+ hovered
+ name="annotation-layer-title"
+ label="Description Columns"
+ description={'Pick one or more columns that should be shown in
the ' +
+ "annotation. If you don't select a column all of them will be
shown. "}
+ multi
+ options={
+ columns
+ }
+ value={descriptionColumns}
+ onChange={v => this.setState({ descriptionColumns: v })}
+ />
+ }
+ <div style={{ marginTop: '1rem' }}>
+ <CheckboxControl
+ hovered
+ name="annotation-override-since"
+ label="Pass down 'Since'"
+ description={'This controls whether the "Since" field from the
current ' +
+ 'view should be passed down to the slice containing the
annotation data.'}
+ value={!!Object.keys(overrides).find(x => x === 'since')}
+ onChange={(v) => {
+ delete overrides.since;
+ if (v) {
+ this.setState({ overrides: { ...overrides, since: null }
});
+ } else {
+ this.setState({ overrides: { ...overrides } });
+ }
+ }}
+ />
+ <CheckboxControl
+ hovered
+ name="annotation-override-until"
+ label="Pass down 'Until'"
+ description={'This controls whether the "Until" field from the
current ' +
+ 'view should be passed down to the slice containing the
annotation data.'}
+ value={!!Object.keys(overrides).find(x => x === 'until')}
+ onChange={(v) => {
+ delete overrides.until;
+ if (v) {
+ this.setState({ overrides: { ...overrides, until: null }
});
+ } else {
+ this.setState({ overrides: { ...overrides } });
+ }
+ }}
+ />
+ </div>
+ </PopoverSection>
+ </div>
+ );
+ }
+ return ('');
+ }
+
+ renderDisplayConfiguration() {
+ const { color, opacity, style, width } = this.state;
+ const colorScheme = ALL_COLOR_SCHEMES[this.props.colorScheme].slice();
+ if (color && color !== AUTOMATIC_COLOR &&
+ !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
+ colorScheme.push(color);
+ }
+ return (
+ <div>
+ <PopoverSection
+ isSelected
+ onSelect={() => {}}
+ title="Display configuration"
+ info={
+ 'Configure your how you overlay is displayed here.'
+ }
+ >
+ <SelectControl
+ name="annotation-layer-stroke"
+ label="Style"
+ // see '../../../../visualizations/nvd3_vis.css'
+ options={[
+ { value: 'solid', label: 'Solid' },
+ { value: 'dashed', label: 'Dashed' },
+ { value: 'wideDashed', label: 'Wide Dashed' },
+ { value: 'dotted', label: 'Dotted' },
+ ]}
+ value={style}
+ onChange={v => this.setState({ style: v })}
+ />
+ <SelectControl
+ name="annotation-layer-opacity"
+ label="Opacity"
+ // see '../../../../visualizations/nvd3_vis.css'
+ options={[
+ { value: '', label: 'Solid' },
+ { value: 'opacityLow', label: '0.2' },
+ { value: 'opacityMedium', label: '0.5' },
+ { value: 'opacityHigh', label: '0.8' },
+ ]}
+ value={opacity}
+ onChange={v => this.setState({ opacity: v })}
+ />
+ <div>
+ <ControlHeader label="Color" />
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
+ <CirclePicker
+ color={color}
+ colors={colorScheme}
+ onChangeComplete={v => this.setState({ color: v.hex })}
+ />
+ <Button
+ style={{ marginTop: '0.5rem', marginBottom: '0.5rem' }}
+ bsStyle={color === AUTOMATIC_COLOR ? 'success' : 'default'}
+ bsSize="xsmall"
+ onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
+ >
+ Automatic Color
+ </Button>
+ </div>
+ </div>
+ <TextControl
+ name="annotation-layer-stroke-width"
+ label="Line Width"
+ isInt
+ placeholder=""
Review comment:
I can get rid of that line alltogether
----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]
With regards,
Apache Git Services