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

Commit 6f5269a

Browse files
committed
Enhancement: add property Graph.prependData to support Plotly.prependTraces
1 parent 0ceb2de commit 6f5269a

File tree

3 files changed

+294
-0
lines changed

3 files changed

+294
-0
lines changed

src/components/Graph.react.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
privateDefaultProps,
1010
} from '../fragments/Graph.privateprops';
1111

12+
const EMPTY_PREPEND_DATA = [];
1213
const EMPTY_EXTEND_DATA = [];
1314

1415
/**
@@ -22,13 +23,20 @@ class PlotlyGraph extends Component {
2223
super(props);
2324

2425
this.state = {
26+
prependData: [],
2527
extendData: [],
2628
};
2729

30+
this.clearPrependData = this.clearPrependData.bind(this);
2831
this.clearExtendData = this.clearExtendData.bind(this);
2932
}
3033

3134
componentDidMount() {
35+
if (this.props.prependData) {
36+
this.setState({
37+
prependData: [this.props.prependData],
38+
});
39+
}
3240
if (this.props.extendData) {
3341
this.setState({
3442
extendData: [this.props.extendData],
@@ -38,11 +46,33 @@ class PlotlyGraph extends Component {
3846

3947
componentWillUnmount() {
4048
this.setState({
49+
prependData: [],
4150
extendData: [],
4251
});
4352
}
4453

4554
UNSAFE_componentWillReceiveProps(nextProps) {
55+
let prependData = this.state.prependData.slice(0);
56+
57+
if (this.props.figure !== nextProps.figure) {
58+
prependData = EMPTY_PREPEND_DATA;
59+
}
60+
61+
if (
62+
nextProps.prependData &&
63+
this.props.prependData !== nextProps.prependData
64+
) {
65+
prependData.push(nextProps.prependData);
66+
} else {
67+
prependData = EMPTY_PREPEND_DATA;
68+
}
69+
70+
if (prependData !== EMPTY_PREPEND_DATA) {
71+
this.setState({
72+
prependData,
73+
});
74+
}
75+
4676
let extendData = this.state.extendData.slice(0);
4777

4878
if (this.props.figure !== nextProps.figure) {
@@ -65,6 +95,19 @@ class PlotlyGraph extends Component {
6595
}
6696
}
6797

98+
clearPrependData() {
99+
this.setState(({prependData}) => {
100+
const res =
101+
prependData && prependData.length
102+
? {
103+
prependData: EMPTY_PREPEND_DATA,
104+
}
105+
: undefined;
106+
107+
return res;
108+
});
109+
}
110+
68111
clearExtendData() {
69112
this.setState(({extendData}) => {
70113
const res =
@@ -82,6 +125,8 @@ class PlotlyGraph extends Component {
82125
return (
83126
<ControlledPlotlyGraph
84127
{...this.props}
128+
prependData={this.state.prependData}
129+
clearPrependData={this.clearPrependData}
85130
extendData={this.state.extendData}
86131
clearExtendData={this.clearExtendData}
87132
/>
@@ -191,6 +236,18 @@ PlotlyGraph.propTypes = {
191236
*/
192237
extendData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
193238

