Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 232 additions & 36 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,25 @@
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
#title { height: 36px; display: flex; align-items: center; padding: 0 14px; background: #1a1a2e; color: #fff; font-size: 14px; font-weight: 600; letter-spacing: 0.3px; }
#title span { opacity: 0.6; margin-left: 8px; font-weight: 400; }
#title a { margin-left: auto; display: flex; align-items: center; color: #fff; opacity: 0.6; transition: opacity 0.15s; }
#title a:hover { opacity: 1; }
#map { height: calc(100vh - 36px); width: 100%; }
.legend { background: white; padding: 8px 12px; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,0,0,0.3); line-height: 1.6; font-size: 13px; }
.legend i { width: 12px; height: 12px; display: inline-block; margin-right: 6px; border-radius: 50%; vertical-align: middle; }
.legend .line { width: 20px; height: 0; border-top: 2px dashed #888; display: inline-block; margin-right: 6px; vertical-align: middle; }
.playback-control a { background: white; width: 34px; height: 34px; line-height: 34px; text-align: center; font-size: 18px; display: block; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,0,0,0.3); text-decoration: none; color: #333; }
.playback-control a:hover { background: #f4f4f4; }
</style>
</head>
<body>
<div id="title">Tezos Protocol Cities</div>
<div id="title"><span id="title-text">Tezos Protocol Cities</span><a href="https://github.com/cotezos/tezosprotocolmap" target="_blank" rel="noopener" aria-label="GitHub"><svg height="20" width="20" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.64 7.64 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg></a></div>
<div id="map"></div>
<script>
(function () {
var PROTOCOLS_URL = "https://raw.githubusercontent.com/cotezos/tezosprotocolmap/main/protocols.json";
var COLOR_MAINNET = "#1f77b4";
var COLOR_TESTNET = "#ff7f0e";
var DAY_MS = 86400000;

var map = L.map("map").setView([30, 20], 3);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
Expand All @@ -39,13 +44,185 @@
return el.innerHTML;
}

// Shared state.
var entries = [];
var markers = [];
var trail = null;
var trailCoords = [];
var playbackTimer = null;
var playbackIndex = 0;
var isPlaying = false;
var sliderInput = null;
var sliderLabel = null;

function buildPopup(d) {
var hashTrunc = d.hash ? d.hash.substring(0, 12) + "..." : "N/A";
var date = d.activationDate ? d.activationDate.split("T")[0] : "N/A";
var network = d.mainnet ? "Mainnet" : "Testnet only";

var html =
"<strong>" + esc(String(d.number).padStart(3, "0") + " " + d.name) + "</strong><br>" +
"Hash: <code>" + esc(hashTrunc) + "</code><br>" +
"Activated: " + esc(date) + "<br>" +
"Network: " + esc(network);

if (d.voting) {
html += "<br>Voted: Epoch " + esc(String(d.voting.epoch)) +
" \u2014 " + esc(d.voting.result) +
"<br>Ballots: " + d.voting.yayBallots + " yay, " +
d.voting.nayBallots + " nay, " + d.voting.passBallots + " pass";
}
Comment on lines +69 to +74

return html;
}

function addMarker(index) {
var d = entries[index];
var color = d.mainnet ? COLOR_MAINNET : COLOR_TESTNET;
var marker = L.circleMarker([d.lat, d.lon], {
radius: 7,
fillColor: color,
color: "#fff",
weight: 1.5,
fillOpacity: 0.9
}).addTo(map);
marker.bindPopup(buildPopup(d));
markers.push(marker);

trailCoords.push([d.lat, d.lon]);
if (trail) {
map.removeLayer(trail);
}
if (trailCoords.length > 1) {
trail = L.polyline(trailCoords, {
color: "#888888",
weight: 1.5,
dashArray: "6 4",
opacity: 0.5
}).addTo(map);
}
}

function clearAll() {
markers.forEach(function (m) { map.removeLayer(m); });
markers = [];
if (trail) {
map.removeLayer(trail);
trail = null;
}
trailCoords = [];
}

function updateTitle(count) {
var text = "Tezos Protocol Cities";
if (typeof count === "number") {
text += " \u2014 " + count + " protocol" + (count !== 1 ? "s" : "");
}
document.getElementById("title-text").textContent = text;
}

function showAll() {
clearAll();
for (var i = 0; i < entries.length; i++) {
addMarker(i);
}
updateTitle(entries.length);
}

// Playback logic.
function stopPlayback() {
if (playbackTimer) {
clearTimeout(playbackTimer);
playbackTimer = null;
}
isPlaying = false;
playbackIndex = 0;
if (playBtn) playBtn.textContent = "\u25B6";
setSliderEnabled(true);
}

function playStep() {
if (playbackIndex >= entries.length) {
stopPlayback();
return;
}
addMarker(playbackIndex);
updateTitle(playbackIndex + 1);
playbackIndex++;
playbackTimer = setTimeout(playStep, 800);
}

function togglePlayback() {
if (isPlaying) {
stopPlayback();
showAll();
} else {
clearAll();
isPlaying = true;
playbackIndex = 0;
if (playBtn) playBtn.textContent = "\u23F8";
setSliderEnabled(false);
playStep();
}
}

var playBtn = null;

// Slider logic.
function setSliderEnabled(enabled) {
if (!sliderInput) return;
sliderInput.disabled = !enabled;
sliderInput.style.opacity = enabled ? "1" : "0.5";
if (enabled) {
sliderInput.value = sliderInput.max;
updateSliderLabel(Number(sliderInput.max));
}
}

function updateSliderLabel(ts) {
if (!sliderLabel) return;
if (ts >= Number(sliderInput.max)) {
sliderLabel.textContent = "All protocols";
} else {
var d = new Date(ts);
var yyyy = d.getUTCFullYear();
var mm = String(d.getUTCMonth() + 1).padStart(2, "0");
var dd = String(d.getUTCDate()).padStart(2, "0");
sliderLabel.textContent = "Protocols as of " + yyyy + "-" + mm + "-" + dd;
}
}

function filterByDate(timestamp) {
if (isPlaying) return;
var atMax = timestamp >= Number(sliderInput.max);
clearAll();
var count = 0;
for (var i = 0; i < entries.length; i++) {
var d = entries[i];
if (d.activationDate) {
if (new Date(d.activationDate).getTime() <= timestamp) {
addMarker(i);
count++;
}
} else {
Comment on lines +201 to +207
// Testnet-only (no activation date): show only when slider at max.
if (atMax) {
addMarker(i);
count++;
}
}
}
updateTitle(count);
updateSliderLabel(timestamp);
}

fetch(PROTOCOLS_URL)
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(function (data) {
var entries = Object.keys(data).map(function (name) {
entries = Object.keys(data).map(function (name) {
var d = data[name];
d.name = name;
return d;
Expand All @@ -55,42 +232,61 @@
return a.number - b.number;
});

document.getElementById("title").innerHTML =
"Tezos Protocol Cities" + "<span>\u2014 " + entries.length + " protocols</span>";

var trailCoords = [];

entries.forEach(function (d) {
var color = d.mainnet ? COLOR_MAINNET : COLOR_TESTNET;
var marker = L.circleMarker([d.lat, d.lon], {
radius: 7,
fillColor: color,
color: "#fff",
weight: 1.5,
fillOpacity: 0.9
}).addTo(map);

var hashTrunc = d.hash ? d.hash.substring(0, 12) + "..." : "N/A";
var date = d.activationDate ? d.activationDate.split("T")[0] : "N/A";
var network = d.mainnet ? "Mainnet" : "Testnet only";

marker.bindPopup(
"<strong>" + esc(String(d.number).padStart(3, "0") + " " + d.name) + "</strong><br>" +
"Hash: <code>" + esc(hashTrunc) + "</code><br>" +
"Activated: " + esc(date) + "<br>" +
"Network: " + esc(network)
);

trailCoords.push([d.lat, d.lon]);
showAll();

// Playback control.
var PlaybackControl = L.Control.extend({
options: { position: "topleft" },
onAdd: function () {
var container = L.DomUtil.create("div", "playback-control leaflet-bar");
playBtn = L.DomUtil.create("a", "", container);
playBtn.href = "#";
playBtn.title = "Play/Pause timeline";
playBtn.textContent = "\u25B6";
playBtn.setAttribute("role", "button");
L.DomEvent.disableClickPropagation(container);
L.DomEvent.on(playBtn, "click", function (e) {
L.DomEvent.preventDefault(e);
togglePlayback();
});
return container;
}
});
new PlaybackControl().addTo(map);

// Date slider control.
var dates = entries
.filter(function (d) { return d.activationDate; })
.map(function (d) { return new Date(d.activationDate).getTime(); });

if (dates.length > 0) {
var minDate = Math.min.apply(null, dates);
var maxDate = Date.now();
Comment on lines +258 to +264

if (trailCoords.length > 1) {
L.polyline(trailCoords, {
color: "#888888",
weight: 1.5,
dashArray: "6 4",
opacity: 0.5
}).addTo(map);
var SliderControl = L.Control.extend({
options: { position: "bottomleft" },
onAdd: function () {
var container = L.DomUtil.create("div", "");
container.style.cssText = "background:white;padding:10px 14px;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,0.3);min-width:200px;";
sliderLabel = L.DomUtil.create("div", "", container);
sliderLabel.style.cssText = "font-size:12px;margin-bottom:6px;color:#333;font-weight:500;";
sliderLabel.textContent = "All protocols";
sliderInput = L.DomUtil.create("input", "", container);
sliderInput.type = "range";
sliderInput.min = String(minDate);
sliderInput.max = String(maxDate);
sliderInput.value = String(maxDate);
sliderInput.step = String(DAY_MS);
sliderInput.style.cssText = "width:100%;margin:0;cursor:pointer;";
L.DomEvent.disableClickPropagation(container);
L.DomEvent.disableScrollPropagation(container);
L.DomEvent.on(sliderInput, "input", function () {
filterByDate(Number(sliderInput.value));
});
return container;
}
});
new SliderControl().addTo(map);
}

// Legend control.
Expand Down