From ef4c8e1b0a8a4806522d29ff7edef894731bd28b Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:09:56 -0500 Subject: [PATCH 1/4] fix(layout): preserve user-set play URL parameter in ring layouts --- graphistry/layout/ring/categorical.py | 9 ++++++++- graphistry/layout/ring/continuous.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/graphistry/layout/ring/categorical.py b/graphistry/layout/ring/categorical.py index f66ab93eec..13e40a709c 100644 --- a/graphistry/layout/ring/categorical.py +++ b/graphistry/layout/ring/categorical.py @@ -250,13 +250,20 @@ def unique() -> List[Any]: #print('axis', axis) + if play_ms != 0: + play_value = play_ms + elif 'play' in g._url_params: + play_value = g._url_params['play'] + else: + play_value = 0 + g2 = ( g .nodes(lambda g: g._nodes.assign(x=x, y=y, r=r)) .encode_axis(axis_out) .bind(point_x='x', point_y='y') .settings(url_params={ - 'play': play_ms, + 'play': play_value, 'lockedR': True, 'bg': '%23E2E2E2' # Light grey due to labels being fixed to dark }) diff --git a/graphistry/layout/ring/continuous.py b/graphistry/layout/ring/continuous.py index 4bcd270a5e..757913aa2c 100644 --- a/graphistry/layout/ring/continuous.py +++ b/graphistry/layout/ring/continuous.py @@ -248,13 +248,20 @@ def ring_continuous( #print('axis', axis) + if play_ms != 0: + play_value = play_ms + elif 'play' in g._url_params: + play_value = g._url_params['play'] + else: + play_value = 0 + g2 = ( g .nodes(lambda g: g._nodes.assign(x=x, y=y, r=r)) .encode_axis(axis_out) .bind(point_x='x', point_y='y') .settings(url_params={ - 'play': play_ms, + 'play': play_value, 'lockedR': True, 'bg': '%23E2E2E2' # Light grey due to labels being fixed to dark }) From 029d761733be311decaccb1ef86fca19fd57db11 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:11:06 -0500 Subject: [PATCH 2/4] fix(layout): added validation --- graphistry/layout/ring/categorical.py | 5 ++++- graphistry/layout/ring/continuous.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/graphistry/layout/ring/categorical.py b/graphistry/layout/ring/categorical.py index 13e40a709c..e8cdcaa6be 100644 --- a/graphistry/layout/ring/categorical.py +++ b/graphistry/layout/ring/categorical.py @@ -253,7 +253,10 @@ def unique() -> List[Any]: if play_ms != 0: play_value = play_ms elif 'play' in g._url_params: - play_value = g._url_params['play'] + try: + play_value = int(g._url_params['play']) + except (ValueError, TypeError): + play_value = 0 else: play_value = 0 diff --git a/graphistry/layout/ring/continuous.py b/graphistry/layout/ring/continuous.py index 757913aa2c..ca8f9722d7 100644 --- a/graphistry/layout/ring/continuous.py +++ b/graphistry/layout/ring/continuous.py @@ -251,7 +251,10 @@ def ring_continuous( if play_ms != 0: play_value = play_ms elif 'play' in g._url_params: - play_value = g._url_params['play'] + try: + play_value = int(g._url_params['play']) + except (ValueError, TypeError): + play_value = 0 else: play_value = 0 From 4a7e1d0764c5ee9f1aa5fdc4dc52d27f5bf91be9 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:21:11 -0500 Subject: [PATCH 3/4] fix(layout): use None default for play_ms --- graphistry/layout/ring/categorical.py | 6 +++--- graphistry/layout/ring/continuous.py | 6 +++--- graphistry/layout/ring/time.py | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/graphistry/layout/ring/categorical.py b/graphistry/layout/ring/categorical.py index e8cdcaa6be..471bd8252a 100644 --- a/graphistry/layout/ring/categorical.py +++ b/graphistry/layout/ring/categorical.py @@ -79,7 +79,7 @@ def ring_categorical( format_axis: Optional[Callable[[List[Dict]], List[Dict]]] = None, format_labels: Optional[Callable[[Any, int, float], str]] = None, reverse: bool = False, - play_ms: int = 0, + play_ms: Optional[int] = None, engine: EngineAbstractType = EngineAbstract.AUTO ) -> Plottable: @@ -102,7 +102,7 @@ def ring_categorical( :format_axis: Optional[Callable[[List[Dict]], List[Dict]]] Optional transform function to format axis :format_label: Optional[Callable[[Any, int, float], str]] Optional transform function to format axis label text based on axis value, ring number, and ring position :reverse: bool Reverse the direction of the rings - :play_ms: int initial layout time in milliseconds, default 2000 + :play_ms: Optional[int] initial layout time in milliseconds. If None (default), honors existing url_params['play'] or defaults to 0 :engine: EngineAbstractType, default EngineAbstract.AUTO, pick CPU vs GPU engine via 'auto', 'pandas', 'cudf' :returns: Plotter @@ -250,7 +250,7 @@ def unique() -> List[Any]: #print('axis', axis) - if play_ms != 0: + if play_ms is not None: play_value = play_ms elif 'play' in g._url_params: try: diff --git a/graphistry/layout/ring/continuous.py b/graphistry/layout/ring/continuous.py index ca8f9722d7..00dcfdb7be 100644 --- a/graphistry/layout/ring/continuous.py +++ b/graphistry/layout/ring/continuous.py @@ -77,7 +77,7 @@ def ring_continuous( format_axis: Optional[Callable[[List[Dict]], List[Dict]]] = None, format_labels: Optional[Callable[[float, int, float], str]] = None, reverse: bool = False, - play_ms: int = 0, + play_ms: Optional[int] = None, engine: EngineAbstractType = EngineAbstract.AUTO ) -> Plottable: @@ -105,7 +105,7 @@ def ring_continuous( :format_axis: Optional[Callable[[List[Dict]], List[Dict]]] Optional transform function to format axis :format_label: Optional[Callable[[float, int, float], str]] Optional transform function to format axis label text based on axis value, ring number, and ring width :reverse: bool Reverse the direction of the rings - :play_ms: int initial layout time in milliseconds, default 2000 + :play_ms: Optional[int] initial layout time in milliseconds. If None (default), honors existing url_params['play'] or defaults to 0 :engine: EngineAbstractType, default EngineAbstract.AUTO, pick CPU vs GPU engine via 'auto', 'pandas', 'cudf' :returns: Plotter @@ -248,7 +248,7 @@ def ring_continuous( #print('axis', axis) - if play_ms != 0: + if play_ms is not None: play_value = play_ms elif 'play' in g._url_params: try: diff --git a/graphistry/layout/ring/time.py b/graphistry/layout/ring/time.py index 6cede809c9..984e0b9f86 100644 --- a/graphistry/layout/ring/time.py +++ b/graphistry/layout/ring/time.py @@ -231,7 +231,7 @@ def time_ring( format_axis: Optional[Callable[[List[Dict]], List[Dict]]] = None, format_label: Optional[Callable[[np.datetime64, int, np.timedelta64], str]] = None, format_labels: Optional[Callable[[np.datetime64, int, np.timedelta64], str]] = None, - play_ms: int = 2000, + play_ms: Optional[int] = None, engine: EngineAbstractType = EngineAbstract.AUTO ) -> Plottable: """Radial graph layout where nodes are positioned based on a datetime64-typed column time_col @@ -250,7 +250,7 @@ def time_ring( :format_axis: Optional[Callable[[List[Dict]], List[Dict]]] Optional transform function to format axis :format_label: Optional[Callable[[numpy.datetime64, int, numpy.timedelta64], str]] Optional transform function to format axis label text based on axis time, ring number, and ring duration width :format_labels: Optional[Callable[[numpy.datetime64, int, numpy.timedelta64], str]] Deprecated alias for ``format_label`` - :play_ms: int initial layout time in milliseconds, default 2000 + :play_ms: Optional[int] initial layout time in milliseconds. If None (default), honors existing url_params['play'] or defaults to 2000 :engine: EngineAbstractType, default EngineAbstract.AUTO, pick CPU vs GPU engine via 'auto', 'pandas', 'cudf' :returns: Plotter @@ -401,13 +401,23 @@ def time_ring( #print('axis', axis) + if play_ms is not None: + play_value = play_ms + elif 'play' in g._url_params: + try: + play_value = int(g._url_params['play']) + except (ValueError, TypeError): + play_value = 2000 + else: + play_value = 2000 + g2 = ( g .nodes(lambda g: g._nodes.assign(x=x, y=y, r=r)) .bind(point_x='x', point_y='y') .encode_axis(axis) .settings(url_params={ - 'play': play_ms, + 'play': play_value, 'lockedR': True, 'bg': '%23E2E2E2' # Light grey due to labels being fixed to dark }) From 763c81bc41afdb2f49507b7f5a915af0e566c047 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:27:18 -0500 Subject: [PATCH 4/4] test(layout): add play_ms tests for ring layouts --- .../tests/layout/ring/test_continuous.py | 43 ++++++++++++ .../layout/ring/test_ring_categorical.py | 65 +++++++++++++++++++ graphistry/tests/layout/ring/test_time.py | 55 ++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/graphistry/tests/layout/ring/test_continuous.py b/graphistry/tests/layout/ring/test_continuous.py index 1a753a337e..6b1ff444ec 100644 --- a/graphistry/tests/layout/ring/test_continuous.py +++ b/graphistry/tests/layout/ring/test_continuous.py @@ -140,3 +140,46 @@ def test_ring_cudf(self): for i, row in enumerate(g._complex_encodings['node_encodings']['default']['pointAxisEncoding']['rows']): assert row['r'] == 500 + 100 * i assert row['label'] == str(2 + 2 * i) + + def test_play_ms_preserves_url_param(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': [1, 2, 3]})) + .settings(url_params={'play': 6000}) + .ring_continuous_layout('t') + ) + assert g._url_params.get('play') == 6000 + + def test_play_ms_explicit_zero(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': [1, 2, 3]})) + .settings(url_params={'play': 6000}) + .ring_continuous_layout('t', play_ms=0) + ) + assert g._url_params.get('play') == 0 + + def test_play_ms_explicit_value(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': [1, 2, 3]})) + .settings(url_params={'play': 6000}) + .ring_continuous_layout('t', play_ms=3000) + ) + assert g._url_params.get('play') == 3000 + + def test_play_ms_default_when_no_url_param(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': [1, 2, 3]})) + .ring_continuous_layout('t') + ) + assert g._url_params.get('play') == 0 diff --git a/graphistry/tests/layout/ring/test_ring_categorical.py b/graphistry/tests/layout/ring/test_ring_categorical.py index 2d4f055c68..302d78cefb 100644 --- a/graphistry/tests/layout/ring/test_ring_categorical.py +++ b/graphistry/tests/layout/ring/test_ring_categorical.py @@ -135,3 +135,68 @@ def test_ring_cudf(self): for i, row in enumerate(g._complex_encodings['node_encodings']['default']['pointAxisEncoding']['rows']): assert row['r'] == 500 + 100 * i assert row['label'] == ['a', 'bb', 'cc', 'dd'][i] + + def test_play_ms_preserves_url_param(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': ['x', 'y', 'z']})) + .settings(url_params={'play': 6000}) + .ring_categorical_layout('t') + ) + assert g._url_params.get('play') == 6000 + + def test_play_ms_explicit_zero(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': ['x', 'y', 'z']})) + .settings(url_params={'play': 6000}) + .ring_categorical_layout('t', play_ms=0) + ) + assert g._url_params.get('play') == 0 + + def test_play_ms_explicit_value(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': ['x', 'y', 'z']})) + .settings(url_params={'play': 6000}) + .ring_categorical_layout('t', play_ms=3000) + ) + assert g._url_params.get('play') == 3000 + + def test_play_ms_default_when_no_url_param(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': ['x', 'y', 'z']})) + .ring_categorical_layout('t') + ) + assert g._url_params.get('play') == 0 + + def test_play_ms_invalid_url_param_fallback(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': ['x', 'y', 'z']})) + .settings(url_params={'play': 'invalid'}) + .ring_categorical_layout('t') + ) + assert g._url_params.get('play') == 0 + + def test_play_ms_string_number_parsed(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({'n': ['a', 'b', 'c'], 't': ['x', 'y', 'z']})) + .settings(url_params={'play': '5000'}) + .ring_categorical_layout('t') + ) + assert g._url_params.get('play') == 5000 diff --git a/graphistry/tests/layout/ring/test_time.py b/graphistry/tests/layout/ring/test_time.py index 320218858d..660748c911 100644 --- a/graphistry/tests/layout/ring/test_time.py +++ b/graphistry/tests/layout/ring/test_time.py @@ -365,3 +365,58 @@ def test_ring_cudf(self): assert 'y' in g._nodes assert not g._nodes.x.isna().any() assert not g._nodes.y.isna().any() + + def test_play_ms_preserves_url_param(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({ + 'n': ['a', 'b', 'c'], + 't': pd.Series(['2015-01-16', '2015-01-17', '2015-01-18'], dtype='datetime64[ns]') + })) + .settings(url_params={'play': 6000}) + .time_ring_layout('t') + ) + assert g._url_params.get('play') == 6000 + + def test_play_ms_explicit_zero(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({ + 'n': ['a', 'b', 'c'], + 't': pd.Series(['2015-01-16', '2015-01-17', '2015-01-18'], dtype='datetime64[ns]') + })) + .settings(url_params={'play': 6000}) + .time_ring_layout('t', play_ms=0) + ) + assert g._url_params.get('play') == 0 + + def test_play_ms_explicit_value(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({ + 'n': ['a', 'b', 'c'], + 't': pd.Series(['2015-01-16', '2015-01-17', '2015-01-18'], dtype='datetime64[ns]') + })) + .settings(url_params={'play': 6000}) + .time_ring_layout('t', play_ms=3000) + ) + assert g._url_params.get('play') == 3000 + + def test_play_ms_default_when_no_url_param(self): + lg = LGFull() + g = ( + lg + .edges(pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}), 's', 'd') + .nodes(pd.DataFrame({ + 'n': ['a', 'b', 'c'], + 't': pd.Series(['2015-01-16', '2015-01-17', '2015-01-18'], dtype='datetime64[ns]') + })) + .time_ring_layout('t') + ) + assert g._url_params.get('play') == 2000