Skip to content
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

Prioritised Trace Updates #253

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,12 @@ cython_debug/
# sphinx-docs
sphinx/_build
sphinx/_autosummary

#testing stuff
logs/
selectionbox_layout_data/
examples/dash_apps/**/*coarse_fine*
examples/dash_apps/2*
examples/dash_apps/00*
tests/log_processing*.ipynb
figure data/
25 changes: 20 additions & 5 deletions examples/dash_apps/01_minimal_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@
import numpy as np
import plotly.graph_objects as go
from dash import Dash, Input, Output, callback_context, dcc, html, no_update

# from graph_reporter import GraphReporter
from trace_updater import TraceUpdater

from plotly_resampler import FigureResampler

# Data that will be used for the plotly-resampler figures
x = np.arange(2_000_000)
n = 500_000
x = np.arange(n)
noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000

flat = np.ones(n)

# --------------------------------------Globals ---------------------------------------
app = Dash(__name__)
fig: FigureResampler = FigureResampler()
fig: FigureResampler = FigureResampler(verbose=False)
# NOTE: in this example, this reference to a FigureResampler is essential to preserve
# throughout the whole dash app! If your dash app wants to create a new go.Figure(),
# you should not construct a new FigureResampler object, but replace the figure of this
Expand All @@ -39,8 +42,11 @@
html.Button("plot chart", id="plot-button", n_clicks=0),
html.Hr(),
# The graph and it's needed components to update efficiently
dcc.Store(id="visible-indices", data={"visible": [], "invisible": []}),
dcc.Graph(id="graph-id"),
TraceUpdater(id="trace-updater", gdID="graph-id"),
# GraphReporter(id="graph-reporter", gId="graph-id"),
# html.Div(id='print')
]
)

Expand All @@ -49,6 +55,7 @@
# The callback used to construct and store the graph's data on the serverside
@app.callback(
Output("graph-id", "figure"),
# Output("visible-indices", "data"),
Input("plot-button", "n_clicks"),
prevent_initial_call=True,
)
Expand All @@ -62,14 +69,22 @@ def plot_graph(n_clicks):
fig.replace(go.Figure())
fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * 0.9999995**x)
fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=noisy_sin * 1.000002**x)
fig.add_trace(go.Scattergl(name="const"), hf_x=x, hf_y=flat)
fig.add_trace(go.Scattergl(name="poly"), hf_x=x, hf_y=noisy_sin * 1.000002**2)

# fig._print_verbose = True
fig.update_layout(showlegend=True)
return fig
else:
return no_update
return no_update, no_update


# Register the graph update callbacks to the layout
fig.register_update_graph_callback(
app=app, graph_id="graph-id", trace_updater_id="trace-updater"
app=app,
graph_id="graph-id",
trace_updater_id="trace-updater",
visibility_store_id="visible-indices",
)

# --------------------------------- Running the app ---------------------------------
Expand Down
13 changes: 13 additions & 0 deletions node_modules/.package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "plotly-resampler",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}
94 changes: 91 additions & 3 deletions plotly_resampler/figure_resampler/figure_resampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

