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

🦌 Adding reset-axes functionality #48

Merged
merged 8 commits into from
May 8, 2022
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ fig
**Note** that you can add dynamic aggregation to plotly figures with the `FigureWidgetResampler` wrapper without needing to forward a port!
* In general, when using downsampling one should be aware of (possible) [aliasing](https://en.wikipedia.org/wiki/Aliasing) effects.
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.
* 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).


## Future work 🔨

Expand Down
17 changes: 14 additions & 3 deletions docs/sphinx/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,26 @@ The gif below demonstrates the example usage of of :class:`FigureWidgetResampler
Important considerations & tips 🚨
----------------------------------

* 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.
* 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>`
**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!
* In general, when using downsampling one should be aware of (possible) `aliasing <https://en.wikipedia.org/wiki/Aliasing>`_ effects. :raw-html:`<br>`
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>`
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.
* 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>`_.


Dynamically adjusting the scatter data 🔩
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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.
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.

Working example ⬇️:

.. code:: py

import plotly.graph_objects as go; import numpy as np
from plotly_resampler import FigureResampler
from plotly_resampler import FigureResampler
# Note: a FigureWidgetResampler can be used here as well

# Construct the hf-data
x = np.arange(1_000_000)
Expand All @@ -110,6 +113,14 @@ Working example ⬇️:

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

.. tip::

The ``FigureWidgetResampler`` graph will not be automatically redrawn after
adjusting the fig its `hf_data` property,. The redrawning can be triggered by
manually calling either:

* :func:`FigureWidgetResampler.reload_data <plotly_resampler.figure_resampler.FigureWidgetResampler.reload_data>`, which keeps the current-graph range.
* :func:`FigureWidgetResampler.reset_axes <plotly_resampler.figure_resampler.FigureWidgetResampler.reset_axes>`, which performs a graph update.

Plotly-resampler & not high-frequency traces 🔍
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
62 changes: 54 additions & 8 deletions plotly_resampler/figure_resampler/figurewidget_resampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ def __init__(
# used for logging purposes to save a history of layout changes
self._relayout_hist = []

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

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

def _update_spike_ranges(self, layout, *showspikes):
def _update_spike_ranges(self, layout, *showspikes, force_update=False):
"""Update the go.Figure based on the changed spike-ranges.

Parameters
Expand All @@ -155,6 +158,11 @@ def _update_spike_ranges(self, layout, *showspikes):
*showspikes: iterable
A iterable where each item is a bool, indicating whether showspikes is set
to true/false for the corresponding xaxis in ``self._xaxis_list``.
force_update: bool
Bool indicating whether the range updates need to take place. This is
especially useful when you have recently updated the figure its data (with
the hf_data property) and want to perform an autoscale, independent from
the current figure-layout.
"""
relayout_dict = {} # variable in which we aim to reconstruct the relayout
# serialize the layout in a new dict object
Expand All @@ -168,11 +176,15 @@ def _update_spike_ranges(self, layout, *showspikes):

for xaxis_str, showspike in zip(self._xaxis_list, showspikes):
if (
force_update
or
# autorange key must be set to True
layout[xaxis_str].get("autorange", False)
# we only perform updates for traces which have 'range' property,
# as we do need to reconstruct the update-data for these traces
and self._prev_layout[xaxis_str].get("range", None) is not None
(
layout[xaxis_str].get("autorange", False)
# we only perform updates for traces which have 'range' property,
# as we do need to reconstruct the update-data for these traces
and self._prev_layout[xaxis_str].get("range", None) is not None
)
):
relayout_dict[f"{xaxis_str}.autorange"] = True
relayout_dict[f"{xaxis_str}.showspikes"] = showspike
Expand All @@ -193,7 +205,8 @@ def _update_spike_ranges(self, layout, *showspikes):

with self.batch_update():
# First update the layout (first item of update_data)
self.layout.update(update_data[0])
if not force_update:
self.layout.update(update_data[0])

# Also: Remove the showspikes from the layout, otherwise the autorange
# will not work as intended (it will not be triggered again)
Expand All @@ -209,3 +222,36 @@ def _update_spike_ranges(self, layout, *showspikes):
elif self._print_verbose:
self._relayout_hist.append(["showspikes", "initial call or showspikes"])
self._relayout_hist.append("-" * 40)

def reset_axes(self):
"""Reset the axes of the FigureWidgetResampler.

This is useful when adjusting the `hf_data` properties of the
``FigureWidgetResampler``.
"""
self._update_spike_ranges(
self.layout, [False] * len(self._xaxis_list), force_update=True
)
# Reset the layout
self.update_layout(
{
axis: {"autorange": True, "range": None}
for axis in self._xaxis_list + self._yaxis_list
}
)

def reload_data(self):
"""Reload all the data of FigureWidgetResampler for the current range-view.

This is useful when adjusting the `hf_data` properties of the
``FigureWidgetResampler``.
"""
self._update_spike_ranges(
self.layout, [False] * len(self._xaxis_list), force_update=True
)
# Resample the data for the current range-view
self._update_x_ranges(
self.layout,
# Pass the current view to trigger a resample operation
*[self.layout[xaxis_str]["range"] for xaxis_str in self._xaxis_list],
)
46 changes: 46 additions & 0 deletions tests/test_figurewidget_resampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,52 @@ def test_hf_data_property():
fr.hf_data[0] = -2 * y


def test_hf_data_property_reset_axes():
fwr = FigureWidgetResampler(go.Figure(), default_n_shown_samples=2_000)
n = 100_000
x = np.arange(n)
y = np.sin(x)

assert len(fwr.hf_data) == 0
fwr.add_trace(go.Scattergl(name="test"), hf_x=x, hf_y=y)

fwr.layout.update(
{"xaxis": {"range": [10_000, 20_000]}, "yaxis": {"range": [-20, 3]}},
overwrite=False,
)

assert len(fwr.hf_data) == 1
assert len(fwr.hf_data[0]["x"]) == n
fwr.hf_data[0] = -2 * y

fwr.reset_axes()
assert fwr.data[0]['x'][-1] > 20_000
assert fwr.layout['yaxis'].range is None or fwr.layout['yaxis'].range[0] > -10


def test_hf_data_property_reload_data():
fwr = FigureWidgetResampler(go.Figure(), default_n_shown_samples=2_000)
n = 100_000
x = np.arange(n)
y = np.sin(x)

assert len(fwr.hf_data) == 0
fwr.add_trace(go.Scattergl(name="test"), hf_x=x, hf_y=y)

fwr.layout.update(
{"xaxis": {"range": [10_000, 20_000]}, "yaxis": {"range": [-20, 3]}},
overwrite=False,
)

assert len(fwr.hf_data) == 1
assert len(fwr.hf_data[0]["x"]) == n
fwr.hf_data[0] = -2 * y

fwr.reload_data()
assert (fwr.data[0]['x'][0] >= 10_000) & (fwr.data[0]['x'][-1] <= 20_000)
assert (fwr.layout['yaxis'].range[0] == -20) & (fwr.layout['yaxis'].range[-1] == 3)


def test_updates_two_traces():
n = 1_000_000
X = np.arange(n)
Expand Down