Audience: adopters embedding the toolkit inside a larger decision- support or process application, who need their OWN buttons, panels, and workflows to control toolkit behavior, and toolkit state to flow back out into their components.
Every snippet in this guide runs in CI: examples/wiring/src/
contains each one as an executable test, so this document cannot rot
silently.
g3t is a component library with a deliberately thin integration surface, in three parts:
useXStore(selector) in React; useXStore.getState() /
useXStore.subscribe(...) anywhere (event handlers, services,
non-React code).ugm), the encoding spec
(encodingSpec), containment, and onReady (which hands you the
Cytoscape Core for camera and layout control).A useful consequence: a "custom button" is almost always one line of
store or function call in an onClick.
Icon, the g3t-btn / g3t-select / g3t-input CSS
classes, design tokens (--g3t-*). Use these to make YOUR controls
look native next to ours.SearchBar, ZoomControls, SpecPort,
ThemeSwitcher, SpecLegend, ContextMenu. Single-purpose,
callback-driven; compose them into your own bars and panels.CytoscapeCanvas, GraphToolbar,
EncodingSpecPanel, AlgorithmPanel, LayoutManager,
StatsPanel, the view components. Opinionated assemblies wired to
the stores; drop them in whole, or rebuild them from molecules
using the same stores (GraphToolbar itself is the worked example:
read its source).import { usePositionPinStore } from "@g3t/react";
function PinAllButton() {
const allPinned = usePositionPinStore((s) => s.allPinned);
return (
<button
className="g3t-btn"
aria-pressed={allPinned}
onClick={() => usePositionPinStore.getState().setAllPinned(!allPinned)}
>
{allPinned ? "Unpin all" : "Pin all"}
</button>
);
}
The canvas owns the locking: flipping the store flag is the whole job, and releasing returns to any per-node pins your users set via the context menu.
import { useRef } from "react";
import type { Core } from "cytoscape";
import { useSelectionStore, CytoscapeCanvas } from "@g3t/react";
function FocusButton({ cy, nodeId }: { cy: Core | null; nodeId: string }) {
return (
<button
className="g3t-btn"
onClick={() => {
useSelectionStore.getState().selectNodes([nodeId]);
const ele = cy?.getElementById(nodeId);
if (ele?.nonempty()) {
cy?.animate({ center: { eles: ele }, zoom: 1.4 }, { duration: 250 });
}
}}
>
Focus suspect asset
</button>
);
}
// cy comes from <CytoscapeCanvas onReady={setCy} />
The canvas also accepts layoutOptions, merged into the layout
object after the built-in tuning (caller wins; keyed by content so
inline literals never re-init the instance). Use it for per-view
spacing (idealEdgeLength, nodeRepulsion, padding) or to switch
fcose to end-mode animation (animate: "end") when per-tick
rendering is too heavy.
import { runGraphLayout, DEFAULT_LAYOUT_OPTIONS } from "@g3t/react";
<button
className="g3t-btn"
onClick={() => runGraphLayout(cy, "force", DEFAULT_LAYOUT_OPTIONS)}
>
Re-layout
</button>;
// Fourth argument `true` randomizes (the "shuffle" escape hatch).
import { useThemeStore } from "@g3t/react";
useThemeStore.getState().setTheme(orgSettings.darkMode ? "dark" : "light");
The spec is plain serializable state YOU own:
const [spec, setSpec] = useState<EncodingSpec>(initialSpec);
// Your button:
<button onClick={() => setSpec(riskViewSpec)}>Risk view</button>
// Our components:
<EncodingSpecPanel ugm={ugm} spec={spec} onChange={setSpec} />
<CytoscapeCanvas ugm={ugm} encodingSpec={spec} />
<SpecLegend ugm={ugm} spec={spec} />
parseEncodingSpec / serializeEncodingSpec round-trip it through
storage or URLs; reserved-channel violations are rejected by name.
FacetFilter emits the set of hidden TYPES. Map that to node ids and pass
it to the canvas as hidden; the canvas hides them with a class
(display:none) in a batched restyle, so node positions and the
Cytoscape instance survive. Do NOT feed a pre-filtered UGM as ugm: a new
ugm reference re-creates the instance and re-runs layout on every
toggle.
const [hiddenTypes, setHiddenTypes] = useState<Set<string>>(new Set());
// A node is hidden only when ALL of its types are hidden; it stays
// visible while it still has any shown type (faceted-filter semantics).
const hidden = useMemo(() => {
const ids = new Set<string>();
if (hiddenTypes.size === 0) return ids;
ugm.forEachNode((id, attrs) => {
if (attrs.types.length > 0 && attrs.types.every((t) => hiddenTypes.has(t)))
ids.add(id);
});
return ids;
}, [ugm, hiddenTypes]);
<FacetFilter ugm={ugm} onFilterChange={setHiddenTypes} />
<CytoscapeCanvas ugm={ugm} hidden={hidden} />
import {
parseAlgorithmResult,
applyAlgorithmResult,
ingestAlgorithmResults,
} from "@g3t/core";
import { useOverlayStore } from "@g3t/react";
async function showCommunities(ugm: UGM) {
const json = await fetch("/api/algorithms/louvain").then((r) => r.text());
const doc = parseAlgorithmResult(json); // versioned, validated
const overlay = applyAlgorithmResult(ugm, doc, ingestAlgorithmResults);
if (overlay) useOverlayStore.getState().register(overlay);
// property-shaped results landed in the UGM: drive the spec from
// them (color by community) and the legend follows.
}
The base menu is functional with zero config, and follows a strict contract: items are wired or absent, never rendered dead. With no menuManager the canvas shows one item, a clipboard-wired copy whose label follows the element id ("Copy IRI" for schemed ids, the RDF case; "Copy ID" otherwise). "Inspect properties" appears only when you wire it, because only the host app knows its detail surface:
import { createDefaultMenuManager } from "@g3t/react";
const manager = createDefaultMenuManager({
// Selection IS the inspect surface in most g3t apps: the inspector,
// table, or lineage panel renders whatever is selected.
onInspect: (t) => {
if (t.id) useSelectionStore.getState().selectNodes([t.id]);
},
// idLabel: "iri" | "id" overrides the heuristic; onCopy replaces
// the clipboard behavior.
});
// <CytoscapeCanvas menuManager={manager} ... />
Register app-specific actions on the same manager (the playground's Scale surface does this live with "Drill into cluster"):
import { ContextMenuManager } from "@g3t/react";
const manager = new ContextMenuManager();
manager.register("my-app", [
{
id: "open-dossier",
label: "Open dossier",
filter: (t) => t.type === "node",
action: (t) => navigate(`/dossier/${t.id}`),
},
]);
// <CytoscapeCanvas menuManager={manager} ... />
registerToolkitActions registers the complete node/edge/multi-select
menu (pin, inspect, neighbors, focus, paths, appearance, hide, bulk
color). Store-backed items work immediately; the rest EMIT events, and
mounting the set without consuming them recreates dead menu items, so
wire every event you expose. The Analytics Dashboard in the playground
consumes all seven (CI-tested there); the pattern:
import { ContextMenuManager, registerToolkitActions } from "@g3t/react";
import { G3tEventBus } from "@g3t/core";
const bus = new G3tEventBus();
const manager = new ContextMenuManager();
registerToolkitActions(manager, { ugm, eventBus: bus, defaultHops: 2 });
useEffect(() => {
const unsubs = [
bus.on("context:focusNode", ({ nodeId, hops }) => {
// hide everything outside the k-hop neighborhood
}),
bus.on("context:findPath", ({ sourceId, targetId }) => {
// findShortestPath + select the route
}),
// context:viewNeighbors, context:hideNodes, context:viewSubgraph,
// context:editAppearance (mount NodeStyleEditor),
// context:pinNodes
];
return () => unsubs.forEach((u) => u());
}, [bus]);
Containers with typed compartment rows and boundary ports come back as a versioned geometry document of absolute boxes; rows are REAL elements, so selection, overlays, and badges apply to them like any node:
import { layoutStructural, isChainEdgeId } from "@g3t/core";
const geometry = await layoutStructural({
nodes: [
{
id: "sensor",
header: { stereotype: "Block", name: "Sensor" },
compartments: [
{
id: "attributes",
title: "attributes",
rows: [
{ id: "sensor.cal", text: "calibrationDate : xsd:date [1..1]" },
],
},
],
ports: [{ id: "sensor.out", side: "EAST" }],
},
{ id: "lens", header: { name: "Lens" } },
],
edges: [
{ id: "feeds", source: "sensor", target: "lens", sourcePort: "sensor.out" },
],
});
// geometry.nodes: absolute top-left boxes; rows carry parent,
// compartment, text, and a divider flag for compartment titles.
// geometry.ports: boundary positions with their declared side.
// Filter synthetic row-ordering edges anywhere you enumerate edges:
// isChainEdgeId(id).
//
// UML edge symbols (A3): set `kind` on a StructuralEdge for the
// relationship arrow vocabulary: "composition" (filled diamond at the
// source/whole end), "aggregation" (hollow diamond), "generalization"
// (hollow triangle at the target/parent end), "dependency" (dashed,
// open arrow), or "association" (plain arrow, the default).
Collapse compartments by feeding their keys to the layout (collapse
is a layout-time input, so the container actually shrinks; re-run on
toggle). Build keys with compartmentKey(nodeId, compartmentId):
import { layoutStructural, compartmentKey } from "@g3t/core";
const collapsed = new Set([compartmentKey("sensor", "operations")]);
const geometry = await layoutStructural(input, {
collapsedCompartments: collapsed,
});
// "operations" now shows only a divider noting the hidden count; the
// sensor container is shorter by the omitted rows. Toggle by adding
// or removing keys and re-running. Hold the set in your own state
// (or a store) and drive it from a button or a context-menu action.
For the per-container right-click toggle, register the built-in action and let the toolkit's collapse store hold the state; subscribe and re-run layout:
import {
ContextMenuManager,
registerCompartmentCollapseActions,
useCompartmentCollapseStore,
collapsedCompartmentSet,
} from "@g3t/react";
const manager = new ContextMenuManager();
registerCompartmentCollapseActions(manager); // right-click a container
// Re-run layout whenever the collapse set changes:
useCompartmentCollapseStore.subscribe((s) => {
void layoutStructural(input, {
collapsedCompartments: collapsedCompartmentSet(s.collapsedKeys),
}).then(setGeometry);
});
// The component-config surface is just seeding the store up front:
// useCompartmentCollapseStore.getState().setCollapsed([...]).
A SHACL shapes graph renders through the identical structural pipeline: shapes become containers, property constraints become compartment rows, and a validation report badges individual rows. No SHACL-specific renderer:
import {
shaclShapesToStructural,
shaclRowSeverities,
closedShapeIds,
layoutStructural,
} from "@g3t/core";
const input = shaclShapesToStructural(shapes, {
references: { "PersonShape::worksFor": "OrgShape" }, // sh:node edges
});
const geometry = await layoutStructural(input);
// Pass closed/open borders and per-row severities as decorations:
<CytoscapeCanvas
ugm={shapeUgm}
structural={{ input, geometry }}
structuralDecorations={{
closedContainers: closedShapeIds(shapes),
rowSeverities: shaclRowSeverities(validationResults), // worst-wins
}}
/>;
A report renders by reusing the overlay + encoding machinery, no SHACL-specific canvas code. Conformance runs wherever (pyshacl, Jena, the in-core validator); the toolkit consumes a versioned document:
import {
validateShacl,
reportFromValidationResults,
severityOverlays,
shaclResultDrivers,
ingestAlgorithmResults,
} from "@g3t/core";
import { useOverlayStore } from "@g3t/react";
const report = reportFromValidationResults(validateShacl(ugm, shapes));
// (or parseShaclReport(externalPyshaclReport) for an external engine)
// Severity tiers as independently toggleable overlays:
for (const overlay of severityOverlays(report)) {
useOverlayStore.getState().register(overlay, true);
}
// Count + worst-severity as encoding drivers (color/size via the grammar):
ingestAlgorithmResults(ugm, shaclResultDrivers(report));
// then point spec.node.color at "_shacl_maxSeverity" and
// spec.node.size at "_shacl_resultCount".
When both canvases are open, cross-link them through the shared selection store: selecting a validation result highlights the focus node (data canvas) and the source shape's container plus the offending property row (shape canvas) at once. No new machinery:
import { resultSelectionIds, resultDetail } from "@g3t/core";
import { useSelectionStore } from "@g3t/react";
// On clicking a result in your report list:
useSelectionStore.getState().selectNodes(resultSelectionIds(result));
// -> selects [focusNode, sourceShapeContainer, propertyRow] across
// every canvas subscribed to the store. A node-level result omits
// the row; a result with no source shape selects only the node.
// For an inspector panel, shape the result for display:
const detail = resultDetail(result); // { focusNode, sourceShape, path, severity, message, value }
Canvas application (compound parents + preset row positions) is the next slice; the document is renderer-neutral, so you can already consume it for SVG export or your own drawing layer.
CI-executed in examples/wiring/src/wiring-examples.test.tsx. The
ProvenanceTrace panel renders a pre-order hop chain (any lineage your
app derives; the auditor shell walks PROV-O edges) with tiers, edge
details, and ABSENCE hops for evidence that should exist and does not:
import { ProvenanceTrace, type ProvenanceChain } from "@g3t/react";
const chain: ProvenanceChain = [
{ id: "rel", tier: "entity", label: "Release 1.2", depth: 0 },
{
id: "build",
tier: "activity",
label: "CI build",
detail: "wasGeneratedBy",
depth: 1,
parentId: "rel",
},
{
id: "rel::gap",
tier: "gap",
label: "No attribution recorded",
depth: 1,
parentId: "rel",
leaf: true,
absence: true,
},
];
<ProvenanceTrace
chain={chain}
title="Lineage"
onSelectHop={(id) => console.log(id)}
/>;
Box selection is on by default (boxSelectionEnabled: true), but with
panning also enabled cytoscape treats a plain background drag as a PAN;
box mode engages only while a multi-select modifier is held. So:
shift+drag (or ctrl/cmd+drag) the background to box-select; the
box-selection sync pushes the picked nodes into useSelectionStore
like any other selection. (Implementation note: cytoscape emits
boxend BEFORE applying the box's selection, so the sync collects the
per-element box events instead of reading :selected in boxend;
see box-selection-sync.ts.) If your app prefers plain-drag box selection,
disable user panning on the canvas and offer another pan affordance.
The "Patterns/Coordinated Selection" story demonstrates it live.
Subscribe to stores from anything:
// React: your detail pane follows the toolkit selection
function MyDossierPane() {
const selected = useSelectionStore((s) => [...s.selectedNodeIds]);
return <DossierLookup ids={selected} />;
}
// Non-React (services, telemetry, process orchestration):
const unsubscribe = useSelectionStore.subscribe((state) => {
processEngine.notify("selection", [...state.selectedNodeIds]);
});
Workspace snapshots make the WHOLE working state portable into your persistence and process layer:
import {
captureWorkspace,
applyWorkspace,
serializeWorkspace,
} from "@g3t/react";
const snapshot = captureWorkspace({ cy, spec });
await saveToCase(caseId, serializeWorkspace(snapshot)); // your storage
// later, possibly another session:
applyWorkspace(parseWorkspace(saved), { cy, setSpec });
CI-executed in examples/wiring/src/wiring-examples.test.tsx
("projection pipeline" describe). The pipeline turns a triple graph
into the labeled-property graph the views render; the collapse steps
are what make an RDF dataset legible on a canvas (rdf:type becomes the
node's type, literals become properties, blank nodes and RDF lists
resolve into the structures they encode).
import { createPresetPipeline } from "@g3t/core";
const ugm = createPresetPipeline("standard").project(rdfGraph);
// rdf:type -> node.types; literals -> node.properties; blank-node and
// list structures resolved. Presets: "standard", "ontology",
// "provenance-preserving".
Compose your own step set when the presets don't fit:
import { ProjectionPipeline, typeCollapse } from "@g3t/core";
const p = new ProjectionPipeline();
p.addStep({ name: "Type Collapse", transform: typeCollapse, enabled: true });
const ugm = p.project(rdfGraph);
p.getSteps(); // inspectable, so a UI can show or toggle steps
The biomedical playground shell renders this live: its canvas toggle
shows the raw triple view beside the projected one, and its caption
lists the preset's step names straight from getSteps().
CI-executed in examples/wiring/src/wiring-examples.test.tsx. When a
graph exceeds what the renderer handles comfortably (~5k nodes),
collapse it to community supernodes; the full graph stays in your UGM
and the canvas sees dozens of nodes. Drill in with buildSubgraph.
The playground's Scale surface runs this live over 8,000 nodes with
the measured timings on screen.
import { collapseByCluster, buildSubgraph } from "@g3t/core";
const {
ugm: clustered,
members,
collapsed,
} = collapseByCluster(big, {
threshold: 2000, // below this, returned unchanged
maxSupernodes: 200, // smallest communities pool into one "other"
rng: seededRng, // deterministic Louvain (optional)
// clusterProperty: "team", // or skip detection: group by a property
});
// Drill-in: the induced member subgraph, working-set capped.
const memberIds = members.get("cluster:c3") ?? [];
const { ugm: sub, truncated } = buildSubgraph(big, memberIds, 1500);
Supernodes carry memberCount (drive the size channel with it),
typeBreakdown, and a label like "Person cluster (847)"; inter-cluster
edges aggregate into weighted cluster-link edges. This is Approach 1
of planning/large-graph-design.md; Approach 4 (worker layout with
viewport culling, for drilled sets past ~5k) is designed but not yet
implemented.
Every snippet here runs under CI in
examples/wiring/src/wiring-examples.test.tsx ("programmatic APIs"
describe). These are the imperative entry points an integrator calls
from their own handlers rather than mounting as components.
import { findShortestPath, allShortestPaths } from "@g3t/core";
const path = findShortestPath(ugm, "a", "c");
// path.found, path.nodeIds (["a","b","c"]), path.edgeIds, path.length
// The UNION of every shortest route (a subgraph, not one
// representative), plus a route count capped at 50 so labels on
// dense graphs stay honest ("50+").
const all = allShortestPaths(ugm, "a", "c");
// all.nodeIds, all.edgeIds, all.pathCount
import { exportSubgraphJson, exportSubgraphCsv } from "@g3t/core";
const json = exportSubgraphJson(ugm); // whole graph
const csv = exportSubgraphCsv(ugm, selection); // or a selection
Turtle export (exportSubgraphTurtle) is demonstrated in
examples/decision-dashboards.
import { applyEncodingSpec } from "@g3t/react";
const patch = applyEncodingSpec(spec, ugm);
// patch.nodes / patch.edges: Maps of materialized visual channels
// (_color, _size, _icon, _shape, label) keyed by element id.
import { createTheme } from "@g3t/react";
const theme = createTheme({ id: "acme", name: "Acme", accentPrimary: "#0af" });
// Derives from LIGHT_THEME and warns when a chosen color fails WCAG
// contrast against its background.
import { createCameraController } from "@g3t/react";
const camera = createCameraController(cy, { padding: 48 });
camera.focusNodes(selectedIds); // zoom-to-subgraph
camera.frameAll(); // fit everything
cy is the Cytoscape core the canvas hands you through onReady.
The same core feeds the Minimap component (an overview inset whose
viewport rectangle tracks and drives the camera): store the core from
onReady in state and render <Minimap core={core} />; while the
core is null it shows a disabled placeholder, so it mounts safely
before the canvas is ready.
pnpm run docs:api →
docs-out/api (every exported type, prop interface, and function).pnpm run storybook.ARCHITECTURE.md, DEVELOPER.md.roadmap/design/algorithm-overlays.md (algorithm documents with
networkx / GraphBLAS exports), roadmap/design/encoding-controls.md
(the spec grammar).