@kylincloud/flamegraph 0.35.14 → 0.35.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,13 +11,14 @@ import React, {
11
11
  SetStateAction,
12
12
  } from 'react';
13
13
  import clsx from 'clsx';
14
- import type { Units } from '../models';
14
+ import { Units } from '../models/units';
15
15
 
16
16
  import RightClickIcon from './RightClickIcon';
17
17
  import LeftClickIcon from './LeftClickIcon';
18
18
 
19
19
  import styles from './Tooltip.module.scss';
20
20
  import { useFlamegraphI18n } from '../i18n';
21
+ import type { FlamegraphPalette } from '../FlameGraph/FlameGraphComponent/colorPalette';
21
22
 
22
23
  export type TooltipData = {
23
24
  units: Units;
@@ -30,13 +31,12 @@ export type TooltipData = {
30
31
  };
31
32
 
32
33
  export interface TooltipProps {
33
- // canvas or table body ref
34
34
  dataSourceRef: RefObject<HTMLCanvasElement | HTMLTableSectionElement>;
35
-
36
35
  shouldShowFooter?: boolean;
37
36
  shouldShowTitle?: boolean;
38
37
  clickInfoSide?: 'left' | 'right';
39
-
38
+ // 新增:当前使用的调色板(包含普通模式 / 色盲模式等)
39
+ palette?: FlamegraphPalette;
40
40
  setTooltipContent: (
41
41
  setContent: Dispatch<
42
42
  SetStateAction<{
@@ -55,12 +55,168 @@ export interface TooltipProps {
55
55
  ) => void;
56
56
  }
57
57
 
58
+ /** 解析各种带单位的字符串,例如:
59
+ * "0.63 seconds" / "0.63 分钟" / "< 1 ms" / "+0.24minutes"
60
+ */
61
+ function parseNumericWithUnit(
62
+ input?: string | number
63
+ ): { value: number; unit: string; hasLessThan: boolean; sign: string } | null {
64
+ if (input == null) return null;
65
+
66
+ if (typeof input === 'number') {
67
+ return { value: input, unit: '', hasLessThan: false, sign: '' };
68
+ }
69
+
70
+ // 统一 Unicode 减号
71
+ let s = String(input).trim().replace(/\u2212/g, '-');
72
+ if (!s) return null;
73
+
74
+ let hasLessThan = false;
75
+ if (s.startsWith('<')) {
76
+ hasLessThan = true;
77
+ s = s.slice(1).trim();
78
+ }
79
+
80
+ const m = s.match(/^([+\-]?)(\d*\.?\d+)\s*(.*)$/);
81
+ if (!m) return null;
82
+
83
+ const sign = m[1] || '';
84
+ const numStr = m[2];
85
+ const unit = (m[3] || '').trim();
86
+
87
+ const value = parseFloat(numStr);
88
+ if (!Number.isFinite(value)) return null;
89
+
90
+ return { value, unit, hasLessThan, sign };
91
+ }
92
+
93
+ /** 把 DurationFormatter 输出的英文单位翻译成中文,并保证数字和单位之间有空格 */
94
+ function localizeDurationString(
95
+ input?: string,
96
+ isZh = false
97
+ ): string | undefined {
98
+ if (!input || !isZh) return input;
99
+
100
+ const parsed = parseNumericWithUnit(input);
101
+ if (!parsed) return input;
102
+
103
+ const { hasLessThan, unit, sign, value } = parsed;
104
+
105
+ let unitZh = unit;
106
+ const u = unit.toLowerCase();
107
+
108
+ if (u === 'ms') unitZh = '毫秒';
109
+ else if (u === 'μs' || u === 'µs' || u === 'us') unitZh = '微秒';
110
+ else if (u === 'second' || u === 'seconds') unitZh = '秒';
111
+ else if (u === 'minute' || u === 'minutes' || u === 'min' || u === 'mins')
112
+ unitZh = '分钟';
113
+ else if (u === 'hour' || u === 'hours') unitZh = '小时';
114
+ else if (u === 'day' || u === 'days') unitZh = '天';
115
+ else if (u === 'month' || u === 'months') unitZh = '月';
116
+ else if (u === 'year' || u === 'years') unitZh = '年';
117
+
118
+ // 尽量保留原来的数值部分
119
+ const m = String(input)
120
+ .trim()
121
+ .replace(/\u2212/g, '-')
122
+ .match(/^<?\s*([+\-]?)(\d*\.?\d+)/);
123
+ const valueStr =
124
+ m && m[2] ? `${m[1] || ''}${m[2]}` : `${sign}${Math.abs(value)}`;
125
+
126
+ const prefix = hasLessThan ? '< ' : '';
127
+ if (!unitZh) {
128
+ return `${prefix}${valueStr}`.trim();
129
+ }
130
+ return `${prefix}${valueStr} ${unitZh}`.trim();
131
+ }
132
+
133
+ /** CPU 时间差异:构造 "+0.24 minutes" / "-0.12 s" 这种文本(注意带空格) */
134
+ function formatTimeDiff(
135
+ baselineFormatted?: string,
136
+ comparisonFormatted?: string
137
+ ): string {
138
+ const baseline = parseNumericWithUnit(baselineFormatted);
139
+ const comparison = parseNumericWithUnit(comparisonFormatted);
140
+
141
+ if (!baseline || !comparison) return '';
142
+
143
+ const diff = comparison.value - baseline.value;
144
+ if (diff === 0) {
145
+ return '0';
146
+ }
147
+
148
+ const sign = diff > 0 ? '+' : '-';
149
+ const abs = Math.abs(diff);
150
+
151
+ let valueText: string;
152
+ if (abs >= 100) valueText = abs.toFixed(0);
153
+ else if (abs >= 10) valueText = abs.toFixed(1);
154
+ else valueText = abs.toFixed(2);
155
+
156
+ const unit = baseline.unit || comparison.unit;
157
+ if (!unit) return `${sign}${valueText}`;
158
+ return `${sign}${valueText} ${unit}`;
159
+ }
160
+
161
+ /** "133,328" → 133328 */
162
+ function parseSamples(input?: string): number | null {
163
+ if (!input) return null;
164
+ const cleaned = input.replace(/,/g, '').trim();
165
+ if (!cleaned) return null;
166
+ const value = Number(cleaned);
167
+ if (!Number.isFinite(value)) return null;
168
+ return value;
169
+ }
170
+
171
+ /** 样本数差异 "+1,430" / "-16,666" */
172
+ function formatSamplesDiff(
173
+ baselineSamples?: string,
174
+ comparisonSamples?: string
175
+ ): string {
176
+ const baseline = parseSamples(baselineSamples);
177
+ const comparison = parseSamples(comparisonSamples);
178
+
179
+ if (baseline == null || comparison == null) return '';
180
+
181
+ const diff = comparison - baseline;
182
+ if (diff === 0) {
183
+ return '0';
184
+ }
185
+
186
+ const sign = diff > 0 ? '+' : '-';
187
+ const abs = Math.abs(diff);
188
+
189
+ const valueText = abs.toLocaleString();
190
+ return `${sign}${valueText}`;
191
+ }
192
+
193
+ /** 按差异文本的正负号,结合 palette 计算颜色(支持色盲模式) */
194
+ function getDiffColorByText(
195
+ text: string | undefined,
196
+ palette?: FlamegraphPalette
197
+ ): string | undefined {
198
+ if (!text || !palette) return undefined;
199
+ const normalized = text.trim().replace(/\u2212/g, '-');
200
+ if (!normalized || normalized === '0') return undefined;
201
+
202
+ if (normalized.startsWith('+')) {
203
+ // “变多”:使用 badColor(普通模式是红,色盲模式可能是红/蓝里代表“多”的那个)
204
+ return palette.badColor.rgb().string();
205
+ }
206
+ if (normalized.startsWith('-')) {
207
+ // “变少”:使用 goodColor
208
+ return palette.goodColor.rgb().string();
209
+ }
210
+ return undefined;
211
+ }
212
+
58
213
  export function Tooltip({
59
214
  shouldShowFooter = true,
60
215
  shouldShowTitle = true,
61
216
  dataSourceRef,
62
217
  clickInfoSide,
63
218
  setTooltipContent,
219
+ palette,
64
220
  }: TooltipProps) {
65
221
  const tooltipRef = useRef<HTMLDivElement>(null);
66
222
  const [content, setContent] = React.useState({
@@ -76,24 +232,16 @@ export function Tooltip({
76
232
  const [style, setStyle] = useState<CSSProperties>();
77
233
 
78
234
  const onMouseOut = () => {
79
- setStyle({
80
- visibility: 'hidden',
81
- });
235
+ setStyle({ visibility: 'hidden' });
82
236
  setContent({
83
- title: {
84
- text: '',
85
- diff: {
86
- text: '',
87
- color: '',
88
- },
89
- },
237
+ title: { text: '', diff: { text: '', color: '' } },
90
238
  tooltipData: [],
91
239
  });
92
240
  };
93
241
 
94
242
  const memoizedOnMouseMove = useCallback(
95
243
  (e: MouseEvent) => {
96
- if (!tooltipRef || !tooltipRef.current) {
244
+ if (!tooltipRef.current) {
97
245
  throw new Error('Missing tooltipElement');
98
246
  }
99
247
 
@@ -103,25 +251,16 @@ export function Tooltip({
103
251
  );
104
252
  const top = e.clientY + 20;
105
253
 
106
- const style: React.CSSProperties = {
107
- top,
108
- left,
109
- visibility: 'visible',
110
- };
111
-
112
254
  setTooltipContent(setContent, onMouseOut, e);
113
- setStyle(style);
255
+ setStyle({ top, left, visibility: 'visible' });
114
256
  },
115
-
116
- // these are the dependencies from props
117
- // that are going to be used in onMouseMove
118
- [tooltipRef, setTooltipContent]
257
+ [setTooltipContent]
119
258
  );
120
259
 
121
260
  useEffect(() => {
122
261
  const dataSourceEl = dataSourceRef.current;
123
262
  if (!dataSourceEl) {
124
- return () => {};
263
+ return () => { };
125
264
  }
126
265
 
127
266
  dataSourceEl.addEventListener(
@@ -145,7 +284,7 @@ export function Tooltip({
145
284
  className={clsx(styles.tooltip, {
146
285
  [styles.flamegraphDiffTooltip]: content.tooltipData.length > 1,
147
286
  })}
148
- style={style}
287
+ style={{ ...style, textAlign: 'center' }}
149
288
  ref={tooltipRef}
150
289
  >
151
290
  {content.tooltipData.length > 0 && (
@@ -165,9 +304,10 @@ export function Tooltip({
165
304
  <TooltipTable
166
305
  data={content.tooltipData}
167
306
  diff={content.title.diff}
307
+ palette={palette}
168
308
  />
169
309
  ) : (
170
- <TooltipTable data={content.tooltipData} />
310
+ <TooltipTable data={content.tooltipData} palette={palette} />
171
311
  )}
172
312
  {shouldShowFooter && <TooltipFooter clickInfoSide={clickInfoSide} />}
173
313
  </>
@@ -179,12 +319,15 @@ export function Tooltip({
179
319
  function TooltipTable({
180
320
  data,
181
321
  diff,
322
+ palette,
182
323
  }: {
183
324
  data: TooltipData[];
184
325
  diff?: { text: string; color: string };
326
+ palette?: FlamegraphPalette;
185
327
  }) {
186
328
  const i18n = useFlamegraphI18n();
187
329
  const [baselineData, comparisonData] = data;
330
+ const isZh = i18n.location !== 'Location';
188
331
 
189
332
  if (!baselineData) {
190
333
  return null;
@@ -194,65 +337,121 @@ function TooltipTable({
194
337
 
195
338
  switch (baselineData.tooltipType) {
196
339
  case 'flamegraph':
197
- renderTable = () => (
198
- <>
199
- {comparisonData && (
200
- <thead>
340
+ renderTable = () => {
341
+ const timeDiffTextRaw =
342
+ comparisonData &&
343
+ formatTimeDiff(
344
+ baselineData.formattedValue,
345
+ comparisonData.formattedValue
346
+ );
347
+ const timeDiffText = localizeDurationString(timeDiffTextRaw, isZh);
348
+
349
+ const samplesDiffText =
350
+ comparisonData &&
351
+ formatSamplesDiff(baselineData.samples, comparisonData.samples);
352
+
353
+ const baselineFormattedValue = localizeDurationString(
354
+ baselineData.formattedValue,
355
+ isZh
356
+ );
357
+ const comparisonFormattedValue = localizeDurationString(
358
+ comparisonData?.formattedValue,
359
+ isZh
360
+ );
361
+
362
+ const percentDiffColor = diff?.color;
363
+ const timeDiffColor = getDiffColorByText(timeDiffTextRaw, palette);
364
+ const samplesDiffColor = getDiffColorByText(samplesDiffText, palette);
365
+
366
+ return (
367
+ <>
368
+ {comparisonData && (
369
+ <thead>
370
+ <tr>
371
+ <th />
372
+ <th>{i18n.baseline}</th>
373
+ <th>{i18n.comparison}</th>
374
+ <th>{i18n.diff}</th>
375
+ </tr>
376
+ </thead>
377
+ )}
378
+ <tbody>
379
+ {/* CPU 占比行:沿用 formatDouble 计算的 diff.color */}
201
380
  <tr>
202
- <th />
203
- <th>{i18n.baseline}</th>
204
- <th>{i18n.comparison}</th>
205
- <th>{i18n.diff}</th>
381
+ <td>
382
+ {i18n.tooltipUnitTitles[baselineData.units].percent}:
383
+ </td>
384
+ <td>{baselineData.percent}</td>
385
+ {comparisonData && (
386
+ <>
387
+ <td>{comparisonData.percent}</td>
388
+ <td style={{ textAlign: 'center' }}>
389
+ {diff && (
390
+ <span
391
+ data-testid="tooltip-diff"
392
+ style={{ color: percentDiffColor }}
393
+ >
394
+ {diff.text}
395
+ </span>
396
+ )}
397
+ </td>
398
+ </>
399
+ )}
206
400
  </tr>
207
- </thead>
208
- )}
209
- <tbody>
210
- <tr>
211
- <td>
212
- {i18n.tooltipUnitTitles[baselineData.units].percent}:
213
- </td>
214
- <td>{baselineData.percent}</td>
215
- {comparisonData && (
216
- <>
217
- <td>{comparisonData.percent}</td>
218
- <td>
219
- {diff && (
220
- <span
221
- data-testid="tooltip-diff"
222
- style={{ color: diff.color }}
223
- >
224
- {diff.text}
225
- </span>
226
- )}
227
- </td>
228
- </>
229
- )}
230
- </tr>
231
- <tr>
232
- <td>
233
- {i18n.tooltipUnitTitles[baselineData.units].formattedValue}:
234
- </td>
235
- <td>{baselineData.formattedValue}</td>
236
- {comparisonData && (
237
- <>
238
- <td>{comparisonData.formattedValue}</td>
239
- <td />
240
- </>
241
- )}
242
- </tr>
243
- <tr>
244
- <td>{i18n.tooltipSamples}</td>
245
- <td>{baselineData.samples}</td>
246
- {comparisonData && (
247
- <>
248
- <td>{comparisonData.samples}</td>
249
- <td />
250
- </>
251
- )}
252
- </tr>
253
- </tbody>
254
- </>
255
- );
401
+
402
+ {/* CPU 时间行:按自己行的 diff 文本和 palette 决定颜色 */}
403
+ <tr>
404
+ <td>
405
+ {i18n.tooltipUnitTitles[baselineData.units].formattedValue}:
406
+ </td>
407
+ <td>{baselineFormattedValue}</td>
408
+ {comparisonData && (
409
+ <>
410
+ <td>{comparisonFormattedValue}</td>
411
+ <td style={{ textAlign: 'center' }}>
412
+ {timeDiffText && (
413
+ <span
414
+ data-testid="tooltip-time-diff"
415
+ style={
416
+ timeDiffColor ? { color: timeDiffColor } : undefined
417
+ }
418
+ >
419
+ {timeDiffText}
420
+ </span>
421
+ )}
422
+ </td>
423
+ </>
424
+ )}
425
+ </tr>
426
+
427
+ {/* 样本数行:同样按自身的 diff 文本和 palette 决定颜色 */}
428
+ <tr>
429
+ <td>{i18n.tooltipSamples}</td>
430
+ <td>{baselineData.samples}</td>
431
+ {comparisonData && (
432
+ <>
433
+ <td>{comparisonData.samples}</td>
434
+ <td style={{ textAlign: 'center' }}>
435
+ {samplesDiffText && (
436
+ <span
437
+ data-testid="tooltip-samples-diff"
438
+ style={
439
+ samplesDiffColor
440
+ ? { color: samplesDiffColor }
441
+ : undefined
442
+ }
443
+ >
444
+ {samplesDiffText}
445
+ </span>
446
+ )}
447
+ </td>
448
+ </>
449
+ )}
450
+ </tr>
451
+ </tbody>
452
+ </>
453
+ );
454
+ };
256
455
  break;
257
456
  case 'table':
258
457
  renderTable = () => (
@@ -293,6 +492,7 @@ function TooltipTable({
293
492
  [styles[`${baselineData.tooltipType}${comparisonData ? 'Diff' : ''}`]]:
294
493
  baselineData.tooltipType,
295
494
  })}
495
+ style={{ textAlign: 'center', margin: '0 auto' }}
296
496
  >
297
497
  {renderTable()}
298
498
  </table>
package/src/i18n.tsx CHANGED
@@ -176,8 +176,8 @@ export const defaultMessages: FlamegraphMessages = {
176
176
 
177
177
  noItemsFound: 'No items found',
178
178
 
179
- diffNew: '(new)',
180
- diffRemoved: '(removed)',
179
+ diffNew: 'new',
180
+ diffRemoved: 'removed',
181
181
 
182
182
  tooltipSamples: 'Samples:',
183
183
  tooltipUnitTitles: defaultTooltipUnitTitles,
@@ -230,8 +230,8 @@ export const zhCNMessages: FlamegraphMessages = {
230
230
 
231
231
  noItemsFound: '没有找到任何条目',
232
232
 
233
- diffNew: '(新增)',
234
- diffRemoved: '(移除)',
233
+ diffNew: '新增',
234
+ diffRemoved: '移除',
235
235
 
236
236
  tooltipSamples: '样本数:',
237
237
  tooltipUnitTitles: zhCNTooltipUnitTitles,
@@ -104,13 +104,6 @@ tt {
104
104
  white-space: nowrap;
105
105
  }
106
106
 
107
- &:first-child .color-reference {
108
- position: absolute;
109
- left: 10px;
110
- bottom: 0;
111
- top: 0;
112
- margin: auto;
113
- }
114
107
  }
115
108
 
116
109
  // Double column variant
@@ -149,11 +142,6 @@ tt {
149
142
  position: relative;
150
143
  }
151
144
 
152
- // element right next the color reference
153
- &:first-child .color-reference + div {
154
- margin-left: 15px;
155
- }
156
-
157
145
  .table-item-button {
158
146
  border: none;
159
147
  display: flex;
@@ -171,18 +159,6 @@ tt {
171
159
  }
172
160
  }
173
161
 
174
- // --------------------------------------------------------------------------
175
- // Color Reference
176
- // --------------------------------------------------------------------------
177
-
178
- .color-reference {
179
- width: 10px;
180
- height: 10px;
181
- border-radius: 2px;
182
- display: inline-block;
183
- margin-right: 5px;
184
- }
185
-
186
162
  // --------------------------------------------------------------------------
187
163
  // Button Group
188
164
  // --------------------------------------------------------------------------
@@ -403,5 +403,5 @@ pre {
403
403
  // --------------------------------------------------------------------------
404
404
 
405
405
  :focus-visible {
406
- outline: revert !important;
406
+ outline: revert;
407
407
  }