fliarbi's picture
Update app
8e6d64a verified
Raw History Blame Contribute Delete
23.1 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Web Technology Explorer</title>
<meta name="description" content="Technology adoption on the web (top 300 technologies by category), Core Web Vitals pass rates by CMS and platform, TLD breakdowns and schema.org structured-data types, May 2025 vs May 2026. Source: HTTP Archive / Chrome UX Report.">
<link rel="stylesheet" href="common.css">
<style>
.note { color: var(--muted); font-size: .86rem; }
.method h2 { font-size: 1.1rem; margin: 4px 0 8px; }
.method h3 { font-size: .95rem; margin: 14px 0 4px; }
.method p, .method li { color: var(--muted); font-size: .9rem; }
.method ul { padding-left: 18px; margin: 4px 0; }
.rowflex { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; margin-bottom: 8px; }
.rowflex label { display: flex; flex-direction: column; font-size: .8rem; color: var(--muted); gap: 4px; min-width: 170px; flex: 1; }
.src { font-size: .86rem; color: var(--muted); }
td .pos { color: var(--good); } td .neg { color: #dc2626; }
</style>
<script src="https://cdn.jsdelivr.net/npm/plotly.js-dist-min@2.35.2/plotly.min.js"></script>
</head>
<body>
<div class="wrap">
<header>
<h1>🌐 Web Technology Explorer</h1>
<p>What do websites run, and how fast are they for real users? Explore the <b>top 300 web technologies</b> by category, how adoption changed
between the <b>May 2025 and May 2026</b> crawls, <b>Core Web Vitals</b> pass rates by CMS and platform, country and generic <b>TLDs</b>,
and the most common <b>schema.org structured-data types</b>. Aggregates only, computed by Fliar BI.</p>
<p class="src">Source: <b>HTTP Archive / Chrome UX Report (public web crawl data)</b>, CC BY 4.0. <span id="covline"></span></p>
</header>
<div class="panel kpi" id="kpi"><div class="loading">Loading the aggregate tables…</div></div>
<div class="panel" id="tabbox">
<div class="tabs"><button>Technology adoption</button><button>By popularity</button><button>Core Web Vitals by platform</button><button>TLDs &amp; countries</button><button>Structured data</button></div>
<div data-pane>
<div class="rowflex">
<label>Category<select id="cat"></select></label>
<label>Search technology<input type="search" id="q" placeholder="e.g. React, Shopify, Cloudflare"></label>
<label>Sort by<select id="sort"><option value="share">Share of sites (2026)</option><option value="change_pp">Change since 2025 (pp)</option><option value="lift">Top-1M vs all sites</option><option value="cwv_pass_rate">CWV pass rate</option></select></label>
</div>
<div id="f_tech" class="chart" style="min-height:520px"></div>
<div id="t_tech"></div>
<p class="note">The table shows the first 60 matches; use the category filter or search to reach any of the 300 technologies. All 300 are in the aggregate dataset.</p>
</div>
<div data-pane>
<div class="rowflex"><label>Technologies (up to 6)<select id="rk_tech" multiple size="6"></select></label></div>
<div id="f_rank" class="chart" style="min-height:420px"></div>
<p class="note">Popularity buckets come from the Chrome UX Report rank of each origin (top 10k = the 10,000 most visited origins). A technology above its line in the top buckets is favoured by large sites.</p>
<div id="f_stack" class="chart"></div>
</div>
<div data-pane>
<div class="rowflex">
<label>Crawl<select id="cwv_crawl"></select></label>
<label>Minimum sites with field data<select id="cwv_min"><option>1000</option><option selected>10000</option><option>50000</option></select></label>
<label>Category<select id="cwv_cat"></select></label>
</div>
<div id="f_cwv" class="chart" style="min-height:560px"></div>
<div id="t_cwv"></div>
</div>
<div data-pane>
<div class="rowflex">
<label>TLD type<select id="tld_kind"><option value="country code">Country-code TLDs</option><option value="generic">Generic TLDs</option><option value="__all__">All</option></select></label>
<label>Metric<select id="tld_metric"><option value="cwv_pass_rate">Core Web Vitals pass rate</option><option value="jsonld_rate">JSON-LD structured data</option><option value="leading_platform_share">Leading platform share</option><option value="share">Share of all sites</option><option value="tech">Share using a technology…</option></select></label>
<label>Technology (for the last metric)<select id="tld_tech"></select></label>
</div>
<div id="f_tld" class="chart" style="min-height:520px"></div>
<div id="t_tld"></div>
</div>
<div data-pane>
<div id="f_sd" class="chart" style="min-height:560px"></div>
<div id="t_sd"></div>
</div>
</div>
<div class="panel method" id="method"></div>
<footer>
Source: <b>HTTP Archive / Chrome UX Report (public web crawl data)</b>, CC BY 4.0. Aggregates: licence <b>CC BY-NC 4.0</b>, attribution "Fliar BI"
(<a href="https://fliarbi.com" target="_blank">fliarbi.com</a>) and the upstream sources.
Tables: <a href="https://huggingface.co/datasets/fliarbi/web-technology-trends-aggregates" target="_blank">fliarbi/web-technology-trends-aggregates</a> ·
<a href="https://fliarbi.com/datasets/" target="_blank">fliarbi.com/datasets</a> ·
<a href="https://github.com/Busineessfree/open-company-datasets" target="_blank">GitHub index</a> ·
<a href="mailto:b2b@fliarbi.com">b2b@fliarbi.com</a>
</footer>
</div>
<script type="module">
import { parquetReadObjects } from "https://cdn.jsdelivr.net/npm/hyparquet@1.31.2/+esm";
import { ALL, groupBy, renderTable, baseLayout, plot, css, esc, int, PALETTE, tabs, sum } from "./common.js";
const $ = (id) => document.getElementById(id);
async function load(name) {
const res = await fetch(`data/${name}.parquet`);
if (!res.ok) throw new Error(`${name}: HTTP ${res.status}`);
const rows = await parquetReadObjects({ file: await res.arrayBuffer() });
for (const r of rows) for (const k in r) if (typeof r[k] === "bigint") r[k] = Number(r[k]);
return rows;
}
const p1 = (v) => (Number.isFinite(v) ? `${(v * 100).toFixed(1)}%` : "–");
const pp = (v) => (Number.isFinite(v) ? `<span class="${v >= 0 ? "pos" : "neg"}">${v >= 0 ? "+" : ""}${v.toFixed(1)}</span>` : "–");
const month = (c) => new Date(c + "T00:00:00Z").toLocaleString("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
const BUCKET = (b) => b.slice(3);
let D = {}, ST, LATEST, PREV;
function techTab() {
const cat = $("cat").value, q = $("q").value.trim().toLowerCase(), key = $("sort").value;
let rows = D.tech.filter((r) => (cat === ALL || r.categories.split("; ").includes(cat)) && (!q || r.technology.toLowerCase().includes(q)));
rows = rows.map((r) => ({ ...r, lift: r.share ? r.share_top1m / r.share : NaN }));
rows.sort((a, b) => (Number.isFinite(b[key]) ? b[key] : -1e9) - (Number.isFinite(a[key]) ? a[key] : -1e9));
const top = rows.slice(0, 25).reverse();
plot($("f_tech"), [
{ type: "bar", orientation: "h", name: month(PREV), y: top.map((r) => r.technology), x: top.map((r) => r.share_2025 * 100), marker: { color: "#94a3b8" } },
{ type: "bar", orientation: "h", name: month(LATEST), y: top.map((r) => r.technology), x: top.map((r) => r.share * 100), marker: { color: css("--accent") },
text: top.map((r) => `${(r.share * 100).toFixed(1)}%`), textposition: "outside", cliponaxis: false },
], baseLayout(`Share of sites using each technology${cat === ALL ? "" : " · " + cat}`, { barmode: "group", height: Math.max(420, 22 * top.length + 120),
xaxis: { ticksuffix: "%", gridcolor: css("--line") }, yaxis: { automargin: true }, margin: { l: 10, r: 40, t: 46, b: 40 } }));
renderTable($("t_tech"), [
{ key: "rank", label: "#", num: true }, { key: "technology", label: "technology" }, { key: "category", label: "category" },
{ key: "s", label: `sites ${month(LATEST)}`, num: true }, { key: "s0", label: month(PREV), num: true }, { key: "d", label: "Δ pp", num: true, html: true },
{ key: "t1", label: "top 1M sites", num: true }, { key: "cs", label: "company sites*", num: true }, { key: "cwv", label: "CWV pass", num: true },
], rows.slice(0, 60).map((r) => ({ rank: r.rank, technology: r.technology, category: r.category, s: p1(r.share), s0: p1(r.share_2025),
d: pp(r.change_pp), t1: p1(r.share_top1m), cs: p1(r.share_company_sites), cwv: p1(r.cwv_pass_rate) })));
}
function rankTab() {
const sel = [...$("rk_tech").selectedOptions].map((o) => o.value).slice(0, 6);
const buckets = [...new Set(D.rank.map((r) => r.rank_bucket))].sort();
const traces = [];
sel.forEach((t, i) => {
for (const [crawl, dash] of [[LATEST, "solid"], [PREV, "dot"]]) {
const m = new Map(D.rank.filter((r) => r.technology === t && r.crawl === crawl).map((r) => [r.rank_bucket, r.share_in_bucket]));
traces.push({ type: "scatter", mode: "lines+markers", name: `${t} (${month(crawl)})`, x: buckets.map(BUCKET), y: buckets.map((b) => (m.get(b) ?? 0) * 100),
line: { color: PALETTE[i % PALETTE.length], dash }, showlegend: true });
}
});
plot($("f_rank"), traces, baseLayout("Share of sites using the technology, by site popularity (solid 2026, dotted 2025)", { height: 420,
yaxis: { ticksuffix: "%", gridcolor: css("--line"), rangemode: "tozero" }, legend: { orientation: "h", y: -0.2, font: { size: 10 } }, margin: { l: 50, r: 10, t: 46, b: 110 } }));
}
function stackChart() {
const traces = [LATEST, PREV].map((c, i) => {
const r = D.stack.filter((x) => x.crawl === c && x.n_tech <= 60).sort((a, b) => a.n_tech - b.n_tech);
const tot = sum(D.stack.filter((x) => x.crawl === c).map((x) => x.pages));
return { type: "scatter", mode: "lines", name: month(c), x: r.map((x) => x.n_tech), y: r.map((x) => x.pages / tot * 100), line: { color: i ? "#94a3b8" : css("--accent"), width: 2 } };
});
plot($("f_stack"), traces, baseLayout("How many technologies a site runs (share of sites)", { height: 340, xaxis: { title: { text: "detected technologies per site" }, gridcolor: css("--line") },
yaxis: { ticksuffix: "%", gridcolor: css("--line") } }));
}
function cwvTab() {
const crawl = $("cwv_crawl").value, min = +$("cwv_min").value, cat = $("cwv_cat").value;
const prev = new Map(D.cwv.filter((r) => r.crawl === PREV).map((r) => [r.technology, r.cwv_pass_rate]));
const rows = D.cwv.filter((r) => r.crawl === crawl && r.cwv_pages >= min && (cat === ALL || r.category === cat)).sort((a, b) => b.cwv_pass_rate - a.cwv_pass_rate);
const top = rows.slice(0, 30).reverse();
plot($("f_cwv"), [
{ type: "bar", orientation: "h", name: "LCP good", y: top.map((r) => r.technology), x: top.map((r) => r.lcp_good_rate * 100), marker: { color: PALETTE[1] }, visible: "legendonly" },
{ type: "bar", orientation: "h", name: "CLS good", y: top.map((r) => r.technology), x: top.map((r) => r.cls_good_rate * 100), marker: { color: PALETTE[2] }, visible: "legendonly" },
{ type: "bar", orientation: "h", name: "INP good", y: top.map((r) => r.technology), x: top.map((r) => r.inp_good_rate * 100), marker: { color: PALETTE[3] }, visible: "legendonly" },
{ type: "bar", orientation: "h", name: "passes all Core Web Vitals", y: top.map((r) => r.technology), x: top.map((r) => r.cwv_pass_rate * 100), marker: { color: css("--accent") },
text: top.map((r) => `${(r.cwv_pass_rate * 100).toFixed(0)}%`), textposition: "outside", cliponaxis: false },
], baseLayout(`Core Web Vitals pass rate by platform, ${month(crawl)} (click legend for LCP / CLS / INP)`, { barmode: "group", height: Math.max(420, 22 * top.length + 140),
xaxis: { ticksuffix: "%", range: [0, 105], gridcolor: css("--line") }, yaxis: { automargin: true }, margin: { l: 10, r: 30, t: 46, b: 40 } }));
renderTable($("t_cwv"), [
{ key: "technology", label: "platform" }, { key: "category", label: "category" }, { key: "n", label: "sites with field data", num: true },
{ key: "pass", label: "pass all", num: true }, { key: "d", label: crawl === LATEST ? "Δ pp vs 2025" : "", num: true, html: true },
{ key: "lcp", label: "LCP good", num: true }, { key: "cls", label: "CLS good", num: true }, { key: "inp", label: "INP good", num: true },
], rows.map((r) => ({ technology: r.technology, category: r.category, n: int(r.cwv_pages), pass: p1(r.cwv_pass_rate),
d: crawl === LATEST && prev.has(r.technology) ? pp((r.cwv_pass_rate - prev.get(r.technology)) * 100) : "",
lcp: p1(r.lcp_good_rate), cls: p1(r.cls_good_rate), inp: p1(r.inp_good_rate) })));
}
function tldTab() {
const kind = $("tld_kind").value, metric = $("tld_metric").value, tech = $("tld_tech").value;
$("tld_tech").disabled = metric !== "tech";
let rows = D.tld.filter((r) => r.crawl === LATEST && r.tld !== "other" && (kind === ALL || r.kind === kind));
const techShare = new Map(D.ttld.filter((r) => r.technology === tech).map((r) => [r.tld, r.share_in_tld]));
const val = (r) => (metric === "tech" ? techShare.get(r.tld) ?? 0 : r[metric]);
rows = rows.map((r) => ({ ...r, v: val(r) })).filter((r) => Number.isFinite(r.v)).sort((a, b) => b.v - a.v);
const top = rows.slice(0, 35).reverse();
const label = { cwv_pass_rate: "Core Web Vitals pass rate", jsonld_rate: "Sites with JSON-LD", leading_platform_share: "Share of the leading platform", share: "Share of all sites", tech: `Sites using ${tech}` }[metric];
plot($("f_tld"), [{ type: "bar", orientation: "h", y: top.map((r) => `.${r.tld}${r.country ? " · " + r.country : ""}`), x: top.map((r) => r.v * 100),
marker: { color: css("--accent") }, text: top.map((r) => `${(r.v * 100).toFixed(1)}%`), textposition: "outside", cliponaxis: false,
hovertext: top.map((r) => `${int(r.pages)} sites · leading platform ${r.leading_platform || "–"}`), hoverinfo: "text+x" }],
baseLayout(`${label} by TLD, ${month(LATEST)}`, { height: Math.max(420, 20 * top.length + 120), xaxis: { ticksuffix: "%", gridcolor: css("--line") }, yaxis: { automargin: true }, margin: { l: 10, r: 40, t: 46, b: 40 } }));
const prev = new Map(D.tld.filter((r) => r.crawl === PREV).map((r) => [r.tld, r]));
renderTable($("t_tld"), [
{ key: "t", label: "TLD" }, { key: "country", label: "country" }, { key: "n", label: "sites", num: true }, { key: "sh", label: "share of web", num: true },
{ key: "cwv", label: "CWV pass", num: true }, { key: "d", label: "Δ pp", num: true, html: true }, { key: "jl", label: "JSON-LD", num: true },
{ key: "lp", label: "leading platform" }, { key: "lps", label: "its share", num: true },
], rows.map((r) => ({ t: "." + r.tld, country: r.country, n: int(r.pages), sh: p1(r.share), cwv: p1(r.cwv_pass_rate),
d: prev.has(r.tld) ? pp((r.cwv_pass_rate - prev.get(r.tld).cwv_pass_rate) * 100) : "–", jl: p1(r.jsonld_rate), lp: r.leading_platform || "–", lps: p1(r.leading_platform_share) })));
}
function sdTab() {
const top = D.sd.slice(0, 30).reverse();
plot($("f_sd"), [
{ type: "bar", orientation: "h", name: month(PREV), y: top.map((r) => r.sd_type), x: top.map((r) => r.share_2025 * 100), marker: { color: "#94a3b8" } },
{ type: "bar", orientation: "h", name: month(LATEST), y: top.map((r) => r.sd_type), x: top.map((r) => r.share * 100), marker: { color: css("--accent") } },
], baseLayout("schema.org types declared on sites (share of sites)", { barmode: "group", height: 760, xaxis: { ticksuffix: "%", gridcolor: css("--line") }, yaxis: { automargin: true }, margin: { l: 10, r: 20, t: 46, b: 40 } }));
renderTable($("t_sd"), [{ key: "i", label: "#", num: true }, { key: "sd_type", label: "@type" }, { key: "s", label: `sites ${month(LATEST)}`, num: true },
{ key: "s0", label: month(PREV), num: true }, { key: "d", label: "Δ pp", num: true, html: true }, { key: "t1", label: "top 1M sites", num: true }],
D.sd.slice(0, 100).map((r, i) => ({ i: i + 1, sd_type: r.sd_type, s: p1(r.share), s0: p1(r.share_2025), d: pp(r.change_pp), t1: p1(r.share_top1m) })));
}
function kpis() {
const agg = (c) => { const r = D.ov.filter((x) => x.crawl === c); return { pages: sum(r.map((x) => x.pages)), cwv: sum(r.map((x) => x.cwv_pages)), good: sum(r.map((x) => x.cwv_good)), jl: sum(r.map((x) => x.jsonld_pages)) }; };
const a = agg(LATEST), b = agg(PREV);
const fastest = D.tech.filter((r) => r.share >= 0.02).sort((x, y) => y.change_pp - x.change_pp)[0];
const wp = D.tech.find((r) => r.technology === "WordPress");
$("kpi").innerHTML = `<span class="big">${int(a.pages)}</span> sites crawled (${month(LATEST)}) &nbsp;·&nbsp; <b>${p1(a.good / a.cwv)}</b> pass Core Web Vitals (${p1(b.good / b.cwv)} in ${month(PREV)})
&nbsp;·&nbsp; <b>${p1(a.jl / a.pages)}</b> use JSON-LD
<div class="sub">${wp ? `WordPress on ${p1(wp.share)} of sites (${pp(wp.change_pp)} pp)` : ""} · biggest riser among technologies on ≥2% of sites: <b>${esc(fastest.technology)}</b> (${pp(fastest.change_pp)} pp) ·
field data (CrUX) for ${p1(a.cwv / a.pages)} of sites</div>`;
}
function method() {
const c = ST.coverage;
$("covline").textContent = `${int(ST.pages[LATEST])} sites (${month(LATEST)}) and ${int(ST.pages[PREV])} (${month(PREV)}); ${c.shards_answering} of ${c.shards_total} database shards answered.`;
const nComp = ST.company_sites_compared;
$("method").innerHTML = `<h2>Methodology and coverage</h2>
<h3>Data</h3>
<ul><li>Source: <b>HTTP Archive / Chrome UX Report (public web crawl data)</b>, mobile crawls of ${month(PREV)} and ${month(LATEST)}. One row = one crawled root page ("site"). HTTP Archive and Chrome UX Report data are CC BY 4.0.</li>
<li>Technologies come from HTTP Archive's open-source detection rules (patterns on page code, headers and scripts). The category is the first category of the rule. ${int(ST.technologies_detected_latest)} technologies were detected in ${month(LATEST)}; this page keeps the 300 most common.</li>
<li><b>Company sites*</b> = share of the ${int(nComp)} company websites in Fliar BI's <a href="https://huggingface.co/datasets/fliarbi/website-technology-stack" target="_blank">website technology stack dataset</a> that run the technology (matched by name; blank when the name differs).</li></ul>
<h3>Core Web Vitals</h3>
<ul><li>Chrome UX Report p75 values for phones: good = LCP ≤ 2.5 s, CLS ≤ 0.1, INP ≤ 200 ms. A site passes when LCP and CLS are good and INP is good or not reported.</li>
<li>Rates use only sites with field data. Popular sites are more likely to have it, so platform comparisons mix speed with audience size.</li></ul>
<h3>Other definitions</h3>
<ul><li>Structured-data types = distinct <code>@type</code> values in JSON-LD (nested types such as ListItem included) or schema.org microdata on the page.</li>
<li>TLD = last label of the domain (co.uk counts as .uk). Country-code TLDs are not the same as the site's country.</li>
<li>Year-on-year changes mix real adoption with detection-rule updates between crawls; a very large jump for one technology can come from a new or changed rule.</li></ul>
<h3>Coverage</h3>
<ul><li>${c.shards_answering} of ${c.shards_total} database shards answered when the tables were built (missing: ${esc(c.missing_shards.join(", ") || "none")}). Domains are sharded by hash, so the gap is an even random sample: shares and rates are unbiased, absolute counts cover about ${Math.round(c.shards_answering / c.shards_total * 100)}% of the crawl.</li>
<li>The crawl covers origins known to the Chrome UX Report (sites with real Chrome traffic), not every registered domain.</li></ul>
<h3>Data and related datasets</h3>
<ul><li>Aggregate tables (CC BY-NC 4.0, upstream CC BY 4.0): <a href="https://huggingface.co/datasets/fliarbi/web-technology-trends-aggregates" target="_blank">fliarbi/web-technology-trends-aggregates</a></li>
<li>Company websites: <a href="https://huggingface.co/datasets/fliarbi/website-technology-stack" target="_blank">fliarbi/website-technology-stack</a> · <a href="https://huggingface.co/datasets/fliarbi/open-company-database" target="_blank">fliarbi/open-company-database</a> · <a href="https://huggingface.co/datasets/fliarbi/google-play-apps-2026" target="_blank">fliarbi/google-play-apps-2026</a></li>
<li>All datasets: <a href="https://fliarbi.com/datasets/" target="_blank">fliarbi.com/datasets</a> · <a href="https://github.com/Busineessfree/open-company-datasets" target="_blank">github.com/Busineessfree/open-company-datasets</a> · contact <a href="mailto:b2b@fliarbi.com">b2b@fliarbi.com</a></li></ul>`;
}
async function main() {
const names = { tech: "technologies_top300", rank: "technology_by_rank", ttld: "technology_by_tld", cwv: "cwv_by_platform", tld: "tlds", sd: "structured_data_types", ov: "overview_by_rank", stack: "stack_size" };
const loaded = await Promise.all(Object.values(names).map(load));
Object.keys(names).forEach((k, i) => (D[k] = loaded[i]));
ST = await (await fetch("data/build_stats.json")).json();
[PREV, LATEST] = ST.crawls;
D.tech.sort((a, b) => a.rank - b.rank);
const cats = [...groupBy(D.tech.flatMap((r) => r.categories.split("; ").filter(Boolean).map((c) => ({ c }))), (x) => x.c).entries()].sort((a, b) => b[1].length - a[1].length);
$("cat").innerHTML = `<option value="${ALL}">All categories (${D.tech.length})</option>` + cats.map(([c, g]) => `<option value="${esc(c)}">${esc(c)} (${g.length})</option>`).join("");
$("rk_tech").innerHTML = D.tech.map((r) => `<option value="${esc(r.technology)}">${esc(r.technology)}</option>`).join("");
for (const t of ["React", "WordPress", "Shopify", "Next.js"]) { const o = [...$("rk_tech").options].find((x) => x.value === t); if (o) o.selected = true; }
$("cwv_crawl").innerHTML = [LATEST, PREV].map((c) => `<option value="${c}">${month(c)}</option>`).join("");
const cwvCats = [...new Set(D.cwv.map((r) => r.category))].sort();
$("cwv_cat").innerHTML = `<option value="${ALL}">All</option>` + cwvCats.map((c) => `<option>${esc(c)}</option>`).join("");
const tt = [...new Set(D.ttld.map((r) => r.technology))];
$("tld_tech").innerHTML = D.tech.filter((r) => tt.includes(r.technology)).map((r) => `<option>${esc(r.technology)}</option>`).join("");
$("tld_tech").value = "WordPress";
D.sd.sort((a, b) => b.pages - a.pages);
kpis(); method();
tabs($("tabbox"));
document.querySelectorAll(".tabs button").forEach((b) => b.addEventListener("click", () => setTimeout(() =>
document.querySelectorAll("[data-pane]:not([hidden]) .js-plotly-plot").forEach((e) => window.Plotly.Plots.resize(e)), 30)));
techTab(); rankTab(); stackChart(); cwvTab(); tldTab(); sdTab();
for (const id of ["cat", "sort"]) $(id).addEventListener("change", techTab);
$("q").addEventListener("input", techTab);
$("rk_tech").addEventListener("change", rankTab);
for (const id of ["cwv_crawl", "cwv_min", "cwv_cat"]) $(id).addEventListener("change", cwvTab);
for (const id of ["tld_kind", "tld_metric", "tld_tech"]) $(id).addEventListener("change", tldTab);
}
main().catch((e) => { $("kpi").innerHTML = `<div class="warn">Could not load the data: ${esc(e.message)}. Please reload.</div>`; console.error(e); });
</script>
</body>
</html>