Skip to content

Commit 52acfb5

Browse files
authored
fix(dashboards): Improve rendering of small and fractional numbers in charts (#104456)
**Before:** <img width="727" height="389" alt="Screenshot 2025-12-05 at 11 33 32 AM" src="https://github.com/user-attachments/assets/7b86c633-e7da-47c7-845b-7dab685bc1e5" /> **After:** <img width="774" height="383" alt="Screenshot 2025-12-05 at 11 33 42 AM" src="https://github.com/user-attachments/assets/6a8d51e8-62ea-40e3-a40c-0f9f98ce5f93" /> The "number" type is susceptible to Y axis and tooltip rendering problems if the values are very small. This PR forces our charts to show "number" values at _full precision_ in the Y axis and the tooltip. The code for the formatters explains in detail the tradeoffs and aspects of the problem. This in particular affects the metrics UI where the type and unit of the values might not be known.
1 parent 3e1613b commit 52acfb5

File tree

4 files changed

+57
-4
lines changed

4 files changed

+57
-4
lines changed

static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.spec.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ describe('formatTooltipValue', () => {
1616

1717
describe('number', () => {
1818
it.each([
19-
[17.1238, '17.124'],
19+
[0.000033452, '0.000033452'],
20+
[0.00003, '0.00003'],
21+
[17.1238, '17.1238'],
22+
[170, '170'],
2023
[1772313.1, '1,772,313.1'],
24+
[1772313.11123, '1,772,313.11123'],
2125
])('Formats %s as %s', (value, formattedValue) => {
2226
expect(formatTooltipValue(value, 'number')).toEqual(formattedValue);
2327
});

static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import {
1313
isASizeUnit,
1414
} from 'sentry/views/dashboards/widgets/common/typePredicates';
1515

16+
/**
17+
* Format a value for the tooltip on an ECharts graph.
18+
*
19+
* The value might be a user submitted metric, or an aggregate. For user metric
20+
* values, it's wise to render the value at full precision, since the user might
21+
* be interested in the exact value, and tooltips should generally show the full
22+
* value. For aggregates, the precision is contrived, and the significant digits
23+
* might not match the original data. In this case, it would be wise to truncate
24+
* the value for display purposes, but we opt to do the safer thing and show the
25+
* value at full precision.
26+
*
27+
* This concept mostly applies to "number" values, since integers, durations,
28+
* and sizes naturally require less precision.
29+
*/
1630
export function formatTooltipValue(
1731
value: number | typeof ECHARTS_MISSING_DATA_VALUE,
1832
type: string,
@@ -25,7 +39,9 @@ export function formatTooltipValue(
2539
switch (type) {
2640
case 'integer':
2741
case 'number':
28-
return value.toLocaleString();
42+
return value.toLocaleString(undefined, {
43+
maximumFractionDigits: 100,
44+
});
2945
case 'percentage':
3046
return formatPercentage(value, 2);
3147
case 'duration': {

static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatYAxisValue.spec.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ describe('formatYAxisValue', () => {
1616

1717
describe('number', () => {
1818
it.each([
19-
[17.1238, '17.124'],
19+
[0.000033452, '0.000033452'],
20+
[0.00003, '0.00003'],
21+
[17.1238, '17.1238'],
22+
[170, '170'],
2023
[1772313.1, '1,772,313.1'],
24+
[1772313.11123, '1,772,313.11123'],
2125
])('Formats %s as %s', (value, formattedValue) => {
2226
expect(formatYAxisValue(value, 'number')).toEqual(formattedValue);
2327
});

static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatYAxisValue.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@ import {
1919

2020
import {formatYAxisDuration} from './formatYAxisDuration';
2121

22+
/**
23+
* Format a value for the Y axis on an ECharts graph.
24+
*
25+
* The values on the Y axis are chosen by ECharts. ECharts will automatically
26+
* select, when possible, nice round values. We always format the chosen value
27+
* at _full precision_ and trust EChart's choices. e.g., if it chooses "100", we
28+
* should show "100", because it probably chose a Y axis scale like "0, 100,
29+
* 200, 300". If it chooses "17.22" we should render "17.22" and not truncate
30+
* the value, since ECharts is probably using a scale like "17.20, 17.21, 17.22"
31+
* because the Y axis range is narrow. Similarly, a value like "0.00006"
32+
* probably means the range was narrow starting from 0, "0.00000, 0.00002
33+
* 0.00004 0.00006" and so on.
34+
*
35+
* This concept does not apply to:
36+
* 1. Integers. There are no fractional values!
37+
* 2. Durations. Durations have multiplier prefixes all the way down to nanoseconds, which should be enough. If needed, we can introduce smaller multipliers.
38+
* 3. Sizes. Sizes are effectively integers, and we have "byte" is already
39+
* small. It's an extremely rare case that we have fractional bytes with high
40+
* precision.
41+
* Rates and percentages would benefit from more precision, but it's not as critical there.
42+
*
43+
* The downside of this approach is that if the precision varies on the scale
44+
* (e.g., "0, 0.5, 1, 1.5") the Y axis labels are not well-aligned. This is a
45+
* limitation of ECharts, since it doesn't provide information about the entire
46+
* scale to each number. The ideal solution here would be to coordinate the
47+
* formatting of all the values.
48+
*/
2249
export function formatYAxisValue(value: number, type: string, unit?: string): string {
2350
if (value === 0) {
2451
return '0';
@@ -28,7 +55,9 @@ export function formatYAxisValue(value: number, type: string, unit?: string): st
2855
case 'integer':
2956
return formatAbbreviatedNumber(value);
3057
case 'number':
31-
return value.toLocaleString();
58+
return value.toLocaleString(undefined, {
59+
maximumFractionDigits: 100,
60+
});
3261
case 'percentage':
3362
return formatPercentage(value, 3);
3463
case 'duration': {

0 commit comments

Comments
 (0)