How Semantica Implements Temporal Graph Queries and Time Travel: A Deep Dive into the GraphWorkspace Architecture

Semantica implements temporal graph queries and time travel through a dedicated GraphTemporalState object, guard predicates that gate API calls, and layered rendering effects that filter nodes by active time windows.

The Semantica explorer (available at semantica-agi/semantica) treats time as a first-class dimension of knowledge graphs. Rather than fetching static snapshots, the system enables interactive "scrubbing" through temporal bounds with efficient, predicate-gated queries. This article examines the implementation details of temporal graph queries and time travel in Semantica's GraphWorkspace layer.

Core Temporal State Architecture

All temporal data flows through a single state container defined in the workspace types.

GraphTemporalState Interface

The GraphTemporalState interface (in [explorer/src/workspaces/GraphWorkspace/types.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/types.ts)) holds three critical pieces of temporal context:

  • currentTime – the user-selected moment in the temporal scrubber
  • bounds – the earliest and latest timestamps available for the current graph
  • snapshot – the sub-graph valid at currentTime, fetched from the backend

This state is managed by the GraphWorkspace store and exposed to UI components through the scene object. The design ensures that temporal graph queries remain synchronized with the base graph load status.

Guard Predicates: Preventing Invalid Temporal Queries

Semantica uses defensive predicates to avoid premature or redundant API calls. These are implemented in [temporalLifecyclePredicates.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts).

shouldFetchTemporalBounds

// From temporalLifecyclePredicates.ts
export function shouldFetchTemporalBounds(
  summary: GraphLoadSummary | null
): boolean {
  // Only fetch bounds after any graph (even empty) has loaded
  return summary !== null;
}

This predicate returns true as soon as the initial GraphLoadSummary exists. It prevents bounds requests during the loading phase when the graph ID or metadata may be unstable.

shouldFetchTemporalSnapshot

// From temporalLifecyclePredicates.ts
export function shouldFetchTemporalSnapshot(params: {
  debouncedTime: number | null;
  isLoading: boolean;
  summary: GraphLoadSummary | null;
}): boolean {
  const { debouncedTime, isLoading, summary } = params;
  // Require: loaded graph, stable time selection, not currently loading
  return (
    summary !== null &&
    debouncedTime !== null &&
    !isLoading
  );
}

The snapshot predicate adds two additional guards: the time value must be debounced (settled after user interaction) and the graph must not be in a loading state. This prevents request storms when users rapidly drag the temporal scrubber.

Scene Integration and Layered Rendering

The temporal state propagates through Semantica's scene model to affect what users see.

Scene Model (scene.ts)

The [scene.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/scene.ts) file defines GraphScene with an optional temporal slot:

interface GraphScene {
  // ... other scene properties
  temporalState?: GraphTemporalState | null;
}

The scene updates when:

  • Bounds are fetched from the backend
  • The user moves the temporal scrubber
  • A new snapshot is received

Rendering Layers (graphSceneLayers.ts)

The [graphSceneLayers.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts) file implements temporal filtering in the "temporal emphasis" effect layer:

// Core filtering logic in graphSceneLayers.ts
function isTemporalNodeActive(
  nodeAttrs: NodeAttributes,
  currentTime: number
): boolean {
  const { temporalStart, temporalEnd } = nodeAttrs;
  // Node is active if its interval overlaps currentTime
  return (
    (temporalStart === null || temporalStart <= currentTime) &&
    (temporalEnd === null || temporalEnd >= currentTime)
  );
}

The layer applies this filter only when effectsState.temporalEmphasisEnabled is true. Active nodes receive:

UI Integration: The Temporal Overlay Plugin

User-facing controls reside in [temporalOverlayPlugin.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.ts), registered through the workspace plugin system.

The plugin:

  1. Reads temporal state via getTemporalState()
  2. Renders the scrubber with bounds as its range
  3. Issues backend calls only when predicates return true
  4. Caches snapshots to enable smooth backward/forward navigation

Activating Temporal Features on Graph Load

The [useLoadGraph.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts) hook bridges graph loading with temporal UI activation:

// From useLoadGraph.ts
const loadGraph = useCallback(async (graphId: string) => {
  const summary = await api.loadGraph(graphId);
  
  // Activate temporal variants if graph contains temporal data
  if (summary.hasTemporalData) {
    setNodeShapeVariant('temporal');
    setBadgeKind('temporal');
    setTemporalEmphasisEnabled(true);
  }
  
  return summary;
}, []);

These variant settings trigger the theme system to apply temporal styling across the visualization.

Practical Code Examples

Complete Temporal Query Flow

