fabianmenges commented on a change in pull request #3518: Full Annotation
Framework
URL:
https://github.com/apache/incubator-superset/pull/3518#discussion_r156259831
##########
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=""
Review comment:
I can put an example in the tool tip. The problem with a default is that it
will most likely not match your data at all. E.g. `2x+5` if your x axis is a
timestamp will be huge.
----------------------------------------------------------------
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