|
1 | 1 | // DOM utils taken from
|
2 | 2 | // https://github.com/recharts/recharts/blob/master/src/util/DOMUtils.ts
|
3 | 3 |
|
| 4 | +import * as React from 'react'; |
| 5 | + |
4 | 6 | function isSsr(): boolean {
|
5 | 7 | return typeof window === 'undefined';
|
6 | 8 | }
|
7 | 9 |
|
8 |
| -interface StringCache { |
9 |
| - widthCache: Record<string, { width: number; height: number }>; |
10 |
| - cacheCount: number; |
11 |
| -} |
| 10 | +type Size = { width: number; height: number }; |
| 11 | + |
| 12 | +const cache = new Map<string, Size>(); |
| 13 | + |
| 14 | +// @ts-ignore |
| 15 | +window.xChartsClearStringCache = () => cache.clear(); |
12 | 16 |
|
13 |
| -const stringCache: StringCache = { |
14 |
| - widthCache: {}, |
15 |
| - cacheCount: 0, |
16 |
| -}; |
17 | 17 | const MAX_CACHE_NUM = 2000;
|
18 | 18 | const SPAN_STYLE = {
|
19 | 19 | position: 'absolute',
|
@@ -45,7 +45,8 @@ const STYLE_LIST = [
|
45 | 45 | 'marginTop',
|
46 | 46 | 'marginBottom',
|
47 | 47 | ];
|
48 |
| -export const MEASUREMENT_SPAN_ID = 'mui_measurement_span'; |
| 48 | + |
| 49 | +const MEASUREMENT_DIV_ID = 'mui_measurement_div'; |
49 | 50 |
|
50 | 51 | /**
|
51 | 52 | *
|
@@ -114,58 +115,101 @@ export const getStringSize = (text: string | number, style: React.CSSProperties
|
114 | 115 | const styleString = getStyleString(style);
|
115 | 116 | const cacheKey = `${str}-${styleString}`;
|
116 | 117 |
|
117 |
| - if (stringCache.widthCache[cacheKey]) { |
118 |
| - return stringCache.widthCache[cacheKey]; |
| 118 | + const result = cache.get(cacheKey); |
| 119 | + |
| 120 | + if (result) { |
| 121 | + return result; |
119 | 122 | }
|
120 | 123 |
|
| 124 | + warmUpStringCache([text], style); |
| 125 | + |
| 126 | + return cache.get(cacheKey) ?? { width: 0, height: 0 }; |
| 127 | +}; |
| 128 | + |
| 129 | +export function warmUpStringCache( |
| 130 | + texts: Iterable<string | number>, |
| 131 | + style: React.CSSProperties = {}, |
| 132 | +) { |
| 133 | + if (isSsr()) { |
| 134 | + return; |
| 135 | + } |
| 136 | + |
| 137 | + const styleString = getStyleString(style); |
| 138 | + |
121 | 139 | try {
|
122 |
| - let measurementSpan = document.getElementById(MEASUREMENT_SPAN_ID); |
123 |
| - if (measurementSpan === null) { |
124 |
| - measurementSpan = document.createElement('span'); |
125 |
| - measurementSpan.setAttribute('id', MEASUREMENT_SPAN_ID); |
126 |
| - measurementSpan.setAttribute('aria-hidden', 'true'); |
127 |
| - document.body.appendChild(measurementSpan); |
| 140 | + let measurementDiv = document.getElementById(MEASUREMENT_DIV_ID); |
| 141 | + if (measurementDiv === null) { |
| 142 | + measurementDiv = document.createElement('div'); |
| 143 | + measurementDiv.setAttribute('id', MEASUREMENT_DIV_ID); |
| 144 | + measurementDiv.setAttribute('aria-hidden', 'true'); |
| 145 | + document.body.appendChild(measurementDiv); |
128 | 146 | }
|
| 147 | + |
129 | 148 | // Need to use CSS Object Model (CSSOM) to be able to comply with Content Security Policy (CSP)
|
130 | 149 | // https://en.wikipedia.org/wiki/Content_Security_Policy
|
131 |
| - const measurementSpanStyle: Record<string, any> = { ...SPAN_STYLE, ...style }; |
| 150 | + const measurementDivStyle: Record<string, any> = { ...SPAN_STYLE, ...style }; |
132 | 151 |
|
133 |
| - Object.keys(measurementSpanStyle).map((styleKey) => { |
134 |
| - (measurementSpan!.style as Record<string, any>)[camelToMiddleLine(styleKey)] = |
135 |
| - autoCompleteStyle(styleKey, measurementSpanStyle[styleKey]); |
| 152 | + Object.keys(style).map((styleKey) => { |
| 153 | + (measurementDiv!.style as Record<string, any>)[camelToMiddleLine(styleKey)] = |
| 154 | + autoCompleteStyle(styleKey, measurementDivStyle[styleKey]); |
136 | 155 | return styleKey;
|
137 | 156 | });
|
138 |
| - measurementSpan.textContent = str; |
139 |
| - const rect = measurementSpan.getBoundingClientRect(); |
140 |
| - const result = { width: rect.width, height: rect.height }; |
141 | 157 |
|
142 |
| - stringCache.widthCache[cacheKey] = result; |
| 158 | + const spans = []; |
| 159 | + for (const text of texts) { |
| 160 | + const str = `${text}`; |
| 161 | + const cacheKey = `${str}-${styleString}`; |
143 | 162 |
|
144 |
| - if (stringCache.cacheCount + 1 > MAX_CACHE_NUM) { |
145 |
| - stringCache.cacheCount = 0; |
146 |
| - stringCache.widthCache = {}; |
147 |
| - } else { |
148 |
| - stringCache.cacheCount += 1; |
| 163 | + if (cache.has(cacheKey)) { |
| 164 | + // If already cached, skip the measurement |
| 165 | + continue; |
| 166 | + } |
| 167 | + |
| 168 | + const span = document.createElement('span'); |
| 169 | + span.textContent = str; |
| 170 | + spans.push(span); |
| 171 | + } |
| 172 | + |
| 173 | + measurementDiv.replaceChildren(...spans); |
| 174 | + |
| 175 | + let i = 0; |
| 176 | + for (const text of texts) { |
| 177 | + const str = `${text}`; |
| 178 | + const cacheKey = `${str}-${styleString}`; |
| 179 | + |
| 180 | + if (cache.has(cacheKey)) { |
| 181 | + // The string is already cached. Do not increment the index because a span wasn't created for cached strings |
| 182 | + continue; |
| 183 | + } |
| 184 | + |
| 185 | + const span = spans[i]; |
| 186 | + const rect = span.getBoundingClientRect(); |
| 187 | + |
| 188 | + cache.set(cacheKey, { width: rect.width, height: rect.height }); |
| 189 | + |
| 190 | + if (cache.size + 1 > MAX_CACHE_NUM) { |
| 191 | + // This is very inefficient, we should be smarter about which entries to purge |
| 192 | + cache.clear(); |
| 193 | + } |
| 194 | + i += 1; |
149 | 195 | }
|
150 | 196 |
|
151 | 197 | if (process.env.NODE_ENV === 'test') {
|
152 |
| - // In test environment, we clean the measurement span immediately |
153 |
| - measurementSpan.textContent = ''; |
| 198 | + // In test environment, we clean the measurement div immediately |
| 199 | + measurementDiv.replaceChildren(); |
154 | 200 | } else {
|
155 | 201 | if (domCleanTimeout) {
|
156 | 202 | clearTimeout(domCleanTimeout);
|
157 | 203 | }
|
158 | 204 | domCleanTimeout = setTimeout(() => {
|
159 | 205 | // Limit node cleaning to once per render cycle
|
160 |
| - measurementSpan.textContent = ''; |
| 206 | + measurementDiv.replaceChildren(); |
161 | 207 | }, 0);
|
162 | 208 | }
|
163 |
| - |
164 |
| - return result; |
165 | 209 | } catch {
|
166 |
| - return { width: 0, height: 0 }; |
| 210 | + /* Intentionally do nothing */ |
167 | 211 | }
|
168 |
| -}; |
| 212 | +} |
169 | 213 |
|
170 | 214 | // eslint-disable-next-line @typescript-eslint/naming-convention
|
171 | 215 | export function unstable_cleanupDOM() {
|
|
0 commit comments