From 1064832af0ca6c46c64353ec8f72d954745cb99d Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Wed, 11 Jun 2025 18:33:19 -0400 Subject: [PATCH 1/5] add playUpdates function to client-api --- projects/client-api/src/index.js | 45 +++++++++++++++++++++++++++++++- projects/client-api/src/rxjs.js | 2 ++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/projects/client-api/src/index.js b/projects/client-api/src/index.js index 1f41114..030aec1 100644 --- a/projects/client-api/src/index.js +++ b/projects/client-api/src/index.js @@ -133,6 +133,7 @@ import { mergeAll, Observable, of, + pairwise, pipe, ReplaySubject, BehaviorSubject, @@ -1670,6 +1671,48 @@ export function subscribeLabels({ onChange, onExit, onError, g }) { .subscribe({ error: onError }); } +/** + * Subscribe to graph simulation completion event + * @function subscribeLabels + * @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable} or depricated, cache an object. + * @return {Subscription} A {@link Subscription} that can be used to react to the play updates + * @example + * GraphistryJS(document.getElementById('viz')) + * .pipe( + * map(playUpdates), + * tap(() => console.log('Play completed')), + * }) + * .subscribe(); + */ +export function playUpdates(g) { + if (!(g.subscriptionAPIVersion >= 1)) { + return throwError(() => new Error('playUpdates is not available the currently embedded graphistry viz.')); + } + + const selectionPath = ".labels"; + return (new BehaviorSubject('Initialize playUpdates stream') + .pipe( + tap(() => { + console.debug('postMessage subscription', '@client-api.playUpdate'); + g.iFrame.contentWindow.postMessage({ type: 'graphistry-subscribe', agent: 'graphistryjs', path: selectionPath }, '*'); + }), + finalize(() => { + console.debug('postMessage unsubscribe', '@client-api.playUpdate'); + g.iFrame.contentWindow.postMessage({ type: 'graphistry-unsubscribe', agent: 'graphistryjs', path: selectionPath }, '*'); + }), + switchMap(() => + fromEvent(window, 'message').pipe( + map(o => o.data), + filter(o => o && o.type === 'graphistry-sub-update' && o.path === selectionPath), + map(o => o.data), + pairwise(), + filter(([{ simulating: prevSim }, { simulating: currSim }]) => prevSim && !currSim), + shareReplay({ bufferSize: 1, refCount: true }) + ), + ) + )); +} + class GraphistryState { constructor(subscriptionAPIVersion, iFrame, models, result) { @@ -1768,7 +1811,7 @@ function addCallbacks(obs, target) { target.labelUpdates = labelUpdates;// lift(obs, labelUpdates); target.subscribeLabels = subscribeLabels;//lift(obs, subscribeLabels); target.selectionUpdates = selectionUpdates; // lift(obs, selectionUpdates); - target.subscribeLabels + target.playUpdates = playUpdates; return target; } diff --git a/projects/client-api/src/rxjs.js b/projects/client-api/src/rxjs.js index 952444f..7a86efc 100644 --- a/projects/client-api/src/rxjs.js +++ b/projects/client-api/src/rxjs.js @@ -28,6 +28,7 @@ import { map, mergeMap, mergeAll, + pairwise, scan, share, shareReplay, @@ -58,6 +59,7 @@ export { mergeAll, Observable, of, + pairwise, pipe, ReplaySubject, retryWhen, From 4600e89cef32ec14afc5f2f3409bcfb76a00dc89 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Wed, 11 Jun 2025 18:36:20 -0400 Subject: [PATCH 2/5] add client-api-react onPlayComplete handler --- projects/client-api-react/src/index.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/projects/client-api-react/src/index.js b/projects/client-api-react/src/index.js index 9d430cd..bb6075b 100644 --- a/projects/client-api-react/src/index.js +++ b/projects/client-api-react/src/index.js @@ -9,7 +9,7 @@ import * as gAPI from '@graphistry/client-api'; import { ajax, catchError, first, forkJoin, map, of, switchMap, tap } from '@graphistry/client-api'; // avoid explicit rxjs dep import { bg } from './bg'; import { bindings, panelNames, calls } from './bindings.js'; -import { Client as ClientBase, ClientPKey as ClientPKeyBase, selectionUpdates, subscribeLabels } from '@graphistry/client-api'; +import { Client as ClientBase, ClientPKey as ClientPKeyBase, selectionUpdates, subscribeLabels, playUpdates } from '@graphistry/client-api'; export const Client = ClientBase; export const ClientPKey = ClientPKeyBase; @@ -104,6 +104,7 @@ const propTypes = { onUpdateObservableG: PropTypes.func, onSelectionUpdate: PropTypes.func, onLabelsUpdate: PropTypes.func, + onPlayComplete: PropTypes.func, selectionUpdateOptions: PropTypes.object, queryParamExtra: PropTypes.object @@ -467,6 +468,7 @@ const Graphistry = forwardRef((props, ref) => { onUpdateObservableG, onSelectionUpdate, onLabelsUpdate, + onPlayComplete, selectionUpdateOptions } = props; @@ -558,6 +560,20 @@ const Graphistry = forwardRef((props, ref) => { } }, [g, onLabelsUpdate]) + useEffect(() => { + if (g && onPlayComplete) { + const sub = playUpdates(g) + .subscribe( + (v) => onPlayComplete(undefined, v), + (error) => onPlayComplete(error) + ); + + return () => { + sub && sub.unsubscribe(); + }; + } + }, [g, onPlayComplete]) + const playNormalized = typeof play === 'boolean' ? play : (play | 0) * 1000; const optionalParams = (type ? `&type=${type}` : ``) + (controls ? `&controls=${controls}` : ``) + From 8082d73f8002ab15744f8f5045d6922136d4e4c3 Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Wed, 11 Jun 2025 18:54:54 -0400 Subject: [PATCH 3/5] add onPlayComplete story --- .../src/stories/Graphistry.stories.jsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/projects/client-api-react/src/stories/Graphistry.stories.jsx b/projects/client-api-react/src/stories/Graphistry.stories.jsx index 70d4162..9e6d5f3 100644 --- a/projects/client-api-react/src/stories/Graphistry.stories.jsx +++ b/projects/client-api-react/src/stories/Graphistry.stories.jsx @@ -97,6 +97,24 @@ export const OnLabelUpdate = { }, }; +export const OnPlayUpdate = { + render: (args) => { + const [numPlayComplete, setNumPlayComplete] = useState(0); + + const onLabelsUpdate = () => { + console.log('onLabelsUpdate'); + setNumPlayComplete(numPlayComplete++); + }; + + return ( +
+ {`Number of play completed: ${numPlayComplete}`} + +
+ ); + }, +}; + export const NoClusteringOnLoad = { render: (args) => , }; From 3bc56e78bbd7ede3f2250ba3e246c057149c728d Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Wed, 11 Jun 2025 19:07:16 -0400 Subject: [PATCH 4/5] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c4da8..19ecb8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Added * **client-api-react**: Add `pointStrokeWidth` and `showCollections` bindings +* **client-api**: Add `playUpdates` observer which creates event when graph simulation completes +* **client-api-react**: Add `onPlayComplete` callback property ### Security From 0a5f108053f3dc43fc74238a443a2b957ed5ad5d Mon Sep 17 00:00:00 2001 From: Manfred Cheung Date: Mon, 16 Jun 2025 13:38:58 -0400 Subject: [PATCH 5/5] PR feedback fixes --- .../client-api-react/src/stories/Graphistry.stories.jsx | 6 +++--- projects/client-api/src/index.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/client-api-react/src/stories/Graphistry.stories.jsx b/projects/client-api-react/src/stories/Graphistry.stories.jsx index 9e6d5f3..77e6dc9 100644 --- a/projects/client-api-react/src/stories/Graphistry.stories.jsx +++ b/projects/client-api-react/src/stories/Graphistry.stories.jsx @@ -101,15 +101,15 @@ export const OnPlayUpdate = { render: (args) => { const [numPlayComplete, setNumPlayComplete] = useState(0); - const onLabelsUpdate = () => { + const onPlayUpdate = () => { console.log('onLabelsUpdate'); - setNumPlayComplete(numPlayComplete++); + setNumPlayComplete(numPlayComplete + 1); }; return (
{`Number of play completed: ${numPlayComplete}`} - +
); }, diff --git a/projects/client-api/src/index.js b/projects/client-api/src/index.js index 030aec1..aa988c4 100644 --- a/projects/client-api/src/index.js +++ b/projects/client-api/src/index.js @@ -1511,7 +1511,7 @@ export function subscribeSelections({ onChange, g }) { * The inner {@link Observable} for a label will complete if the label is removed from the screen. *

* @function labelUpdates - * @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable} or depricated, cache an object. + * @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable} or deprecated, cache an object. * @return {Observable>} An {@link Observable} of inner {Observables}, where each * inner {@link Observable} represents the lifetime of a label in the visualization. * @example @@ -1673,8 +1673,8 @@ export function subscribeLabels({ onChange, onExit, onError, g }) { /** * Subscribe to graph simulation completion event - * @function subscribeLabels - * @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable} or depricated, cache an object. + * @function playUpdates + * @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable} * @return {Subscription} A {@link Subscription} that can be used to react to the play updates * @example * GraphistryJS(document.getElementById('viz'))