Skip to main content

Temporal Graph logo

temporal-graph

Model graphs that evolve over time, compute on them, and render them as SVG, PNG or animated GIF.

Python 3.9+ MIT license No runtime dependencies

Giuliano Crenna
Facultad de Ciencias Exactas, Ingeniería y Agrimensura, Universidad Nacional de Rosario
giulicrenna@fceia.unr.edu.ar


A temporal graph is a graph whose edges exist only at certain instants. This library stores one snapshot per instant and offers three things on top:

  • Visualization. Timeline charts in two modes and animated node-link views, written natively as SVG, PNG and GIF. There is no matplotlib, cairo or Pillow underneath.
  • Algorithms. Journeys (time-respecting paths), temporal distance and diameter, connectivity metrics, the temporal Menger theorems, information dissemination, temporal design, periodic availability, random temporal graphs and exact solvers for temporal matching, exploration and TSP.
  • Data tools. Graph transformations, per-snapshot analysis, and JSON, CSV and GEXF export.

Every algorithm is checked against the worked examples of the notes Grafos temporales: una introducción desde la perspectiva algorítmica and cross-checked against brute force on random graphs.

decisions mode

Contents

Install

pip install temporal-graph

From source:

pip install -e ".[dev]"

Quick start

from temporal_graph import Edge, TemporalGraph, animate, journeys, render

g = TemporalGraph(nodes=["a", "b", "c", "d"])
g.add_snapshot(0, [Edge("a", "b"), Edge("b", "c"), Edge("c", "d")])
g.add_snapshot(1, [
    Edge("a", "b", directed=True, weight=2.5, color="#d62728"),
    Edge("a", "c", color="#1f77b4"),
])
g.add_snapshot(2, [Edge("a", "c")])

render(g, mode="decisions").savefig("decisions.png")   # or .svg
animate(g).save("evolution.gif")
journeys.earliest_arrival(g, "d")                      # who hears from d, and when

Visualization

Modes

Both modes share one layout: a horizontal lifeline per node, time on the x-axis. Every figure embeds its own legend.

Mode Shows
connections A full snapshot per timestep: each active edge is a vertical line with a dot per end.
decisions When edges appear and disappear: lifelines, solid connect / dashed disconnect marks, and faded connections for edges that carry on unchanged.

Edge properties

Property Effect
directed=True Arrowhead at the v end.
weight > 1.0 Sinusoidal "snake" line whose amplitude grows with the weight; a w= label marks the first occurrence.
color="..." Any CSS color; applied to the line, dots and arrowhead.

Edge identity is the unordered endpoint pair: Edge("a", "b") and Edge("b", "a") are the same edge. Direction, weight and color may change between snapshots without creating a new edge.

Saving figures

render(graph, mode) returns a Figure:

Member Purpose
savefig(path, format=None, scale=2.0) Write .svg or .png; returns the absolute path. scale is pixels per SVG unit and only affects PNG.
to_svg() The SVG document as a string.
to_png_bytes(scale=2.0) PNG file contents, e.g. to upload or embed without touching disk.
width, height Size in SVG units.

In Jupyter a Figure displays inline as SVG. render(graph, mode, path) still works and returns the written path.

PNG output uses a built-in anti-aliasing rasterizer and an embedded copy of the DejaVu Sans outlines (see THIRD_PARTY_NOTICES.md). A typical graph renders in a fraction of a second. Very large ones, thousands of pixels per side, take seconds, so lower scale if needed. Colors may be hex, rgb() / rgba() or any CSS color name; an unsupported color raises ValueError when saving PNG.

Animating the evolution (GIF)

animate(graph) builds a node-link view with one frame per timestep and saves it as an animated GIF, also natively:

from temporal_graph import animate

anim = animate(g, frame_duration=800, loop=0)   # ms per frame; 0 = loop forever
anim.save("evolution.gif")                      # scale=1.5 pixels per unit
anim.frames[2].savefig("t2.png")                # every frame is a Figure

In each frame:

Glyph Meaning
Line with a soft halo Edge that appeared at this timestep
Plain line Edge that carries on from the previous timestep
Dashed grey line Edge that disappeared at this timestep
Arrowhead Directed edge
Thickness, w= label Weight
Grey node Node without edges at this timestep

A header shows the timestep and a change summary, and a timeline bar at the bottom marks progress. Animation.to_gif_bytes() returns the file contents.

Nodes sit on a fixed circle by default. Pass positions={node: (x, y)} (any unit, y pointing down) to use your own layout, e.g. a hierarchy for a tree. Coordinates are scaled so that the two closest nodes do not overlap. Short labels are drawn inside the nodes and longer ones below.

binary tree being built and taken apart

probability flow of a Markov chain social network evolution

Algorithms

