Files
pymc_console-dist/docs/cosmograph-v2.md
T
GitHub Actions Bot ec0e888042 Release v0.9.194
Automated sync from private repository.
Built with obfuscation enabled.
2026-01-29 18:28:52 +00:00

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:

  1. Converts raw data to Arrow tables (DuckDB-compatible)
  2. Generates index columns automatically
  3. Returns a cosmographConfig with 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 Kit
  • linkSourceIndexBy — auto-generated by Data Kit
  • linkTargetsIndexBy — 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.
}
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
}

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 from linkWidthBy column 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

  1. Data not loaded:

    await cosmograph.dataUploaded();
    console.log(cosmograph.stats); // Should show non-zero counts
    
  2. Nodes off-screen:

    cosmograph.fitView();
    
  3. 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:

  1. Column has fewer than 2 values
  2. 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/cosmograph for vanilla)
  • Create raw points array with unique id field
  • Create raw links array with source and target string fields
  • Define dataConfig with pointIdBy: "id" and linkSourceBy/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 preparedPoints exists)
  • 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
  • linkTargetsBy is an array (supports multi-target edges)