diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c4da8a..19ecb8bd 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 diff --git a/projects/client-api-react/src/index.js b/projects/client-api-react/src/index.js index 9d430cdd..bb6075bb 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}` : ``) + diff --git a/projects/client-api-react/src/stories/Graphistry.stories.jsx b/projects/client-api-react/src/stories/Graphistry.stories.jsx index 70d41620..77e6dc95 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 onPlayUpdate = () => { + console.log('onLabelsUpdate'); + setNumPlayComplete(numPlayComplete + 1); + }; + + return ( +
+ {`Number of play completed: ${numPlayComplete}`} + +
+ ); + }, +}; + export const NoClusteringOnLoad = { render: (args) => , }; diff --git a/projects/client-api/src/index.js b/projects/client-api/src/index.js index 1f41114b..aa988c4d 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, @@ -1510,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 @@ -1670,6 +1671,48 @@ export function subscribeLabels({ onChange, onExit, onError, g }) { .subscribe({ error: onError }); } +/** + * Subscribe to graph simulation completion event + * @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')) + * .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 952444fe..7a86efc2 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,