Building an evidence-first timeline and map for contested historical data
A timeline is one of the simplest interfaces to build.
Give it a list of dates and events, connect the entries, add a map with location markers, and you have something that looks informative, navigable, and authoritative.
But suppose the second event is described as retaliation for the first. Who established that connection? Did investigators verify it, or did one of the parties merely claim it?
And suppose the location is known only at the provincial level. What happens when the interface drops a perfectly positioned marker onto the map?
The software hasn't necessarily invented any facts. It has invented confidence.
This is a peculiar problem in data visualization: the interface can communicate more certainty than the underlying evidence supports, even when every field has been faithfully rendered.
I've recently encountered this problem while building ScopeSignal 003, an interactive, bilingual reading of interventions and confrontations involving Saudi Arabia and multiple Yemeni authorities and armed groups.
The edition starts with 231 documented records across two reading bands: 159 core confrontation records and 72 historical context records, covering selected material from 1934 through October 18, 2026.
These are units of documentation, not a comprehensive count of attacks. Some records describe extended periods or multiple operations, and the coverage has acknowledged gaps.
The interface combines an interactive timeline, a geographic view, actor filters, source references, and explicit evidence classifications.
The challenge was to prevent the interface from quietly claiming knowledge the records did not establish.
I approached that through three engineering decisions.
1. Make causality a separate question
Consider a hypothetical dataset:
[
{
"date": "2026-07-01",
"action": "Strike on military facility",
"verified": true
},
{
"date": "2026-07-03",
"action": "Retaliatory strike",
"verified": true
}
]
There are several problems here.
The first event may be independently documented. The second may also have occurred. But the word retaliatory implies a causal relationship that requires additional evidence.
Even if both events are established, their relationship is not necessarily established.
A single verified field cannot represent that distinction.
In ScopeSignal's Read Model 1.1, event assessment and causal-link classification are separate fields.
These are the actual type declarations from the production model:
export type TimelineStatus =
| "ok"
| "disputed"
| "unsupported"
| "unverified";
export type LinkType =
| "declared_response"
| "supported_causation"
| "sequence_only"
| "pending";
The TimelineEvent interface carries its own status and an optional relationship:
// Selected fields from the production TimelineEvent interface.
status: TimelineStatus;
link?: {
to: string;
type: LinkType;
basis?: string;
};
The distinction matters.
declared_response means the response relationship was asserted by an actor. It documents a declaration; it does not independently establish the claimed motive.
supported_causation represents a causal connection supported by evidence.
sequence_only means that events can be placed in chronological order without treating that order as proof of causation.
pending allows a relationship to remain unresolved.
The Python validator requires a non-empty basis for links classified as declared_response or supported_causation. The interface can then display the relationship's classification and stated basis rather than leaving the connecting line to speak for itself.
This is not automatic fact-checking. A required explanation is not proof that the explanation is correct. The classification still depends on editorial review of the sources.
But it prevents a particular class of error: a causal connection entering the published data without an explicit, inspectable basis.
Dates can also claim too much
A related problem appears when historical sources provide a year but not a specific day.
A conventional database might store an event as 1934-01-01 because its date field requires a full ISO date. The UI later prints "January 1, 1934."
An implementation convenience has become an apparent historical fact.
My event dates therefore carry a separate precision field:
export type DatePrecision = "day" | "month" | "year";
The validator checks that the stored date matches its declared precision. The display formatter prints only the available granularity.
A record known to the year is displayed as a year. A record known to the month is displayed as a month and year.
The interface does not add days simply because JavaScript can handle them.
Even actor names are a data-model decision
The Yemen dataset presented another problem: naming the country as an actor often erased the distinction between competing authorities.
A statement such as "Yemen attacked" could refer to different armed forces or political authorities, depending on the period.
So the validation pipeline includes an explicit rule against using the bare country name as an actor label.
For example, the actual Python validation vocabulary includes:
_YEMEN_AS_ACTOR = {
"اليمن",
"دولة اليمن",
"حكومة اليمن",
"الدولة اليمنية",
"الجمهورية اليمنية"
}
Additional validation catches generic force labels built around the same ambiguity.
The record must identify the relevant force or authority, or use an explicit unknown-actor classification when appropriate.
This does not settle who legitimately represents Yemen. It prevents a data-entry shortcut from silently making that decision.
2. Make the map show location, not apparent certainty
Maps carry an unusual kind of authority.
A marker at an exact coordinate looks like a measurement. A glowing region looks like a concentration of activity. A cluster of markers can look like an intensity map, even when the dataset was never designed to measure intensity.
And my map needed to do something narrower.
To show where the reader was in the record, not how much violence had occurred in a particular place.
The map component, MapStage.tsx, uses a glow to highlight the location associated with the currently selected event.
When the selected event changes, the previous glow fades. The visual treatment does not accumulate incidents into a heatmap.
Importantly, the map also contains ordinary navigation markers representing places in the reading sequence. Those markers help readers move between records; their number or visual density should not be interpreted as a measure of conflict severity.
The interface deliberately distinguishes these two functions:
- Navigation markers: help readers select documented places.
- Active glow: identifies the geographic location associated with the record being read.
Neither encodes casualty totals or an event's significance.
A region or a point?
The geographic model distinguishes two levels of place precision:
export type PlacePrecision = "city" | "area";
A city-level location can receive a localized glow.
For an area-level location, the component can illuminate the relevant administrative or geographic polygon through its area_ref geometry.
The map may still use a representative center for navigation or camera positioning. That is a UI coordinate.
This distinction is supported by two separate parts of the model: event-to-place mapping and the place registry, which contains geographic metadata such as coord_basis.
Where an event has no usable mapped place, the interface should not manufacture an event-specific glow.
The general principle is straightforward:
A coordinate needed to operate an interface is not necessarily a coordinate established by historical evidence.
For our geographic data, the map-building process uses sources including Natural Earth and administrative boundary datasets. These provide geographic reference shapes; they do not independently verify the location of any military incident.
That verification remains the responsibility of the event record and its sources.
The map is a reading instrument
This design makes the map less spectacular than a cumulative animated conflict heatmap.
If 30 documented records refer to a particular area, the map does not automatically conclude that the area experienced 30 distinct attacks. One record might summarize a campaign, another might describe an agreement, and several records could overlap in what they cover.
The map is a geographic companion to the reading.
3. Refuse to publish an unaudited change
The third problem isn't visible on the map.
It happens between review and publication.
Imagine completing a source audit for an edition, then changing a sentence, replacing a link, or updating an event record shortly before deployment.
The original audit no longer necessarily applies to the file being published.
The publishing pipeline addresses this with a checksum-based release gate.
The production function is named require_signature, although the source explicitly describes what it enforces as a checksum receipt rather than a cryptographic signature.
The relevant comparison is:
# Extract from scopesignal/publish.py
digest = hashlib.sha256(manifest.read_bytes()).hexdigest()
if audit.get("manifest_sha256") != digest:
raise UnsignedEdition(
f"{audit_file.name} sha256 does not match {manifest.name}: the record "
"changed after its links were checked, so re-run links-check"
)
The comparison isn't the entire gate.
The publisher also requires the audit receipt to exist, report a passing result, contain a non-empty ledger of checked links, and include a check timestamp.
If the manifest's SHA-256 digest no longer matches the audited version, publication is refused until the check is rerun.
That gives us a useful internal invariant:
The manifest passed to the publisher must be the same manifest for which the link-audit receipt was produced.
What does this prove?
It establishes consistency between a publication candidate and its audit receipt, within the workflow's trust assumptions.
It addresses a more ordinary operational risk: accidentally publishing content that has changed since its supporting links were checked.
Publish the exclusions, too
I wanted the reader to understand something that a timeline normally conceals: what was investigated but not admitted into the published record.
The edition therefore includes a dedicated, curated refusal ledger.
It is not an interactive query engine or a complete log of every discarded research lead. It is a public editorial section containing selected exclusions and their reasons.
ScopeSignal 003 publishes four such records:
- Two classified as
UNSTABLE-COUNT. - Two classified as
NO-IDENTITY.
For example, a combined death toll from two fishing-boat incidents was excluded as an aggregate figure because the underlying incidents had their own recorded totals. Using the combined figure as another event count would risk double counting.
Another excluded aggregate described nine ground attacks in a year without sufficient individual dates and locations to represent each as a separate event.
The problem in those cases was that the proposed record could not be represented at the required level of specificity.
Each refusal is published with its classification and explanation.
This allows readers to inspect some of the boundaries of the edition rather than assuming everything encountered during research was included.
What this design does not solve
No interface architecture can guarantee objective history.
This edition contains selected records, not a comprehensive census. Coverage varies across periods and places. Some casualty counts remain disputed. Other records rely on attributed accounts.
The data model can preserve those distinctions, and the interface can make them more visible.
It cannot eliminate the need for reporting, source criticism, or editorial judgment.
Even a well-labeled interface can influence interpretation through event selection, ordering, framing, and omissions.
So the goal was to make certain forms of unjustified certainty harder to introduce—and easier for readers to notice.
Three lessons for developers
If you build historical timelines, investigative databases, incident registers, or public-interest dashboards, three practices are worth considering.
1. Model claims at the level where uncertainty exists. An event can be documented without its alleged cause being established. Do not force different questions into one verification flag.
2. Treat precision as data, not presentation. Dates and locations should carry the granularity supported by their sources. A renderer should not manufacture precision to satisfy its preferred format.
3. Make editorial boundaries inspectable. Publication checks, sourced relationships, stated coverage limits, and documented exclusions can all be first-class parts of the system.
A polished interface creates trust. Sometimes more trust than its data deserves.
Good engineering cannot solve that problem entirely.
But it can stop adding to it.
Explore the implementation in context: ScopeSignal 003 — Yemen and Saudi Arabia: A Documented Reading of Interventions and Confrontations.
Arabic edition: Doxascope ScopeSignal 003.
Built at Doxascope, a project exploring how belief, evidence, and interpretation take shape across stories, ideas, and live events.


Top comments (0)