Timesteps are instants: a snapshot at t lists the edges available at t, and unrecorded instants have no edges. A journey crosses edges at strictly increasing instants (strict=False allows equal instants, for chained instantaneous contacts). Directed edges are one-way.

from temporal_graph import TemporalGraph, connectivity, flows, journeys

g = TemporalGraph.from_time_edges(
    ["u1", "u2", "u3", "u4", "u5"],
    [("u1", "u2", 5), ("u1", "u3", 1), ("u2", "u3", 2), ("u2", "u4", 6),
     ("u2", "u5", 3), ("u3", "u4", 4), ("u4", "u5", 7)],
)
journeys.earliest_arrival(g, "u1", start=0)     # {'u1': 0, 'u3': 1, 'u2': 2, 'u5': 3, 'u4': 4}
journeys.foremost_journey(g, "u1", "u5")        # u1 -1-> u3 -2-> u2 -3-> u5
flows.max_out_disjoint_journeys(g, "u1", "u5")  # 2
Module Contents Cost
TemporalGraph time_edges(), labels(u, v) (λ(e)), age (α), label_count (|λ|), from_time_edges()
expansion static_expansion(): time-unrolled DAG, with or without waiting arcs n(α+1) nodes
journeys earliest_arrival, foremost_journey, latest_departure, fastest_journey, shortest_journey, enumerate_journeys, reachable, reachability, is_temporally_connected, temporal_distance foremost O(n + |λ|); shortest O(n·|λ|); fastest O(deg·|λ|)
connectivity temporal_diameter (d), connectivity_time (ct), outgoing_influence_time (oit), future_set_sizes, is_continuously_connected, is_f_connected, vertex_connectivity_at, is_interval_connected d, oit: n·α foremost searches
flows Temporal Menger: max_out_disjoint_journeys / min_departure_separator (any graph); max_edge_disjoint_journeys / min_edge_separator (single labels); max_node_disjoint_journeys (exact, NP-hard) max-flow on the expansion
dissemination flooding_times, k_token_dissemination (online token forwarding, pluggable policy), harmonic_potential
design dag_labeling (temporality 1), directed_ring_labeling ({i, n+i}), reachability_labeling (≤ 2 labels, strongly connected digraphs), preserves_all_paths, preserves_reachability, temporality, temporal_cost
periodic Linear availability a·x + b: common_instants, edges_coincide, solve_congruences (generalized CRT), linear_earliest_arrival (no time materialization), linear_availability_graph O(m log n) foremost
generators moving_star, alternating_matchings, soifer_graph, rotating_center_star
random_models random_temporal_graph(n, r, seed), path_journey_probability, expected_journey_count, foremost_within_two_probability
classic Exact small-instance solvers: max_temporal_matching, temporal_exploration, temporal_tsp exponential

The test suite reproduces these results from the notes:

  • the journeys of figure 2;
  • the static-expansion sizes;
  • the Menger examples;
  • the table 4 metrics;
  • the labelings of section 7;
  • the periodic coincidences;
  • formulas (5) to (7);
  • the exploration times of figure 7.

It also cross-checks against brute force on random graphs: BFS on the static expansion, exhaustive journey enumeration and exhaustive disjoint-journey search.

Data tools

Module Contents
utils from_edge_list(nodes, {t: [(u, v), ...]}), snapshot_diff(graph, t1, t2), filter_edges(graph, predicate), subgraph(graph, nodes)
analysis degree_timeline(graph, node), connected_components_at(graph, t), edge_persistence(graph), is_temporally_acyclic(graph)
exporters to_json / from_json (lossless round trip), to_csv, to_gexf (dynamic GEXF for Gephi)

Core API of TemporalGraph:

Name Purpose
Edge(u, v, directed=False, weight=1.0, color=None) Immutable, validated edge.
TemporalGraph(nodes) Container of per-timestep snapshots.
add_snapshot(t, edges) Record (or replace) the edges active at t.
edges_at(t), edge_at(t, key), snapshots(), timesteps, nodes Read access.
change_events() (t, "connect" | "disconnect", key) between consecutive snapshots.
edge_intervals() Half-open (start, end) presence intervals per edge.
from temporal_graph import analysis, exporters, utils

utils.snapshot_diff(g, 0, 1)           # (added, removed)
analysis.degree_timeline(g, "a")       # {0: 1, 1: 2, 2: 1}
exporters.to_gexf(g, "graph.gexf")

Examples

Every script in examples/ runs from the repo root and writes its output into output/.

