Skip to content

Commit

Permalink
annotation layers UI
Browse files Browse the repository at this point in the history
for #3502
  • Loading branch information
Grace Guo committed Sep 25, 2017
1 parent 3aba1f2 commit cc29e51
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 16 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,62 @@
/* global notify */
import React from 'react';
import PropTypes from 'prop-types';
import Select from '../../../components/AsyncSelect';
import { t } from '../../../locales';

const propTypes = {
onChange: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(PropTypes.number),
]),
};

const defaultProps = {
onChange: () => {},
};

class SelectAsyncControl extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
value: this.props.value,
};
}

onChange(options) {
const optionValues = options.map(option => option.value);
this.setState({ value: optionValues });
this.props.onChange(optionValues);
}

mutator(data) {
if (!data || !data.result) {
return [];
}

return data.result.map(layer => ({ value: layer.id, label: layer.name }));
}

render() {
return (
<Select
dataEndpoint={'/annotationlayermodelview/api/read?'}
onChange={this.onChange.bind(this)}
onAsyncError={() => notify.error(t('Error while fetching annotation layers'))}
mutator={this.mutator}
multi
value={this.state.value}
placeholder={t('Select a annotation layer')}
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
13 changes: 3 additions & 10 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,11 @@ export const controls = {
},

annotation_layers: {
type: 'SelectControl',
type: 'SelectAsyncControl',
multi: true,
label: 'Annotation Layers',
label: t('Annotation Layers'),
default: [],
description: 'Annotation layers to overlay on the visualization',
// Mocking with hard coded values for now
// this should be out of a new SelectAsyncControl who would fetch the
// choices on its own
choices: [
[1, 'Outages'],
[2, 'Holidays'],
],
description: t('Annotation layers to overlay on the visualization'),
},

metric: {
Expand Down
7 changes: 6 additions & 1 deletion superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export const sections = {
description: t('This section exposes ways to include snippets of SQL in your query'),
},
annotations: {
label: 'Annotations',
label: t('Annotations'),
expanded: true,
controlSetRows: [
['annotation_layers'],
],
Expand Down Expand Up @@ -216,6 +217,7 @@ export const visTypes = {
['metric_2', 'y_axis_2_format'],
],
},
sections.annotations,
],
controlOverrides: {
metric: {
Expand Down Expand Up @@ -258,6 +260,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand All @@ -280,6 +283,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand Down Expand Up @@ -313,6 +317,7 @@ export const visTypes = {
],
},
sections.NVD3TimeSeries[1],
sections.annotations,
],
controlOverrides: {
x_axis_format: {
Expand Down
67 changes: 66 additions & 1 deletion 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 @@ -74,7 +75,6 @@ function getMaxLabelSize(container, axisClass) {
}

function nvd3Vis(slice, payload) {
console.log("Annotations are here!", payload.annotations);
let chart;
let colorKey = 'key';
const isExplore = $('#explore-container').length === 1;
Expand Down Expand Up @@ -477,6 +477,71 @@ 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 => (d && d.layer ? d.layer : ''));

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', '#00A699')
.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)')
.attr('fill-opacity', 0.1)
.attr('stroke-width', 1)
.attr('stroke', '#00A699')
.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
22 changes: 22 additions & 0 deletions superset/migrations/versions/f959a6652acd_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""empty message
Revision ID: f959a6652acd
Revises: ('472d2f73dfd4', 'd39b1e37131d')
Create Date: 2017-09-24 20:18:35.791707
"""

# revision identifiers, used by Alembic.
revision = 'f959a6652acd'
down_revision = ('472d2f73dfd4', 'd39b1e37131d')

from alembic import op
import sqlalchemy as sa


def upgrade():
pass


def downgrade():
pass
2 changes: 1 addition & 1 deletion superset/views/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AnnotationModelView(SupersetModelView, DeleteMixin): # noqa

class AnnotationLayerModelView(SupersetModelView, DeleteMixin):
datamodel = SQLAInterface(AnnotationLayer)
list_columns = ['name']
list_columns = ['id', 'name']
edit_columns = ['name', 'descr']
add_columns = edit_columns

Expand Down
2 changes: 2 additions & 0 deletions superset/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(self, datasource, form_data):
'token', 'token_' + uuid.uuid4().hex[:8])
self.metrics = self.form_data.get('metrics') or []
self.groupby = self.form_data.get('groupby') or []
self.annotation_layers = []

self.status = None
self.error_message = None
Expand Down Expand Up @@ -111,6 +112,7 @@ def get_extra_filters(self):

def query_obj(self):
"""Building a query object"""
print("running query_obj============================")
form_data = self.form_data
gb = form_data.get("groupby") or []
metrics = form_data.get("metrics") or []
Expand Down

0 comments on commit cc29e51

Please sign in to comment.