diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py index f6dc968e67..8b15d81439 100644 --- a/panel/pane/plotly.py +++ b/panel/pane/plotly.py @@ -14,7 +14,8 @@ from bokeh.models import ColumnDataSource from pyviz_comms import JupyterComm -from ..util import isdatetime, lazy_load +from ..util import lazy_load +from ..util.checks import datetime_types, isdatetime from ..viewable import Layoutable from .base import ModelPane @@ -132,8 +133,8 @@ def _get_sources_for_trace(json, data, parent_path=''): for key, value in list(json.items()): full_path = key if not parent_path else f"{parent_path}.{key}" if isinstance(value, np.ndarray): - # Extract numpy array - data[full_path] = [json.pop(key)] + array = json.pop(key) + data[full_path] = [array] elif isinstance(value, dict): # Recurse into dictionaries: Plotly._get_sources_for_trace(value, data=data, parent_path=full_path) @@ -251,17 +252,21 @@ def _plotly_json_wrapper(fig): For #382: Map datetime elements to strings. """ json = fig.to_plotly_json() + layout = json['layout'] data = json['data'] - - for idx in range(len(data)): - for key in data[idx]: - if isdatetime(data[idx][key]): - arr = data[idx][key] - if isinstance(arr, np.ndarray): - arr = arr.astype(str) - else: - arr = [str(v) for v in arr] - data[idx][key] = arr + shapes = layout.get('shapes', []) + for trace in data+shapes: + for key in trace: + if not isdatetime(trace[key]): + continue + arr = trace[key] + if isinstance(arr, np.ndarray): + arr = arr.astype(str) + elif isinstance(arr, datetime_types): + arr = str(arr) + else: + arr = [str(v) for v in arr] + trace[key] = arr return json def _init_params(self): diff --git a/panel/tests/pane/test_plotly.py b/panel/tests/pane/test_plotly.py index 88690bc9e0..cb40d7b7b5 100644 --- a/panel/tests/pane/test_plotly.py +++ b/panel/tests/pane/test_plotly.py @@ -243,3 +243,58 @@ def test_plotly_swap_traces(document, comm): assert 'x' not in cds.data assert len(cds.data['y'][0]) == 500 + + +@plotly_available +def test_plotly_shape_datetime_converted(document, comm): + # see https://github.com/holoviz/panel/issues/5252 + start = pd.Timestamp('2022-05-11 0:00:00', tz=dt.timezone.utc) + date_range = pd.date_range(start=start, periods=20, freq='h') + + df = pd.DataFrame({ + "x": date_range, + "y": list(range(len(date_range))), + }) + + fig = px.scatter(df, x="x", y="y") + fig.add_vline(x=pd.Timestamp('2022-05-11 9:00:00', tz=dt.timezone.utc)) + + p = Plotly(fig) + + model = p.get_root(document, comm) + + assert model.layout['shapes'][0]['x0'] == '2022-05-11 09:00:00+00:00' + + +@plotly_available +def test_plotly_datetime_converted_2d_array(document, comm): + # see https://github.com/holoviz/panel/issues/7309 + n_points = 3 + data = pd.DataFrame({ + 'timestamp': pd.date_range(start='2023-01-01', periods=n_points, freq='min'), + 'latitude': np.cumsum(np.random.randn(n_points) * 0.01) + 52.3702, # Centered around Amsterdam + 'longitude': np.cumsum(np.random.randn(n_points) * 0.01) + 4.8952, # Centered around Amsterdam + }) + + fig = px.scatter_mapbox(data, lon='longitude', lat='latitude', custom_data='timestamp') + + p = Plotly(fig) + + model = p.get_root(document, comm) + + assert len(model.data_sources) == 1 + + cds = model.data_sources[0] + assert isinstance(cds.data['customdata'], list) + assert len(cds.data['customdata']) == 1 + data = cds.data['customdata'][0] + assert isinstance(data, np.ndarray) + assert data.dtype.kind == 'U' + np.testing.assert_equal( + data, + np.array([ + ['2023-01-01 00:00:00'], + ['2023-01-01 00:01:00'], + ['2023-01-01 00:02:00'] + ]) + ) diff --git a/panel/util/checks.py b/panel/util/checks.py index ed3cf748ca..139c2598ff 100644 --- a/panel/util/checks.py +++ b/panel/util/checks.py @@ -109,7 +109,7 @@ def isdatetime(value) -> bool: return ( value.dtype.kind == "M" or (value.dtype.kind == "O" and len(value) != 0 and - isinstance(value[0], datetime_types)) + isinstance(np.take(value, 0), datetime_types)) ) elif isinstance(value, list): return all(isinstance(d, datetime_types) for d in value)