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

Commit

Permalink
Enhancement: add property Graph.prependData to support Plotly.prepend…
Browse files Browse the repository at this point in the history
…Traces
  • Loading branch information
sleighsoft committed Aug 25, 2020
1 parent 0ceb2de commit 4a09f70
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/components/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
privateDefaultProps,
} from '../fragments/Graph.privateprops';

const EMPTY_PREPEND_DATA = [];
const EMPTY_EXTEND_DATA = [];

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

this.state = {
prependData: [],
extendData: [],
};

this.clearPrependData = this.clearPrependData.bind(this);
this.clearExtendData = this.clearExtendData.bind(this);
}

componentDidMount() {
if (this.props.prependData) {
this.setState({
prependData: [this.props.prependData],
});
}
if (this.props.extendData) {
this.setState({
extendData: [this.props.extendData],
Expand All @@ -38,11 +46,33 @@ class PlotlyGraph extends Component {

componentWillUnmount() {
this.setState({
prependData: [],
extendData: [],
});
}

UNSAFE_componentWillReceiveProps(nextProps) {
let prependData = this.state.prependData.slice(0);

if (this.props.figure !== nextProps.figure) {
prependData = EMPTY_PREPEND_DATA;
}

if (
nextProps.prependData &&
this.props.prependData !== nextProps.prependData
) {
prependData.push(nextProps.prependData);
} else {
prependData = EMPTY_PREPEND_DATA;
}

if (prependData !== EMPTY_PREPEND_DATA) {
this.setState({
prependData,
});
}

let extendData = this.state.extendData.slice(0);

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

clearPrependData() {
this.setState(({prependData}) => {
const res =
prependData && prependData.length
? {
prependData: EMPTY_PREPEND_DATA,
}
: undefined;

return res;
});
}

clearExtendData() {
this.setState(({extendData}) => {
const res =
Expand All @@ -82,6 +125,8 @@ class PlotlyGraph extends Component {
return (
<ControlledPlotlyGraph
{...this.props}
prependData={this.state.prependData}
clearPrependData={this.clearPrependData}
extendData={this.state.extendData}
clearExtendData={this.clearExtendData}
/>
Expand Down Expand Up @@ -191,6 +236,18 @@ PlotlyGraph.propTypes = {
*/
extendData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),

/**
* Data that should be prepended to existing traces. Has the form
* `[updateData, traceIndices, maxPoints]`, where `updateData` is an object
* containing the data to prepend, `traceIndices` (optional) is an array of
* trace indices that should be prepended, 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.prependTraces API for full usage:
* https://plotly.com/javascript/plotlyjs-function-reference/#plotlyprependtraces
*/
prependData: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),

/**
* Data from latest restyle event which occurs
* when the user toggles a legend item, changes
Expand Down Expand Up @@ -523,6 +580,7 @@ PlotlyGraph.defaultProps = {
hoverData: null,
selectedData: null,
relayoutData: null,
prependData: null,
extendData: null,
restyleData: null,
figure: {
Expand Down
48 changes: 48 additions & 0 deletions src/fragments/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,42 @@ class PlotlyGraph extends Component {
});
}

prepend(props) {
const {clearPrependData, prependData: prependDataArray} = props;

prependDataArray.forEach(prependData => {
let updateData, traceIndices, maxPoints;
if (
Array.isArray(prependData) &&
typeof prependData[0] === 'object'
) {
[updateData, traceIndices, maxPoints] = prependData;
} else {
updateData = prependData;
}

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);
}

const gd = this.gd.current;
return Plotly.prependTraces(
gd,
updateData,
traceIndices,
maxPoints
);
});
clearPrependData();
}

extend(props) {
const {clearExtendData, extendData: extendDataArray} = props;

Expand Down Expand Up @@ -348,6 +384,9 @@ class PlotlyGraph extends Component {

componentDidMount() {
this.plot(this.props);
if (this.props.prependData) {
this.prepend(this.props);
}
if (this.props.extendData) {
this.extend(this.props);
}
Expand Down Expand Up @@ -392,6 +431,10 @@ class PlotlyGraph extends Component {
this.plot(nextProps);
}

if (this.props.prependData !== nextProps.prependData) {
this.prepend(nextProps);
}

if (this.props.extendData !== nextProps.extendData) {
this.extend(nextProps);
}
Expand Down Expand Up @@ -432,14 +475,19 @@ class PlotlyGraph extends Component {

PlotlyGraph.propTypes = {
...graphPropTypes,
prependData: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.array, PropTypes.object])
),
extendData: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.array, PropTypes.object])
),
clearPrependData: PropTypes.func.isRequired,
clearExtendData: PropTypes.func.isRequired,
};

PlotlyGraph.defaultProps = {
...graphDefaultProps,
prependData: [],
extendData: [],
};

Expand Down
188 changes: 188 additions & 0 deletions tests/integration/graph/test_graph_varia.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,194 @@ def render_content(click, prev_graph):
dash_dcc.percy_snapshot("render-empty-graph ({})".format("eager" if is_eager else "lazy"))


@pytest.mark.parametrize("is_eager", [True, False])
def test_graph_prepend_trace(dash_dcc, is_eager):
app = dash.Dash(__name__, eager_loading=is_eager)

def generate_with_id(id, data=None):
if data is None:
data = [{"x": [10, 11, 12, 13, 14], "y": [0, 0.5, 1, 0.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_prepend",
"trace_will_prepend_with_no_indices",
"trace_will_prepend_with_max_points",
]

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

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

figs.append("trace_will_prepend_selectively")
data = [
{"x": [10, 11, 12, 13, 14], "y": [0, 0.5, 1, 0.5, 0]},
{"x": [10, 11, 12, 13, 14], "y": [1, 1, 1, 1, 1]},
]
layout.append(generate_with_id(figs[-1], data))

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

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

app.layout = html.Div(layout)

@app.callback(
Output("trace_will_allow_repeated_prepend", "prependData"),
[Input("interval_prependablegraph_prependtwice", "n_intervals")],
)
def trace_will_allow_repeated_prepend(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

return dict(y=[[0.1, 0.2, 0.3, 0.4, 0.5]])

@app.callback(
Output("trace_will_prepend", "prependData"),
[Input("interval_prependablegraph_update", "n_intervals")],
)
def trace_will_prepend(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

x_new = [5, 6, 7, 8, 9]
y_new = [0.1, 0.2, 0.3, 0.4, 0.5]
return dict(x=[x_new], y=[y_new]), [0]

@app.callback(
Output("trace_will_prepend_selectively", "prependData"),
[Input("interval_prependablegraph_update", "n_intervals")],
)
def trace_will_prepend_selectively(n_intervals):
if n_intervals is None or n_intervals < 1:
raise PreventUpdate

x_new = [5, 6, 7, 8, 9]
y_new = [0.1, 0.2, 0.3, 0.4, 0.5]
return dict(x=[x_new], y=[y_new]), [1]

@app.callback(
Output("trace_will_prepend_with_no_indices", "prependData"),
[Input("interval_prependablegraph_update", "n_intervals")],
)
def trace_will_prepend_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 = [0.1, 0.2, 0.3, 0.4, 0.5]
return dict(x=[x_new], y=[y_new])

@app.callback(
Output("trace_will_prepend_with_max_points", "prependData"),
[Input("interval_prependablegraph_update", "n_intervals")],
)
def trace_will_prepend_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 = [0.1, 0.2, 0.3, 0.4, 0.5]
return dict(x=[x_new], y=[y_new]), [0], 7

for id in figs:

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

dash_dcc.start_server(app)

comparison = json.dumps(
[
dict(
x=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
y=[0.1, 0.2, 0.3, 0.4, 0.5, 0, 0.5, 1, 0.5, 0],
)
]
)
dash_dcc.wait_for_text_to_equal("#output_trace_will_prepend", comparison)
dash_dcc.wait_for_text_to_equal(
"#output_trace_will_prepend_with_no_indices", comparison
)
comparison = json.dumps(
[
dict(x=[10, 11, 12, 13, 14], y=[0, 0.5, 1, 0.5, 0]),
dict(
x=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
y=[0.1, 0.2, 0.3, 0.4, 0.5, 1, 1, 1, 1, 1],
),
]
)
dash_dcc.wait_for_text_to_equal(
"#output_trace_will_prepend_selectively", comparison
)

comparison = json.dumps(
[
dict(
x=[5, 6, 7, 8, 9, 10, 11],
y=[0.1, 0.2, 0.3, 0.4, 0.5, 0, 0.5],
)
]
)
dash_dcc.wait_for_text_to_equal(
"#output_trace_will_prepend_with_max_points", comparison
)

comparison = json.dumps(
[
dict(
y=[
0.1,
0.2,
0.3,
0.4,
0.5,
0.1,
0.2,
0.3,
0.4,
0.5,
0,
0,
0,
]
)
]
)
dash_dcc.wait_for_text_to_equal(
"#output_trace_will_allow_repeated_prepend", comparison
)


@pytest.mark.parametrize("is_eager", [True, False])
def test_graph_extend_trace(dash_dcc, is_eager):
app = dash.Dash(__name__, eager_loading=is_eager)
Expand Down

0 comments on commit 4a09f70

Please sign in to comment.