单目3D初始代码

This commit is contained in:
zhao.zhu
2026-06-24 09:35:46 +08:00
commit 04a5895b6b
1153 changed files with 340700 additions and 0 deletions

View File

@@ -0,0 +1,236 @@
// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
// Auto-load chart-widget.js if not already loaded
const loadChartWidget = () =>
new Promise((resolve) => {
if (window.ChartWidget) {
return resolve();
}
const s = document.createElement("script");
const base =
(document.currentScript || document.querySelector('script[src*="benchmark.js"]'))?.src.replace(/[^/]*$/, "") ||
"./";
s.src = `${base}chart-widget.js`;
s.onload = s.onerror = resolve;
document.head.appendChild(s);
});
// YOLO models chart ---------------------------------------------------------------------------------------------------
const data = {
YOLO26: {
n: { speed: 1.7, mAP: 40.9 },
s: { speed: 2.5, mAP: 48.6 },
m: { speed: 4.7, mAP: 53.1 },
l: { speed: 6.2, mAP: 55.0 },
x: { speed: 11.8, mAP: 57.5 },
},
YOLO11: {
n: { speed: 1.5, mAP: 39.5 },
s: { speed: 2.5, mAP: 47.0 },
m: { speed: 4.7, mAP: 51.5 },
l: { speed: 6.2, mAP: 53.4 },
x: { speed: 11.3, mAP: 54.7 },
},
YOLOv10: {
n: { speed: 1.56, mAP: 39.5 },
s: { speed: 2.66, mAP: 46.7 },
m: { speed: 5.48, mAP: 51.3 },
b: { speed: 6.54, mAP: 52.7 },
l: { speed: 8.33, mAP: 53.3 },
x: { speed: 12.2, mAP: 54.4 },
},
YOLOv9: {
t: { speed: 2.3, mAP: 38.3 },
s: { speed: 3.54, mAP: 46.8 },
m: { speed: 6.43, mAP: 51.4 },
c: { speed: 7.16, mAP: 53.0 },
e: { speed: 16.77, mAP: 55.6 },
},
YOLOv8: {
n: { speed: 1.47, mAP: 37.3 },
s: { speed: 2.66, mAP: 44.9 },
m: { speed: 5.86, mAP: 50.2 },
l: { speed: 9.06, mAP: 52.9 },
x: { speed: 14.37, mAP: 53.9 },
},
YOLOv7: { l: { speed: 6.84, mAP: 51.4 }, x: { speed: 11.57, mAP: 53.1 } },
"YOLOv6-3.0": {
n: { speed: 1.17, mAP: 37.5 },
s: { speed: 2.66, mAP: 45.0 },
m: { speed: 5.28, mAP: 50.0 },
l: { speed: 8.95, mAP: 52.8 },
},
YOLOv5: {
n: { speed: 1.12, mAP: 28.0 },
s: { speed: 1.92, mAP: 37.4 },
m: { speed: 4.03, mAP: 45.4 },
l: { speed: 6.61, mAP: 49.0 },
x: { speed: 11.89, mAP: 50.7 },
},
"PP-YOLOE+": {
t: { speed: 2.84, mAP: 39.9 },
s: { speed: 2.62, mAP: 43.7 },
m: { speed: 5.56, mAP: 49.8 },
l: { speed: 8.36, mAP: 52.9 },
x: { speed: 14.3, mAP: 54.7 },
},
"DAMO-YOLO": {
t: { speed: 2.32, mAP: 42.0 },
s: { speed: 3.45, mAP: 46.0 },
m: { speed: 5.09, mAP: 49.2 },
l: { speed: 7.18, mAP: 50.8 },
},
YOLOX: {
s: { speed: 2.56, mAP: 40.5 },
m: { speed: 5.43, mAP: 46.9 },
l: { speed: 9.04, mAP: 49.7 },
x: { speed: 16.1, mAP: 51.1 },
},
RTDETRv2: {
s: { speed: 5.03, mAP: 48.1 },
m: { speed: 7.51, mAP: 51.9 },
l: { speed: 9.76, mAP: 53.4 },
x: { speed: 15.03, mAP: 54.3 },
},
EfficientDet: {
d0: { speed: 3.92, mAP: 34.6 },
d1: { speed: 7.31, mAP: 40.5 },
d2: { speed: 10.92, mAP: 43.0 },
d3: { speed: 19.59, mAP: 47.5 },
// d4: { speed: 33.55, mAP: 49.4 },
// d5: { speed: 67.86, mAP: 50.7 },
// d6: { speed: 89.29, mAP: 51.7 },
// d7: { speed: 128.07, mAP: 53.7 },
// d8: { speed: 157.57, mAP: 55.1 }
},
};
// Color overrides for specific models
const colorOverrides = {
YOLO26: "#0b23a9",
YOLO11: "#1e90ff",
YOLOv10: "#ff7f0e",
YOLOv9: "#2ca02c",
YOLOv8: "#d62728",
YOLOv7: "#9467bd",
"YOLOv6-3.0": "#8c564b",
YOLOv5: "#e377c2",
"PP-YOLOE+": "#7f7f7f",
"DAMO-YOLO": "#bcbd22",
YOLOX: "#17becf",
RTDETRv2: "#eccd22",
EfficientDet: "#000000",
};
let chart = null;
let chartWidget = null;
const lighten = (hex, amt = 0.6) => {
const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16));
return `#${[r, g, b]
.map((c) =>
Math.min(255, Math.round(c + (255 - c) * amt))
.toString(16)
.padStart(2, "0"),
)
.join("")}`;
};
const createDataset = (algo, i, activeModels) => {
const baseColor = colorOverrides[algo] || `hsl(${(i * 137) % 360}, 70%, 50%)`;
const isFirst = i === 0;
return {
label: algo,
data: Object.entries(data[algo]).map(([ver, pt]) => ({
x: pt.speed,
y: pt.mAP,
version: ver.toUpperCase(),
})),
fill: false,
borderColor: isFirst ? baseColor : lighten(baseColor),
tension: 0.2,
pointRadius: isFirst ? 7 : 4,
pointHoverRadius: isFirst ? 9 : 6,
pointBackgroundColor: isFirst ? baseColor : lighten(baseColor),
pointBorderColor: "#ffffff",
borderWidth: isFirst ? 3 : 1.5,
hidden: activeModels.length > 0 && !activeModels.includes(algo),
};
};
const chartConfig = {
type: "line",
data: { datasets: Object.keys(data).map(createDataset) },
options: {
plugins: {
legend: {
display: true,
position: "right",
align: "start",
labels: { color: "#808080" },
},
tooltip: {
callbacks: {
label: ({ dataset, dataIndex }) => {
const pt = dataset.data[dataIndex];
return `${dataset.label}${pt.version.toLowerCase()}: Speed = ${pt.x}ms/img, mAP50-95 = ${pt.y}`;
},
},
mode: "nearest",
intersect: false,
},
},
interaction: { mode: "nearest", axis: "x", intersect: false },
scales: {
x: {
type: "linear",
position: "bottom",
title: {
display: true,
text: "Latency T4 TensorRT10 FP16 (ms/img)",
color: "#808080",
},
grid: { color: "#e0e0e0" },
ticks: { color: "#808080" },
min: 0,
max: 18,
},
y: {
title: { display: true, text: "COCO mAP 50-95", color: "#808080" },
grid: { color: "#e0e0e0" },
ticks: { color: "#808080" },
min: 36,
max: 58,
},
},
},
};
const updateChart = async (activeModels = []) => {
chart?.destroy();
chartWidget?.destroy();
chartConfig.data.datasets = Object.keys(data).map((algo, i) => createDataset(algo, i, activeModels));
chart = new Chart(document.getElementById("modelComparisonChart").getContext("2d"), chartConfig);
// Load widget and add to chart
await loadChartWidget();
if (window.ChartWidget) {
chartWidget = new ChartWidget(chart, { position: "top-right" });
}
};
// Get active models from page config or use default
// e.g. <canvas id="modelComparisonChart" width="1024" height="400" active-models='["YOLOv8", "YOLO11"]'></canvas>
const initChart = () => {
const activeModels = JSON.parse(
document.getElementById("modelComparisonChart").getAttribute("active-models") || "[]",
);
updateChart(activeModels);
};
document$.subscribe(() => {
const init = () => (typeof Chart !== "undefined" ? initChart() : setTimeout(init, 50));
init();
});

