/* eslint-disable no-unused-vars */
import shallowEqual from 'shallowequal';
import { Model } from '@graphistry/falcor-model-rxjs';
import { PostMessageDataSource } from '@graphistry/falcor-socket-datasource';
import { $ref, $atom, $value } from '@graphistry/falcor-json-graph';
import { Client as ClientBase, ClientPKey as ClientPKeyBase, Dataset as DatasetBase, File as FileBase, EdgeFile as EdgeFileBase, NodeFile as NodeFileBase } from '@graphistry/js-upload-api';
const CLIENT_SUBSCRIPTION_API_VERSION = 1;
// Warning: must export variable seperately from declaration as workaround for JSDoc parsing error
/**
* Class from @graphistry/js-upload-api::Dataset for combining files into a single visualizable graph.
* @global
*/
const Dataset = DatasetBase;
export { Dataset };
/**
* Class from @graphistry/js-upload-api::File for uploading data and then reusing as part of Dataset graph visualizations.
* @global
*/
const File = FileBase;
export { File };
/**
* Helper class from @graphistry/js-upload-api::EdgeFile for tracking intent when creating a File object for uploading.
* @global
*/
const EdgeFile = EdgeFileBase;
export { EdgeFile };
/**
* Helper class from @graphistry/js-upload-api::NodeFile for tracking intent when creating a File object for uploading.
* @global
*/
const NodeFile = NodeFileBase;
export { NodeFile };
/**
* Class wrapping @graphistry/js-upload-api::Client for client->server File and Dataset uploads using username and password authentication.
* @global
* @extends ClientBase
*/
class Client extends ClientBase {
/**
* Create a Client
* @constructor
* @param {string} username - Graphistry server username
* @param {string} password - Graphistry server password
* @param {string} org - Graphistry organization (optional)
* @param {string} [protocol='https'] - 'http' or 'https' for client->server upload communication
* @param {string} [host='hub.graphistry.com'] - Graphistry server hostname
* @param {clientProtocolHostname} clientProtocolHostname - Override URL base path shown in browsers. By default uses protocol/host combo, e.g., https://hub.graphistry.com
*
* For more examples, see @graphistry/node-api and @graphistry/js-upload-api docs
*
* @example **Authenticate against Graphistry Hub**
* ```javascript
* import { Client } from '@graphistry/client-api';
* const client = new Client('my_username', 'my_password');
* ```
*/
constructor(
username, password, org = undefined,
protocol = 'https', host = 'hub.graphistry.com',
clientProtocolHostname,
version
) {
// console.debug('new client', { username }, { password }, { protocol }, { host }, { clientProtocolHostname }, { version });
super(
username, password, org,
protocol, host, clientProtocolHostname,
window.fetch.bind(window), version, '@graphistry/client-api');
}
}
export { Client };
/**
* Class wrapping @graphistry/js-upload-api::ClientPKey for client->server File and Dataset uploads using personal key authentication.
* @global
* @extends ClientPKeyBase
*/
class ClientPKey extends ClientPKeyBase {
/**
* Create a Client
* @constructor
* @param {string} personalKeyId - Graphistry server personal key ID
* @param {string} personalKeySecret - Graphistry server personal key secret
* @param {string} org - Graphistry organization (optional)
* @param {string} [protocol='https'] - 'http' or 'https' for client->server upload communication
* @param {string} [host='hub.graphistry.com'] - Graphistry server hostname
* @param {clientProtocolHostname} clientProtocolHostname - Override URL base path shown in browsers. By default uses protocol/host combo, e.g., https://hub.graphistry.com
*
* For more examples, see @graphistry/node-api and @graphistry/js-upload-api docs
*
* @example **Authenticate against Graphistry Hub**
* ```javascript
* import { Client } from '@graphistry/client-api';
* const client = new Client('my_personal_key_id', 'my_personal_key_secret');
* ```
*/
constructor(
personalKeyId, personalKeySecret, org = undefined,
protocol = 'https', host = 'hub.graphistry.com',
clientProtocolHostname,
version
) {
// console.debug('new client', { personalKeyId }, { personalKeySecret }, { protocol }, { host }, { clientProtocolHostname }, { version });
super(
personalKeyId, personalKeySecret, org,
protocol, host, clientProtocolHostname,
window.fetch.bind(window), version, '@graphistry/client-api');
}
}
export { ClientPKey };
import {
ajax,
catchError,
concatMap,
delay,
filter,
first,
forkJoin,
fromEvent,
isEmpty,
last,
map,
mergeMap,
mergeAll,
Observable,
of,
pipe,
ReplaySubject,
BehaviorSubject,
finalize,
retryWhen,
scan,
share,
shareReplay,
startWith,
Subject,
switchMap,
take,
takeLast,
tap,
timer,
throwError
} from './rxjs'; // abstract to simplify tolerating constant rxjs namespace manglings
const chainList = {};
// //////////////////////////////////////////////////////////////////////////////
/**
* @function makeCaller
* @private
* @description Serialization and coordination for formatting postMessage API calls, used with {@link GraphistryState} {@link Observable}s
* @param {string} modelName - 'view' or 'workbook'
* @param {any} args - anything to pass as falcor .call(...args)
* @return {@link GraphistryState} {@link Observable}
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(
* makeCaller('view', 'tick', []),
* delay(2000),
* makeCaller('view', 'tick', []))
* .subscribe();
**/
export function makeCaller(modelName, ...args) {
return switchMap(g => {
console.debug('makeCaller switchMap', { g });
//Wrap in Observable to insulate from PostMessageDataSource's rxjs version of Observable
return (new Observable((subscriber) => {
console.debug('caller hit', modelName, args, { g });
let runs = 0;
const sub = g.models[modelName]
.call(...args)
.subscribe(
(x) => {
runs++;
console.debug('caller tick', x, { runs });
subscriber.next(x);
},
(e) => {
runs++;
console.error('caller error', e, { runs });
subscriber.error(e);
},
() => {
console.debug('caller complete', modelName, args, { runs });
subscriber.complete();
});
return () => {
console.debug('caller unsub skip', modelName, args, { runs });
sub.unsubscribe();
};
}))
.pipe(
tap(v => console.debug('caller got', modelName, args, v, { g })),
map(v => g.updateStateWithResult(v)));
});
}
/**
* @function makeCallerJSON
* @private
* @description Serialization and coordination for formatting postMessage API calls, used with {@link GraphistryState} {@link Observable}s. Adds json desrialization to {@link makeCaller}.
* @param {string} modelName - 'view' or 'workbook'
* @param {any} args - anything to pass as falcor .call(...args)
* @return {@link GraphistryState} {@link Observable}
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(
* makeCaller('view', 'tick', []),
* delay(2000),
* makeCaller('view', 'tick', []))
* .subscribe();
**/
export function makeCallerJSON(modelName, ...args) {
return switchMap(g =>
of(g)
.pipe(
makeCaller(modelName, ...args),
map(({ json }) => json.toJSON())));
}
/**
*
* @function makeGetter
* @description Serialization and coordination for formatting postMessage API calls, used with {@link GraphistryState} {@link Observable}
* @param {string} modelName
* @param {...any} args - anything to pass as falcor .get(...args)
* @returns {@link GraphistryState} {@link Observable}
*/
export function makeGetter(modelName, ...args) {
return switchMap(g => {
//Wrap in Observable to insulate from PostMessageDataSource's rxjs version of Observable
return (new Observable((subscriber) => {
console.debug('getter hit', modelName, args);
const sub = g.models[modelName]
.get(...args)
.subscribe(
(x) => { subscriber.next(x); },
(e) => { subscriber.error(e); },
() => {
console.debug('getter complete', modelName, args);
subscriber.complete();
});
return () => {
console.debug('getter unsub', modelName, args);
sub.unsubscribe();
};
}))
.pipe(
tap(v => console.debug('getter got', modelName, args, v)),
map(v => g.updateStateWithResult(v)));
});
}
/**
* @function makeGetterJSON
* @description Serialization and coordination for formatting postMessage API calls, used with {@link GraphistryState} {@link Observable}. Adds json desrialization to {@link makeGetter}.
* @param {string} modelName
* @param {...any} args
* @returns {@link GraphistryState} {@link Observable}
*/
export function makeGetterJSON(modelName, ...args) {
return switchMap(g =>
of(g)
.pipe(
makeGetter(modelName, ...args)));
}
/*
* /*
const { workbook } = this;
return new this(workbook.get('id')
.map(({ json }) => json.toJSON())
.toPromise());
*/
/**
* @function makeSetterWithModel
* @private
* @description Serialization and coordination for formatting postMessage API calls, used with {@link GraphistryState} {@link Observable}s
* @param {string} modelName - 'view' or 'workbook'
* @param {string} value - {@link $value} path/value pair
* @return {@link GraphistryState} {@link Observable}
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(makeSetterWithModel('view', model => myValuesFromModel(model)))
* .subscribe();
**/
export function makeSetterWithModel(modelName, valuesFromModel) {
return switchMap((g) => {
const values = valuesFromModel(g.models[modelName]);
const out = g.models[modelName].set(...values);
//Wrap in Observable to insulate from PostMessageDataSource's rxjs version of Observable
return (new Observable((subscriber) => {
console.debug('starting makeSetterWithModel postMessage cmds', values);
const sub = out.subscribe(
((v) => { subscriber.next(v); }),
((e) => { subscriber.error({ msg: 'iframe setter fail', e, modelName, values }); }),
(() => { subscriber.complete(); }));
return () => {
console.debug('finished makeSetterWithModel; unsubscribe postMessage', { sub, values });
sub.unsubscribe();
};
}))
.pipe(
tap((v) => { console.debug('setter resp pre', v); }),
map(({ json }) => g.updateStateWithResult(json.toJSON())),
tap((v) => { console.debug('setter resp post', v); }))
});
}
/**
* @function makeSetter
* @private
* @description Serialization and coordination for formatting postMessage API calls, used with {@link GraphistryState} {@link Observable}s
* @param {string} modelName - 'view' or 'workbook'
* @param {string} value - {@link $value} path/value pair
* @return {@link GraphistryState} {@link Observable}
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(makeSetter('view', $value('viz.width', 500)))
* .subscribe();
**/
export function makeSetter(modelName, ...values) {
return makeSetterWithModel(modelName, () => { return values; });
}
// //////////////////////////////////////////////////////////////////////////////
/*
static _getIds(componentType, name, dataType, values = []) {
const { view } = this;
return new this(view
.call(`componentsByType['${componentType}'].rows.filter`, [name, dataType, values], ['_index'])
.takeLast(1)
.map(({ json = {} }) => {
const { componentsByType = {} } = json;
const { [componentType]: componentsForType = {} } = componentsByType;
const { rows = {} } = componentsForType;
return Array
.from(rows.filter || [])
.filter(Boolean).map(({ _index }) => _index);
})
.toPromise()
);
}
}
*/
/**
* Add columns to the current graph visuzliation's dataset
* GraphistryJS(document.getElementById('viz'))
* .flatMap(function(g) {
* window.g = g;
* const columns = [
* ['edge', 'highways', [66, 101, 280], 'number'],
* ['point', 'theme parks', ['six flags', 'disney world', 'great america'], 'string']
* ];
* console.log('adding columns', columns);
* return g.addColumns.apply(g, columns);
* })
* .subscribe();
*/
/*
Graphistry.addColumns = function (...columns) {
const { view } = this;
return new this(this
.from(columns)
.concatMap((column) => view.call('columns.add', column))
.map(({ json: { columns }}) => columns).filter(Boolean)
.map((columns) => columns[columns.length - 1].toJSON())
.toArray()
.toPromise()
);
}
*/
/**
* @function encodeColor
* @description Change colors based on an attribute. Pass null for attribute, mapping to clear.
* @param {GraphType} [graphType] - 'point' or 'edge'
* @param {Attribute} [attribute] - name of data column, e.g., 'degree'
* @param {Variant} [variation] - If there are more bins than colors, use 'categorical' to repeat colors and 'continuous' to interpolate
* @param {any} [colorsOrMapping] - array of color name or hex codes, or object mapping
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeColor('point', 'degree', 'categorical', ['black', 'white']))
* .subscribe();
*/
export function encodeColor(graphType, attribute, variation, colorsOrMapping) {
const colorDict = Array.isArray(colorsOrMapping) ? { colors: colorsOrMapping } : { mapping: colorsOrMapping };
const value = $value(`encodings.${graphType}.color`,
{
reset: attribute === undefined, variation, name: 'user_' + Math.random(),
encodingType: 'color', graphType, attribute, ...colorDict
});
return makeSetter('view', value);
}
chainList.encodeColor = encodeColor;
/**
* @function resetColor
* @description Reset color encoding
* @param {GraphType} [graphType] - 'point' or 'edge'
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetColor('point'))
* .subscribe();
*/
export function resetColor(graphType) {
return encodeColor(graphType);
}
chainList.resetColor = resetColor;
/**
* @function encodePointColor
* @description Single-argument version of {@link encodeColor} used for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, 'categorical' or 'continuous', [ str ] or mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointColor())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointColor('prebaked_colors_col'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointColor(['degree', 'categorical', ['black', 'white']]))
* .subscribe();
**/
export function encodePointColor(opts) {
const args = ['point'];
if (opts !== undefined) {
if (opts instanceof Array) {
for (let v of opts) {
args.push(v);
}
} else if (typeof (opts) === 'string') {
args.push(opts);
}
}
return encodeColor.apply(this, args);
}
chainList.encodePointColor = encodePointColor;
/**
* @function resetPointColor
* @description Reset the point color encoding
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetPointColor())
* .subscribe();
*/
export function resetPointColor() {
return encodePointColor(undefined);
}
chainList.resetPointColor = resetPointColor;
/**
* @function encodeEdgeColor
* @description Single-argument version of {@link encodeColor} used for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, 'categorical' or 'continuous', [ str ] or mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeEdgeColor())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeEdgeColor('prebaked_colors_col'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeEdgeColor(['degree', 'categorical', ['black', 'white']]))
* .subscribe();
**/
export function encodeEdgeColor(opts) {
const args = ['edge'];
if (opts !== undefined) {
if (opts instanceof Array) {
for (let v of opts) {
args.push(v);
}
} else if (typeof (opts) === 'string') {
args.push(opts);
}
}
return encodeColor.apply(this, args);
}
chainList.encodeEdgeColor = encodeEdgeColor;
/**
* @function resetEdgeColor
* @description Reset the edge color encoding
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetEdgeColor())
* .subscribe();
*/
export function resetEdgeColor() {
return encodeEdgeColor(undefined);
}
chainList.resetEdgeColor = resetEdgeColor;
/**
* @function encodeAxis
* @description Add an axis to the graph
* @param {Object}
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
*/
export function encodeAxis(axis) {
const value = $value(`encodings.point.axis`,
{
reset: axis === undefined, name: 'user_' + Math.random(),
encodingType: 'axis', graphType: 'point', attribute: 'degree', variation: 'categorical',
rows: axis
});
return makeSetter('view', value);
}
chainList.encodeAxis = encodeAxis;
/**
* @function encodeIcons
* @description Change icons based on an attribute. Pass undefined for attribute, mapping to clear.
* @param {GraphType} [graphType] - 'point' or 'edge'
* @param {Attribute} [attribute] - name of data column, e.g., 'icon'
* @param {Mapping} [object] - optional value mapping, e.g., {categorical: {fixed: {ip: 'laptop', alert: 'alaram'}, other: 'question'}}
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeIcons('point', 'icon', 'some_attr'))
* .subscribe();
**/
export function encodeIcons(graphType, attribute, mapping) {
const value = $value(`encodings.${graphType}.icon`,
{
reset: attribute === undefined, name: 'user_' + Math.random(),
encodingType: 'icon', graphType, attribute, mapping
});
return makeSetter('view', value);
}
chainList.encodeIcons = encodeIcons;
/**
* @function resetIcons
* @description Reset the icon encoding
* @param {GraphType} [graphType] - 'point' or 'edge'
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetIcons())
* .subscribe();
*/
export function resetIcons(graphType) {
return encodeIcons(graphType);
}
chainList.resetIcons = resetIcons;
/**
* @function encodePointIcons
* @description Single-argument point change icons based on an attribute for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointIcons())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointIcons('some_attr'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointIcons(['some_attr', some_mapping]))
* .subscribe();
**/
export function encodePointIcons(opts) {
const args = ['point'];
if (opts !== undefined) {
if (opts instanceof Array) {
for (let v of opts) {
args.push(v);
}
} else if (typeof (opts) === 'string') {
args.push(opts);
}
}
return encodeIcons.apply(this, args);
}
chainList.encodePointIcons = encodePointIcons;
/**
* @function resetPointIcons
* @description Reset the point icon encoding
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetPointIcons())
* .subscribe();
*/
export function resetPointIcons() {
return encodeIcons('point');
}
/**
* @function encodeEdgeIcons
* @description Single-argument edge change icons based on an attribute for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeEdgeIcons())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeEdgeIcons('some_attr'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeEdgeIcons(['some_attr', some_mapping]))
* .subscribe();
**/
export function encodeEdgeIcons(opts) {
const args = ['edge'];
if (opts !== undefined) {
if (opts instanceof Array) {
for (let v of opts) {
args.push(v);
}
} else if (typeof (opts) === 'string') {
args.push(opts);
}
}
return encodeIcons.apply(this, args);
}
chainList.encodeEdgeIcons = encodeEdgeIcons;
/**
* @function resetEdgeIcons
* @description Reset the edge icon encoding
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetEdgeIcons())
* .subscribe();
*/
export function resetEdgeIcons() {
return encodeIcons('edge');
}
chainList.resetEdgeIcons = resetEdgeIcons;
/**
* @function encodeSize
* @description Change size based on an attribute. Pass null for attribute, mapping to clear.
* @param {GraphType} [graphType] - 'point'
* @param {Attribute} [attribute] - name of data column, e.g., 'degree'
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeSize('point', 'community_infomap'))
* .subscribe();
*/
export function encodeSize(graphType, attribute, mapping) {
const value = $value(`encodings.${graphType}.size`,
{
reset: attribute === undefined, name: 'user_' + Math.random(),
encodingType: 'size', graphType, attribute, ...(mapping ? { mapping } : {})
});
return makeSetter('view', value);
}
chainList.encodeSize = encodeSize;
/**
* @function resetSize
* @description Reset the size encoding
* @param {GraphType} [graphType] - 'point'
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetSize())
* .subscribe();
*/
export function resetSize(graphType) {
return encodeSize(graphType);
}
chainList.resetSize = resetSize;
/**
* @function encodePointSize
* @description Single-argument point change size based on an attribute for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointSize())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointSize('some_attr'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodePointSize(['some_attr', some_mapping]))
* .subscribe();
**/
export function encodePointSize(opts) {
const args = ['point'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeSize.apply(this, args);
}
chainList.encodePointSize = encodePointSize;
/**
* @function resetPointSize
* @description Reset the point size encoding
* @returns {GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(resetPointSize())
* .subscribe();
*/
export function resetPointSize() {
return encodeSize('point');
}
chainList.resetPointSize = resetPointSize;
/**
* @function togglePanel
* @description Toggle a top menu panel on/off. If panel is an array, interpret as [panel, turnOn]. Only one panel is turned on; the rest are turned off.
* @param {string} [panel] - Name of panel: filters, exclusions, scene, labels, layout
* @param {boolean} [turnOn] - Whether to make panel visible, or turn all off
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(togglePanel('filters', true));
* .subscribe();
*/
export function togglePanel(panel, turnOn) {
if (!panel) {
return map(g => g);
}
if (Array.isArray(panel)) {
turnOn = panel.length > 1 ? panel[1] : undefined;
panel = panel[0];
}
if (turnOn) {
return makeSetterWithModel('view', (view) => {
const values = [
$value(`filters.controls[0].selected`, panel === 'filters'),
$value(`scene.controls[1].selected`, panel === 'scene'),
$value(`labels.controls[0].selected`, panel === 'labels'),
$value(`layout.controls[0].selected`, panel === 'layout'),
$value(`exclusions.controls[0].selected`, panel === 'exclusions'),
$value(`panels.left`,
panel === 'filters' ? $ref(view._path.concat(`filters`))
: panel === 'scene' ? $ref(view._path.concat(`scene`))
: panel === 'labels' ? $ref(view._path.concat(`labels`))
: panel === 'layout' ? $ref(view._path.concat(`layout`))
: $ref(view._path.concat(`exclusions`)))
];
return values;
});
} else {
const values = [
$value(`panels.left`, undefined),
$value(`filters.controls[0].selected`, false),
$value(`scene.controls[1].selected`, false),
$value(`labels.controls[0].selected`, false),
$value(`layout.controls[0].selected`, false),
$value(`exclusions.controls[0].selected`, false)
];
return makeSetter('view', ...values);
}
}
chainList.togglePanel = togglePanel;
export function toggleClustering(selected) {
const values = [
$value(`scene.simulating`, selected),
$value(`scene.controls[0].selected`, selected)
];
return makeSetter('view', ...values);
}
chainList.toggleClustering = toggleClustering;
/**
* @function encodeDefaultIcons
* @description Change default (user-unset) icons based on an attribute. Pass undefined for attribute, mapping to clear.
* @param {GraphType} [graphType] - 'point' or 'edge'
* @param {Attribute} [attribute] - name of data column, e.g., 'icon'
* @param {Mapping} [object] - optional value mapping, e.g., {categorical: {fixed: {ip: 'laptop', alert: 'alaram'}, other: 'question'}}
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultIcons('point', 'icon', 'some_attr'))
* .subscribe();
**/
export function encodeDefaultIcons(graphType, attribute, mapping) {
const value = $value(`encodings.defaults.${graphType}.icon`,
{
reset: attribute === undefined, name: 'user_' + Math.random(),
encodingType: 'icon', graphType, attribute, mapping
});
return makeSetter('view', value);
}
chainList.encodeDefaultIcons = encodeDefaultIcons;
/**
* @function encodeDefaultPointIcons
* @description Single-argument point default icons (user-unset) based on an attribute for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultPointIcons())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultPointIcons('some_attr'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultPointIcons(['some_attr', some_mapping]))
* .subscribe();
**/
export function encodeDefaultPointIcons(opts) {
const args = ['point'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeDefaultIcons.apply(this, args);
}
chainList.encodeDefaultPointIcons = encodeDefaultPointIcons;
/**
* @function encodeDefaultEdgeIcons
* @description Single-argument edge default icons (user-unset) based on an attribute for React props
* @param {Array} array: undefined to reset; str to use directly; [str attr, mapping]
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultEdgeIcons())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultEdgeIcons('some_attr'))
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(encodeDefaultEdgeIcons(['some_attr', some_mapping]))
* .subscribe();
**/
export function encodeDefaultEdgeIcons(opts) {
const args = ['edge'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeDefaultIcons.apply(this, args);
}
chainList.encodeDefaultEdgeIcons = encodeDefaultEdgeIcons;
export function encodeDefaultSize(graphType, attribute, mapping) {
const { view } = this;
return new this(view.set(
$value(`encodings.defaults.${graphType}.size`,
{
reset: attribute === undefined, name: 'user_' + Math.random(),
encodingType: 'size', graphType, attribute, mapping
}))
.map(({ json }) => json.toJSON())
.toPromise());
}
chainList.encodeDefaultSize = encodeDefaultSize;
export function encodeDefaultPointSize(opts) {
const args = ['point'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeDefaultSize.apply(this, args);
}
chainList.encodeDefaultPointSize = encodeDefaultPointSize;
export function encodeDefaultEdgeSize(opts) {
const args = ['edge'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeDefaultSize.apply(this, args);
}
chainList.encodeDefaultEdgeSize = encodeDefaultEdgeSize;
export function encodeDefaultColor(graphType, attribute, variation, mapping) {
const value = $value(`encodings.defaults.${graphType}.color`,
{
reset: attribute === undefined, variation, name: 'user_' + Math.random(),
encodingType: 'color', graphType, attribute, mapping
});
return makeSetter('view', value);
}
chainList.encodeDefaultColor = encodeDefaultColor;
export function encodeDefaultPointColor(opts) {
const args = ['point'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeDefaultColor.apply(this, args);
}
chainList.encodeDefaultPointColor = encodeDefaultPointColor;
export function encodeDefaultEdgeColor(opts) {
const args = ['edge'];
if (opts !== undefined) {
const attribute = opts instanceof Array ? opts[0] : opts;
args.push(attribute);
if (opts instanceof Array && opts.length > 1) {
const mapping = opts[1];
args.push(mapping);
}
}
return encodeDefaultColor.apply(this, args);
}
chainList.encodeDefaultEdgeColor = encodeDefaultEdgeColor;
/**
* @function toggleInspector
* @description Toggle inspector panel
* @param {boolean} [turnOn] - Whether to make panel visible
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(toggleInspector(true))
* .subscribe();
*/
export function toggleInspector(turnOn) {
if (!turnOn) {
const values = [
$value(`panels.bottom`, undefined),
$value(`inspector.controls[0].selected`, false)
];
return makeSetter('view', ...values);
} else {
return makeSetterWithModel('view', (view => {
const values = [
$value(`inspector.controls[0].selected`, true),
$value(`panels.bottom`, $ref(view._path.concat(`inspector`)))
];
return values;
}));
}
}
chainList.toggleInspector = toggleInspector;
/**
* @function toggleTimebars
* @description Toggle timebars panel
* @param {boolean} [turnOn] - Whether to make panel visible
* @return {@link Graphistry} A {@link Graphistry} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(toggleTimebars(true));
* .subscribe();
*/
export function toggleTimebars(turnOn) {
if (!turnOn) {
const values = [
$value(`panels.bottom`, undefined),
$value(`timebars.controls[0].selected`, false)
];
return makeSetter('view', ...values);
} else {
return makeSetterWithModel('view', (view => {
const values = [
$value(`timebars.controls[0].selected`, true),
$value(`panels.bottom`, $ref(view._path.concat(`timebars`)))
];
return values;
}));
}
}
chainList.toggleTimebars = toggleTimebars;
/**
* @function toggleHistograms
* @description Toggle histogram panel
* @param {boolean} [turnOn] - Whether to make panel visible
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(toggleHistograms(true))
* .subscribe();
*/
export function toggleHistograms(turnOn) {
return makeSetterWithModel('view', (view => {
if (!turnOn) {
return [
$value(`panels.right`, undefined),
$value(`histograms.controls[0].selected`, false)
];
} else {
return [
$value(`histograms.controls[0].selected`, true),
$value(`panels.right`, $ref(view._path.concat(`histograms`)))
];
}
}));
}
chainList.toggleHistograms = toggleHistograms;
/**
* @function Graphistry.tickClustering
* @description Run a number of milliseconds of Graphistry's clustering algorithm
* @param {number} ticks - The number of milliseconds to run
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(tickClustering())
* .subscribe();
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(tickClustering(10))
* .subscribe();
*/
export function tickClustering(ticks = 1000) {
if (typeof ticks !== 'number') {
return map(g => g);
}
return switchMap(g =>
of(g).pipe(
toggleClustering(true),
delay(ticks),
toggleClustering(false),
takeLast(1)));
}
chainList.tickClustering = tickClustering;
/**
* Center the view of the graph
* @function autocenter
* @todo Implement this function
* @static
* @param {number} percentile - Controls sensitivity to outliers
* @param {function} [cb] - Callback function of type callback(error, result)
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(autocenter(.90))
* .subscribe();
*/
export function autocenter(percentile) {
return makeCallerJSON('view', 'autocenter', [percentile]);
}
chainList.autocenter = autocenter;
/**
* Read the workbook ID
* @function getCurrentWorkbook
* @param {function} [cb] - Callback function of type callback(error, result)
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(getCurrentWorkbook())
* .subscribe(function (workbook) {
* alert('id: ' + workbook.id)
* });
*/
export function getCurrentWorkbook() {
return makeGetterJSON('workbook', 'id');
}
chainList.getCurrentWorkbook = getCurrentWorkbook;
/**
* Save the current workbook. A saved workbook will persist the analytics state
* of the visualization, including active filters and exclusions
* @function saveWorkbook
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(saveWorkbook())
* .subscribe();
*/
export function saveWorkbook() {
return makeCallerJSON('workbook', 'save', []);
}
chainList.saveWorkbook = saveWorkbook;
/**
* Hide or Show Toolbar UI
* @function toogleToolbar
* @param {boolean} show - Set to true to show toolbar, and false to hide toolbar.
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
*
* <button onclick="g.pipe(toggleToolbar(false)).subcribe()">Hide toolbar</button>
* <button onclick="g.pipe(toggleToolbar(true)).subscribe()">Show toolbar</button>
*
*/
export function toggleToolbar(show) {
return updateSetting('showToolbar', !!show);
}
chainList.toggleToolbar = toggleToolbar;
/**
* Add a filter to the visualization with the given expression
* @function addFilter
* @param {string} expr - An expression using the same language as our in-tool
* exclusion and filter panel
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(
* addFilter('point:degree > 0'),
* addFilter('edge:value > 0'))
* .subscribe();
*/
export function addFilter(expr) {
return expr ? makeCaller('view', 'filters.add', [expr]) : map(g => g)
}
chainList.addFilter = addFilter;
/**
* Add filters to the visualization with the given expression
* @function addFilter
* @param {array} expr - An array of expressions using the same language as our in-tool
* exclusion and filter panel
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(addFilters(['point:degree > 0', 'edge:value > 0'])));
* .subscribe();
*/
export function addFilters(expr) {
if (typeof (expr) === 'string') {
return addFilter(expr);
}
if (!Array.isArray(expr)) {
throw new Error('Expected an array of filters');
}
return switchMap(g => {
return forkJoin(expr.map(e => of(g).pipe(addFilter(e))))
.pipe(map((results) => g.updateStateWithResult(results)));
});
}
chainList.addFilters = addFilters;
/**
* Add an exclusion to the visualization with the given expression
* @function addExclusion
* @param {string} expr - An expression using the same language as our in-tool
* exclusion and filter panel
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(addExclusion('point:degree > 0'))
* .subscribe();
*/
export function addExclusion(expr) {
return expr ? makeCaller('view', 'exclusions.add', [expr]) : map(g => g)
}
chainList.addExclusion = addExclusion;
/**
* Add an exclusion to the visualization with the given expression
* @function addExclusions
* @param {array} expr - Expressions using the same language as our in-tool
* exclusion and filter panel
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(addExclusions(['point:degree > 0'], ['edge:value > 0']))
* .subscribe();
*/
export function addExclusions(expr) {
if (typeof (expr) === 'string') {
return addExclusion(expr);
}
if (!Array.isArray(expr)) {
throw new Error('Expected an array of exclusions');
}
return switchMap(g => {
return forkJoin(expr.map(e => of(g).pipe(addExclusion(e))))
.pipe(map((results) => g.updateStateWithResult(results)));
});
}
chainList.addExclusions = addExclusions;
/**
* UNSTABLE: Set the selection.
* Currently, this uses internal ids and will be updated to use external ids.
* @function setSelectionExternal
* @param {Object} selection - point array and edge array
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(setSelectionExternal({point: [1, 2, 3], edge: []}))
* .subscribe();
*/
export function setSelectionExternal({point = [], edge = []} = {}) {
return makeCaller('view', 'selection.setExternal', {point, edge});
}
chainList.setSelectionExternal = setSelectionExternal;
/**
* UNSTABLE: Set the highlight.
* Currently, this uses internal ids and will be updated to use external ids.
* @function setHighlightExternal
* @param {Object} selection - point array, edge array and darken boolean
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(setHighlightExternal({point: [1, 2, 3], edge: []}))
* .subscribe();
*/
export function setHighlightExternal({point = [], edge = [], darken = true} = {}) {
return makeCaller('view', 'highlight.setExternal', {point, edge});
}
chainList.setHighlightExternal = setHighlightExternal;
const G_API_SETTINGS = {
//models/toolbar.js
'showToolbar': ['view', 'toolbar.visible'],
//models/scene/scene.js
'pruneOrphans': ['view', 'pruneOrphans'],
'showArrows': ['view', 'scene.renderer.showArrows'],
'background': ['view', 'scene.renderer.background.color'],
'edgeOpacity': ['view', 'scene.renderer.edges.opacity'],
'edgeSize': ['view', 'scene.renderer.edges.scaling'],
'edgeCurvature': ['view', 'scene.renderer.edges.curvature'],
'pointOpacity': ['view', 'scene.renderer.points.opacity'],
'pointSize': ['view', 'scene.renderer.points.scaling'],
'neighbhorhoodHighlight': ['view', 'scene.renderer.points.neighborhoodHighlight'],
'neighbhorhoodHighlightHops': ['view', 'scene.renderer.points.neighborhoodHighlightHops'],
//models/camera.js
'zoom': ['view', 'camera.zoom'],
'center': ['view', 'camera.center["x", "y", "z"]'],
//models/label.js
'labelOpacity': ['view', 'labels.opacity'],
'labelEnabled': ['view', 'labels.enabled'],
'labelPropertiesEnabled': ['view', 'labels.propertiesEnabled'],
'labelInspectorEnabled': ['view', 'labels.inspectorEnabled'],
'labelShowActions': ['view', 'labels.showActions'],
'labelPOI': ['view', 'labels.poiEnabled'],
'labelLabelPOI': ['view', 'labels.poiLabelEnabled'],
'labelPOIMax': ['view', 'labels.poiMax'],
'labelHighlightEnabled': ['view', 'labels.highlightEnabled'],
'labelColor': ['view', 'labels.foreground.color'],
'labelBackground': ['view', 'labels.background.color'],
//models/layout.js
'precisionVsSpeed': ['view', 'layout.options.forceatlas2barnes[0].value'],
'gravity': ['view', 'layout.options.forceatlas2barnes[1].value'],
'scalingRatio': ['view', 'layout.options.forceatlas2barnes[2].value'],
'edgeInfluence': ['view', 'layout.options.forceatlas2barnes[3].value'],
'strongGravity': ['view', 'layout.options.forceatlas2barnes[4].value'],
'dissuadeHubs': ['view', 'layout.options.forceatlas2barnes[5].value'],
'linLog': ['view', 'layout.options.forceatlas2barnes[6].value'],
'lockedX': ['view', 'layout.options.forceatlas2barnes[7].value'],
'lockedY': ['view', 'layout.options.forceatlas2barnes[8].value'],
'lockedR': ['view', 'layout.options.forceatlas2barnes[9].value'],
};
/**
* @description
* Modify a settings value in the visualization
*
* | Available Settings | Value Type |
* | ------------------ | ---------- |
* | `showToolbar` | `boolean` |
* | `pruneOrphans` | `boolean` |
* | `showArrows` | `boolean` |
* | `background` | color as hex or rgba `string` |
* | `edgeOpacity` | `number` (0 to 1) |
* | `edgeSize` | `number` (0.1 to 10) |
* | `edgeCurvature` | `number` (0.1 to 1) |
* | `pointOpacity` | `number` (0 to 1) |
* | `pointSize` | `number` (0.1 to 10) |
* | `neighborhoodHighlight` | `string` ("incoming", "outgoing", "both", "none") |
* | `neighborhoodHighlightHops` | `uint` |
* | `zoom` | `uint` |
* | `center` | `const 0` |
* | `labelOpacity` | `boolean` |
* | `labelEnabled` | `boolean` |
* | `labelPOI` | `boolean` |
* | `labelLabelPOI` | `boolean` |
* | `labelHighlightEnabled` | `boolean` |
* | `labelInspectorEnabled` | `boolean` |
* | `labelShowActions` | `boolean` |
* | `labelColor` | color as hex or rgba `string` |
* | `labelBackground` | color as hex or rgba `string` |
* | `precisionVsSpeed` | `int` (-5 to +5) |
* | `gravity` | `number` (0 to 10) |
* | `scalingRatio` | `number` (0 to 10) |
* | `edgeInfluence` | `number` (0 to 10) |
* | `strongGravity` | `boolean` |
* | `dissuadeHubs` | `boolean` |
* | `linLog` | `boolean` |
* | `lockedX` | `boolean` |
* | `lockedY` | `boolean` |
* | `lockedR` | `boolean` |
* @function updateSetting
* @param {string} name - the name of the setting to change
* @param {string} val - the value to set the setting to.
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* graphistryJS(document.getElementById('viz'))
* .pipe(updateSetting('background', 'red'))
* .subscribe();
*/
export function updateSetting(name, val) {
console.debug('updating setting called', { G_API_SETTINGS, name, val });
if (G_API_SETTINGS[name] === undefined) {
throw new Error(`Property "${name}" is not a valid setting, available are: ${Object.keys(G_API_SETTINGS).join(', ')}`);
}
const [modelType, path] = G_API_SETTINGS[name];
const value = $value(path, $atom(val, { $timestamp: Date.now() }));
return makeSetter(modelType, value);
}
chainList.updateSetting = updateSetting;
/**
* Update the camera zoom level
* @function updateZoom
* @param {number} level - Controls how far to zoom in or out.
* @return {@link GraphistryState} A {@link GraphistryState} {@link Observable} that emits the result of the operation
* @example
* graphistryJS(document.getElementById('viz'))
* .pipe(updateZoom(2), delay(2000), updateZoom(0.5))
* .subscribe();
*/
export function updateZoom(level) {
return updateSetting('zoom', level);
}
chainList.updateZoom = updateZoom;
/**
* Get or create an {@link Observable} stream of all selection updates from the visualization.
* UNSTABLE: This API is subject to change in future versions.
* The security model for this is being developed, in the mean time it is disabled by default.
* Contact us for help to prototype this feature.
* @function selectionUpdates
* @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable}
* @return {Subscription} A {@link Subscription} that can be used to react to the selection updates
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(
* map(selectionUpdates),
* tap(({ edge, point}) => console.log('Edge array:', edge, 'Point array:', point)),
* })
* .subscribe();
*/
export function selectionUpdates(g, {withColumns=false, pageSize=1000} = {}) {
if (!(g.subscriptionAPIVersion >= 1)) {
return throwError(() => new Error('selectionUpdates is not available the currently embedded graphistry viz.'));
}
const selectionPath = ".selection.labels";
return g.selectionStream || (g.selectionStream = new BehaviorSubject('Initialize selectionUpdates stream')
.pipe(
tap(() => {
console.debug('postMessage subscription', '@client-api.selectionUpdates');
g.iFrame.contentWindow.postMessage({ type: 'graphistry-subscribe', agent: 'graphistryjs', path: selectionPath, options: { pageSize, withColumns } }, '*');
}),
finalize(() => {
console.debug('postMessage unsubscribe', '@client-api.selectionUpdates');
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),
tap(({ edge, point, labels }) => {
g.models.model.setCache({
json: {
workbooks: {
open: {
views: {
current: {
selection: {
edge,
point,
labels
}
}
}
}
}
}
});
}),
shareReplay({ bufferSize: 1, refCount: true })
),
)
));
}
/**
* Subscribe to selection change events
* @function subscribeSelections
* @param {Object} - An Object with `onChange` and `onExit` callbacks
* @return {Subscription} A {@link Subscription} that can be used to stop reacting to label updates
* @example
* var sub = graphistryJS(document.getElementById('viz'))
* .pipe((g) => subscribeSelections({
* g,
* onChange: ({ edge, point}) => console.log('Edge array:', edge, 'Point array:', point)
* }
* }));
* setTimeout(() => { sub.unsubscribe(); }, 5000);
*/
export function subscribeSelections({ onChange, g }) {
return selectionUpdates(g).subscribe({ next: onChange });
}
/**
* Get or create an {@link Observable} stream of all label updates from the visualization.
* <p>
* The {@link Observable} returned by this method emits inner Observables, where each
* inner {@link Observable} is tied to the lifetime of the label for which it was created.
* </p><p>
* For each label rendered in the visualization, the {@link Observable} returned by this
* method will create and emit a new inner {@link Observable}. The inner {@link Observable} will
* emit events when the label changes. For example, if the user clicks on the label,
* or the label changes position because of a pan/zoom, the inner {@link Observable} will
* emit an event.
* </p><p>
* The inner {@link Observable} for a label will complete if the label is removed from the screen.
* </p><p>
* @function labelUpdates
* @param {@link GraphistryState} [g] A {@link GraphistryState} {@link Observable} or depricated, cache an object.
* @return {Observable<Observable<LabelEvent>>} An {@link Observable} of inner {Observables}, where each
* inner {@link Observable} represents the lifetime of a label in the visualization.
* @example
* GraphistryJS(document.getElementById('viz'))
* .pipe(
* map(g => labelUpdates(g)),
* tap(({ id, tag, pageX, pageY }) => {
* // prints messages like
* // > 'Label 13 added at (200, 340)'
* // > 'Label 74 updated at (750, 100)'
* console.log(`Label ${id} ${tag} at (${pageX}, ${pageY})`);
* }),
* takeLast(1),
* tap(function ({ id, pageX, pageY }) {
* console.log(`Label ${id} removed at (${pageX}, ${pageY})`);
* });
* })
* .subscribe();
* @example
* //first use
* GraphistryJS(document.getElementById('viz'))
* .pipe(map((g) => labelUpdates(g)))
* .subscribe();
* //second time reuse the cache to avoid excess event queue slowdowns
* GraphistryJS(document.getElementById('viz'))
* .pipe(map((g) => labelUpdates(g)))
* .subscribe()
*/
export function labelUpdates(g={}) {
const LABELS_PATH = ".labels"
var src;
if (!(g.subscriptionAPIVersion >= 1)) {
console.debug('Using legacy source, version:', g.subscriptionAPIVersion, '@client-api.labelUpdates');
src = fromEvent(window, 'message')
.pipe(
map(o => o.data),
filter(o => o && o.type === 'labels-update'),
shareReplay({ bufferSize: 1, refCount: true }),
);
} else {
src = new BehaviorSubject('value').pipe(
tap((v) => {
console.debug('postMessage subscription', '@client-api.labelUpdates');
g.iFrame.contentWindow.postMessage({ type: 'graphistry-subscribe', agent: 'graphistryjs', path: LABELS_PATH }, '*');
}),
finalize(() => {
console.debug('postMessage subscription', '@client-api.labelUpdates');
g.iFrame.contentWindow.postMessage({ type: 'graphistry-unsubscribe', agent: 'graphistryjs', path: LABELS_PATH }, '*');
}),
switchMap(() =>
fromEvent(window, 'message').pipe(
map(o => o.data),
filter(o => o && o.type === 'graphistry-sub-update' && o.path === LABELS_PATH),
map(o => o.data),
)
),
shareReplay({ bufferSize: 1, refCount: true }),
);
}
return g.labelsStream || (g.labelsStream = src.pipe(
scan((memo, { labels, simulating, semanticZoomLevel }) => {
labels = labels || [];
const updates = [], newSources = [];
const labelsById = Object.create(null);
const nextSources = Object.create(null);
const { sources, prevLabelsById } = memo;
let idx = -1, len = labels.length, label;
while (++idx < len) {
let source;
label = labels[idx];
const { id } = label;
if (id in sources) {
source = sources[id];
delete sources[id];
if (memo.simulating !== simulating ||
memo.semanticZoomLevel !== semanticZoomLevel ||
!shallowEqual(prevLabelsById[id], label)) {
updates.push({ ...label, simulating, semanticZoomLevel, tag: 'updated' });
}
} else {
newSources.push(source = new ReplaySubject(1));
updates.push({ ...label, simulating, semanticZoomLevel, tag: 'added' });
source.key = id;
}
labelsById[id] = label;
nextSources[id] = source;
}
for (const id in sources) {
sources[id].complete();
}
idx = -1;
len = updates.length;
while (++idx < len) {
label = updates[idx];
nextSources[label.id].next(label);
}
return {
newSources,
simulating,
semanticZoomLevel,
sources: nextSources,
prevLabelsById: labelsById
};
},
{
newSources: [],
sources: Object.create(null),
prevLabelsById: Object.create(null),
}),
mergeMap(({ newSources }) => newSources)
));
}
/**
* Subscribe to label change and exit events
* @function subscribeLabels
* @param {Object} - An Object with `onChange` and `onExit` callbacks
* @return {Subscription} A {@link Subscription} that can be used to stop reacting to label updates
* @example
* var sub = graphistryJS(document.getElementById('viz'))
* .pipe((g) => subscribeLabels({
* g,
* onChange: (label) => {
* console.log(`Label ${label.id} changed at (${label.pageX}, ${label.pageY})`);
* },
* onExit: (label) => {
* console.log(`Label ${label.id} removed at (${label.pageX}, ${label.pageY})`);
* },
* onError: (e) => {
* console.error('Error in label subscription', e);
* }
* }));
* setTimeout(() => { sub.unsubscribe(); }, 5000);
*/
export function subscribeLabels({ onChange, onExit, onError, g }) {
return labelUpdates(g)
.pipe(
mergeMap((group) => {
return group
.pipe(
tap((event) => onChange && onChange(event)),
takeLast(1),
tap((event) => onExit && onExit(event)));
}))
.subscribe({ error: onError });
}
class GraphistryState {
constructor(subscriptionAPIVersion, iFrame, models, result) {
this.subscriptionAPIVersion = subscriptionAPIVersion;
this._iFrame = iFrame;
this._models = models;
this.result = result;
}
clone() {
return new GraphistryState(this.subscriptionAPIVersion, this.iFrame, this.models, this.result);
}
get iFrame() {
return this._iFrame;
}
get models() {
return this._models;
}
get workbook() {
return this.models.workbook;
}
get view() {
return this.models.view;
}
updateStateWithResult(result) {
const clone = this.clone();
clone.result = result;
return clone;
}
}
function wrapCallback(obs, pipeable, withCB = false) {
return function (...args) {
var val, hasVal = false;
const cb = withCB
&& args.length && (args[args.length - 1] instanceof Function)
? args[args.length - 1]
: function () { };
return (obs
.pipe(pipeable(...args))
.subscribe(
x => { hasVal = true; val = x; },
e => cb(e),
() => { hasVal && cb(null, val) }
));
};
}
function aliasLegacyToplevels(o) {
o.do = function (f) {
const out = this.pipe(tap(f));
return aliasLegacyToplevels(out);
};
o.map = function (f) {
const out = this.pipe(map(f));
return aliasLegacyToplevels(out);
};
o.switchMap = function (f) {
const out = this.pipe(switchMap(f));
return aliasLegacyToplevels(out);
};
return o;
}
//Convenience functions that match the old API
//(Deprecate?)
function addCallbacks(obs, target) {
if (!target) {
target = obs;
}
//We used to extend Observable to make chaining extensions monadic...
//... but RxJS is phasing that out, so we no longer, and just keep initial top-level
//returns Subscriptions
Object.keys(chainList).forEach(key => {
target[key] = wrapCallback(obs, chainList[key]);
target[key + 'CB'] = wrapCallback(obs, chainList[key], true);
});
/*
const lift = function (obs, f) {
return function (...args) {
return obs.pipe(f(...args));
};
};
*/
target.labelUpdates = labelUpdates;// lift(obs, labelUpdates);
target.subscribeLabels = subscribeLabels;//lift(obs, subscribeLabels);
target.selectionUpdates = selectionUpdates; // lift(obs, selectionUpdates);
target.subscribeLabels
return target;
}
/**
* Function that wraps an IFrame as an {@link Observable} {@link GraphistryState} - other methods in this library can be piped with it
* @func graphistryJS
* @exports module:Graphistry
* @param {Object} IFrame - An IFrame that hosts a Graphistry visualization.
* @return {@link GraphistryState} - Observable that emits {@link GraphistryState} (must .subscribe() to start listening)
* @example
*
* <iframe id="viz" src="https://hub.graphistry.com/graph/graph.html?dataset=Miserables" />
* <script>
* document.addEventListener("DOMContentLoaded", function () {
*
* graphistryJS(document.getElementById('viz'))
* .pipe(
* tap((g) => {
* console.log('iframe ready; opening filters, pausing, then adding columns');
* document.getElementById('controls').style.opacity=1.0);
* window.g = g;
* }),
* openFilters,
* delay(5000),
* switchMap((g) => {
* console.log('filters opened & delayed; adding columns');
* const columns = [
* ['edge', 'highways', [66, 101, 280], 'number'],
* ['point', 'theme parks', ['six flags', 'disney world', 'great america'], 'string']
* ];
* return (
* forkJoin(columns.map(([type, name, values, type]) => addColumn(type, name, values, type)))
* .pipe(map(() => g)))
* })
* .subscribe(
* (g) => { console.log('event', g); },
* (err) => { console.log('error', err); },
* () => { console.log('all done'); }
* });
* </script>
*
*/
export function graphistryJS(iFrame) {
if (!iFrame) {
throw new Error('No iframe provided to Graphistry');
}
console.debug('init graphistryJS: modified', { iFrame, fromEvent, updateSetting, ajax });
const flow = (
fromEvent(iFrame, 'load')
.pipe(
tap((v) => { console.debug('Starting iframe protocol listen flow: Load trigger'), v }),
startWith(iFrame),
tap((v) => { console.debug('Starting iframe protocol listen flow: v', v) }),
filter(target => target && target.contentWindow && target.contentWindow.postMessage),
map(target => target.contentWindow),
tap((target) => {
console.info(`Graphistry API: connecting to client`, target);
target.postMessage({ type: 'ready', agent: 'graphistryjs', subscriptionAPIVersion: CLIENT_SUBSCRIPTION_API_VERSION }, '*');
}),
switchMap(((target) =>
fromEvent(window, 'message') //FIXME why not target? how to ensure proper frame?
.pipe(
tap((v) => { console.debug('Starting iframe protocol listen flow: Message', v) }),
filter(({ data}) => data && data.type === 'init'),
tap((v) => {
target.postMessage({ type: 'graphistry-init-ack', agent: 'graphistryjs', subscriptionAPIVersion: CLIENT_SUBSCRIPTION_API_VERSION }, '*');
console.debug('Starting iframe protocol listen flow: Got type: init and sent graphistry-init-ack', v)
}),
map(({ data: { cache, subscriptionAPIVersion }, cache: cache2 }) => ({ target, cache, cache2, subscriptionAPIVersion })))
)),
switchMap(({ target, cache, cache2, subscriptionAPIVersion }) => {
console.debug('Graphistry API: init filter passed 2, handling', { target, cache, cache2, subscriptionAPIVersion });
if (!subscriptionAPIVersion) {
console.error('Viz is using a previous version of the subscription API. Downgrade for labelUpdates.');
}
//Observable wrapper insulating from Model's rxjs version
// ... assume just new/get/subscribe/unsubscribe
const model = new Model({
cache: cache || cache2 || {},
recycleJSON: true,
//scheduler: Scheduler.async, //TODO use default?
allowFromWhenceYouCame: true
});
console.debug('Graphistry API: model created', model);
model._source = new PostMessageDataSource(window, target, model, '*');
console.debug('Graphistry API: model source created', model._source);
return (new Observable((subscriber) => {
console.debug('Graphistry API: New observable', { target, cache, cache2 });
const sub = model.get(`workbooks.open.views.current.id`)
.subscribe(
(result) => { console.debug('client new observable next', result); subscriber.next(result); },
(error) => { subscriber.error('iframe model initialization error', { error }); },
() => {
console.debug('PostMessageDataSource: teardown');
subscriber.complete();
});
return () => { console.debug('client observable unsub'); sub.unsubscribe(); };
}))
.pipe(
tap((v) => { console.debug('Starting iframe protocol obs tap', v) }),
map(({ json, ...rest }) => {
console.debug('got postMessage model hit', json, rest)
const workbook = model.deref(json.workbooks.open);
const view = model.deref(json.workbooks.open.views.current);
console.debug(`PostMessageDataSource: connected to client`, { workbook, view });
return { workbook, view };
}),
tap((v) => { console.debug('Starting iframe protocol obs tap2', v) }),
map(({ workbook, view }) => new GraphistryState(subscriptionAPIVersion, iFrame, { model, view, workbook })),
tap((result) => {
console.info(`Graphistry API: connected to client`, result)
}));
}),
retryWhen(errors => {
console.error('Graphistry API: retrying2 get', errors);
return errors.pipe(
tap((e) => { console.error('Graphistry API: retrying2 get e', e); }),
delay(1000),
tap((v) => { console.debug('Graphistry API: retrying2 get v', v); })
);
}),
tap((result) => {
console.debug(`Graphistry API (pre-replay): connected to client`, result)
})
));
//https://rxjs.dev/deprecations/multicasting
const resubscribable =
flow.pipe(
shareReplay(1),
tap((result) => { console.debug(`Graphistry API (replay): connected to client`, result) }),
);
const flowEnriched = resubscribable.pipe(map((g) => addCallbacks(resubscribable, g)));
addCallbacks(flowEnriched);
aliasLegacyToplevels(flowEnriched);
return flowEnriched;
}
//https://github.com/evanw/esbuild/issues/1719
//export default graphistryJS;
export const GraphistryJS = graphistryJS;
/* legacy */
(function () {
try {
window.GraphistryJS = graphistryJS;
} catch (e) { } // eslint-disable-line no-empty
}());
export {
//rxjs: reexport for end-user convenience without explicit dependency / rxjs expertise
ajax,
catchError,
concatMap,
delay,
filter,
first,
forkJoin,
fromEvent,
isEmpty,
last,
map,
mergeMap,
mergeAll,
Observable,
of,
pipe,
ReplaySubject,
Subject,
scan,
share,
shareReplay,
startWith,
switchMap,
take,
takeLast,
tap,
timer
//g api
//updateSetting, // exported upon definition
};