1+ import { PLOT_FONT_CONFIG } from "@/hooks/use-plot-theme" ;
2+
13export function getPlotSVG ( container : HTMLElement ) : SVGElement {
24 // Get the main plot SVG that contains everything
35 const svg = container . querySelector (
@@ -8,21 +10,152 @@ export function getPlotSVG(container: HTMLElement): SVGElement {
810 throw new Error ( "Plot SVG not found" ) ;
911 }
1012
11- // Clone the SVG to avoid modifying the original
12- const clonedSvg = svg . cloneNode ( true ) as SVGElement ;
13-
1413 // Get the original dimensions
1514 const box = svg . getBoundingClientRect ( ) ;
1615
16+ // Clone the SVG to avoid modifying the original
17+ const clonedSvg = svg . cloneNode ( true ) as SVGElement ;
18+
1719 // Set the dimensions explicitly
1820 clonedSvg . setAttribute ( "width" , String ( box . width ) ) ;
1921 clonedSvg . setAttribute ( "height" , String ( box . height ) ) ;
2022 clonedSvg . setAttribute ( "viewBox" , `0 0 ${ box . width } ${ box . height } ` ) ;
2123
24+ // Apply explicit font sizes to ensure they're preserved
25+ applyExplicitFontSizes ( clonedSvg ) ;
26+
27+ // Also ensure all text has proper font family and other properties
28+ ensureTextProperties ( clonedSvg ) ;
29+
2230 return clonedSvg ;
2331}
2432
33+ function applyExplicitFontSizes ( svg : SVGElement ) : void {
34+ // Find all text elements and apply explicit font sizes using inline styles
35+ const textElements = svg . querySelectorAll ( "text, tspan" ) ;
36+
37+ textElements . forEach ( ( element ) => {
38+ const textElement = element as SVGTextElement ;
39+
40+ // Get current style attribute or create empty one
41+ let currentStyle = textElement . getAttribute ( "style" ) || "" ;
42+
43+ // Use data attributes for robust identification
44+ const plotElement = textElement . getAttribute ( "data-plot-element" ) ;
45+
46+ let targetFontSize = `${ PLOT_FONT_CONFIG . axisTick } px` ; // Default
47+
48+ // Identify element type using data attributes and class names
49+ if ( plotElement === "axis-label" ) {
50+ targetFontSize = `${ PLOT_FONT_CONFIG . axisLabel } px` ;
51+ } else if ( plotElement === "axis-tick" ) {
52+ targetFontSize = `${ PLOT_FONT_CONFIG . axisTick } px` ;
53+ } else if ( plotElement === "legend-title" ) {
54+ targetFontSize = `${ PLOT_FONT_CONFIG . legendTitle } px` ;
55+ } else if ( plotElement === "legend-item" ) {
56+ targetFontSize = `${ PLOT_FONT_CONFIG . legendItem } px` ;
57+ } else if ( plotElement === "text-annotation" ) {
58+ targetFontSize = `${ PLOT_FONT_CONFIG . annotation } px` ;
59+ } else {
60+ // Fallback to class name identification
61+ const hasAxisLabelClass =
62+ textElement . classList . contains ( "plot-axis-label" ) ;
63+ const hasAxisTickClass = textElement . classList . contains ( "plot-axis-tick" ) ;
64+
65+ if ( hasAxisLabelClass ) {
66+ targetFontSize = `${ PLOT_FONT_CONFIG . axisLabel } px` ;
67+ } else if ( hasAxisTickClass ) {
68+ targetFontSize = `${ PLOT_FONT_CONFIG . axisTick } px` ;
69+ } else {
70+ // Default fallback - ensure minimum font size
71+ const hasFontSize = textElement . getAttribute ( "fontSize" ) ;
72+ if ( hasFontSize ) {
73+ const currentSize = parseInt ( hasFontSize ) ;
74+ const minSize = PLOT_FONT_CONFIG . axisTick ;
75+ targetFontSize =
76+ currentSize < minSize ? `${ minSize } px` : `${ currentSize } px` ;
77+ }
78+ }
79+ }
80+
81+ // Remove any existing font-size from style attribute
82+ currentStyle = currentStyle . replace ( / f o n t - s i z e \s * : \s * [ ^ ; ] + ; ? / g, "" ) ;
83+
84+ // Add the new font-size to the style attribute
85+ if ( currentStyle && ! currentStyle . endsWith ( ";" ) ) {
86+ currentStyle += ";" ;
87+ }
88+ currentStyle += `font-size: ${ targetFontSize } ;` ;
89+
90+ // Set the updated style attribute
91+ textElement . setAttribute ( "style" , currentStyle ) ;
92+
93+ // Also set fontSize attribute as backup
94+ textElement . setAttribute ( "fontSize" , targetFontSize . replace ( "px" , "" ) ) ;
95+ } ) ;
96+
97+ // Find axis labels using data attributes and class names
98+ const axisLabels = svg . querySelectorAll (
99+ // eslint-disable-next-line quotes
100+ '[data-plot-element="axis-label"], .plot-axis-label' ,
101+ ) ;
102+ axisLabels . forEach ( ( element ) => {
103+ const textElement = element as SVGTextElement ;
104+ if ( textElement . tagName === "text" || textElement . tagName === "tspan" ) {
105+ let currentStyle = textElement . getAttribute ( "style" ) || "" ;
106+ currentStyle = currentStyle . replace ( / f o n t - s i z e \s * : \s * [ ^ ; ] + ; ? / g, "" ) ;
107+ if ( currentStyle && ! currentStyle . endsWith ( ";" ) ) {
108+ currentStyle += ";" ;
109+ }
110+ currentStyle += `font-size: ${ PLOT_FONT_CONFIG . axisLabel } px;` ;
111+ textElement . setAttribute ( "style" , currentStyle ) ;
112+ textElement . setAttribute ( "fontSize" , String ( PLOT_FONT_CONFIG . axisLabel ) ) ;
113+ }
114+ } ) ;
115+ }
116+
117+ function ensureTextProperties ( svg : SVGElement ) : void {
118+ // Find all text elements and ensure they have proper font properties
119+ const textElements = svg . querySelectorAll ( "text, tspan" ) ;
120+
121+ textElements . forEach ( ( element ) => {
122+ const textElement = element as SVGTextElement ;
123+ let currentStyle = textElement . getAttribute ( "style" ) || "" ;
124+
125+ // Ensure font-family is set (use system default if not specified)
126+ if ( ! currentStyle . includes ( "font-family" ) ) {
127+ if ( currentStyle && ! currentStyle . endsWith ( ";" ) ) {
128+ currentStyle += ";" ;
129+ }
130+ currentStyle +=
131+ "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;" ;
132+ }
133+
134+ // Ensure font-weight is set for better rendering
135+ if ( ! currentStyle . includes ( "font-weight" ) ) {
136+ if ( currentStyle && ! currentStyle . endsWith ( ";" ) ) {
137+ currentStyle += ";" ;
138+ }
139+ currentStyle += "font-weight: 400;" ;
140+ }
141+
142+ // Ensure text-rendering is optimized
143+ if ( ! currentStyle . includes ( "text-rendering" ) ) {
144+ if ( currentStyle && ! currentStyle . endsWith ( ";" ) ) {
145+ currentStyle += ";" ;
146+ }
147+ currentStyle += "text-rendering: optimizeLegibility;" ;
148+ }
149+
150+ // Set the updated style attribute
151+ textElement . setAttribute ( "style" , currentStyle ) ;
152+ } ) ;
153+ }
154+
25155export function svgToDataURL ( svg : SVGElement ) : string {
156+ // Add font definitions to ensure fonts are preserved
157+ addFontDefinitions ( svg ) ;
158+
26159 const serializer = new XMLSerializer ( ) ;
27160 const svgString = serializer . serializeToString ( svg ) ;
28161 const svgBlob = new Blob ( [ svgString ] , {
@@ -31,12 +164,56 @@ export function svgToDataURL(svg: SVGElement): string {
31164 return URL . createObjectURL ( svgBlob ) ;
32165}
33166
167+ function addFontDefinitions ( svg : SVGElement ) : void {
168+ // Get the document's font information
169+ const fontFaces = Array . from ( document . fonts )
170+ . map ( ( font ) => {
171+ const family = font . family ;
172+ const style = font . style ;
173+ const weight = font . weight ;
174+ const stretch = font . stretch ;
175+ const unicodeRange = font . unicodeRange ;
176+
177+ return `@font-face {
178+ font-family: "${ family } ";
179+ font-style: ${ style } ;
180+ font-weight: ${ weight } ;
181+ font-stretch: ${ stretch } ;
182+ unicode-range: ${ unicodeRange } ;
183+ src: local("${ family } ");
184+ }` ;
185+ } )
186+ . join ( "\n" ) ;
187+
188+ // Add a style element with font definitions
189+ if ( fontFaces ) {
190+ const styleElement = document . createElementNS (
191+ "http://www.w3.org/2000/svg" ,
192+ "style" ,
193+ ) ;
194+ styleElement . textContent = fontFaces ;
195+
196+ // Insert at the beginning of the SVG
197+ const defs =
198+ svg . querySelector ( "defs" ) ||
199+ svg . insertBefore (
200+ document . createElementNS ( "http://www.w3.org/2000/svg" , "defs" ) ,
201+ svg . firstChild ,
202+ ) ;
203+ defs . appendChild ( styleElement ) ;
204+ }
205+ }
206+
34207export async function svgToPngDataURL (
35208 svg : SVGElement ,
36209 scale = 2 ,
37210 backgroundColor = "white" ,
38211) : Promise < string > {
39212 return new Promise ( ( resolve , reject ) => {
213+ // Ensure font sizes are explicitly set before converting to PNG
214+ applyExplicitFontSizes ( svg ) ;
215+ ensureTextProperties ( svg ) ;
216+ addFontDefinitions ( svg ) ;
40217 const svgURL = svgToDataURL ( svg ) ;
41218 const img = new Image ( ) ;
42219 const width = parseFloat ( svg . getAttribute ( "width" ) || "0" ) ;
@@ -53,6 +230,10 @@ export async function svgToPngDataURL(
53230 throw new Error ( "Failed to get canvas context" ) ;
54231 }
55232
233+ // Set high quality rendering
234+ ctx . imageSmoothingEnabled = true ;
235+ ctx . imageSmoothingQuality = "high" ;
236+
56237 // Fill background if specified
57238 if ( backgroundColor ) {
58239 ctx . fillStyle = backgroundColor ;
@@ -64,7 +245,7 @@ export async function svgToPngDataURL(
64245 ctx . drawImage ( img , 0 , 0 ) ;
65246
66247 // Convert to data URL
67- const dataURL = canvas . toDataURL ( "image/png" ) ;
248+ const dataURL = canvas . toDataURL ( "image/png" , 1.0 ) ; // Maximum quality
68249 URL . revokeObjectURL ( svgURL ) ;
69250 resolve ( dataURL ) ;
70251 } catch ( error ) {
0 commit comments