g3-toolkit API Reference
    Preparing search index...

    Wiring Guide: Driving g3-toolkit from Your Application

    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:

    1. Stores (zustand): selection, position pins, structural overlays, style overrides, and theme are global stores you can read, write, and subscribe to from ANY component, yours or ours. useXStore(selector) in React; useXStore.getState() / useXStore.subscribe(...) anywhere (event handlers, services, non-React code).
    2. Props + callbacks: graph data (ugm), the encoding spec (encodingSpec), containment, and onReady (which hands you the Cytoscape Core for camera and layout control).
    3. Documents (versioned JSON contracts): the encoding spec, workspace snapshots, and algorithm results all serialize to validated documents, which is how external processes (Python services, pipelines, saved state) participate.

    A useful consequence: a "custom button" is almost always one line of store or function call in an onClick.

    • Atoms: Icon, the g3t-btn / g3t-select / g3t-input CSS classes, design tokens (--g3t-*). Use these to make YOUR controls look native next to ours.
    • Molecules: SearchBar, ZoomControls, SpecPort, ThemeSwitcher, SpecLegend, ContextMenu. Single-purpose, callback-driven; compose them into your own bars and panels.
    • Compounds: 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.

    • API reference (generated from source): pnpm run docs:apidocs-out/api (every exported type, prop interface, and function).
    • Component gallery: pnpm run storybook.
    • Architecture and boundaries: ARCHITECTURE.md, DEVELOPER.md.
    • Interchange contracts in depth: roadmap/design/algorithm-overlays.md (algorithm documents with networkx / GraphBLAS exports), roadmap/design/encoding-controls.md (the spec grammar).