239+
/**
240+
* Data that should be prepended to existing traces. Has the form
241+
* `[updateData, traceIndices, maxPoints]`, where `updateData` is an object
242+
* containing the data to prepend, `traceIndices` (optional) is an array of
243+
* trace indices that should be prepended, and `maxPoints` (optional) is
244+
* either an integer defining the maximum number of points allowed or an
245+
* object with key:value pairs matching `updateData`
246+
* Reference the Plotly.prependTraces API for full usage:
247+
* https://plotly.com/javascript/plotlyjs-function-reference/#plotlyprependtraces
248+
*/
249+
prependData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
250+
194251
/**
195252
* Data from latest restyle event which occurs
196253
* when the user toggles a legend item, changes
@@ -523,6 +580,7 @@ PlotlyGraph.defaultProps = {
523580
hoverData: null,
524581
selectedData: null,
525582
relayoutData: null,
583+
prependData: null,
526584
extendData: null,
527585
restyleData: null,
528586
figure: {

src/fragments/Graph.react.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,42 @@ class PlotlyGraph extends Component {
188188
});
189189
}
190190

191+
prepend(props) {
192+
const {clearPrependData, prependData: prependDataArray} = props;
193+
194+
prependDataArray.forEach(prependData => {
195+
let updateData, traceIndices, maxPoints;
196+
if (
197+
Array.isArray(prependData) &&
198+
typeof prependData[0] === 'object'
199+
) {
200+
[updateData, traceIndices, maxPoints] = prependData;
201+
} else {
202+
updateData = prependData;
203+
}
204+
205+
if (!traceIndices) {
206+
function getFirstProp(data) {
207+
return data[Object.keys(data)[0]];
208+
}
209+
210+
function generateIndices(data) {
211+
return Array.from(Array(getFirstProp(data).length).keys());
212+
}
213+
traceIndices = generateIndices(updateData);
214+
}
215+
216+
const gd = this.gd.current;
217+
return Plotly.prependTraces(
218+
gd,
219+
updateData,
220+
traceIndices,
221+
maxPoints
222+
);
223+
});
224+
clearPrependData();
225+
}
226+
191227
extend(props) {
192228
const {clearExtendData, extendData: extendDataArray} = props;
193229

@@ -348,6 +384,9 @@ class PlotlyGraph extends Component {
348384

349385
componentDidMount() {
350386
this.plot(this.props);
387+
if (this.props.prependData) {
388+
this.prepend(this.props);
389+
}
351390
if (this.props.extendData) {
352391
this.extend(this.props);
353392
}
@@ -392,6 +431,10 @@ class PlotlyGraph extends Component {
392431
this.plot(nextProps);
393432
}
394433

434+
if (this.props.prependData !== nextProps.prependData) {
435+
this.prepend(nextProps);
436+
}
437+
395438
if (this.props.extendData !== nextProps.extendData) {
396439
this.extend(nextProps);
397440
}
@@ -432,14 +475,19 @@ class PlotlyGraph extends Component {
432475

433476
PlotlyGraph.propTypes = {
434477
...graphPropTypes,
478+
prependData: PropTypes.arrayOf(
479+
PropTypes.oneOfType([PropTypes.array, PropTypes.object])
480+
),
435481
extendData: PropTypes.arrayOf(
436482
PropTypes.oneOfType([PropTypes.array, PropTypes.object])
437483
),
484+
clearPrependData: PropTypes.func.isRequired,
438485
clearExtendData: PropTypes.func.isRequired,
439486
};
440487

441488
PlotlyGraph.defaultProps = {
442489
...graphDefaultProps,
490+
prependData: [],
443491
extendData: [],
444492
};
445493

tests/integration/graph/test_graph_varia.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,194 @@ def render_content(click, prev_graph):
203203
dash_dcc.percy_snapshot("render-empty-graph ({})".format("eager" if is_eager else "lazy"))
204204

205205

206+
@pytest.mark.parametrize("is_eager", [True, False])
207+
def test_graph_prepend_trace(dash_dcc, is_eager):
208+
app = dash.Dash(__name__, eager_loading=is_eager)
209+
210+
def generate_with_id(id, data=None):
211+
if data is None:
212+
data = [{"x": [10, 11, 12, 13, 14], "y": [0, 0.5, 1, 0.5, 0]}]
213+
214+
return html.Div(
215+
[
216+
html.P(id),
217+
dcc.Graph(id=id, figure=dict(data=data)),
218+
html.Div(id="output_{}".format(id)),
219+
]
220+
)
221+
222+
figs = [
223+
"trace_will_prepend",
224+
"trace_will_prepend_with_no_indices",
225+
"trace_will_prepend_with_max_points",
226+
]
227+
228+
layout = [generate_with_id(id) for id in figs]
229+
230+
figs.append("trace_will_allow_repeated_prepend")
231+
data = [{"y": [0, 0, 0]}]
232+
layout.append(generate_with_id(figs[-1], data))
233+
234+
figs.append("trace_will_prepend_selectively")
235+
data = [
236+
{"x": [10, 11, 12, 13, 14], "y": [0, 0.5, 1, 0.5, 0]},
237+
{"x": [10, 11, 12, 13, 14], "y": [1, 1, 1, 1, 1]},
238+
]
239+
layout.append(generate_with_id(figs[-1], data))
240+
241+
layout.append(
242+
dcc.Interval(
243+
id="interval_prependablegraph_update",
244+
interval=10,
245+
n_intervals=0,
246+
max_intervals=1,
247+
)
248+
)
249+
250+
layout.append(
251+
dcc.Interval(
252+
id="interval_prependablegraph_prependtwice",
253+
interval=500,
254+
n_intervals=0,
255+
max_intervals=2,
256+
)
257+
)
258+
259+
app.layout = html.Div(layout)
260+
261+
@app.callback(
262+
Output("trace_will_allow_repeated_prepend", "prependData"),
263+
[Input("interval_prependablegraph_prependtwice", "n_intervals")],
264+
)
265+
def trace_will_allow_repeated_prepend(n_intervals):
266+
if n_intervals is None or n_intervals < 1:
267+
raise PreventUpdate
268+
269+
return dict(y=[[0.1, 0.2, 0.3, 0.4, 0.5]])
270+
271+
@app.callback(
272+
Output("trace_will_prepend", "prependData"),
273+
[Input("interval_prependablegraph_update", "n_intervals")],
274+
)
275+
def trace_will_prepend(n_intervals):
276+
if n_intervals is None or n_intervals < 1:
277+
raise PreventUpdate
278+
279+
x_new = [5, 6, 7, 8, 9]
280+
y_new = [0.1, 0.2, 0.3, 0.4, 0.5]
281+
return dict(x=[x_new], y=[y_new]), [0]
282+
283+
@app.callback(
284+
Output("trace_will_prepend_selectively", "prependData"),
285+
[Input("interval_prependablegraph_update", "n_intervals")],
286+
)
287+
def trace_will_prepend_selectively(n_intervals):
288+
if n_intervals is None or n_intervals < 1:
289+
raise PreventUpdate
290+
291+
x_new = [5, 6, 7, 8, 9]
292+
y_new = [0.1, 0.2, 0.3, 0.4, 0.5]
293+
return dict(x=[x_new], y=[y_new]), [1]
294+
295+
@app.callback(
296+
Output("trace_will_prepend_with_no_indices", "prependData"),
297+
[Input("interval_prependablegraph_update", "n_intervals")],
298+
)
299+
def trace_will_prepend_with_no_indices(n_intervals):
300+
if n_intervals is None or n_intervals < 1:
301+
raise PreventUpdate
302+
303+
x_new = [5, 6, 7, 8, 9]
304+
y_new = [0.1, 0.2, 0.3, 0.4, 0.5]
305+
return dict(x=[x_new], y=[y_new])
306+
307+
@app.callback(
308+
Output("trace_will_prepend_with_max_points", "prependData"),
309+
[Input("interval_prependablegraph_update", "n_intervals")],
310+
)
311+
def trace_will_prepend_with_max_points(n_intervals):
312+
if n_intervals is None or n_intervals < 1:
313+
raise PreventUpdate
314+
315+
x_new = [5, 6, 7, 8, 9]
316+
y_new = [0.1, 0.2, 0.3, 0.4, 0.5]
317+
return dict(x=[x_new], y=[y_new]), [0], 7
318+
319+
for id in figs:
320+
321+
@app.callback(
322+
Output("output_{}".format(id), "children"),
323+
[Input(id, "prependData")],
324+
[State(id, "figure")],
325+
)
326+
def display_data(trigger, fig):
327+
return json.dumps(fig["data"])
328+
329+
dash_dcc.start_server(app)
330+
331+
comparison = json.dumps(
332+
[
333+
dict(
334+
x=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
335+
y=[0.1, 0.2, 0.3, 0.4, 0.5, 0, 0.5, 1, 0.5, 0],
336+
)
337+
]
338+
)
339+
dash_dcc.wait_for_text_to_equal("#output_trace_will_prepend", comparison)
340+
dash_dcc.wait_for_text_to_equal(
341+
"#output_trace_will_prepend_with_no_indices", comparison
342+
)
343+
comparison = json.dumps(
344+
[
345+
dict(x=[10, 11, 12, 13, 14], y=[0, 0.5, 1, 0.5, 0]),
346+
dict(
347+
x=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
348+
y=[0.1, 0.2, 0.3, 0.4, 0.5, 1, 1, 1, 1, 1],
349+
),
350+
]
351+
)
352+
dash_dcc.wait_for_text_to_equal(
353+
"#output_trace_will_prepend_selectively", comparison
354+
)
355+
356+
comparison = json.dumps(
357+
[
358+
dict(
359+
x=[5, 6, 7, 8, 9, 10, 11],
360+
y=[0.1, 0.2, 0.3, 0.4, 0.5, 0, 0.5],
361+
)
362+
]
363+
)
364+
dash_dcc.wait_for_text_to_equal(
365+
"#output_trace_will_prepend_with_max_points", comparison
366+
)
367+
368+
comparison = json.dumps(
369+
[
370+
dict(
371+
y=[
372+
0.1,
373+
0.2,
374+
0.3,
375+
0.4,
376+
0.5,
377+
0.1,
378+
0.2,
379+
0.3,
380+
0.4,
381+
0.5,
382+
0,
383+
0,
384+
0,
385+
]
386+
)
387+
]
388+
)
389+
dash_dcc.wait_for_text_to_equal(
390+
"#output_trace_will_allow_repeated_prepend", comparison
391+
)
392+
393+
206394
@pytest.mark.parametrize("is_eager", [True, False])
207395
def test_graph_extend_trace(dash_dcc, is_eager):
208396
app = dash.Dash(__name__, eager_loading=is_eager)

0 commit comments

Comments
 (0)