Automated sync from private repository. Built with obfuscation enabled.
17 KiB
Cosmograph v2 — AI Agent Reference
Purpose: This document provides AI coding assistants (Claude, GPT, etc.) with the essential context needed to correctly implement Cosmograph v2 visualizations. It consolidates the v2 API patterns, data requirements, and common pitfalls into a single reference optimized for agent consumption.
Quick Context
What is Cosmograph? Cosmograph is a high-performance graph visualization library that uses WebGL and DuckDB-Wasm for rendering large networks. v2 introduced a new data pipeline requiring explicit data preparation.
Packages:
@cosmograph/cosmograph— Core library (vanilla JS/TS)@cosmograph/react— React wrapper + Data Kit utilities
Key Concept — Data Kit:
Raw data (arrays, CSV, JSON, etc.) must be processed through prepareCosmographData() before rendering. This function:
- Converts raw data to Arrow tables (DuckDB-compatible)
- Generates index columns automatically
- Returns a
cosmographConfigwith correct mapping props
Critical Pattern: Data Preparation
The Golden Rule
// ❌ WRONG - Passing raw data directly
<Cosmograph points={rawPoints} links={rawLinks} />
// ✅ CORRECT - Using Data Kit
const result = await prepareCosmographData(dataConfig, rawPoints, rawLinks);
const { points, links, cosmographConfig } = result;
<Cosmograph points={points} links={links} {...cosmographConfig} />
Minimal Data Config
Case A: You have a points array with IDs (most common)
const dataConfig = {
points: { pointIdBy: "id" },
links: { linkSourceBy: "source", linkTargetsBy: ["target"] },
};
Case B: Generate points from links (no separate points array)
const dataConfig = {
points: { linkSourceBy: "source", linkTargetsBy: ["target"] },
links: { linkSourceBy: "source", linkTargetsBy: ["target"] },
};
What NOT to Include in dataConfig
DO NOT manually specify index columns:
pointIndexBy— auto-generated by Data KitlinkSourceIndexBy— auto-generated by Data KitlinkTargetsIndexBy— auto-generated by Data Kit
These are internal indices that Data Kit computes. Specifying them manually will cause errors.
React Integration
Complete Working Example
import { useEffect, useMemo, useState } from "react";
import { Cosmograph, prepareCosmographData } from "@cosmograph/react";
import type { CosmographConfig, CosmographData } from "@cosmograph/react";
export function Graph() {
// Raw data - your actual graph data
const rawPoints = useMemo(() => [
{ id: "a", label: "Node A", color: "#FF0000" },
{ id: "b", label: "Node B", color: "#00FF00" },
{ id: "c", label: "Node C", color: "#0000FF" },
], []);
const rawLinks = useMemo(() => [
{ source: "a", target: "b" },
{ source: "b", target: "c" },
], []);
// Prepared data state
const [preparedPoints, setPreparedPoints] = useState<CosmographData | null>(null);
const [preparedLinks, setPreparedLinks] = useState<CosmographData | null>(null);
const [config, setConfig] = useState<CosmographConfig>({});
// Prepare data on mount or when raw data changes
useEffect(() => {
const prepare = async () => {
const dataConfig = {
points: {
pointIdBy: "id",
pointColorBy: "color", // Optional: map color column
pointLabelBy: "label", // Optional: map label column
},
links: {
linkSourceBy: "source",
linkTargetsBy: ["target"], // Note: array!
linkColorBy: "color", // Optional: if links have colors
},
};
const result = await prepareCosmographData(dataConfig, rawPoints, rawLinks);
if (!result) return;
const { points, links, cosmographConfig } = result;
setPreparedPoints(points);
setPreparedLinks(links);
setConfig(cosmographConfig);
};
prepare();
}, [rawPoints, rawLinks]);
// Don't render until data is ready
if (!preparedPoints) return <div>Loading...</div>;
return (
<Cosmograph
points={preparedPoints}
links={preparedLinks}
{...config}
// Override with your settings (these don't conflict with mapping props)
backgroundColor="#1a1a2e"
fitViewOnInit={true}
showLabels={true}
/>
);
}
Imperative Access via onMount
const [api, setApi] = useState<any>(null);
<Cosmograph
{...config}
onMount={(cosmograph) => setApi(cosmograph)}
/>
// Later: call methods imperatively
<button onClick={() => api?.fitView(300)}>Fit View</button>
<button onClick={() => api?.pause()}>Pause Simulation</button>
Using CosmographProvider + useCosmograph
import { CosmographProvider, Cosmograph, useCosmograph } from "@cosmograph/react";
function Controls() {
const { cosmograph } = useCosmograph();
return <button onClick={() => cosmograph?.fitView()}>Fit</button>;
}
function App() {
return (
<CosmographProvider>
<Cosmograph {...config} />
<Controls />
</CosmographProvider>
);
}
Vanilla JS/TS Integration
import { Cosmograph, prepareCosmographData } from "@cosmograph/cosmograph";
const container = document.getElementById("graph")!;
const rawPoints = [{ id: "a" }, { id: "b" }, { id: "c" }];
const rawLinks = [{ source: "a", target: "b" }, { source: "b", target: "c" }];
const dataConfig = {
points: { pointIdBy: "id" },
links: { linkSourceBy: "source", linkTargetsBy: ["target"] },
};
async function init() {
const result = await prepareCosmographData(dataConfig, rawPoints, rawLinks);
if (!result) return;
const { points, links, cosmographConfig } = result;
const graph = new Cosmograph(container, { points, links, ...cosmographConfig });
// Wait for data to load
await graph.dataUploaded();
// Sanity check
console.log(graph.stats); // { pointsCount: 3, linksCount: 2, ... }
// Ensure visible
graph.fitView();
// Cleanup later
// await graph.destroy();
}
init();
Data Format Requirements
Points Array
interface Point {
id: string; // Required: unique identifier
label?: string; // Optional: display label
color?: string; // Optional: hex color (#RRGGBB)
size?: number; // Optional: node size
x?: number; // Optional: fixed x position
y?: number; // Optional: fixed y position
[key: string]: any; // Additional columns for tooltips, filtering, etc.
}
Links Array
interface Link {
source: string; // Required: source point ID
target: string; // Required: target point ID
color?: string; // Optional: hex color
width?: number; // Optional: line width (for linkWidthBy)
strength?: number; // Optional: physics spring strength (for linkStrengthBy)
[key: string]: any; // Additional columns
}
Link Width Configuration
To use custom edge widths based on data (e.g., traffic volume):
const dataConfig = {
points: { pointIdBy: "id" },
links: {
linkSourceBy: "source",
linkTargetsBy: ["target"],
linkColorBy: "color",
// Map width column for edge thickness
linkWidthBy: "width",
// Auto-scale widths to this range (pixels)
linkWidthRange: [0.5, 6.0],
// Default width for edges without width data
linkDefaultWidth: 0.5,
},
};
Width Strategies (via linkWidthStrategy):
undefined(default): Auto-select based on data"direct": Use values fromlinkWidthBycolumn as-is (1:1 pixel values)"sum": Sum of values for multi-edges between same nodes"average": Average of values for multi-edges"count": Width based on number of links between same nodes
Custom Width Function (for dynamic calculation):
const dataConfig = {
links: {
linkWidthBy: "rawTraffic", // Column with raw values
linkWidthByFn: (value: number, index: number) => {
// Custom calculation (e.g., logarithmic scaling)
return 0.5 + Math.log1p(value) * 0.5;
},
},
};
Position Handling (x/y coordinates)
When you have fixed positions for some or all nodes:
const dataConfig = {
points: {
pointIdBy: "id",
pointXBy: "x", // Map x position column
pointYBy: "y", // Map y position column
},
links: { ... },
};
Important DuckDB constraint: If mapping x/y positions, you need at least 2 points with valid positions. DuckDB's STDDEV_SAMP function (used internally) requires ≥2 values. If you have fewer, omit pointXBy/pointYBy from the config.
Common Config Props
Layout & Simulation
{
disableSimulation: false, // true = fixed layout, no physics
fitViewOnInit: true, // Auto-fit view after data loads
fitViewDelay: 300, // Delay before fitView (ms)
fitViewPadding: 0.1, // Padding around nodes (0-1)
// Simulation physics (only when disableSimulation: false)
simulationRepulsion: 1.0, // Node repulsion strength
simulationLinkSpring: 0.5, // Link spring strength
simulationLinkDistance: 50, // Target link length
simulationGravity: 0.1, // Pull toward center
simulationFriction: 0.85, // Velocity damping
simulationAlpha: 1.0, // Initial simulation "heat"
simulationAlphaDecay: 0.01, // How fast simulation cools
}
Visual Styling
{
backgroundColor: "#1a1a2e",
// Points
pointDefaultSize: 10,
pointDefaultColor: "#666666",
pointGreyoutOpacity: 0.3, // Opacity when not selected
// Links
linkDefaultWidth: 1,
linkDefaultColor: "#444444",
linkGreyoutOpacity: 0.3,
// Labels
showLabels: true,
showDynamicLabels: true, // Show labels on zoom
showTopLabels: true, // Always show top N labels
showTopLabelsLimit: 50,
showHoveredPointLabel: true,
pointLabelColor: "#ffffff",
pointLabelFontSize: 12,
}
Cosmograph Class API (Imperative Methods)
Lifecycle
await cosmograph.dataUploaded() // Wait for async data to load
await cosmograph.destroy() // Cleanup (important!)
cosmograph.stats // { pointsCount, linksCount, ... }
View Control
cosmograph.fitView(duration?) // Fit all nodes in view
cosmograph.fitViewByIndices(indices, duration?) // Fit specific nodes
cosmograph.setZoomLevel(level, duration?) // Set zoom (1 = default)
cosmograph.zoomToPoint(index, duration?, scale?) // Zoom to specific node
Simulation Control
cosmograph.isSimulationAvailable // Check if simulation is possible
cosmograph.isSimulationRunning // Check if currently running
cosmograph.start(alpha?) // Start simulation
cosmograph.pause() // Pause simulation
cosmograph.unpause() // Resume simulation
cosmograph.stop() // Stop simulation
Selection
// Activate selection modes
cosmograph.activateRectSelection()
cosmograph.activatePolygonalSelection()
cosmograph.deactivateRectSelection()
// Programmatic selection
cosmograph.selectPoint(index, addToSelection?)
cosmograph.selectPoints(indices, addToSelection?)
cosmograph.selectPointsInRect(rect, addToSelection?)
cosmograph.unselectAllPoints()
// Read selection
cosmograph.getSelectedPointIndices() // number[] | undefined
ID ↔ Index Conversion
// Point ID = your data's identifier (string)
// Point Index = Cosmograph's internal array index (number)
await cosmograph.getPointIndicesByIds(["node-a", "node-b"]) // [0, 1]
await cosmograph.getPointIdsByIndices([0, 1]) // ["node-a", "node-b"]
Dynamic Data Updates
await cosmograph.addPoints([{ id: "new", label: "New Node" }])
await cosmograph.addLinks([{ source: "a", target: "new" }])
await cosmograph.removePointsByIds(["node-to-remove"])
await cosmograph.removeLinksByPointIdPairs([["a", "b"]])
Troubleshooting
Blank Canvas / Nothing Renders
-
Data not loaded:
await cosmograph.dataUploaded(); console.log(cosmograph.stats); // Should show non-zero counts -
Nodes off-screen:
cosmograph.fitView(); -
Config not spread correctly:
// ❌ Wrong <Cosmograph config={cosmographConfig} /> // ✅ Correct <Cosmograph {...cosmographConfig} points={points} links={links} />
"STDDEV_SAMP is out of range" Error
This DuckDB error occurs when computing statistics on columns with insufficient variance:
- Column has fewer than 2 values
- All values are identical (variance = 0) ← Common gotcha!
Cause: Mapping numeric columns (x, y, size, width, etc.) when there isn't enough variance.
Fix for positions: Check for actual variance in BOTH x AND y coordinates:
// Filter to points with valid position data
const pointsWithPos = rawPoints.filter(
(p): p is typeof p & { x: number; y: number } =>
'x' in p && 'y' in p && typeof p.x === 'number' && typeof p.y === 'number'
);
const positionCount = pointsWithPos.length;
// IMPORTANT: Check variance, not just count!
// Nodes clustered at identical coordinates will still fail with positionCount >= 2
const uniqueX = new Set(pointsWithPos.map(p => p.x));
const uniqueY = new Set(pointsWithPos.map(p => p.y));
const hasVaryingPositions = positionCount >= 2 && uniqueX.size >= 2 && uniqueY.size >= 2;
const dataConfig = {
points: {
pointIdBy: "id",
// Only map positions when BOTH x and y have variance
...(hasVaryingPositions ? { pointXBy: "x", pointYBy: "y" } : {}),
},
links: { ... },
};
Fix for link widths: Only include linkWidthBy when ≥2 links have varying widths:
const linkWidths = rawLinks.map(l => l.width).filter((w): w is number => w != null);
const uniqueWidths = new Set(linkWidths);
const hasVaryingWidths = linkWidths.length >= 2 && uniqueWidths.size >= 2;
const dataConfig = {
points: { ... },
links: {
linkSourceBy: "source",
linkTargetsBy: ["target"],
linkColorBy: "color", // String columns are safe - no STDDEV
// Only map width when we have variance
...(hasVaryingWidths ? {
linkWidthBy: "width",
linkWidthRange: [0.5, 6.0],
} : {}),
linkDefaultWidth: 0.5, // Fallback when no width mapping
},
};
Why this happens: DuckDB's STDDEV_SAMP function computes sample standard deviation, which requires variance in the data. When all values are identical (e.g., nodes at same GPS coordinates), the variance is 0, causing the "out of range" error. The count check alone is insufficient.
"Missing required properties: pointIdBy" Error
Cause: Passing raw data without going through Data Kit.
Fix: Always use prepareCosmographData():
const result = await prepareCosmographData(dataConfig, rawPoints, rawLinks);
// result.cosmographConfig contains the required mapping props
Simulation Not Working
// Check if simulation is even possible
console.log(cosmograph.isSimulationAvailable); // Should be true
// If false, check:
// - disableSimulation: true in config?
// - No links in data?
// - Fixed positions for all nodes?
// If true but not running:
cosmograph.start(); // or cosmograph.unpause();
Wrong Node Selected (ID vs Index Confusion)
Symptom: Clicking node "A" selects node "B"
Cause: Confusing user IDs with internal indices.
// Your data uses string IDs
const nodeId = "my-node-a";
// Cosmograph uses numeric indices internally
const index = await cosmograph.getPointIndicesByIds([nodeId]);
cosmograph.selectPoint(index[0]);
// Or convert back
const ids = await cosmograph.getPointIdsByIndices([selectedIndex]);
External DuckDB Tables (Advanced)
When loading pre-existing DuckDB-Wasm tables (not using Data Kit):
<Cosmograph
duckDBConnection={{ duckdb: db, connection }}
points="my_points_table"
links="my_links_table"
// You MUST specify all mapping columns manually:
pointIdBy="id"
pointIndexBy="idx" // Your table must have this column
linkSourceBy="source"
linkSourceIndexBy="source_idx"
linkTargetsBy={["target"]}
linkTargetsIndexBy={["target_idx"]}
/>
Required table columns for external loading:
- Points:
id(string),idx(integer index) - Links:
source(string),source_idx(integer),target(string),target_idx(integer)
Checklist for Implementation
When implementing Cosmograph v2:
- Install
@cosmograph/react(or@cosmograph/cosmographfor vanilla) - Create raw points array with unique
idfield - Create raw links array with
sourceandtargetstring fields - Define
dataConfigwithpointIdBy: "id"andlinkSourceBy/linkTargetsBy - Call
prepareCosmographData(dataConfig, rawPoints, rawLinks) - Extract
{ points, links, cosmographConfig }from result - Pass to Cosmograph:
<Cosmograph points={points} links={links} {...cosmographConfig} /> - Add visual overrides (backgroundColor, showLabels, etc.) AFTER spreading config
- Handle loading state (don't render until
preparedPointsexists) - Call
fitView()if nodes appear off-screen - Call
destroy()on cleanup if using imperative API
Version Notes
This document covers Cosmograph v2. Key differences from v1:
- Data Kit is required (no direct raw data passing)
- DuckDB-Wasm is used internally for data processing
- Index columns are auto-generated (don't specify manually)
- Arrow tables are the internal format
linkTargetsByis an array (supports multi-target edges)