Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time Series Annotation Layers #3521

Merged
merged 6 commits into from
Sep 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -371,3 +373,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 @@ -476,6 +477,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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bigint?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hoping we never go above 2,147,483,647 annotations, saving some bytes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have automated processes writing to this table (airflow) we should change this to BIGINT. The disk space you save is only relevant in cases when you have a lot of rows, which is also when you want to have BIGINTs as your IDs.

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