Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/modules/graph-layers/api-reference/layers/graph-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ updates the layout and stylesheet state automatically during drags.
Inherited from `CompositeLayer`. Defaults to `true` so nodes and edges respond to
hover and click events. Disable if you only need a static rendering.

#### `layoutUpdateInterval` (number, optional)

Throttle how often the layer re-renders while the layout is iterating. The value
represents the minimum number of milliseconds between layout-driven updates.
Set to `0` (default) to render every change or increase the interval to slow the
animation for inspection. When throttled, layout change events are queued and
flushed at the requested cadence so intermediate frames are not dropped.

## Notes

- The layer resolves the stylesheet through the `GraphStyleEngine`, which
Expand Down
34 changes: 34 additions & 0 deletions examples/graph-layers/graph-viewer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const INITIAL_VIEW_STATE = {
// the default cursor in the view
const DEFAULT_CURSOR = 'default';
const DEFAULT_LAYOUT = DEFAULT_EXAMPLE?.layouts[0] ?? 'd3-force-layout';
const DEFAULT_LAYOUT_UPDATE_INTERVAL = 120;

type LayoutFactory = (options?: Record<string, unknown>) => GraphLayout;

Expand Down Expand Up @@ -101,6 +102,7 @@ export function App(props) {
const [dagChainSummary, setDagChainSummary] = useState<
{chainIds: string[]; collapsedIds: string[]}
| null>(null);
const [layoutUpdateInterval, setLayoutUpdateInterval] = useState(DEFAULT_LAYOUT_UPDATE_INTERVAL);

const graphData = useMemo(() => selectedExample?.data(), [selectedExample]);
const layoutOptions = useMemo(
Expand Down Expand Up @@ -336,6 +338,11 @@ export function App(props) {
setSelectedLayout(layoutType);
}, []);

const handleLayoutUpdateIntervalChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(event.target.value);
setLayoutUpdateInterval(Number.isFinite(value) ? Math.max(0, value) : 0);
}, []);

return (
<div
style={{
Expand Down Expand Up @@ -398,6 +405,7 @@ export function App(props) {
new GraphLayer({
engine,
stylesheet: selectedStyles,
layoutUpdateInterval,
resumeLayoutAfterDragging
})
]
Expand Down Expand Up @@ -425,6 +433,32 @@ export function App(props) {
defaultExample={DEFAULT_EXAMPLE}
onExampleChange={handleExampleChange}
>
<section style={{marginBottom: '0.5rem', fontSize: '0.875rem', lineHeight: 1.5}}>
<h3 style={{margin: '0 0 0.5rem', fontSize: '0.875rem', fontWeight: 600, color: '#0f172a'}}>
Layout refresh rate
</h3>
<p style={{margin: '0 0 0.75rem', color: '#334155'}}>
Slow down the layout updates to observe position changes between iterations.
</p>
<label
htmlFor="graph-viewer-layout-update-interval"
style={{display: 'flex', flexDirection: 'column', gap: '0.5rem'}}
>
<input
id="graph-viewer-layout-update-interval"
type="range"
min="0"
max="500"
step="10"
value={layoutUpdateInterval}
onChange={handleLayoutUpdateIntervalChange}
style={{width: '100%'}}
/>
<span style={{fontSize: '0.8125rem', color: '#475569'}}>
Updating every {layoutUpdateInterval}ms
</span>
</label>
</section>
{isDagLayout ? (
<section style={{marginBottom: '0.5rem', fontSize: '0.875rem', lineHeight: 1.5}}>
<h3 style={{margin: '0 0 0.5rem', fontSize: '0.875rem', fontWeight: 600, color: '#0f172a'}}>
Expand Down
54 changes: 53 additions & 1 deletion modules/graph-layers/src/core/graph-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import type {Node} from '../graph/node';
import {Edge} from '../graph/edge';
import {Graph} from '../graph/graph';
import {GraphLayout} from './graph-layout';
import {GraphLayout, type GraphLayoutState} from './graph-layout';
import {Cache} from './cache';
import {log} from '../utils/log';

Expand All @@ -24,6 +24,26 @@ export class GraphEngine extends EventTarget {
private _layoutDirty = false;
private _transactionInProgress = false;

private static _cloneLayoutValue<T>(value: T): T {
if (value === null || typeof value !== 'object') {
return value;
}

if (Array.isArray(value)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return value.map((item) => GraphEngine._cloneLayoutValue(item)) as unknown as T;
}

const cloned: Record<string | number | symbol, unknown> = {};
for (const key of Object.keys(value as Record<string, unknown>)) {
cloned[key] = GraphEngine._cloneLayoutValue(
(value as Record<string, unknown>)[key]
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return cloned as T;
}

constructor(props: GraphEngineProps);
/** @deprecated Use props constructor: new GraphEngine(props) */
constructor(graph: Graph, layout: GraphLayout);
Expand Down Expand Up @@ -194,4 +214,36 @@ export class GraphEngine extends EventTarget {
_updateCache(key, updateValue) {
this._cache.set(key, updateValue, this._graph.version + this._layout.version);
}

captureLayoutFrame(): GraphLayoutFrame {
const nodePositions = new Map<Node['id'], [number, number] | null>();
const edgePositions = new Map<Edge['id'], ReturnType<GraphLayout['getEdgePosition']> | null>();

for (const node of this.getNodes()) {
const position = this.getNodePosition(node);
nodePositions.set(node.getId(), position ? [...position] : null);
}

for (const edge of this.getEdges()) {
const layoutInfo = this.getEdgePosition(edge);
edgePositions.set(
edge.getId(),
layoutInfo ? GraphEngine._cloneLayoutValue(layoutInfo) : null
);
}

return {
version: this._layout.version,
state: this._layout.state,
nodePositions,
edgePositions
};
}
}

export type GraphLayoutFrame = {
version: number;
state: GraphLayoutState;
nodePositions: Map<Node['id'], [number, number] | null>;
edgePositions: Map<Edge['id'], ReturnType<GraphLayout['getEdgePosition']> | null>;
};
Loading