Files
edr-platform/apps/edr-freight-web/backoffice/src/shared/utils/chartExportUtils.ts
natib21 e6e44e773b fix ui
2026-07-10 11:25:59 +00:00

173 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Shared utilities for exporting dashboard/report charts via html2canvas.
*
* Problem: html2canvas serializes SVG elements to a data URL before drawing
* them on canvas. Inside that data URL the parent document's CSS is not
* available, so `fill="var(--primary)"` renders as nothing / black.
* Additionally, html2canvas's internal CSS parser cannot handle oklch() color
* values used by Tailwind v4.
*
* Solution:
* 1. Patch oklch() in <style> tags and inject :root overrides so html2canvas's
* CSS parser sees rgb() values everywhere.
* 2. Resolve CSS variable references in SVG presentation attributes
* (fill, stroke, stop-color) to their computed rgb() values before capture,
* then restore them afterward.
*/
// ── Oklch → RGB conversion ────────────────────────────────────────────────────
export function oklchToRgb(l: number, c: number, h: number): [number, number, number] {
const hr = (h * Math.PI) / 180;
const a = c * Math.cos(hr);
const b = c * Math.sin(hr);
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
const lc = l_ ** 3, mc = m_ ** 3, sc = s_ ** 3;
const gam = (x: number) =>
x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055;
const clamp = (x: number) => Math.max(0, Math.min(255, Math.round(x * 255)));
return [
clamp(gam(+4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc)),
clamp(gam(-1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc)),
clamp(gam(-0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc)),
];
}
export const OKLCH_RE = /oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)[^)]*\)/g;
export function oklchMatchToRgb(_: string, l: string, c: string, h: string): string {
const lv = l.endsWith("%") ? parseFloat(l) / 100 : parseFloat(l);
const [r, g, b] = oklchToRgb(lv, parseFloat(c), parseFloat(h));
return `rgb(${r},${g},${b})`;
}
// ── CSS oklch patching ────────────────────────────────────────────────────────
function collectOklchOverrides(
rules: CSSRuleList,
overrides: Map<string, string>,
): void {
for (const rule of Array.from(rules)) {
if (rule instanceof CSSStyleRule) {
for (let i = 0; i < rule.style.length; i++) {
const prop = rule.style[i];
const val = rule.style.getPropertyValue(prop).trim();
if (!val.includes("oklch") || overrides.has(prop)) continue;
const m = val.match(/oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)/);
if (m) {
const lv = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
const [r, g, b] = oklchToRgb(lv, parseFloat(m[2]), parseFloat(m[3]));
overrides.set(prop, `rgb(${r},${g},${b})`);
}
}
}
if ("cssRules" in rule && (rule as CSSGroupingRule).cssRules) {
collectOklchOverrides((rule as CSSGroupingRule).cssRules, overrides);
}
}
}
/**
* Patches oklch() values in inline <style> tags and injects a :root override
* block for external stylesheets so html2canvas's CSS parser sees only rgb().
* Returns a `restore()` function that undoes all patches.
*/
export function patchDocumentOklch(): { restore: () => void } {
const patched = new Map<HTMLStyleElement, string>();
for (const el of Array.from(document.querySelectorAll("style"))) {
const s = el as HTMLStyleElement;
if (s.textContent?.includes("oklch")) {
patched.set(s, s.textContent);
s.textContent = s.textContent.replace(OKLCH_RE, oklchMatchToRgb);
}
}
const overrideVars = new Map<string, string>();
for (const sheet of Array.from(document.styleSheets)) {
try { collectOklchOverrides(sheet.cssRules, overrideVars); } catch {}
}
let varOverride: HTMLStyleElement | null = null;
if (overrideVars.size > 0) {
varOverride = document.createElement("style");
varOverride.textContent = `:root { ${
Array.from(overrideVars.entries())
.map(([p, v]) => `${p}: ${v}`)
.join("; ")
} }`;
document.head.appendChild(varOverride);
}
return {
restore() {
for (const [s, orig] of patched.entries()) s.textContent = orig;
varOverride?.remove();
},
};
}
// ── SVG presentation attribute inlining ──────────────────────────────────────
const SVG_PRES_ATTRS = ["fill", "stroke", "stop-color"] as const;
/**
* Walks every SVG descendant of `root` and replaces any presentation attribute
* that contains a CSS variable (`var(…)`) with its computed rgb() value.
*
* This must be called AFTER patchDocumentOklch() so that getComputedStyle
* already returns rgb values rather than oklch.
*
* Returns a `restore()` function that reverts all attribute changes.
* For off-screen clones that are discarded after capture, calling restore()
* is optional.
*/
export function inlineSvgPresentationAttrs(
root: Element,
): { restore: () => void } {
const patches: Array<{ el: SVGElement; attr: string; orig: string }> = [];
for (const node of Array.from(root.querySelectorAll("*"))) {
if (!(node instanceof SVGElement)) continue;
const cs = window.getComputedStyle(node);
for (const attr of SVG_PRES_ATTRS) {
const val = node.getAttribute(attr);
if (!val || !val.includes("var(")) continue;
const resolved = cs.getPropertyValue(attr).trim().replace(OKLCH_RE, oklchMatchToRgb);
if (!resolved || resolved === val) continue;
patches.push({ el: node, attr, orig: val });
node.setAttribute(attr, resolved);
}
}
return {
restore() {
for (const { el, attr, orig } of patches) el.setAttribute(attr, orig);
},
};
}
/**
* Convenience wrapper that runs the full pre-capture patch sequence on a DOM
* element:
* 1. patchDocumentOklch fixes oklch in <style>/<link> CSS
* 2. inlineSvgPresentationAttrs resolves var() in SVG fill/stroke attrs
*
* Returns a single `restore()` that undoes both steps in reverse order.
*
* Usage:
* const patch = prepareForHtml2Canvas(element);
* try { await html2canvas(element, …); } finally { patch.restore(); }
*/
export function prepareForHtml2Canvas(root: Element): { restore: () => void } {
const oklch = patchDocumentOklch();
const svg = inlineSvgPresentationAttrs(root);
return {
restore() {
svg.restore();
oklch.restore();
},
};
}