Skip to content

Commit 50a2a2b

Browse files
committed
✨ improve registering implementation
1 parent 2b7f2e7 commit 50a2a2b

File tree

6 files changed

+158
-75
lines changed

6 files changed

+158
-75
lines changed

plotly_resampler/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .aggregation import LTTB, EfficientLTTB, EveryNthPoint
44
from .figure_resampler import FigureResampler, FigureWidgetResampler
5-
from .module import register_plotly_resampler, unregister_plotly_resampler
5+
from .registering import register_plotly_resampler, unregister_plotly_resampler
66

77
__docformat__ = "numpy"
88
__author__ = "Jonas Van Der Donckt, Jeroen Van Der Donckt, Emiel Deprost"

plotly_resampler/figure_resampler/figure_resampler.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __init__(
4848
else:
4949
# Create a new figure object and make sure that the trace uid will not get
5050
# adjusted when they are added.
51-
f = go.Figure()
51+
f = self._get_figure_class(go.Figure)()
5252
f._data_validator.set_uid = False
5353

5454
if isinstance(figure, BaseFigure): # go.FigureWidget or AbstractFigureAggregator
@@ -92,6 +92,12 @@ def __init__(
9292
self._port: int | None = None
9393
self._host: str | None = None
9494

95+
# @staticmethod
96+
# def _get_figure_class() -> type:
97+
# """Return the class of the underlying figure."""
98+
# from ..module import get_plotly_constr
99+
# return get_plotly_constr(go.Figure)
100+
95101
def show_dash(
96102
self,
97103
mode=None,

plotly_resampler/figure_resampler/figure_resampler_interface.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ def __init__(
9696

9797
self._global_downsampler = default_downsampler
9898

99+
# Given figure should always be a BaseFigure (that is not wrapped by a plotly-resampler class)
100+
assert isinstance(figure, BaseFigure)
101+
assert not issubclass(type(figure), AbstractFigureAggregator)
99102
self._figure_class = figure.__class__
100103

101104
if convert_existing_traces:
@@ -422,6 +425,29 @@ def _check_update_figure_dict(
422425
updated_trace_indices.append(idx)
423426
return updated_trace_indices
424427

428+
@staticmethod
429+
def _get_figure_class(constr: type) -> type:
430+
"""Get the plotly figure class (constructor) for the given class (constructor).
431+
432+
.. Note::
433+
This method will always return a plotly constructor, even when the given
434+
`constr` is decorated (after executing the ``register_plotly_resampler``
435+
function).
436+
437+
Parameters
438+
----------
439+
constr: type
440+
The constructor class for which we want to retrieve the plotly constructor.
441+
442+
Returns
443+
-------
444+
type:
445+
The plotly figure class (constructor) of the given `constr`.
446+
447+
"""
448+
from ..registering import get_plotly_constr # To avoid ImportError
449+
return get_plotly_constr(constr)
450+
425451
@staticmethod
426452
def _slice_time(
427453
hf_series: pd.Series,

plotly_resampler/figure_resampler/figurewidget_resampler.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def __init__(
5252
verbose: bool = False,
5353
):
5454
# Parse the figure input before calling `super`
55-
f = go.FigureWidget()
55+
f = self._get_figure_class(go.FigureWidget)()
5656
f._data_validator.set_uid = False
5757

5858
if isinstance(figure, BaseFigure): # go.Figure or go.FigureWidget or AbstractFigureAggregator
@@ -244,6 +244,12 @@ def _update_spike_ranges(self, layout, *showspikes, force_update=False):
244244
self._relayout_hist.append(["showspikes", "initial call or showspikes"])
245245
self._relayout_hist.append("-" * 40)
246246

247+
# @staticmethod
248+
# def _get_figure_class() -> type:
249+
# """Return the class of the underlying figure."""
250+
# from ..module import get_plotly_constr
251+
# return get_plotly_constr(go.FigureWidget)
252+
247253
def reset_axes(self):
248254
"""Reset the axes of the FigureWidgetResampler.
249255

plotly_resampler/module.py

Lines changed: 0 additions & 72 deletions
This file was deleted.

plotly_resampler/registering.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
__author__ = "Jeroen Van Der Donckt, Jonas Van Der Donckt, Emiel Deprost"
2+
3+
from plotly_resampler import FigureResampler, FigureWidgetResampler
4+
from plotly_resampler.figure_resampler.figure_resampler_interface import (
5+
AbstractFigureAggregator,
6+
)
7+
from functools import wraps
8+
9+
import plotly
10+
11+
WRAPPED_PREFIX = "[Plotly-Resampler]__"
12+
PLOTLY_MODULES = [
13+
plotly.graph_objs,
14+
plotly.graph_objects,
15+
] # wait for this PR https://github.com/plotly/plotly.py/pull/3779
16+
PLOTLY_CONSTRUCTOR_WRAPPER = {
17+
"Figure": FigureResampler,
18+
"FigureWidget": FigureWidgetResampler,
19+
}
20+
21+
22+
def _already_wrapped(constr):
23+
return constr.__name__.startswith(WRAPPED_PREFIX)
24+
25+
26+
def get_plotly_constr(constr):
27+
if _already_wrapped(constr):
28+
return constr.__wrapped__ # get the original constructor
29+
return constr
30+
31+
32+
### Registering the wrappers
33+
34+
35+
def _is_ipython_env():
36+
"""Check if we are in an IPython environment (with a kernel)."""
37+
try:
38+
from IPython import get_ipython
39+
40+
return "IPKernelApp" in get_ipython().config
41+
except Exception:
42+
return False
43+
44+
45+
def _register_wrapper(
46+
module: type,
47+
constr_name: str,
48+
pr_class: AbstractFigureAggregator,
49+
**aggregator_kwargs,
50+
):
51+
constr = getattr(module, constr_name)
52+
constr = get_plotly_constr(constr) # get the original plotly constructor
53+
54+
# print(f"Wrapping {constr_name} with {pr_class}")
55+
56+
@wraps(constr)
57+
def wrapped_constr(*args, **kwargs):
58+
# print(f"Executing constructor wrapper for {constr_name}", constr)
59+
return pr_class(constr(*args, **kwargs), **aggregator_kwargs)
60+
61+
wrapped_constr.__name__ = WRAPPED_PREFIX + constr_name
62+
setattr(module, constr_name, wrapped_constr)
63+
64+
65+
def register_plotly_resampler(mode="auto", **aggregator_kwargs): # TODO: show kwargs (e.g., port)?
66+
"""Register plotly-resampler to plotly.graph_objects.
67+
68+
This function results in the use of plotly-resampler under the hood.
69+
70+
.. Note::
71+
We advise to
72+
73+
Parameters
74+
----------
75+
mode : str, optional
76+
The mode of the plotly-resampler.
77+
Possible values are: 'auto', 'figure', 'widget', None.
78+
If 'auto' is used, the mode is determined based on the environment; if it is in
79+
an ipython environment, the mode is 'widget', otherwise it is 'figure'.
80+
If 'figure' is used, all plotly figures are wrapped as FigureResampler objects.
81+
If 'widget' is used, all plotly figure widgets are wrapped as
82+
FigureWidgetResampler objects (we advise to use this mode in ipython environment
83+
with a kernel).
84+
If None is used, wrapping is done as expected (go.Figure -> FigureResampler,
85+
go.FigureWidget -> FigureWidgetResampler).
86+
aggregator_kwargs : dict, optional
87+
The keyword arguments to pass to the plotly-resampler decorator its constructor.
88+
See more details in :class:`FigureResampler <FigureResampler>` and
89+
:class:`FigureWidgetResampler <FigureWidgetResampler>`.
90+
91+
"""
92+
for constr_name, pr_class in PLOTLY_CONSTRUCTOR_WRAPPER.items():
93+
if (mode == "auto" and _is_ipython_env()) or mode == "widget":
94+
pr_class = FigureWidgetResampler
95+
elif mode == "figure":
96+
pr_class = FigureResampler
97+
# else: default mode -> wrap according to PLOTLY_CONSTRUCTOR_WRAPPER
98+
99+
for module in PLOTLY_MODULES:
100+
_register_wrapper(module, constr_name, pr_class, **aggregator_kwargs)
101+
102+
103+
### Unregistering the wrappers
104+
105+
106+
def _unregister_wrapper(module: type, constr_name: str):
107+
constr = getattr(module, constr_name)
108+
if _already_wrapped(constr):
109+
constr = constr.__wrapped__
110+
setattr(module, constr_name, constr)
111+
112+
113+
def unregister_plotly_resampler():
114+
"""Unregister plotly-resampler from plotly.graph_objects."""
115+
for constr in PLOTLY_CONSTRUCTOR_WRAPPER.keys():
116+
for module in PLOTLY_MODULES:
117+
_unregister_wrapper(module, constr)

0 commit comments

Comments
 (0)