@@ -38,24 +38,35 @@ export function toImage(chartContainer?: HTMLElement | null, opts: IImageExportO
3838 } ) ;
3939}
4040
41+ const SVG_STYLE_PROPERTIES = [ 'display' , 'fill' , 'fill-opacity' , 'opacity' , 'stroke' , 'stroke-width' , 'transform' ] ;
42+ const SVG_TEXT_STYLE_PROPERTIES = [ 'font-family' , 'font-size' , 'font-weight' , 'text-anchor' ] ;
43+
4144function toSVG ( chartContainer : HTMLElement , background : string ) {
4245 const svg = chartContainer . querySelector < SVGSVGElement > ( 'svg' ) ;
4346 if ( ! svg ) {
4447 throw new Error ( 'SVG not found' ) ;
4548 }
4649
47- const { width : svgWidth , height : svgHeight } = svg . getBoundingClientRect ( ) ;
48- const classNames = new Set < string > ( ) ;
49- const legendGroup = cloneLegendsToSVG ( chartContainer , svgWidth , svgHeight , classNames ) ;
50- const w1 = Math . max ( svgWidth , legendGroup . width ) ;
51- const h1 = svgHeight + legendGroup . height ;
5250 const clonedSvg = d3Select ( svg . cloneNode ( true ) as SVGSVGElement )
5351 . attr ( 'width' , null )
5452 . attr ( 'height' , null )
5553 . attr ( 'viewBox' , null ) ;
56- const { fontFamily } = getComputedStyle ( chartContainer ) ;
54+ const svgElements = svg . getElementsByTagName ( '*' ) ;
55+ const clonedSvgElements = clonedSvg . node ( ) ! . getElementsByTagName ( '*' ) ;
56+
57+ for ( let i = 0 ; i < svgElements . length ; i ++ ) {
58+ if ( svgElements [ i ] . tagName . toLowerCase ( ) === 'text' ) {
59+ copyStyle ( [ ...SVG_STYLE_PROPERTIES , ...SVG_TEXT_STYLE_PROPERTIES ] , svgElements [ i ] , clonedSvgElements [ i ] ) ;
60+ } else {
61+ copyStyle ( SVG_STYLE_PROPERTIES , svgElements [ i ] , clonedSvgElements [ i ] ) ;
62+ }
63+ }
64+
65+ const { width : svgWidth , height : svgHeight } = svg . getBoundingClientRect ( ) ;
66+ const legendGroup = cloneLegendsToSVG ( chartContainer , svgWidth , svgHeight ) ;
67+ const w1 = Math . max ( svgWidth , legendGroup . width ) ;
68+ const h1 = svgHeight + legendGroup . height ;
5769
58- clonedSvg . selectAll ( 'text' ) . style ( 'font-family' , fontFamily ) ;
5970 if ( legendGroup . node ) {
6071 clonedSvg . append ( ( ) => legendGroup . node ) ;
6172 }
@@ -66,36 +77,6 @@ function toSVG(chartContainer: HTMLElement, background: string) {
6677 . attr ( 'width' , w1 )
6778 . attr ( 'height' , h1 )
6879 . attr ( 'fill' , background ) ;
69-
70- const svgElements = svg . getElementsByTagName ( '*' ) ;
71- const styleSheets = document . styleSheets ;
72- let styleRules : string = '' ;
73-
74- for ( let i = svgElements . length - 1 ; i -- ; ) {
75- svgElements [ i ] . classList . forEach ( className => {
76- classNames . add ( `.${ className } ` ) ;
77- } ) ;
78- }
79-
80- for ( let i = 0 ; i < styleSheets . length ; i ++ ) {
81- const rules = styleSheets [ i ] . cssRules ;
82- for ( let j = 0 ; j < rules . length ; j ++ ) {
83- if ( rules [ j ] . constructor . name === 'CSSStyleRule' ) {
84- const selectorText = ( rules [ j ] as CSSStyleRule ) . selectorText ;
85- const hasClassName = selectorText . split ( ' ' ) . some ( word => classNames . has ( word ) ) ;
86-
87- if ( hasClassName ) {
88- styleRules += rules [ j ] . cssText + ' ' ;
89- }
90- }
91- }
92- }
93- styleRules = resolveCSSVariables ( chartContainer , styleRules ) ;
94-
95- const xmlDocument = new DOMParser ( ) . parseFromString ( '<svg></svg>' , 'image/svg+xml' ) ;
96- const styleNode = xmlDocument . createCDATASection ( styleRules ) ;
97- clonedSvg . insert ( 'defs' , ':first-child' ) . append ( 'style' ) . attr ( 'type' , 'text/css' ) . node ( ) ! . appendChild ( styleNode ) ;
98-
9980 clonedSvg . attr ( 'width' , w1 ) . attr ( 'height' , h1 ) . attr ( 'viewBox' , `0 0 ${ w1 } ${ h1 } ` ) ;
10081
10182 return {
@@ -105,7 +86,19 @@ function toSVG(chartContainer: HTMLElement, background: string) {
10586 } ;
10687}
10788
108- function cloneLegendsToSVG ( chartContainer : HTMLElement , svgWidth : number , svgHeight : number , classNames : Set < string > ) {
89+ const LEGEND_RECT_STYLE_PROPERTIES_MAP = {
90+ 'background-color' : 'fill' ,
91+ 'border-color' : 'stroke' ,
92+ } ;
93+ const LEGEND_TEXT_STYLE_PROPERTIES_MAP = {
94+ color : 'fill' ,
95+ 'font-family' : 'font-family' ,
96+ 'font-size' : 'font-size' ,
97+ 'font-weight' : 'font-weight' ,
98+ opacity : 'opacity' ,
99+ } ;
100+
101+ function cloneLegendsToSVG ( chartContainer : HTMLElement , svgWidth : number , svgHeight : number ) {
109102 const legendButtons = chartContainer . querySelectorAll < HTMLElement > ( `
110103 button[class^="legend-"],
111104 [class^="legendContainer-"] div[class^="overflowIndicationTextStyle-"],
@@ -146,35 +139,29 @@ function cloneLegendsToSVG(chartContainer: HTMLElement, svgWidth: number, svgHei
146139
147140 if ( legendButtons [ i ] . tagName . toLowerCase ( ) === 'button' ) {
148141 const legendRect = legendButtons [ i ] . querySelector < HTMLDivElement > ( '[class^="rect"]' ) ;
149- const { backgroundColor : legendColor , borderColor : legendBorderColor } = getComputedStyle ( legendRect ! ) ;
150142
151143 legendText = legendButtons [ i ] . querySelector < HTMLDivElement > ( '[class^="text"]' ) ;
152- legendText ! . classList . forEach ( className => classNames . add ( `.${ className } ` ) ) ;
153144 legendItem
154145 . append ( 'rect' )
155146 . attr ( 'x' , legendX + 8 )
156147 . attr ( 'y' , svgHeight + legendY + 8 )
157148 . attr ( 'width' , 12 )
158149 . attr ( 'height' , 12 )
159- . attr ( 'fill' , legendColor )
160150 . attr ( 'stroke-width' , 1 )
161- . attr ( 'stroke' , legendBorderColor ) ;
151+ . call ( selection => copyStyle ( LEGEND_RECT_STYLE_PROPERTIES_MAP , legendRect ! , selection . node ( ) ! ) ) ;
162152 textOffset = 28 ;
163153 } else {
164154 legendText = legendButtons [ i ] as HTMLDivElement ;
165- legendText . classList . forEach ( className => classNames . add ( `.${ className } ` ) ) ;
166155 textOffset = 8 ;
167156 }
168157
169- const { color : textColor } = getComputedStyle ( legendText ! ) ;
170158 legendItem
171159 . append ( 'text' )
172160 . attr ( 'x' , legendX + textOffset )
173161 . attr ( 'y' , svgHeight + legendY + 8 )
174162 . attr ( 'dominant-baseline' , 'hanging' )
175- . attr ( 'class' , legendText ! . getAttribute ( 'class' ) )
176- . attr ( 'fill' , textColor )
177- . text ( legendText ! . textContent ) ;
163+ . text ( legendText ! . textContent )
164+ . call ( selection => copyStyle ( LEGEND_TEXT_STYLE_PROPERTIES_MAP , legendText ! , selection . node ( ) ! ) ) ;
178165 legendX += legendWidth ;
179166 }
180167
@@ -271,3 +258,16 @@ function unescapePonyfill(str: string) {
271258 }
272259 return result ;
273260}
261+
262+ function copyStyle ( properties : string [ ] | Record < string , string > , fromEl : Element , toEl : Element ) {
263+ const styles = getComputedStyle ( fromEl ) ;
264+ if ( Array . isArray ( properties ) ) {
265+ properties . forEach ( prop => {
266+ d3Select ( toEl ) . style ( prop , styles . getPropertyValue ( prop ) ) ;
267+ } ) ;
268+ } else {
269+ Object . entries ( properties ) . forEach ( ( [ fromProp , toProp ] ) => {
270+ d3Select ( toEl ) . style ( toProp , styles . getPropertyValue ( fromProp ) ) ;
271+ } ) ;
272+ }
273+ }
0 commit comments