Skip to content

Commit

Permalink
✈️ fix for #270 (#272)
Browse files Browse the repository at this point in the history
* :plane: fix for #270

* ✨ add `customdata` to hf_data_container

* 💪 adding tests

* 💨 linting

* 🙈 fix futurewarning

* 🧹 formatting

* 🙈 fix tests

* ✈️ ➡️ 🇧🇪 Autosize support (#273)

* ✈️ 🇧🇪 fix for #259

* 💨 linting

* 💨 adding autosize support

* ✨ updating action versions

* 🙈 skip dtype test on window

* 🙏 skip test

---------

Co-authored-by: Jeroen Van Der Donckt <[email protected]>
  • Loading branch information
jonasvdd and jvdd authored Nov 20, 2023
1 parent 780062c commit 741da5d
Show file tree
Hide file tree
Showing 7 changed files with 456 additions and 231 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
lfs: true
- name: Set up Python 3.10
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Poetry
uses: snok/install-poetry@v1
- name: Cache poetry
id: cached-poetry-dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}-python-${{ matrix.python-version }}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ jobs:
shell: bash

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
lfs: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -50,7 +50,7 @@ jobs:
version: 1.5.1
- name: Cache poetry
id: cached-poetry-dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}-python-${{ matrix.python-version }}
Expand All @@ -70,12 +70,12 @@ jobs:
run: |
poetry run pytest --cov=plotly_resampler --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov-report=xml tests
- name: Upload pytest test results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: pytest-results-${{ matrix.python-version }}
path: junit/test-results-${{ matrix.python-version }}.xml
# Use always() to always run this step to publish test results when there are test failures
if: ${{ always() }}

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
563 changes: 355 additions & 208 deletions examples/basic_example.ipynb

Large diffs are not rendered by default.

21 changes: 12 additions & 9 deletions plotly_resampler/figure_resampler/figure_resampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ def show_dash(
), f"mode must be one of {available_modes}"
graph_properties = {} if graph_properties is None else graph_properties
assert "config" not in graph_properties # There is a param for config
if self["layout"]["autosize"] is True and self["layout"]["height"] is None:
graph_properties.setdefault("style", {}).update({"height": "100%"})

