/** * ============================================================================= * QWHPI โ€” Quebec Weekly Housing Price Index * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : web/lib/exportPng.ts * Purpose : Chart โ†’ PNG export (SVG serialization to canvas, no dependency). * ============================================================================= */ /** Resolve CSS custom properties so the serialized SVG is standalone. */ function inlineColors(svg: SVGSVGElement): SVGSVGElement { const clone = svg.cloneNode(true) as SVGSVGElement; const styles = getComputedStyle(document.documentElement); const resolve = (v: string) => v.replace(/var\((--[a-z0-9-]+)\)/gi, (_, name) => styles.getPropertyValue(name).trim() || "#888"); const walk = (el: Element) => { for (const attr of ["fill", "stroke", "color", "style"]) { const v = el.getAttribute(attr); if (v && v.includes("var(")) el.setAttribute(attr, resolve(v)); } for (const child of Array.from(el.children)) walk(child); }; walk(clone); return clone; } export async function exportChartPng( container: HTMLElement, filename: string, credit = "QHPI โ€” indexqc.house ยท Simon-Pierre Boucher", ): Promise { const svg = container.querySelector("svg.recharts-surface, svg"); if (!svg) return; const rect = svg.getBoundingClientRect(); const scale = 2; const w = Math.round(rect.width); const h = Math.round(rect.height); const clone = inlineColors(svg); clone.setAttribute("width", String(w)); clone.setAttribute("height", String(h)); const xml = new XMLSerializer().serializeToString(clone); const url = URL.createObjectURL( new Blob([xml], { type: "image/svg+xml;charset=utf-8" })); const img = new Image(); await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = url; }); const surface = getComputedStyle(document.documentElement) .getPropertyValue("--surface-1").trim() || "#fff"; const ink2 = getComputedStyle(document.documentElement) .getPropertyValue("--text-secondary").trim() || "#555"; const canvas = document.createElement("canvas"); canvas.width = w * scale; canvas.height = (h + 26) * scale; const ctx = canvas.getContext("2d")!; ctx.scale(scale, scale); ctx.fillStyle = surface; ctx.fillRect(0, 0, w, h + 26); ctx.drawImage(img, 0, 0, w, h); ctx.fillStyle = ink2; ctx.font = "10px system-ui, sans-serif"; ctx.fillText(credit, 8, h + 16); URL.revokeObjectURL(url); const a = document.createElement("a"); a.download = filename; a.href = canvas.toDataURL("image/png"); a.click(); }