import {
  shouldFetchTemporalBounds,
  shouldFetchTemporalSnapshot,
} from './temporalLifecyclePredicates';
import type { GraphTemporalState, GraphLoadSummary } from './types';

function useTemporalQueries(graphId: string) {
  const [summary, setSummary] = useState<GraphLoadSummary | null>(null);
  const [temporalState, setTemporalState] = useState<GraphTemporalState>({
    currentTime: null,
    bounds: null,
    snapshot: null,
  });

  // Phase 1: Load base graph
  const loadGraph = async () => {
    const loaded = await api.loadGraph(graphId);
    setSummary(loaded);
  };

  // Phase 2: Fetch bounds once graph loads
  useEffect(() => {
    if (!shouldFetchTemporalBounds(summary)) return;
    
    api.fetchTemporalBounds(graphId).then((bounds) => {
      setTemporalState((prev) => ({ ...prev, bounds }));
    });
  }, [summary, graphId]);

  // Phase 3: Fetch snapshot when user selects time
  const selectTime = useCallback((debouncedTime: number | null) => {
    if (!shouldFetchTemporalSnapshot({
      debouncedTime,
      isLoading: false,
      summary,
    })) return;

    api.fetchTemporalSnapshot(graphId, debouncedTime).then((snapshot) => {
      setTemporalState({
        currentTime: debouncedTime,
        bounds: temporalState.bounds,
        snapshot,
      });
    });
  }, [summary, graphId, temporalState.bounds]);

  return { temporalState, selectTime, loadGraph };
}

Temporal Node Rendering

import { isTemporalNodeActive } from './graphSceneLayers';

function TemporalNodeLayer({
  nodes,
  temporalState,
}: {
  nodes: GraphNode[];
  temporalState: GraphTemporalState | null;
}) {
  if (!temporalState?.currentTime) {
    // No temporal filtering—render all nodes
    return <AllNodes nodes={nodes} />;
  }

  const activeNodes = nodes.filter((n) =>
    isTemporalNodeActive(n.attributes, temporalState.currentTime!)
  );

  return (
    <>
      <DimmedNodes nodes={nodes.filter(n => !activeNodes.includes(n))} />
      <HighlightedNodes nodes={activeNodes} accent="#49D7FF" />
    </>
  );
}

Performance Characteristics

Semantica's temporal implementation optimizes for interactive exploration:

  • Debounced queries prevent backend overload during scrubbing
  • Snapshot caching in GraphTemporalState eliminates refetching for visited times
  • Predicate-gated fetches ensure API calls only occur in valid state transitions
  • Zoom-tiered rendering (via minZoomTier) reduces visual clutter at distant zoom levels

Summary

  • Temporal state is centralized in GraphTemporalState with currentTime, bounds, and snapshot fields
  • Guard predicates (shouldFetchTemporalBounds, shouldFetchTemporalSnapshot) prevent invalid API calls
  • Layered rendering filters nodes by temporal overlap with configurable emphasis effects
  • Plugin architecture isolates UI concerns in temporalOverlayPlugin.ts while preserving clean state boundaries
  • Theme integration activates temporal styling through nodeShapeVariant and badgeKind set during graph load

Frequently Asked Questions

How does Semantica prevent redundant temporal snapshot requests?

The shouldFetchTemporalSnapshot predicate in temporalLifecyclePredicates.ts requires three conditions: a loaded graph (summary !== null), a settled time value (debouncedTime !== null), and no active loading state (!isLoading). Additionally, the GraphTemporalState caches snapshots, so revisiting a previous time retrieves data from local state rather than the backend.

What determines when temporal emphasis effects appear on nodes?

Effects render in graphSceneLayers.ts when isTemporalNodeActive returns true for a node's temporal interval relative to currentTime. Visual styling (cyan #49D7FF highlight, "T" badge) further requires temporalEmphasisEnabled and sufficient zoom level per GRAPH_THEME.effects.temporalEmphasis.minZoomTier.

Can temporal features work with any graph, or do they require special data?

Any graph can display temporal boundaries and snapshots if the backend provides temporal metadata. The useLoadGraph.ts hook detects this via summary.hasTemporalData and automatically activates nodeShapeVariant = "temporal" and related theme settings. Graphs without temporal data load normally but skip temporal API calls.

Where does the actual time-travel navigation happen in the codebase?

Navigation occurs in temporalOverlayPlugin.ts, which renders the scrubber UI and responds to user input by calling selectTime() with debounced values. The plugin reads temporalState.bounds to set scrubber limits and updates temporalState.currentTime and temporalState.snapshot through the workspace store, triggering re-renders in graphSceneLayers.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →