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,