Skip to content

Commit a14331e

Browse files
jonasvddjvdd
andauthored
Fixes #210 (#211)
* 💪 add tests for #208 * 🙏 fix for #208 * ✨ tests for #210 * 💪 code-fix for #210 * 🔧 tests for setting hf_x dynamically for #210 * 🔥 fix for setting hf_series x to a tz-aware pd.Series * 🖊️ review * 🔍 review code * 🔍 fix helper method * 🙏 --------- Co-authored-by: jvdd <[email protected]>
1 parent 8fbb0f4 commit a14331e

File tree

3 files changed

+78
-7
lines changed

3 files changed

+78
-7
lines changed

plotly_resampler/figure_resampler/figure_resampler_interface.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,11 @@ def _check_update_trace_data(
326326
):
327327
# is faster to escape the loop here than check inside the hasattr if
328328
continue
329-
if hasattr(hf_trace_data[k], "values"):
329+
elif pd.core.dtypes.common.is_datetime64tz_dtype(hf_trace_data[k]):
330+
# When we use the .values method, timezone information is lost
331+
# so convert it to pd.DatetimeIndex, which preserves the tz-info
332+
hf_trace_data[k] = pd.Index(hf_trace_data[k])
333+
elif hasattr(hf_trace_data[k], "values"):
330334
# when not a range index or datetime index
331335
hf_trace_data[k] = hf_trace_data[k].values
332336

@@ -582,13 +586,17 @@ def _parse_get_trace_props(
582586
583587
"""
584588
hf_x: np.ndarray = (
589+
# fmt: off
585590
(np.asarray(trace["x"]) if trace["x"] is not None else None)
586591
if hasattr(trace, "x") and hf_x is None
587-
else hf_x.values
588-
if isinstance(hf_x, pd.Series)
589-
else hf_x
590-
if isinstance(hf_x, pd.Index)
592+
# If we cast a tz-aware datetime64 array to `.values` we lose the tz-info
593+
# and the UTC time will be displayed instead of the tz-localized time,
594+
# hence we cast to a pd.DatetimeIndex, which preserves the tz-info
595+
else pd.Index(hf_x) if pd.core.dtypes.common.is_datetime64tz_dtype(hf_x)
596+
else hf_x.values if isinstance(hf_x, pd.Series)
597+
else hf_x if isinstance(hf_x, pd.Index)
591598
else np.asarray(hf_x)
599+
# fmt: on
592600
)
593601

594602
hf_y = (

tests/test_figure_resampler.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ def test_hf_text_and_hf_hovertext():
529529
def test_multiple_timezones():
530530
n = 5_050
531531

532+
# NOTE: date-range returns a (tz-aware) DatetimeIndex
532533
dr = pd.date_range("2022-02-14", freq="s", periods=n, tz="UTC")
533534
dr_v = np.random.randn(n)
534535

@@ -538,6 +539,12 @@ def test_multiple_timezones():
538539
dr.tz_convert("Europe/Brussels"),
539540
dr.tz_convert("Australia/Perth"),
540541
dr.tz_convert("Australia/Canberra"),
542+
# NOTE: this pd.Series tests the functionality of a Pandas series with (tz-aware) DatetimeIndex
543+
pd.Series(dr),
544+
pd.Series(dr.tz_localize(None).tz_localize("Europe/Amsterdam")),
545+
pd.Series(dr.tz_convert("Europe/Brussels")),
546+
pd.Series(dr.tz_convert("Australia/Perth")),
547+
pd.Series(dr.tz_convert("Australia/Canberra")),
541548
]
542549

543550
plain_plotly_fig = make_subplots(rows=len(cs), cols=1, shared_xaxes=True)
@@ -564,7 +571,60 @@ def test_multiple_timezones():
564571
col=1,
565572
)
566573
# Assert that the time parsing is exactly the same
567-
assert plain_plotly_fig.data[0].x[0] == fr_fig.data[0].x[0]
574+
assert plain_plotly_fig.data[i - 1].x[0] == fr_fig.data[i - 1].x[0]
575+
576+
577+
def test_set_hfx_tz_aware_series():
578+
df = pd.DataFrame(
579+
{
580+
"timestamp": pd.date_range(
581+
"2020-01-01", "2020-01-02", freq="1s"
582+
).tz_localize("Asia/Seoul")
583+
}
584+
)
585+
df["value"] = np.random.randn(len(df))
586+
587+
fr = FigureResampler()
588+
fr.add_trace({}, hf_x=pd.Index(df.timestamp), hf_y=df.value)
589+
assert isinstance(fr.hf_data[0]["x"], pd.DatetimeIndex)
590+
# Now we set the pd.Series as hf_x
591+
fr.hf_data[0]["x"] = df.timestamp
592+
assert not isinstance(fr.hf_data[0]["x"], pd.DatetimeIndex)
593+
# perform an update
594+
out = fr.construct_update_data({"xaxis.autorange": True, "xaxis.showspikes": False})
595+
assert len(out) == 2
596+
# assert that the update was performed correctly
597+
assert isinstance(fr.hf_data[0]["x"], pd.DatetimeIndex)
598+
assert all(fr.hf_data[0]["x"] == pd.DatetimeIndex(df.timestamp))
599+
600+
601+
def test_datetime_hf_x_no_index_():
602+
df = pd.DataFrame(
603+
{"timestamp": pd.date_range("2020-01-01", "2020-01-02", freq="1s")}
604+
)
605+
df["value"] = np.random.randn(len(df))
606+
607+
# add via hf_x kwargs
608+
fr = FigureResampler()
609+
fr.add_trace({}, hf_x=df.timestamp, hf_y=df.value)
610+
output = fr.construct_update_data(
611+
{
612+
"xaxis.range[0]": "2020-01-01 00:00:00",
613+
"xaxis.range[1]": "2020-01-01 00:00:20",
614+
}
615+
)
616+
assert len(output) == 2
617+
618+
# add via scatter kwargs
619+
fr = FigureResampler()
620+
fr.add_trace(go.Scatter(x=df.timestamp, y=df.value))
621+
output = fr.construct_update_data(
622+
{
623+
"xaxis.range[0]": "2020-01-01 00:00:00",
624+
"xaxis.range[1]": "2020-01-01 00:00:20",
625+
}
626+
)
627+
assert len(output) == 2
568628

569629

570630
def test_datetime_hf_x_no_index():

tests/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ def construct_hf_data_dict(hf_x, hf_y, **kwargs):
2727
hf_data_dict = {
2828
"x": hf_x,
2929
"y": hf_y,
30-
"axis_type": "date" if isinstance(hf_x, pd.DatetimeIndex) else "linear",
30+
"axis_type": "date"
31+
if isinstance(hf_x, pd.DatetimeIndex)
32+
or pd.core.dtypes.common.is_datetime64_any_dtype(hf_x)
33+
else "linear",
3134
"downsampler": MinMaxLTTB(),
3235
"gap_handler": MedDiffGapHandler(),
3336
"max_n_samples": 1_000,

0 commit comments

Comments
 (0)