View File

@@ -0,0 +1,171 @@
// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
//Reusable Chart.js toolbar widget with download functionality
class ChartWidget {
constructor(chart, options = {}) {
this.chart = chart;
this.options = { position: "top-right", ...options };
this.init();
}
init() {
const { canvas } = this.chart;
const container = canvas.parentElement;
container.style.position = "relative";
this.toolbar = document.createElement("div");
this.toolbar.innerHTML = this.getHTML();
container.appendChild(this.toolbar);
// Wait for chart to settle before positioning
requestAnimationFrame(() => {
this.toolbar.style.cssText = this.getCSS(canvas);
this.attachEvents();
this.setupHover(canvas);
});
}
getHTML() {
return `
<button data-action="png" data-tip="Download chart as PNG image">📷</button>
<button data-action="csv" data-tip="Download chart data as CSV file">📊</button>
<button data-action="ultralytics" data-tip="Made with Chart.js and Ultralytics">
<img src="https://github.com/ultralytics/assets/raw/main/logo/Ultralytics-logomark-color.png" width="18" height="18">
</button>
<div class="tip" style="position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:CanvasText;color:Canvas;padding:4px 8px;border-radius:3px;font-size:11px;white-space:nowrap;display:none;margin-bottom:5px;z-index:1001;pointer-events:none;"></div>
`;
}
getCSS(canvas) {
const rect = canvas.getBoundingClientRect();
const containerRect = canvas.parentElement.getBoundingClientRect();
const top = rect.top - containerRect.top - 50;
const left = rect.right - containerRect.left - 110;
return `
position: absolute; top: ${top}px; left: ${left}px;
background: Canvas; border: 1px solid rgba(128,128,128,0.3);
border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
backdrop-filter: blur(8px); display: flex; gap: 2px; padding: 4px;
opacity: 0; pointer-events: none; z-index: 1000;
transition: all 0.25s cubic-bezier(0.4,0,0.2,1);
transform: translateY(-5px) scale(0.95);
`;
}
attachEvents() {
const tip = this.toolbar.querySelector(".tip");
this.toolbar.addEventListener("click", (e) => {
const { action } = e.target.closest("button").dataset;
if (action === "png") {
this.downloadPNG();
}
if (action === "csv") {
this.downloadCSV();
}
if (action === "ultralytics") {
window.open("https://ultralytics.com", "_blank");
}
});
this.toolbar.querySelectorAll("button").forEach((btn) => {
Object.assign(btn.style, {
border: "none",
background: "none",
padding: "8px",
cursor: "pointer",
borderRadius: "4px",
fontSize: "18px",
minWidth: "32px",
minHeight: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.15s ease",
position: "relative",
});
btn.onmouseenter = () => {
btn.style.background = "ButtonFace";
btn.style.transform = "scale(1.20)";
tip.textContent = btn.dataset.tip;
tip.style.display = "block";
};
btn.onmouseleave = () => {
btn.style.background = "none";
btn.style.transform = "scale(1)";
tip.style.display = "none";
};
});
}
setupHover(canvas) {
let timeout;
const show = () => {
clearTimeout(timeout);
Object.assign(this.toolbar.style, {
opacity: "1",
pointerEvents: "auto",
transform: "translateY(0) scale(1)",
});
};
const hide = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
Object.assign(this.toolbar.style, {
opacity: "0",
pointerEvents: "none",
transform: "translateY(-5px) scale(0.95)",
});
}, 1000);
};
canvas.addEventListener("mouseenter", show);
canvas.addEventListener("mouseleave", hide);
this.toolbar.addEventListener("mouseenter", show);
this.toolbar.addEventListener("mouseleave", hide);
}
downloadPNG() {
const a = document.createElement("a");
a.download = `chart-${Date.now()}.png`;
a.href = this.chart.toBase64Image("image/png", 1);
a.click();
}
downloadCSV() {
const xTitle = this.chart.options?.scales?.x?.title?.text || "x";
const yTitle = this.chart.options?.scales?.y?.title?.text || "y";
const data = [];
this.chart.data.datasets.forEach((dataset, i) => {
if (this.chart.getDatasetMeta(i).hidden) {
return;
} // Skip unselected models
dataset.data.forEach((point) => {
data.push({
model: dataset.label,
version: point.version || "",
[xTitle]: point.x,
[yTitle]: point.y,
});
});
});
const headers = Object.keys(data[0]);
const csv = [headers.join(","), ...data.map((row) => headers.map((h) => `"${row[h] || ""}"`).join(","))].join("\n");
const a = document.createElement("a");
a.download = `chart-data-${Date.now()}.csv`;
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.click();
}
destroy() {
this.toolbar?.remove();
}
}
window.ChartWidget = ChartWidget;

