168 lines
5.2 KiB
JavaScript
168 lines
5.2 KiB
JavaScript
// 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.canvas;
|
|
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.action;
|
|
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;
|