-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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
Changes from 5 commits
8ed1f4b
e83c0e4
6aaa4bc
3aba1f2
cc29e51
1313151
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -476,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 : '')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should surface more here, the layer name, the short and the long description. |
||
|
||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looked like this color is |
||
.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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: I think the annotations look good on the gif, we can talk to Eli & Chris to fine tune their look if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. class could be called |
||
.attr('stroke-width', 1) | ||
.attr('stroke', '#00A699') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, let's not hard code the color |
||
.on('mouseover', tip.show) | ||
.on('mouseout', tip.hide); | ||
|
||
annotationLayer.selectAll('rect').call(tip); | ||
} | ||
} | ||
|
||
// on scroll, hide tooltips. throttle to only 4x/second. | ||
|
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 |
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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bigint? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""empty message | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised to see 2 different merge migrations. Legit or mistake? |
||
|
||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many of the other controls to not maintain internal state and rely solely on the
value
prop andonChange
to act as a controlled component. There's nothing bad with maintaining the state here but it adds a bid of complexity that is not needed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.