live renderhtml
htmlmultitimeline.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multidimensional Timeline Calendar</title>
<!-- Bootstrap 5 CDN -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<style>
:root {
/* category colours (extend as needed) */
--climate: #86d06d;
--astronomy: #63a2ff;
--culture: #ff66aa;
--mythic: #f9bf3b;
--economy: #ff8763;
}
html,
body {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
}
/* controls */
#controls {
background: #212123;
color: #f8f9fa;
}
#timelineWrapper {
flex: 1 1 auto;
overflow-y: auto;
position: relative;
background: #f7f0e6;
}
/* svg timeline */
svg.timeline-svg {
position: absolute;
left: 0;
top: 0;
}
.timeline-base {
stroke: #212123;
stroke-width: 2px;
}
.event-circle {
stroke: #212123;
stroke-width: 1;
fill-opacity: 0.6;
transition: r 0.2s ease, fill-opacity 0.2s ease;
cursor: pointer;
}
.event-circle:hover {
r: calc(var(--r) + 8px);
fill-opacity: 1;
}
.event-label {
font-size: 0.85rem;
fill: #212123;
pointer-events: none;
user-select: none;
}
.event-date {
font-size: 0.75rem;
fill: #666;
pointer-events: none;
user-select: none;
}
/* colour mapping for categories */
.cat-astronomy {
fill: var(--astronomy);
}
.cat-climate {
fill: var(--climate);
}
.cat-culture {
fill: var(--culture);
}
.cat-mythic {
fill: var(--mythic);
}
.cat-economy {
fill: var(--economy);
}
</style>
</head>
<body>
<div id="app">
<!-- CONTROLS -->
<nav id="controls" class="navbar navbar-dark px-3 py-2">
<div class="navbar-brand">Multidimensional Timeline</div>
<form class="d-flex gap-3 flex-wrap" role="search" id="controlForm">
<select id="calendarSelect" class="form-select form-select-sm">
<option value="gregorian">Gregorian</option>
<option value="julian">Julian</option>
<option value="mayan">Mayan Long Count</option>
<option value="chinese">Chinese Zodiac</option>
<option value="indian">Indian Lunisolar</option>
<option value="aztec">Aztec Tonalpohualli</option>
<option value="nazca">Nazca Horizon</option>
<option value="abstract">Planetary Abstract</option>
</select>
<div class="form-check form-switch text-nowrap">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="liveDataSwitch"
/>
<label class="form-check-label" for="liveDataSwitch"
>Live Celestial Data</label
>
</div>
<div class="form-check form-switch text-nowrap">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="fictionSwitch"
checked
/>
<label class="form-check-label" for="fictionSwitch"
>Mythic & Fictional</label
>
</div>
</form>
</nav>
<!-- TIMELINE WRAPPER -->
<div id="timelineWrapper">
<svg class="timeline-svg" id="timelineSvg"></svg>
</div>
</div>
<!-- Optionally, Bootstrap JS (popper included) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
/* =============================================================
* Multidimensional Timeline Calendar – Vanilla JS (no libs)
* Author: ChatGPT o3, 2025
* =========================================================== */
const SVG_NS = "http://www.w3.org/2000/svg";
/* Utility: date helpers */
function iso(date) {
return date.toISOString().split("T")[0];
}
// Julian = Gregorian - 13 days (valid 1900‑2100 for demo)
function toJulian(date) {
const jul = new Date(date);
jul.setDate(jul.getDate() - 13);
return iso(jul);
}
// Stub converters (placeholders) – return symbolic strings
function toMayanLongCount(date) {
// Very rough: days since 2025‑06‑13 → add offset to 13.0.0.0.0 (Dec 21, 2012)
const base = new Date("2012-12-21");
const diff = Math.floor((date - base) / 864e5);
return `13.${Math.floor(diff / 144000)}.${Math.floor((diff % 144000) / 7200)}.${Math.floor((diff % 7200) / 360)}.${diff % 360}`;
}
function toChineseZodiac(date) {
const animals = [
"Rat",
"Ox",
"Tiger",
"Rabbit",
"Dragon",
"Snake",
"Horse",
"Goat",
"Monkey",
"Rooster",
"Dog",
"Pig",
];
const baseYear = 1900; // Rat
const idx = (date.getFullYear() - baseYear) % 12;
return `${animals[idx]} Year`;
}
function toIndianLunisolar(date) {
return `Paksha ${date.getDate() % 15}, Month ${(date.getMonth() + 1)}`;
}
function toAztec(date) {
return `Tonalpohualli ${(date.getTime() / 864e5) % 260 | 0}`;
}
function toNazca(date) {
return `Solstice offset ${(Math.abs(date - new Date(date.getFullYear(), 5, 21)) / 864e5).toFixed(0)}d`;
}
function toAbstract(date) {
const k101start = new Date("2025-06-13");
const days = Math.floor((date - k101start) / 864e5);
return `K101+${days}`;
}
function convertDate(date, calendar) {
switch (calendar) {
case "julian":
return toJulian(date);
case "mayan":
return toMayanLongCount(date);
case "chinese":
return toChineseZodiac(date);
case "indian":
return toIndianLunisolar(date);
case "aztec":
return toAztec(date);
case "nazca":
return toNazca(date);
case "abstract":
return toAbstract(date);
default:
return iso(date);
}
}
/* Event generation (demo) */
const BASE_EVENTS = [
{
id: 1,
date: new Date("2025-06-13"),
label: "Friday 13 // K101 Era begins",
category: "culture",
intensity: 0.8,
},
{
id: 2,
date: new Date("2025-07-03"),
label: "Earth Aphelion",
category: "astronomy",
intensity: 0.7,
},
{
id: 3,
date: new Date("2026-02-13"),
label: "Super El Niño Peak",
category: "climate",
intensity: 0.9,
},
];
// Generate placeholder events year +/- range (fictional & real)
function generateSyntheticEvents(yearsBack = 50, yearsForward = 50) {
const synthetic = [];
const categories = ["astronomy", "climate", "culture", "mythic", "economy"];
const now = new Date();
const startYear = now.getFullYear() - yearsBack;
const endYear = now.getFullYear() + yearsForward;
let id = 1000;
for (let y = startYear; y <= endYear; y++) {
// couple per year
const count = Math.random() * 3 + 1;
for (let i = 0; i < count; i++) {
const d = new Date(y, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1);
const cat = categories[(Math.random() * categories.length) | 0];
synthetic.push({
id: id++,
date: d,
label: `${cat.charAt(0).toUpperCase() + cat.slice(1)} Event`,
category: cat,
intensity: Math.random(),
});
}
}
return synthetic;
}
const EVENTS = [...BASE_EVENTS, ...generateSyntheticEvents()];
/* SVG Timeline rendering */
const timelineWrapper = document.getElementById("timelineWrapper");
const svg = document.getElementById("timelineSvg");
// timeline dimensions will be recalculated on resize/scroll
let timelineX = 120;
const marginTop = 40;
const marginBottom = 40;
const circleRadiusMin = 6;
const circleRadiusMax = 28;
// scale helpers
function linearScale(domainMin, domainMax, rangeMin, rangeMax) {
return (val) => {
const ratio = (val - domainMin) / (domainMax - domainMin || 1);
return rangeMin + ratio * (rangeMax - rangeMin);
};
}
function renderTimeline() {
// Clean svg
svg.innerHTML = "";
const wrapperRect = timelineWrapper.getBoundingClientRect();
const width = wrapperRect.width;
const height = EVENTS.length * 80 + marginTop + marginBottom; // rough initial height
svg.setAttribute("width", width);
svg.setAttribute("height", height);
// baseline
const baseLine = document.createElementNS(SVG_NS, "line");
baseLine.setAttribute("x1", timelineX);
baseLine.setAttribute("y1", 0);
baseLine.setAttribute("x2", timelineX);
baseLine.setAttribute("y2", height);
baseLine.setAttribute("class", "timeline-base");
svg.appendChild(baseLine);
// domain (time) – earliest to latest
const minDate = EVENTS.reduce((a, b) => (a < b.date ? a : b.date), EVENTS[0].date);
const maxDate = EVENTS.reduce((a, b) => (a > b.date ? a : b.date), EVENTS[0].date);
const domainMin = minDate.getTime();
const domainMax = maxDate.getTime();
const scaleY = linearScale(domainMin, domainMax, marginTop, height - marginBottom);
// radius scale – intensity based
const scaleR = linearScale(0, 1, circleRadiusMin, circleRadiusMax);
// calendar conversion
const calendarType = document.getElementById("calendarSelect").value;
EVENTS.forEach((ev, index) => {
const y = scaleY(ev.date.getTime());
const r = scaleR(ev.intensity);
// group
const g = document.createElementNS(SVG_NS, "g");
// circle
const circle = document.createElementNS(SVG_NS, "circle");
circle.setAttribute("cx", timelineX);
circle.setAttribute("cy", y);
circle.setAttribute("r", r);
circle.setAttribute("class", `event-circle cat-${ev.category}`);
circle.style.setProperty("--r", r + "px");
// label
const label = document.createElementNS(SVG_NS, "text");
label.setAttribute("x", timelineX + r + 12);
label.setAttribute("y", y + 4);
label.setAttribute("class", "event-label");
label.textContent = ev.label;
// date
const dateTxt = document.createElementNS(SVG_NS, "text");
dateTxt.setAttribute("x", timelineX - r - 12);
dateTxt.setAttribute("y", y + 4);
dateTxt.setAttribute("text-anchor", "end");
dateTxt.setAttribute("class", "event-date");
dateTxt.textContent = convertDate(ev.date, calendarType);
// tooltip (simple using title)
circle.setAttribute("title", `${ev.label}\n${convertDate(ev.date, calendarType)}`);
g.appendChild(circle);
g.appendChild(label);
g.appendChild(dateTxt);
svg.appendChild(g);
});
}
// UI interactions
document.getElementById("calendarSelect").addEventListener("change", renderTimeline);
document.getElementById("fictionSwitch").addEventListener("change", (e) => {
if (e.target.checked) {
// ensure mythic are visible
EVENTS.forEach((ev) => {
if (ev.category === "mythic") ev.hidden = false;
});
} else {
EVENTS.forEach((ev) => {
if (ev.category === "mythic") ev.hidden = true;
});
}
renderTimeline();
});
// Live data fetch (placeholder)
document.getElementById("liveDataSwitch").addEventListener("change", async (e) => {
if (e.target.checked) {
// Example: fetch today’s NASA APOD date and add to timeline
try {
const res = await fetch(
"https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"
);
const json = await res.json();
EVENTS.push({
id: Date.now(),
date: new Date(json.date),
label: `NASA APOD: ${json.title}`,
category: "astronomy",
intensity: 0.5,
});
renderTimeline();
} catch (err) {
console.error("API error", err);
}
}
});
// initial render
renderTimeline();
/* Infinite scroll: if near bottom, add more future events */
timelineWrapper.addEventListener("scroll", () => {
const threshold = 300; // px from bottom
if (
timelineWrapper.scrollTop + timelineWrapper.clientHeight + threshold >=
timelineWrapper.scrollHeight
) {
// generate +10 future years events
const futureEvents = generateSyntheticEvents(0, 10);
EVENTS.push(...futureEvents);
renderTimeline();
}
});
// responsive resize
window.addEventListener("resize", renderTimeline);
</script>
</body>
</html>