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 scrubberbounds– the earliest and latest timestamps available for the current graphsnapshot– the sub-graph valid atcurrentTime, 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:
- Highlight color:
#49D7FF(cyan from [graphTheme.ts](https://github.com/semantica-agi/semantica/blob/main/explorer/src/workspaces/GraphWorkspace/graphTheme.ts)) - Badge: "T" indicator when zoom meets
GRAPH_THEME.effects.temporalEmphasis.minZoomTier
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:
- Reads temporal state via
getTemporalState() - Renders the scrubber with
boundsas its range - Issues backend calls only when predicates return
true - 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
GraphTemporalStateeliminates 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
GraphTemporalStatewithcurrentTime,bounds, andsnapshotfields - 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.tswhile preserving clean state boundaries - Theme integration activates temporal styling through
nodeShapeVariantandbadgeKindset 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →