Skip to content

Commit

Permalink
Time Series Annotation Layers (#3521)
Browse files Browse the repository at this point in the history
* Adding annotations to backend

* Auto fetching Annotations on the backend

* Closing the loop

* Adding missing files

* annotation layers UI

for #3502

* a few fixes per code review.

- add annotation input sanity check before add and before update.
- make SelectAsyncControl component statelesis, and generic
- add annotation description in d3 tool tip
- use less variable to replace hard-coded color
  • Loading branch information
Grace Guo authored and mistercrunch committed Sep 28, 2017
1 parent 3d72eb4 commit d1a7a7b
Show file tree
Hide file tree
Showing 18 changed files with 434 additions and 4 deletions.
6 changes: 5 additions & 1 deletion superset/assets/javascripts/components/AsyncSelect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const propTypes = {
onChange: PropTypes.func.isRequired,
mutator: PropTypes.func.isRequired,
onAsyncError: PropTypes.func,
value: PropTypes.number,
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.arrayOf(PropTypes.number),
]),
valueRenderer: PropTypes.func,
placeholder: PropTypes.string,
autoSelect: PropTypes.bool,
Expand Down Expand Up @@ -63,6 +66,7 @@ class AsyncSelect extends React.PureComponent {
isLoading={this.state.isLoading}
onChange={this.onChange.bind(this)}
valueRenderer={this.props.valueRenderer}
{...this.props}
/>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion superset/assets/javascripts/explore/components/Control.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ 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 ColorSchemeControl from './controls/ColorSchemeControl';

const controlMap = {
BoundsControl,
Expand All @@ -25,6 +26,7 @@ const controlMap = {
TextControl,
VizTypeControl,
ColorSchemeControl,
SelectAsyncControl,
};
const controlTypes = Object.keys(controlMap);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
import Select from '../../../components/AsyncSelect';
import { t } from '../../../locales';

const propTypes = {
dataEndpoint: PropTypes.string.isRequired,
multi: PropTypes.bool,
mutator: PropTypes.func,
onAsyncErrorMessage: PropTypes.string,
onChange: PropTypes.func,
placeholder: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(PropTypes.number),
]),
};

const defaultProps = {
multi: true,
onAsyncErrorMessage: t('Error while fetching data'),
onChange: () => {},
placeholder: t('Select ...'),
};

const SelectAsyncControl = ({ value, onChange, dataEndpoint,
multi, mutator, placeholder, onAsyncErrorMessage }) => {
const onSelectionChange = (options) => {
const optionValues = options.map(option => option.value);
onChange(optionValues);
};

return (
<Select
dataEndpoint={dataEndpoint}
onChange={onSelectionChange}
onAsyncError={() => notify.error(onAsyncErrorMessage)}
mutator={mutator}
multi={multi}
value={value}
placeholder={placeholder}
valueRenderer={v => (<div>{v.label}</div>)}
/>
);
};

SelectAsyncControl.propTypes = propTypes;
SelectAsyncControl.defaultProps = defaultProps;

export default SelectAsyncControl;
5 changes: 4 additions & 1 deletion superset/assets/javascripts/explore/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@
}

.control-panel-section {
margin-bottom: 0px;
margin-bottom: 0;
box-shadow: none;
}
.control-panel-section:last-child {
padding-bottom: 40px;
}

.control-panel-section .Select-multi-value-wrapper .Select-input > input {
width: 100px;
Expand Down
17 changes: 17 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ export const controls = {
}),
},

annotation_layers: {
type: 'SelectAsyncControl',
multi: true,
label: t('Annotation Layers'),
default: [],
description: t('Annotation layers to overlay on the visualization'),
dataEndpoint: '/annotationlayermodelview/api/read?',
placeholder: t('Select a annotation layer'),
onAsyncErrorMessage: t('Error while fetching annotation layers'),
mutator: (data) => {
if (!data || !data.result) {
return [];
}
return data.result.map(layer => ({ value: layer.id, label: layer.name }));
},
},

metric: {
type: 'SelectControl',
label: t('Metric'),
Expand Down
12 changes: 12 additions & 0 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export const sections = {
],
description: t('This section exposes ways to include snippets of SQL in your query'),
},
annotations: {
label: t('Annotations'),
expanded: true,
controlSetRows: [
['annotation_layers'],
],
},
NVD3TimeSeries: [
{
label: t('Query'),
Expand Down Expand Up @@ -177,6 +184,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand Down Expand Up @@ -209,6 +217,7 @@ export const visTypes = {
['metric_2', 'y_axis_2_format'],
],
},
sections.annotations,
],
controlOverrides: {
metric: {
Expand Down Expand Up @@ -251,6 +260,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand All @@ -273,6 +283,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand Down Expand Up @@ -306,6 +317,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand Down
2 changes: 2 additions & 0 deletions superset/assets/stylesheets/less/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
@import "~bootstrap/less/bootstrap.less";
@import "./cosmo/variables.less";
@import "./cosmo/bootswatch.less";

@stroke-primary: @brand-primary;
14 changes: 14 additions & 0 deletions superset/assets/stylesheets/superset.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import './less/index.less';

body {
margin: 0px !important;
}
Expand Down Expand Up @@ -368,3 +370,15 @@ iframe {
.float-right {
float: right;
}

g.annotation-container {
line {
stroke: @stroke-primary;
}

rect.annotation {
stroke: @stroke-primary;
fill-opacity: 0.1;
stroke-width: 1;
}
}
73 changes: 73 additions & 0 deletions superset/assets/visualizations/nvd3_vis.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import $ from 'jquery';
import throttle from 'lodash.throttle';
import d3 from 'd3';
import nv from 'nvd3';
import d3tip from 'd3-tip';

import { getColorFromScheme } from '../javascripts/modules/colors';
import { customizeToolTip, d3TimeFormatPreset, d3FormatPreset, tryNumify } from '../javascripts/modules/utils';
Expand Down Expand Up @@ -503,6 +504,78 @@ function nvd3Vis(slice, payload) {
.attr('height', height)
.attr('width', width)
.call(chart);

// add annotation_layer
if (isTimeSeries && payload.annotations.length) {
const tip = d3tip()
.attr('class', 'd3-tip')
.direction('n')
.offset([-5, 0])
.html((d) => {
if (!d || !d.layer) {
return '';
}

const title = d.short_descr ?
d.short_descr + ' - ' + d.layer :
d.layer;
const body = d.long_descr;
return '<div><strong>' + title + '</strong></div><br/>' +
'<div>' + body + '</div>';
});

const hh = chart.yAxis.scale().range()[0];

let annotationLayer;
let xScale;
let minStep;
if (vizType === 'bar') {
const xMax = d3.max(payload.data[0].values, d => (d.x));
const xMin = d3.min(payload.data[0].values, d => (d.x));
minStep = chart.xAxis.range()[1] - chart.xAxis.range()[0];
annotationLayer = svg.select('.nv-barsWrap')
.insert('g', ':first-child');
xScale = d3.scale.quantile()
.domain([xMin, xMax])
.range(chart.xAxis.range());
} else {
minStep = 1;
annotationLayer = svg.select('.nv-background')
.append('g');
xScale = chart.xScale();
}

annotationLayer
.attr('class', 'annotation-container')
.append('defs')
.append('pattern')
.attr('id', 'diagonal')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 8)
.attr('height', 10)
.attr('patternTransform', 'rotate(45 50 50)')
.append('line')
.attr('stroke-width', 7)
.attr('y2', 10);

annotationLayer.selectAll('rect')
.data(payload.annotations)
.enter()
.append('rect')
.attr('class', 'annotation')
.attr('x', d => (xScale(d.start_dttm)))
.attr('y', 0)
.attr('width', (d) => {
const w = xScale(d.end_dttm) - xScale(d.start_dttm);
return w === 0 ? minStep : w;
})
.attr('height', hh)
.attr('fill', 'url(#diagonal)')
.on('mouseover', tip.show)
.on('mouseout', tip.hide);

annotationLayer.selectAll('rect').call(tip);
}
}

// on scroll, hide tooltips. throttle to only 4x/second.
Expand Down
22 changes: 22 additions & 0 deletions superset/migrations/versions/d39b1e37131d_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""empty message
Revision ID: d39b1e37131d
Revises: ('a9c47e2c1547', 'ddd6ebdd853b')
Create Date: 2017-09-19 15:09:14.292633
"""

# revision identifiers, used by Alembic.
revision = 'd39b1e37131d'
down_revision = ('a9c47e2c1547', 'ddd6ebdd853b')

from alembic import op
import sqlalchemy as sa


def upgrade():
pass


def downgrade():
pass
56 changes: 56 additions & 0 deletions superset/migrations/versions/ddd6ebdd853b_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""annotations
Revision ID: ddd6ebdd853b
Revises: ca69c70ec99b
Create Date: 2017-09-13 16:36:39.144489
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'ddd6ebdd853b'
down_revision = 'ca69c70ec99b'


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'annotation_layer',
sa.Column('created_on', sa.DateTime(), nullable=True),
sa.Column('changed_on', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=250), nullable=True),
sa.Column('descr', sa.Text(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table(
'annotation',
sa.Column('created_on', sa.DateTime(), nullable=True),
sa.Column('changed_on', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('start_dttm', sa.DateTime(), nullable=True),
sa.Column('end_dttm', sa.DateTime(), nullable=True),
sa.Column('layer_id', sa.Integer(), nullable=True),
sa.Column('short_descr', sa.String(length=500), nullable=True),
sa.Column('long_descr', sa.Text(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['layer_id'], [u'annotation_layer.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(
'ti_dag_state',
'annotation', ['layer_id', 'start_dttm', 'end_dttm'], unique=False)


def downgrade():
op.drop_index('ti_dag_state', table_name='annotation')
op.drop_table('annotation')
op.drop_table('annotation_layer')
Loading

0 comments on commit d1a7a7b

Please sign in to comment.