Skip to content

Commit 3d8f6af

Browse files
authored
Merge pull request #48 from predict-idlab/reset_axes
🦌 Adding reset-axes functionality
2 parents 61f46fe + 47f1801 commit 3d8f6af

File tree

4 files changed

+116
-11
lines changed

4 files changed

+116
-11
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ fig
106106
**Note** that you can add dynamic aggregation to plotly figures with the `FigureWidgetResampler` wrapper without needing to forward a port!
107107
* In general, when using downsampling one should be aware of (possible) [aliasing](https://en.wikipedia.org/wiki/Aliasing) effects.
108108
The <b><a style="color:orange">[R]</a></b> in the legend indicates when the corresponding trace is being resampled (and thus possibly distorted) or not. Additionally, the `~<range>` suffix represent the mean aggregation bin size in terms of the sequence index.
109+
* The plotly **autoscale** event (triggered by the autoscale button or a double-click within the graph), **does not reset the axes but autoscales the current graph-view** of plotly-resampler figures. This design choice was made as it seemed more intuitive for the developers to support this behavior with double-click than the default axes-reset behavior. The graph axes can ofcourse be resetted by using the `reset_axis` button. If you want to give feedback and discuss this further with the developers, see issue [#49](https://github.com/predict-idlab/plotly-resampler/issues/49).
110+
109111

110112
## Future work 🔨
111113

docs/sphinx/getting_started.rst

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,26 @@ The gif below demonstrates the example usage of of :class:`FigureWidgetResampler
7676
Important considerations & tips 🚨
7777
----------------------------------
7878

79-
* When running the code on a server, you should forward the port of the :func:`FigureResampler.show_dash <plotly_resampler.figure_resampler.FigureResampler.show_dash>` method to your local machine.
79+
* When running the code on a server, you should forward the port of the :func:`FigureResampler.show_dash <plotly_resampler.figure_resampler.FigureResampler.show_dash>` method to your local machine. :raw-html:`<br>`
80+
**Note** that you can add dynamic aggregation to plotly figures with the :class:`FigureWidgetResampler <plotly_resampler.figure_resampler.FigureWidgetResampler>` wrapper without needing to forward a port!
8081
* In general, when using downsampling one should be aware of (possible) `aliasing <https://en.wikipedia.org/wiki/Aliasing>`_ effects. :raw-html:`<br>`
8182
The :raw-html:`<b><a style="color:orange">[R]</a></b>` in the legend indicates when the corresponding trace is resampled (and thus possibly distorted). :raw-html:`<br>`
8283
The :raw-html:`<a style="color:orange"><b>~</b> <i>delta</i></a>` suffix in the legend represents the mean index delta for consecutive aggregated data points.
84+
* The plotly **autoscale** event (triggered by the autoscale button or a double-click within the graph), **does not reset the axes but autoscales the current graph-view of plotly-resampler figures**. This design choice was made as it seemed more intuitive for the developers to support this behavior with double-click than the default axes-reset behavior. The graph axes can ofcourse be resetted by using the `reset_axis` button. If you want to give feedback and discuss this further with the developers, see this issue `#49 <https://github.com/predict-idlab/plotly-resampler/issues/49>`_.
8385

8486

8587
Dynamically adjusting the scatter data 🔩
8688
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8789

88-
The raw high-frequency trace data can be adjusted using the :func:`hf_data <plotly_resampler.figure_resampler.FigureResampler.hf_data>` property of the FigureResampler instance.
90+
The raw high-frequency trace data can be adjusted using the :func:`hf_data <plotly_resampler.figure_resampler.FigureResampler.hf_data>` property of the plotly-resampler Figure instance.
8991

9092
Working example ⬇️:
9193

9294
.. code:: py
9395
9496
import plotly.graph_objects as go; import numpy as np
95-
from plotly_resampler import FigureResampler
97+
from plotly_resampler import FigureResampler
98+
# Note: a FigureWidgetResampler can be used here as well
9699
97100
# Construct the hf-data
98101
x = np.arange(1_000_000)
@@ -110,6 +113,14 @@ Working example ⬇️:
110113

111114
`hf_data` only withholds high-frequency traces (i.e., traces that are aggregated)
112115

116+
.. tip::
117+
118+
The ``FigureWidgetResampler`` graph will not be automatically redrawn after
119+
adjusting the fig its `hf_data` property,. The redrawning can be triggered by
120+
manually calling either:
121+
122+
* :func:`FigureWidgetResampler.reload_data <plotly_resampler.figure_resampler.FigureWidgetResampler.reload_data>`, which keeps the current-graph range.
123+
* :func:`FigureWidgetResampler.reset_axes <plotly_resampler.figure_resampler.FigureWidgetResampler.reset_axes>`, which performs a graph update.
113124

114125
Plotly-resampler & not high-frequency traces 🔍
115126
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

plotly_resampler/figure_resampler/figurewidget_resampler.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,14 @@ def __init__(
6969
# used for logging purposes to save a history of layout changes
7070
self._relayout_hist = []
7171

72-
# A list of al xaxis string names e.g., "xaxis", "xaxis2", "xaxis3", ....
72+
# A list of al xaxis and yaxis string names
73+
# e.g., "xaxis", "xaxis2", "xaxis3", .... for _xaxis_list
7374
self._xaxis_list = self._re_matches(re.compile("xaxis\d*"), self._layout.keys())
74-
# edge case: an empty `go.Figure()` does not yet contain xaxis keys
75+
self._yaxis_list = self._re_matches(re.compile("yaxis\d*"), self._layout.keys())
76+
# edge case: an empty `go.Figure()` does not yet contain axes keys
7577
if not len(self._xaxis_list):
7678
self._xaxis_list = ["xaxis"]
79+
self._yaxis_list = ["yaxis"]
7780

7881
# Assign the the update-methods to the corresponding classes
7982
showspike_keys = [f"{xaxis}.showspikes" for xaxis in self._xaxis_list]
@@ -143,7 +146,7 @@ def _update_x_ranges(self, layout, *x_ranges):
143146
trace_idx = updated_trace.pop("index")
144147
self.data[trace_idx].update(updated_trace)
145148

146-
def _update_spike_ranges(self, layout, *showspikes):
149+
def _update_spike_ranges(self, layout, *showspikes, force_update=False):
147150
"""Update the go.Figure based on the changed spike-ranges.
148151
149152
Parameters
@@ -155,6 +158,11 @@ def _update_spike_ranges(self, layout, *showspikes):
155158
*showspikes: iterable
156159
A iterable where each item is a bool, indicating whether showspikes is set
157160
to true/false for the corresponding xaxis in ``self._xaxis_list``.
161+
force_update: bool
162+
Bool indicating whether the range updates need to take place. This is
163+
especially useful when you have recently updated the figure its data (with
164+
the hf_data property) and want to perform an autoscale, independent from
165+
the current figure-layout.
158166
"""
159167
relayout_dict = {} # variable in which we aim to reconstruct the relayout
160168
# serialize the layout in a new dict object
@@ -168,11 +176,15 @@ def _update_spike_ranges(self, layout, *showspikes):
168176

169177
for xaxis_str, showspike in zip(self._xaxis_list, showspikes):
170178
if (
179+
force_update
180+
or
171181
# autorange key must be set to True
172-
layout[xaxis_str].get("autorange", False)
173-
# we only perform updates for traces which have 'range' property,
174-
# as we do need to reconstruct the update-data for these traces
175-
and self._prev_layout[xaxis_str].get("range", None) is not None
182+
(
183+
layout[xaxis_str].get("autorange", False)
184+
# we only perform updates for traces which have 'range' property,
185+
# as we do need to reconstruct the update-data for these traces
186+
and self._prev_layout[xaxis_str].get("range", None) is not None
187+
)
176188
):
177189
relayout_dict[f"{xaxis_str}.autorange"] = True
178190
relayout_dict[f"{xaxis_str}.showspikes"] = showspike
@@ -193,7 +205,8 @@ def _update_spike_ranges(self, layout, *showspikes):
193205

194206
with self.batch_update():
195207
# First update the layout (first item of update_data)
196-
self.layout.update(update_data[0])
208+
if not force_update:
209+
self.layout.update(update_data[0])
197210

198211
# Also: Remove the showspikes from the layout, otherwise the autorange
199212
# will not work as intended (it will not be triggered again)
@@ -209,3 +222,36 @@ def _update_spike_ranges(self, layout, *showspikes):
209222
elif self._print_verbose:
210223
self._relayout_hist.append(["showspikes", "initial call or showspikes"])
211224
self._relayout_hist.append("-" * 40)
225+
226+
def reset_axes(self):
227+
"""Reset the axes of the FigureWidgetResampler.
228+
229+
This is useful when adjusting the `hf_data` properties of the
230+
``FigureWidgetResampler``.
231+
"""
232+
self._update_spike_ranges(
233+
self.layout, [False] * len(self._xaxis_list), force_update=True
234+
)
235+
# Reset the layout
236+
self.update_layout(
237+
{
238+
axis: {"autorange": True, "range": None}
239+
for axis in self._xaxis_list + self._yaxis_list
240+
}
241+
)
242+
243+
def reload_data(self):
244+
"""Reload all the data of FigureWidgetResampler for the current range-view.
245+
246+
This is useful when adjusting the `hf_data` properties of the
247+
``FigureWidgetResampler``.
248+
"""
249+
self._update_spike_ranges(
250+
self.layout, [False] * len(self._xaxis_list), force_update=True
251+
)
252+
# Resample the data for the current range-view
253+
self._update_x_ranges(
254+
self.layout,
255+
# Pass the current view to trigger a resample operation
256+
*[self.layout[xaxis_str]["range"] for xaxis_str in self._xaxis_list],
257+
)

tests/test_figurewidget_resampler.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,52 @@ def test_hf_data_property():
507507
fr.hf_data[0] = -2 * y
508508

509509

510+
def test_hf_data_property_reset_axes():
511+
fwr = FigureWidgetResampler(go.Figure(), default_n_shown_samples=2_000)
512+
n = 100_000
513+
x = np.arange(n)
514+
y = np.sin(x)
515+
516+
assert len(fwr.hf_data) == 0
517+
fwr.add_trace(go.Scattergl(name="test"), hf_x=x, hf_y=y)
518+
519+
fwr.layout.update(
520+
{"xaxis": {"range": [10_000, 20_000]}, "yaxis": {"range": [-20, 3]}},
521+
overwrite=False,
522+
)
523+
524+
assert len(fwr.hf_data) == 1
525+
assert len(fwr.hf_data[0]["x"]) == n
526+
fwr.hf_data[0] = -2 * y
527+
528+
fwr.reset_axes()
529+
assert fwr.data[0]['x'][-1] > 20_000
530+
assert fwr.layout['yaxis'].range is None or fwr.layout['yaxis'].range[0] > -10
531+
532+
533+
def test_hf_data_property_reload_data():
534+
fwr = FigureWidgetResampler(go.Figure(), default_n_shown_samples=2_000)
535+
n = 100_000
536+
x = np.arange(n)
537+
y = np.sin(x)
538+
539+
assert len(fwr.hf_data) == 0
540+
fwr.add_trace(go.Scattergl(name="test"), hf_x=x, hf_y=y)
541+
542+
fwr.layout.update(
543+
{"xaxis": {"range": [10_000, 20_000]}, "yaxis": {"range": [-20, 3]}},
544+
overwrite=False,
545+
)
546+
547+
assert len(fwr.hf_data) == 1
548+
assert len(fwr.hf_data[0]["x"]) == n
549+
fwr.hf_data[0] = -2 * y
550+
551+
fwr.reload_data()
552+
assert (fwr.data[0]['x'][0] >= 10_000) & (fwr.data[0]['x'][-1] <= 20_000)
553+
assert (fwr.layout['yaxis'].range[0] == -20) & (fwr.layout['yaxis'].range[-1] == 3)
554+
555+
510556
def test_updates_two_traces():
511557
n = 1_000_000
512558
X = np.arange(n)

0 commit comments

Comments
 (0)