# 0. Check if the traces need to be updated when there is a xrange set
# This will be the case when the users has set a xrange (via the `update_layout`
Expand Down Expand Up @@ -569,17 +571,18 @@ def show_dash(
else:
# jupyter dash uses a normal Dash app as figure
app = dash.Dash("local_app", **app_init_kwargs)

# fmt: off
div = dash.html.Div(
[
dash.dcc.Graph(
id="resample-figure", figure=self, config=config, **graph_properties
),
TraceUpdater(
id="trace-updater", gdID="resample-figure", sequentialUpdate=False
),
]
style={
"display": "flex", "flex-flow": "column",
"height": "95vh", "width": "100%",
},
children=[
dash.dcc.Graph(id="resample-figure", figure=self, config=config, **graph_properties),
TraceUpdater(id="trace-updater", gdID="resample-figure", sequentialUpdate=False),
],
)
# # fmt: on
if self._create_overview:
overview_config = config.copy() if config is not None else {}
overview_config["displayModeBar"] = False
Expand Down
23 changes: 18 additions & 5 deletions plotly_resampler/figure_resampler/figure_resampler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
# `_hf_data_container._asdict()` function is used in
# `AbstractFigureAggregator._construct_hf_data_dict`.
_hf_data_container = namedtuple(
"DataContainer", ["x", "y", "text", "hovertext", "marker_size", "marker_color"]
"DataContainer",
["x", "y", "text", "hovertext", "marker_size", "marker_color", "customdata"],
)


Expand Down Expand Up @@ -152,8 +153,12 @@ def __init__(

# 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())
self._yaxis_list = self._re_matches(re.compile("yaxis\d*"), self._layout.keys())
self._xaxis_list = self._re_matches(
re.compile(r"xaxis\d*"), self._layout.keys()
)
self._yaxis_list = self._re_matches(
re.compile(r"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"]
Expand Down Expand Up @@ -382,7 +387,7 @@ def _nest_dict_rec(k: str, v: any, out: dict) -> None:
out[k] = v

# Check if (hover)text also needs to be downsampled
for k in ["text", "hovertext", "marker_size", "marker_color"]:
for k in ["text", "hovertext", "marker_size", "marker_color", "customdata"]:
k_val = hf_trace_data.get(k)
if isinstance(k_val, (np.ndarray, pd.Series)):
assert isinstance(
Expand Down Expand Up @@ -641,6 +646,8 @@ def _parse_get_trace_props(
else hf_marker_color
)

hf_customdata = trace["customdata"] if hasattr(trace, "customdata") else None

if trace["type"].lower() in self._high_frequency_traces:
if hf_x is None: # if no data as x or hf_x is passed
if hf_y.ndim != 0: # if hf_y is an array
Expand Down Expand Up @@ -742,7 +749,13 @@ def _parse_get_trace_props(
trace.marker.color = hf_marker_color

return _hf_data_container(
hf_x, hf_y, hf_text, hf_hovertext, hf_marker_size, hf_marker_color
hf_x,
hf_y,
hf_text,
hf_hovertext,
hf_marker_size,
hf_marker_color,
hf_customdata,
)

def _construct_hf_data_dict(
Expand Down
9 changes: 8 additions & 1 deletion tests/test_figure_resampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

__author__ = "Jonas Van Der Donckt, Jeroen Van Der Donckt, Emiel Deprost"


import datetime
import multiprocessing
import subprocess
import sys
import time
from datetime import timedelta
from typing import List
Expand Down Expand Up @@ -129,6 +129,10 @@ def test_add_trace_not_resampling_insert_gaps():


def test_various_dtypes(float_series):
# skip the test on windows
if sys.platform.lower().startswith("win"):
pytest.skip("Skipping test on windows")

# List of dtypes supported by orjson >= 3.8
valid_dtype_list = [
np.bool_,
Expand Down Expand Up @@ -1192,6 +1196,9 @@ def test_showdash_not_hanging_when_port_in_use():


def test_manual_jupyterdashpersistentinline():
if sys.platform.lower().startswith("win"):
pytest.skip("This test is currently not supported on windows")

# Manually call the JupyterDashPersistentInline its method
# This requires some gimmicky stuff to mimmick the behaviour of a jupyter notebook.

Expand Down
55 changes: 55 additions & 0 deletions tests/test_plotly_express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import numpy as np
import pandas as pd
import plotly.express as px

from plotly_resampler import register_plotly_resampler, unregister_plotly_resampler


def test_px_hoverlabel_figureResampler():
labels = list(range(0, 3))
N = 60_000
x = np.arange(N)
y = np.random.normal(size=N)
label = np.random.randint(low=labels[0], high=labels[-1] + 1, size=N).astype(str)
description = np.random.randint(low=3, high=5, size=N)

df = pd.DataFrame.from_dict(
{"x": x, "y": y, "label": label, "description": description}
)

x_label = "x"
y_label = "y"
label_label = "label"
df = df.sort_values(by=[x_label])

# Without resampler, shows correct hover data
fig = px.scatter(
df,
x=x_label,
y=y_label,
color=label_label,
title="Without resampler",
hover_data=["description"],
)

# With resampler, shows incorrect hover data
register_plotly_resampler(mode="auto", default_n_shown_samples=1000)
fig2 = px.scatter(
df,
x=x_label,
y=y_label,
color=label_label,
title="With resampler",
hover_data=["description"],
)

# verify whether the selected has the same y and customdata as the original
for idx in range(len(fig.data)):
trc_orig = fig.data[idx]
trc_res = fig2.data[idx]

agg_indices = np.searchsorted(trc_orig["x"], trc_res["x"]).ravel()
for k in ["customdata", "y"]:
assert all(trc_orig[k].ravel()[agg_indices] == trc_res[k].ravel())

unregister_plotly_resampler()

0 comments on commit 741da5d

Please sign in to comment.