Script Shows Output
example.py Direction, weight, color and reconnection in one graph. connections, decisions
social_network.py Friendships that strengthen, form and fade; weight and color as relationship strength. connections, decisions
process_flow.py Directed handoffs in a delivery pipeline, plus a per-snapshot acyclicity check. connections, decisions
custom_colors.py Color as health and weight as traffic on the same edges over time. connections, decisions
binary_tree.py A 63-node binary tree built breadth-first and pruned back, with a hierarchical positions layout. Takes about 20 s. binary_tree.gif, full tree, decisions
markov_chain.py A non-reversible Markov chain in two parts. First, a simulated trajectory whose foremost arrivals are the first hitting times, checked against the exact h = 1 + P h. Second, the exact net probability flow converging to the stationary cycle current. trajectory, markov_flow.gif, stationary flow
physics/harmonic_chain.py Sound in a chain of masses and springs. Foremost journeys over energetic springs track the wavefront, which matches the exact Bessel solution, and recover c = a sqrt(k/m) within 2 %. See examples/physics. light cone, harmonic_chain.gif
physics/hard_disk_gas.py Collisions of a hard-disk gas as a contact network. The collision rate is checked against Enskog theory, speeds against Maxwell-Boltzmann, and causal spreading against a well-mixed SI process. printed results, cascade
temporal_algorithms.py Journeys, metrics, Menger, labelings, periodic and random models, exact classic problems on the notes' examples. printed results, moving_star.gif
animation.py Evolution of the social network and the pipeline as animated GIFs. social_network.gif, process_flow.gif
analysis_and_export.py Degree, components, persistence, diff, subgraph, and JSON / CSV / GEXF export. printed metrics, network.json, network.csv, network.gexf

Social network, decisions mode:

social network, decisions mode

Delivery pipeline, connections mode:

process flow, connections mode

Behavior notes

  • Self-loops and undeclared nodes are rejected (ValueError).
  • Duplicate edges within one snapshot are deduplicated; the first one wins.
  • An edge absent from a snapshot is disconnected; it must be declared again to reappear.
  • Because edge identity ignores direction, u -> v and v -> u cannot both be present at the same instant.
  • When several edges act at the same timestep they are placed in sub-lanes inside that timestep's column.

Development

pip install -e ".[dev]"
ruff check temporal_graph tests
mypy temporal_graph
pytest --cov=temporal_graph

See CONTRIBUTING.md, CHANGELOG.md and PUBLISHING.md.

Author

Giuliano Crenna Facultad de Ciencias Exactas, Ingeniería y Agrimensura Universidad Nacional de Rosario giulicrenna@fceia.unr.edu.ar

Questions, bug reports and contributions are welcome by email or through the repository's issues.

Citing

If you use this library in academic work, please cite it together with the notes its algorithms follow:

@software{crenna_temporal_graph,
  author      = {Crenna, Giuliano},
  title       = {temporal-graph: modelling, computing on and rendering temporal graphs},
  year        = {2026},
  institution = {Facultad de Ciencias Exactas, Ingenier{\'\i}a y Agrimensura, Universidad Nacional de Rosario}
}

@misc{crenna_grafos_temporales,
  author = {Crenna, Giuliano},
  title  = {Grafos temporales: una introducci{\'o}n desde la perspectiva algor{\'\i}tmica},
  year   = {2026},
  month  = oct,
  note   = {Facultad de Ciencias Exactas, Ingenier{\'\i}a y Agrimensura, Universidad Nacional de Rosario}
}

License

MIT, see LICENSE. The embedded DejaVu Sans outlines are under the Bitstream Vera license, see THIRD_PARTY_NOTICES.md.

Metadata

Release files for temporal-graph 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for temporal-graph 0.1.0
File Size Uploaded Provenance Actions
temporal_graph-0.1.0.tar.gz 99.7 kB Without provenance Details for temporal_graph-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for temporal-graph 0.1.0
File Interpreter ABI Platform Actions
temporal_graph-0.1.0-py3-none-any.whl Python 3 none any Details for temporal_graph-0.1.0-py3-none-any.whl

Total release size: 182.3 kB

Release files / temporal_graph-0.1.0.tar.gz

Download URL temporal_graph-0.1.0.tar.gz
Size 99.7 kB
Tags Source
SHA-256 checksum
How to use checksums
806473d4f943e4ba3ea4cb51c5e36a0a3506567294918154d9b53210c6fb7cda
BLAKE2b-256 checksum
How to use checksums
5b538f2853d1562b14106a59192831d0435adf5cdde81370998dcfc28f6bf3e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.0

Release files / temporal_graph-0.1.0-py3-none-any.whl

Download URL temporal_graph-0.1.0-py3-none-any.whl
Size 82.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
33ea6a1e8810e745831fb11b7f67c3fdda23dac57be2a540a4c2beb858a874bf
BLAKE2b-256 checksum
How to use checksums
ef4c6b8fbf1fb88f69f9c5196c147442fd105b9ca6f37b1e303d35e0077a3f3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.0

Security for temporal-graph 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page