Technical documentation for the SCC Timeline Explorer. It covers the architecture of the application and the solutions to the historical-chronology problems that shaped it. The project is part of the research of the group Governance of the Universal Church after the Council of Trent at the Max Planck Institute for Legal History and Legal Theory.
The Timeline Explorer renders the chronology of the Congregation of the Council — pontificates, prefects, secretaries, cardinals, tribunals, congregations and sessions — on a single interactive axis, with a details panel for every entity.
The application was originally written in R with shiny and timevis. It has since been ported to plain HTML/CSS/JavaScript: there is no server-side component, no R runtime and no session state. The page is a static artefact that can be opened directly from disk or served from any static host.
Three files and one generated dataset:
index.html page shell, site chrome, sidebar markup
app.js all application logic
tl-style.css styles, scoped under #tl-app
data/data.js generated dataset (const DATA = [...])
csv_to_js.py build step: CSV -> data.js
The timeline itself is drawn by vis-timeline, the library that timevis wrapped in the R version, so the item classes carried over unchanged. The year filter uses noUiSlider.
Data is delivered as a <script> tag rather than fetched. Browsers block fetch() on the file:// protocol, so a fetched dataset would force every user to run a local web server; a script tag has no such restriction and the page opens on a double click.
All application styles are scoped under #tl-app. The surrounding site loads Bootstrap 3.3.5 and 5.3.3, both of which define .panel and .collapse; scoping prevents the two rule sets from colliding.
The CSV export remains the source of truth. csv_to_js.py performs the preprocessing that the R server once did on every request — date parsing, row ids, URL fields turned into links, empty cells filled with a placeholder — and writes the result once, at build time:
python csv_to_js.py data/data.csv data/data.js
Moving this work out of the runtime is the main performance change of the port: the browser receives records that are ready to render.
Every row carries a subgroup value. In vis-timeline a subgroup is a parallel track inside one group row, which is what keeps overlapping prefects, or the background/bar/point triple belonging to a single cardinal, on separate lines instead of collapsing onto each other.
The vis-timeline axis is physical time rendered through the JavaScript Date object, i.e. the proleptic Gregorian calendar. The sources, however, record dates before the reform of 1582 in the Julian calendar. Placing a Julian date on a Gregorian axis without conversion misplaces it by up to ten days — enough to put an event in the wrong month and to break any comparison across the reform.
The application therefore separates position from label.
At load time every source date is resolved to a Julian Day Number, the calendar-independent count of days that acts as the common denominator between the two systems:
function sourceToInstant(iso) {
const p = iso.split("-").map(Number);
const gj = gregorianToJDN(p[0], p[1], p[2]);
if (gj >= REFORM_JDN) return { jdn: gj, julian: false };
return { jdn: julianToJDN(p[0], p[1], p[2]), julian: true };
}
A date that already falls on or after the reform is read as Gregorian; anything earlier is treated as a Julian source date and converted. The JDN is then mapped to a physical instant for vis-timeline. Positions are computed once and never change when the display is toggled — an item always sits on its true instant.
The Show Julian / Gregorian Calendar switch changes only how dates are written. When it is on, axis labels and the details panel show pre-reform dates in Julian reckoning — exactly as they appear in the sources — and post-reform dates in Gregorian. When it is off, everything is displayed in proleptic Gregorian.
Switching modes calls timeline.redraw(), which re-evaluates the label formatters; no data is rebuilt and nothing moves. A vertical marker is placed at the reform itself, so the discontinuity is visible rather than implied.
The reform is dated to the Roman transition: Julian 4 October 1582 is followed by Gregorian 15 October 1582 (REFORM_JDN = 2299161). Adoption elsewhere was later and uneven; the application uses the Roman date because the material is curial.
Scope. "Julian year" here means the civil year beginning 1 January. Regional year-start styles — Annunciation, Easter style and others — are a property of the individual source and are resolved when the data is edited, not by the display layer.
A third reckoning runs on the same physical axis. Show the papal calendar adds the anno pontificatus to axis labels and to the details panel: 1650 · Innocentius X · an. VI.
Following the convention of the Annuario Pontificio, a pontificate begins at the election of the pope — which is the start of every pontificate range in the dataset — so year I runs from election day. The reign covering an instant is found by binary search over a sorted index built at load time; the year is anniversary arithmetic on calendar parts, each date taken in the calendar of its own era, which reproduces the curial practice of continuing the same nominal date across the reform.
While the mode is active, hatched full-height stripes mark every sede vacante, so a vacancy is not visually absorbed by the pontificates on either side.
Label density is measured rather than assumed: the axis reports its pixels-per-year, and the pope's name, the an. suffix, or neither is drawn depending on the width actually available per column. Without this the labels overlap as soon as the window is zoomed out.
Show Easter dates generates a dedicated row with Easter Sunday for every year in range. Years up to 1582 use the Julian computus; from 1583 the Gregorian computus in the Meeus/Jones/Butcher formulation. Both produce a date in their own calendar, which is converted to JDN before it reaches the axis — the same pipeline as the rest of the data, so the row stays consistent with it.
These items are synthetic and carry ids far above any CSV row id, which keeps them out of the search index and the details panel. Because background items are not selectable in vis-timeline, the row has its own hover tooltip resolved through timeline.getEventProperties().
Group rows follow a fixed canonical order rather than the order in which filters are switched on, so the reading of the timeline is stable across sessions.
Zoom is driven by a slider with logarithmic interpolation between the full range and one year; the mouse wheel pans instead of zooming. The lower bound of one visible year keeps the axis on months and prevents it from descending to days, a precision the material does not support. The upper bound of the timeline is 1925.
vis-timeline applies its item colours as inline styles, which override any stylesheet rule by specificity. The item classes therefore carry !important — this is a requirement of the library, not a stylistic choice.
Every entity is addressable. Selecting an item writes its id into the URL, and opening that URL restores the selection — enabling the group if necessary, centring the axis and opening the details panel:
index.html?data=42
This makes individual entities citable, and lets the companion search application hand a result directly to the timeline.