"""


from __future__ import annotations

__author__ = "Jonas Van Der Donckt, Jeroen Van Der Donckt, Emiel Deprost"
Expand All @@ -15,6 +16,8 @@

import dash
import plotly.graph_objects as go

# from graph_reporter import GraphReporter
from plotly.basedatatypes import BaseFigure
from trace_updater import TraceUpdater

Expand Down Expand Up @@ -200,6 +203,7 @@ def show_dash(
self,
mode=None,
config: dict | None = None,
testing: bool | None = False,
graph_properties: dict | None = None,
**kwargs,
):
Expand Down Expand Up @@ -298,15 +302,24 @@ def show_dash(
app = dash.Dash("local_app")
app.layout = dash.html.Div(
[
dash.dcc.Store(
id="visible-indices", data={"visible": [], "invisible": []}
),
dash.dcc.Graph(
id="resample-figure", figure=self, config=config, **graph_properties
),
TraceUpdater(
id="trace-updater", gdID="resample-figure", sequentialUpdate=False
id="trace-updater",
gdID="resample-figure",
sequentialUpdate=False,
verbose=testing,
),
# GraphReporter(id="graph-reporter", gId="resample-figure"),
]
)
self.register_update_graph_callback(app, "resample-figure", "trace-updater")
self.register_update_graph_callback(
app, "resample-figure", "trace-updater", "visible-indices"
)

height_param = "height" if self._is_persistent_inline else "jupyter_height"

Expand Down Expand Up @@ -365,8 +378,14 @@ def stop_server(self, warn: bool = True):
+ "\t- the dash-server wasn't started with 'show_dash'"
)

# TODO: check if i should put the clientside callback to fill the store here or in a different function
# for now, here
def register_update_graph_callback(
self, app: dash.Dash, graph_id: str, trace_updater_id: str
self,
app: dash.Dash,
graph_id: str,
trace_updater_id: str,
visibility_store_id: str,
):
"""Register the [`construct_update_data`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.construct_update_data] method as callback function to
the passed dash-app.
Expand All @@ -382,14 +401,83 @@ def register_update_graph_callback(
The id of the ``TraceUpdater`` component. This component is leveraged by
``FigureResampler`` to efficiently POST the to-be-updated data to the
front-end.
visibility_store_id
The id of the ``dcc.Store`` component which holds the indices of the visible
traces in the client. Leveraged to efficiently perform the asynchronous update of
the visible and invisible traces of the ``Graph``.

"""
# Callback triggers when a stylistic change is made to the graph
# this includes hiding traces or making them visible again, which is the
# desired use-case
app.clientside_callback(
"""
function(restyleData, gdID) {
// HELPER FUNCTIONS

function getGraphDiv(gdID){
// see this link for more information https://stackoverflow.com/a/34002028
let graphDiv = document?.querySelectorAll('div[id*="' + gdID + '"][class*="dash-graph"]');
if (graphDiv.length > 1) {
throw new SyntaxError("UpdateStore: multiple graphs with ID=" + gdID + " found; n=" + graphDiv.length + " (either multiple graphs with same ID's or current ID is a str-subset of other graph IDs)");
} else if (graphDiv.length < 1) {
throw new SyntaxError("UpdateStore: no graphs with ID=" + gdID + " found");
}
graphDiv = graphDiv?.[0]?.getElementsByClassName('js-plotly-plot')?.[0];
const isDOMElement = el => el instanceof HTMLElement
if (!isDOMElement) {
throw new Error(`Invalid gdID '${gdID}'`);
}
return graphDiv;
}

//MAIN CALLBACK
let storeData = {'visible':[], 'invisible':[]};
if (restyleData) {
let graphDiv = getGraphDiv(gdID);

//console.log("restyleData:");
//console.log(restyleData);
//console.log("\tgraph data -> visibility of traces: ");

let visible_traces = [];
let invisible_traces = [];
graphDiv.data.forEach((trace, index) => {
//console.log('\tvisible: ' + trace.visible);
if (trace.visible == true || trace.visible == undefined) {
visible_traces.push(index);
} else {
invisible_traces.push(index);
}
});
storeData = {'visible':visible_traces, 'invisible':invisible_traces};
}
//console.log(storeData);
return storeData;
}
""",
dash.dependencies.Output(visibility_store_id, "data"),
dash.dependencies.Input(graph_id, "restyleData"),
dash.dependencies.State(graph_id, "id"),
)

app.callback(
dash.dependencies.Output(trace_updater_id, "updateData"),
dash.dependencies.Input(graph_id, "relayoutData"),
# dash.dependencies.State(graph_id, "restyleData"),
dash.dependencies.State(visibility_store_id, "data"),
prevent_initial_call=True,
)(self.construct_update_data)

app.callback(
dash.dependencies.Output(trace_updater_id, "invisibleUpdateData"),
# dash.dependencies.Input(trace_updater_id, "updateData"),
dash.dependencies.Input(trace_updater_id, "visibleUpdate"),
dash.dependencies.State(graph_id, "relayoutData"),
dash.dependencies.State(visibility_store_id, "data"),
prevent_initial_call=True,
)(self.construct_invisible_update_data)

def _get_pr_props_keys(self) -> List[str]:
# Add the additional plotly-resampler properties of this class
return super()._get_pr_props_keys() + ["_show_dash_kwargs"]
Expand Down
Loading