View File

@@ -0,0 +1,191 @@
// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
// Block sitemap.xml fetches triggered by Weglot's hreflang tags detected by MkDocs Material
(() => {
const EMPTY_SITEMAP = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>`;
const originalFetch = window.fetch;
window.fetch = function (url, options) {
if (typeof url === "string" && url.includes("/sitemap.xml")) {
return Promise.resolve(
new Response(EMPTY_SITEMAP, { status: 200, headers: { "Content-Type": "application/xml" } }),
);
}
return originalFetch.apply(this, arguments);
};
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
if (typeof url === "string" && url.includes("/sitemap.xml")) {
this._blockRequest = true;
}
return originalXHROpen.apply(this, arguments);
};
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
if (this._blockRequest) {
Object.defineProperty(this, "status", { value: 200 });
Object.defineProperty(this, "responseText", { value: EMPTY_SITEMAP });
Object.defineProperty(this, "response", { value: EMPTY_SITEMAP });
Object.defineProperty(this, "responseXML", {
value: new DOMParser().parseFromString(EMPTY_SITEMAP, "application/xml"),
});
this.dispatchEvent(new Event("load"));
return;
}
return originalXHRSend.apply(this, arguments);
};
})();
// Apply theme colors based on dark/light mode
const applyTheme = (isDark) => {
document.body.setAttribute("data-md-color-scheme", isDark ? "slate" : "default");
document.body.setAttribute("data-md-color-primary", isDark ? "black" : "indigo");
};
// Sync widget theme with Material theme
const syncWidgetTheme = () => {
const isDark = document.body.getAttribute("data-md-color-scheme") === "slate";
document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light");
};
// Check and apply appropriate theme based on system/user preference
const checkTheme = () => {
const palette = JSON.parse(localStorage.getItem(".__palette") || "{}");
if (palette.index === 0) {
applyTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
syncWidgetTheme();
}
};
// Initialize theme handling on page load
document.addEventListener("DOMContentLoaded", () => {
checkTheme();
syncWidgetTheme();
// Watch for system theme changes
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", checkTheme);
// Watch for theme toggle changes
document.getElementById("__palette_1")?.addEventListener("change", (e) => {
if (e.target.checked) setTimeout(checkTheme);
});
// Watch for Material theme changes and sync to widget
new MutationObserver(syncWidgetTheme).observe(document.body, {
attributes: true,
attributeFilter: ["data-md-color-scheme"],
});
});
// Ultralytics Chat Widget ---------------------------------------------------------------------------------------------
document.addEventListener("DOMContentLoaded", () => {
const ultralyticsChat = new UltralyticsChat({
welcome: {
title: "Hello 👋",
message: "Ask about YOLO, tutorials, training, export, deployment, or troubleshooting.",
chatExamples: [
"What's new in SAM 3?",
"How can I get started with YOLO26?",
"How does Enterprise Licensing work?",
],
searchExamples: [
"YOLO26 quickstart",
"custom dataset training",
"model export formats",
"object detection tutorial",
"hyperparameter tuning",
],
},
});
const headerElement = document.querySelector(".md-header__inner");
const searchContainer = headerElement?.querySelector(".md-header__source");
if (headerElement && searchContainer) {
const searchBar = document.createElement("div");
searchBar.className = "ult-header-search";
const hotkey = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? "⌘K" : "Ctrl+K";
searchBar.innerHTML = `
<button class="ult-search-button" title="Search documentation (${hotkey})" aria-label="Search documentation">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<span>Search</span>
<span class="ult-search-hotkey" aria-hidden="true">${hotkey}</span>
</button>
`;
headerElement.insertBefore(searchBar, searchContainer);
const defaultSearchToggle = headerElement.querySelector('label[for="__search"]');
const defaultSearchInput = document.getElementById("__search");
const defaultSearchDialog = document.querySelector(".md-search");
if (defaultSearchToggle) {
defaultSearchToggle.setAttribute("aria-hidden", "true");
defaultSearchToggle.style.display = "none";
}
if (defaultSearchInput) {
defaultSearchInput.setAttribute("tabindex", "-1");
defaultSearchInput.setAttribute("aria-hidden", "true");
}
if (defaultSearchDialog) defaultSearchDialog.style.display = "none";
searchBar.querySelector(".ult-search-button").addEventListener("click", () => {
ultralyticsChat?.toggle(true, "search");
});
}
});
// Fix language switcher links to preserve current page path, query string, and hash
(() => {
function fixLanguageLinks() {
const path = location.pathname;
const links = document.querySelectorAll(".md-select__link[hreflang]");
if (!links.length) return;
// Derive language codes from the actual links (config-driven)
const langCodes = Array.from(links)
.map((link) => link.getAttribute("hreflang"))
.filter(Boolean);
const defaultLang =
Array.from(links)
.find((link) => link.getAttribute("href") === "/")
?.getAttribute("hreflang") || "en";
// Extract base path (without leading slash and language prefix)
let basePath = path.startsWith("/") ? path.slice(1) : path;
for (const code of langCodes) {
if (code === defaultLang) continue;
const prefix = `${code}/`;
if (basePath === code || basePath === prefix) {
basePath = "";
break;
}
if (basePath.startsWith(prefix)) {
basePath = basePath.slice(prefix.length);
break;
}
}
// Preserve query string and hash
const suffix = location.search + location.hash;
// Update all language links
links.forEach((link) => {
const lang = link.getAttribute("hreflang");
link.href =
lang === defaultLang
? `${location.origin}/${basePath}${suffix}`
: `${location.origin}/${lang}/${basePath}${suffix}`;
});
}
// Run on load and navigation
fixLanguageLinks();
if (typeof document$ !== "undefined") {
document$.subscribe(() => setTimeout(fixLanguageLinks, 50));
}
})();

View File

@@ -0,0 +1,84 @@
// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
// Giscus functionality
function loadGiscus() {
const giscusContainer = document.getElementById("giscus-container");
if (!giscusContainer || giscusContainer.querySelector("script")) {
return;
}
const script = document.createElement("script");
script.src = "https://giscus.app/client.js";
script.setAttribute("data-repo", "ultralytics/ultralytics");
script.setAttribute("data-repo-id", "R_kgDOH-jzvQ");
script.setAttribute("data-category", "Docs");
script.setAttribute("data-category-id", "DIC_kwDOH-jzvc4CWLkL");
script.setAttribute("data-mapping", "pathname");
script.setAttribute("data-strict", "1");
script.setAttribute("data-reactions-enabled", "1");
script.setAttribute("data-emit-metadata", "0");
script.setAttribute("data-input-position", "top");
script.setAttribute("data-theme", "preferred_color_scheme");
script.setAttribute("data-lang", "en");
script.setAttribute("data-loading", "lazy");
script.setAttribute("crossorigin", "anonymous");
script.setAttribute("async", "");
giscusContainer.appendChild(script);
// Synchronize Giscus theme with palette
const palette = __md_get("__palette");
if (palette && typeof palette.color === "object") {
const theme = palette.color.scheme === "slate" ? "dark" : "light";
script.setAttribute("data-theme", theme);
}
// Register event handlers for theme changes
const ref = document.querySelector("[data-md-component=palette]");
if (ref) {
ref.addEventListener("change", () => {
const palette = __md_get("__palette");
if (palette && typeof palette.color === "object") {
const theme = palette.color.scheme === "slate" ? "dark" : "light";
// Instruct Giscus to change theme
const frame = document.querySelector(".giscus-frame");
if (frame) {
frame.contentWindow.postMessage({ giscus: { setConfig: { theme } } }, "https://giscus.app");
}
}
});
}
}
// Use Intersection Observer to load Giscus when the container is visible
function setupGiscusLoader() {
const giscusContainer = document.getElementById("giscus-container");
if (giscusContainer) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadGiscus();
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 },
); // Trigger when 10% of the element is visible
observer.observe(giscusContainer);
}
}
// Hook into MkDocs' navigation system
if (typeof document$ !== "undefined") {
document$.subscribe(() => {
// This function is called on every page load/change
setupGiscusLoader();
});
} else {
console.warn("MkDocs document$ not found. Falling back to DOMContentLoaded.");
document.addEventListener("DOMContentLoaded", setupGiscusLoader);
}

View File

@@ -0,0 +1,76 @@
// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
// tablesort.filesize.min.js
!(() => {
const filesizeRegex = /^(\d+(\.\d+)?) ?((K|M|G|T|P|E|Z|Y|B$)i?B?)$/i;
function r(t) {
t = t.match(filesizeRegex);
if (!t) {
return 0;
}
const value = Number.parseFloat(t[1].replace(/[^\-?0-9.]/g, ""));
const unit = t[3].toLowerCase();
const base = unit[1] === "i" ? 1024 : 1e3;
const powers = { k: 2, m: 3, g: 4, t: 5, p: 6, e: 7, z: 8, y: 9 };
return value * (powers[unit[0]] ? base ** powers[unit[0]] : base);
}
Tablesort.extend(
"filesize",
(t) => filesizeRegex.test(t),
(t, e) => {
t = r(t);
e = r(e);
return (Number.isNaN(e) ? 0 : e) - (Number.isNaN(t) ? 0 : t);
},
);
})();
// tablesort.dotsep.min.js
Tablesort.extend(
"dotsep",
(t) => /^(\d+\.)+\d+$/.test(t),
(t, r) => {
t = t.split(".");
r = r.split(".");
for (let i = 0, s = t.length; i < s; i++) {
const e = Number.parseInt(t[i], 10);
const n = Number.parseInt(r[i], 10);
if (e !== n) {
return n < e ? -1 : 1;
}
}
return 0;
},
);
// tablesort.number.min.js
(() => {
const cleanNumber = (i) =>
i
.split("±")[0]
.trim()
.replace(/[^\-?0-9.]/g, "");
const compareNumber = (a, b) => (Number.parseFloat(a) || 0) - (Number.parseFloat(b) || 0);
Tablesort.extend(
"number",
(item) =>
item.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,.]\d{0,2})/) || // Prefixed currency
item.match(/^[-+]?\d+\s*([,.]\d{0,2})?[£\x24Û¢´€]/) || // Suffixed currency
item.match(/^[-+]?(\d)*-?([,.]){0,1}-?(\d)+([E,e][-+][\d]+)?%?$/), // Number
(a, b) => compareNumber(cleanNumber(b), cleanNumber(a)),
);
})();
// subscribe
document$.subscribe(() => {
document.querySelectorAll("article table:not([class])").forEach((table) => {
new Tablesort(table);
});
});