Skip to content
This repository has been archived by the owner on Jun 3, 2024. It is now read-only.

Commit

Permalink
Merge pull request #461 from bcliang/graph-extend-then-add
Browse files Browse the repository at this point in the history
Enhancement: add property Graph.extendData to support Plotly.extendTraces
  • Loading branch information
alexcjohnson authored Mar 25, 2019
2 parents cd0f9cf + 0c9a480 commit b53cd5d
Show file tree
Hide file tree
Showing 9 changed files with 538 additions and 283 deletions.
14 changes: 11 additions & 3 deletions dash_core_components/Graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class Graph(Component):
when the user zooms or pans on the plot or other
layout-level edits. Has the form `{<attr string>: <value>}`
describing the changes made. Read-only.
- extendData (dict; optional): Data that should be appended to existing traces. Has the form
`[updateData, traceIndices, maxPoints]`, where `updateData` is an object
containing the data to extend, `traceIndices` (optional) is an array of
trace indices that should be extended, and `maxPoints` (optional) is
either an integer defining the maximum number of points allowed or an
object with key:value pairs matching `updateData`
Reference the Plotly.extendTraces API for full usage:
https://plot.ly/javascript/plotlyjs-function-reference/#plotlyextendtraces
- restyleData (list; optional): Data from latest restyle event which occurs
when the user toggles a legend item, changes
parcoords selections, or other trace-level edits.
Expand Down Expand Up @@ -102,12 +110,12 @@ class Graph(Component):
- prop_name (string; optional): Holds which property is loading
- component_name (string; optional): Holds the name of the component that is loading"""
@_explicitize_args
def __init__(self, id=Component.UNDEFINED, clickData=Component.UNDEFINED, clickAnnotationData=Component.UNDEFINED, hoverData=Component.UNDEFINED, clear_on_unhover=Component.UNDEFINED, selectedData=Component.UNDEFINED, relayoutData=Component.UNDEFINED, restyleData=Component.UNDEFINED, figure=Component.UNDEFINED, style=Component.UNDEFINED, className=Component.UNDEFINED, animate=Component.UNDEFINED, animation_options=Component.UNDEFINED, config=Component.UNDEFINED, loading_state=Component.UNDEFINED, **kwargs):
self._prop_names = ['id', 'clickData', 'clickAnnotationData', 'hoverData', 'clear_on_unhover', 'selectedData', 'relayoutData', 'restyleData', 'figure', 'style', 'className', 'animate', 'animation_options', 'config', 'loading_state']
def __init__(self, id=Component.UNDEFINED, clickData=Component.UNDEFINED, clickAnnotationData=Component.UNDEFINED, hoverData=Component.UNDEFINED, clear_on_unhover=Component.UNDEFINED, selectedData=Component.UNDEFINED, relayoutData=Component.UNDEFINED, extendData=Component.UNDEFINED, restyleData=Component.UNDEFINED, figure=Component.UNDEFINED, style=Component.UNDEFINED, className=Component.UNDEFINED, animate=Component.UNDEFINED, animation_options=Component.UNDEFINED, config=Component.UNDEFINED, loading_state=Component.UNDEFINED, **kwargs):
self._prop_names = ['id', 'clickData', 'clickAnnotationData', 'hoverData', 'clear_on_unhover', 'selectedData', 'relayoutData', 'extendData', 'restyleData', 'figure', 'style', 'className', 'animate', 'animation_options', 'config', 'loading_state']
self._type = 'Graph'
self._namespace = 'dash_core_components'
self._valid_wildcard_attributes = []
self.available_properties = ['id', 'clickData', 'clickAnnotationData', 'hoverData', 'clear_on_unhover', 'selectedData', 'relayoutData', 'restyleData', 'figure', 'style', 'className', 'animate', 'animation_options', 'config', 'loading_state']
self.available_properties = ['id', 'clickData', 'clickAnnotationData', 'hoverData', 'clear_on_unhover', 'selectedData', 'relayoutData', 'extendData', 'restyleData', 'figure', 'style', 'className', 'animate', 'animation_options', 'config', 'loading_state']
self.available_wildcard_properties = []

_explicit_args = kwargs.pop('_explicit_args')
Expand Down
384 changes: 221 additions & 163 deletions dash_core_components/dash_core_components.dev.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash_core_components/dash_core_components.dev.js.map

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions dash_core_components/dash_core_components.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dash_core_components/dash_core_components.min.js.map

Large diffs are not rendered by default.

227 changes: 119 additions & 108 deletions dash_core_components/metadata.json

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions src/components/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ class PlotlyGraph extends Component {
);
}

extend(props) {
const {id, extendData} = props;
let updateData, traceIndices, maxPoints;
if (Array.isArray(extendData) && typeof extendData[0] === 'object') {
[updateData, traceIndices, maxPoints] = extendData;
} else {
updateData = extendData;
}

if (!traceIndices) {
function getFirstProp(data) {
return data[Object.keys(data)[0]];
}

function generateIndices(data) {
return Array.from(Array(getFirstProp(data).length).keys());
}
traceIndices = generateIndices(updateData);
}

return Plotly.extendTraces(id, updateData, traceIndices, maxPoints);
}

bindEvents() {
const {id, setProps, clear_on_unhover} = this.props;

Expand Down Expand Up @@ -211,6 +234,13 @@ class PlotlyGraph extends Component {
if (figureChanged) {
this.plot(nextProps);
}

const extendDataChanged =
this.props.extendData !== nextProps.extendData;

if (extendDataChanged) {
this.extend(nextProps);
}
}

componentDidUpdate(prevProps) {
Expand Down Expand Up @@ -279,6 +309,18 @@ const graphPropTypes = {
*/
relayoutData: PropTypes.object,

/**
* Data that should be appended to existing traces. Has the form
* `[updateData, traceIndices, maxPoints]`, where `updateData` is an object
* containing the data to extend, `traceIndices` (optional) is an array of
* trace indices that should be extended, and `maxPoints` (optional) is
* either an integer defining the maximum number of points allowed or an
* object with key:value pairs matching `updateData`
* Reference the Plotly.extendTraces API for full usage:
* https://plot.ly/javascript/plotlyjs-function-reference/#plotlyextendtraces
*/
extendData: PropTypes.object,

/**
* Data from latest restyle event which occurs
* when the user toggles a legend item, changes
Expand Down Expand Up @@ -527,6 +569,7 @@ const graphDefaultProps = {
hoverData: null,
selectedData: null,
relayoutData: null,
extendData: null,
restyleData: null,
figure: {data: [], layout: {}},
animate: false,
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ export {
DatePickerRange,
Upload,
Store,
LogoutButton
LogoutButton,
};
135 changes: 135 additions & 0 deletions test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,141 @@ def render_content(click, prev_graph):
time.sleep(2) # Wait for graph to re-render
self.snapshot('render-empty-graph')

def test_graph_extend_trace(self):
app = dash.Dash(__name__)

def generate_with_id(id, data=None):
if data is None:
data = [{'x': [0, 1, 2, 3, 4],
'y': [0, .5, 1, .5, 0]
}]

return html.Div([html.P(id),
dcc.Graph(id=id,
figure=dict(data=data)),
html.Div(id='output_{}'.format(id))])

figs = ['trace_will_extend',
'trace_will_extend_with_no_indices',
'trace_will_extend_with_max_points']

layout = [generate_with_id(id) for id in figs]

figs.append('trace_will_allow_repeated_extend')
data = [{'y': [0, 0, 0]}]
layout.append(generate_with_id(figs[-1], data))

figs.append('trace_will_extend_selectively')
data = [{'x': [0, 1, 2, 3, 4], 'y': [0, .5, 1, .5, 0]},
{'x': [0, 1, 2, 3, 4], 'y': [1, 1, 1, 1, 1]}]
layout.append(generate_with_id(figs[-1], data))

layout.append(dcc.Interval(
id='interval_extendablegraph_update',
interval=10,
n_intervals=0,
max_intervals=1))

layout.append(dcc.Interval(
id='interval_extendablegraph_extendtwice',
interval=500,
n_intervals=0,
max_intervals=2))

app.layout = html.Div(layout)

@app.callback(Output('trace_will_allow_repeated_extend', 'extendData'),
[Input('interval_extendablegraph_extendtwice', 'n_intervals')])
def trace_will_allow_repeated_extend(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

return dict(y=[[.1, .2, .3, .4, .5]])

@app.callback(Output('trace_will_extend', 'extendData'),
[Input('interval_extendablegraph_update', 'n_intervals')])
def trace_will_extend(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

x_new = [5, 6, 7, 8, 9]
y_new = [.1, .2, .3, .4, .5]
return dict(x=[x_new], y=[y_new]), [0]

@app.callback(Output('trace_will_extend_selectively', 'extendData'),
[Input('interval_extendablegraph_update', 'n_intervals')])
def trace_will_extend_selectively(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

x_new = [5, 6, 7, 8, 9]
y_new = [.1, .2, .3, .4, .5]
return dict(x=[x_new], y=[y_new]), [1]

@app.callback(Output('trace_will_extend_with_no_indices', 'extendData'),
[Input('interval_extendablegraph_update', 'n_intervals')])
def trace_will_extend_with_no_indices(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

x_new = [5, 6, 7, 8, 9]
y_new = [.1, .2, .3, .4, .5]
return dict(x=[x_new], y=[y_new])

@app.callback(Output('trace_will_extend_with_max_points', 'extendData'),
[Input('interval_extendablegraph_update', 'n_intervals')])
def trace_will_extend_with_max_points(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

x_new = [5, 6, 7, 8, 9]
y_new = [.1, .2, .3, .4, .5]
return dict(x=[x_new], y=[y_new]), [0], 7

for id in figs:
@app.callback(Output('output_{}'.format(id), 'children'),
[Input(id, 'extendData')],
[State(id, 'figure')])
def display_data(trigger, fig):
return json.dumps(fig['data'])

self.startServer(app)

comparison = json.dumps([
dict(
x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
y=[0, .5, 1, .5, 0, .1, .2, .3, .4, .5]
)
])
self.wait_for_text_to_equal('#output_trace_will_extend', comparison)
self.wait_for_text_to_equal('#output_trace_will_extend_with_no_indices', comparison)
comparison = json.dumps([
dict(
x=[0, 1, 2, 3, 4],
y=[0, .5, 1, .5, 0]
),
dict(
x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
y=[1, 1, 1, 1, 1, .1, .2, .3, .4, .5]
)
])
self.wait_for_text_to_equal('#output_trace_will_extend_selectively', comparison)

comparison = json.dumps([
dict(
x=[3, 4, 5, 6, 7, 8, 9],
y=[.5, 0, .1, .2, .3, .4, .5]
)
])
self.wait_for_text_to_equal('#output_trace_will_extend_with_max_points', comparison)

comparison = json.dumps([
dict(
y=[0, 0, 0, .1, .2, .3, .4, .5, .1, .2, .3, .4, .5]
)
])
self.wait_for_text_to_equal('#output_trace_will_allow_repeated_extend', comparison)

def test_storage_component(self):
app = dash.Dash(__name__)

Expand Down

0 comments on commit b53cd5d

Please sign in to comment.