Skip to content

Commit 45c9be5

Browse files
fix(plugin-charts): data labels print in their value axis's units and the tallest mark keeps its label on the chart (#27407)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8YKe7vvuJjvvPs61VEnCbHs)_ Fixes #27372 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:图表数据标签改用其所在数值轴自己的刻度格式化器(无作者格式时与轴同为紧凑单位,如 `174K`),并在开启数据标签时给竖向图表加顶部留白,最高柱的标签不再画到图外。 - 为什么改:HotCRM 18.2.0 浏览器走查第 2–3 行:标签印 `174000` 而轴印 `400K`;最高柱的值标签整段被裁掉。表格末列与全零图两行实测后不改,理由见下。 - 风险与回滚:只动 `@object-ui/plugin-charts` 的 `AdvancedChartImpl.tsx`;关闭数据标签的图表与横向柱图布局字节不变。回滚 = revert 本 PR。 - 先看:`AdvancedChartImpl.tsx` 的 `DATA_LABEL_CHART_MARGIN` 与 `dataLabel` / `valueTickFormatterOf`;再看新测试 `AdvancedChartImpl.dataLabels-27372.test.tsx`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-charts/src/AdvancedChartImpl.dataLabels-27372.test.tsx`,预期 11 passed。 - 需要你做的:无,席位落地。 ## What changes `ui/packages/plugin-charts/src/AdvancedChartImpl.tsx` only: - **Row 2: data labels read in their axis's units.** `dataLabel` now takes the tick formatter of the value axis the series is drawn against: `yTickFormatter` on a single-axis chart, `leftYTickFormatter` / `rightYTickFormatter` by the series' `yAxis` on a dual-axis chart (`valueTickFormatterOf`), and by the series' axis id on a combo. Each of those is already `formatterFor(axis.format) ?? formatYTick`, so an authored `yAxis[].format` still wins and an axis without one gives its labels the compact default. Before, the label called `formatterFor(axis.format)` with no fallback and printed the raw number. Dashboard charts are the common case: `chartConfig` carries no axis format at all. - **Row 3, tallest bar: label headroom.** With `showDataLabels` on, a vertical bar, line, area or combo chart takes `DATA_LABEL_CHART_MARGIN`: a top margin of recharts' 5px plus the label's offset (5px) and font (11px). A margin rather than a domain pad, so an authored `yAxis.max` keeps pinning the domain. Charts without data labels and horizontal bars keep recharts' default margin. The label's font size and offset are now named constants (`DATA_LABEL_FONT_PX`, `DATA_LABEL_OFFSET_PX`); their values are unchanged. - The unused `axisOfSeries` helper is replaced by `valueTickFormatterOf`. **Serial constraint with #27361:** this diff does not touch `DatasetWidget.tsx` or `utils.ts` and does not regenerate the sdui manifest, so it does not wait on #27361. ## Why Rows 2–4 of #27355 (HotCRM's 18.2.0 browser pass), carded as #27372. Each row was measured in real Chromium (`/opt/pw-browsers/chromium`) on a fixture dashboard rendered by the real `DashboardRenderer`, with dataset-bound bar, line, table and all-zero widgets and an object-provider table, at 1280px (tables also at 768px and 390px). HotCRM's metadata was not used. | Row | Reading before (`c4c515ad`) | Reading after | Outcome | |---|---|---|---| | 2 | Label `174000` beside y ticks `0 / 100K / … / 400K` | Label `174K`, ticks unchanged | done | | 3a | Tallest label `400000` box top at −11px from the svg top: outside the svg | `400K` box top at +5px: inside the svg | done | | 3b | Dataset table: scroller `overflow-x: auto`, `scrollWidth` 910 vs `clientWidth` 566, inside the card. Object-provider table: 971 vs 564, inside the card | (no change) | will not do: not reproduced | | 4 | All-zero bar and line: y ticks `0 1 2 3 4`, 5 grid/axis lines, the line drawn on 0 | (no change) | will not do | ## Acceptance notes - **Row 3b, not reproduced.** Neither dashboard table widget clips its last column out of reach. The dataset `table` (DatasetWidget's flat table) and the `provider: 'object'` table (ObjectDataTable → `data-table`) each keep their own `overflow-auto` scroller inside the card, and the last column scrolls into view at 1280, 768 and 390px. What reads as a clip is the scroll edge with no visible scrollbar: headless Chromium hides scrollbars by default, macOS shows them only while scrolling, and `data-table` shows its scrollbar on hover only. A visible overflow cue (an edge fade, say) would be polish, not a reachability defect, so no table widget is changed. If HotCRM's screen shows a column that cannot be scrolled to, the reading needed is the widget type and the scroller's `scrollWidth` / `clientWidth` there. - **Row 4, will not do.** The recorded decision in `AdvancedChartImpl.tsx` (the "all-zero BAR drew 5,128 ink pixels of axes and ticks" paragraph) stands: zero is a value, the measured chart draws a usable zero baseline and ticks, and a "no data" state over real zeros would be false. - **Field format does not reach labels or ticks.** A measure's field `currency` / `percent` format reaches neither the axis ticks nor the labels on a dashboard chart (both read only `yAxis[].format`, which `chartConfig` does not carry). Not widened here: the labels now match the axis, which is the card's ask. - A horizontal bar's labels sit in the band beside each bar and were not measured as clipped; they get no new margin. ## Verification Head `27cff9de`. <details><summary>New pin: <code>AdvancedChartImpl.dataLabels-27372.test.tsx</code> (11 cases)</summary> - Compact default beside a compact axis, for bar, line and area (`['174K', '400K', '251K', '92.5K']`, the axis itself asserted to read `400K`). - An authored `$0,0` format wins (`$174,000` …). - Dual-axis bar and combo: each series prints in its own axis's units (`174K …` left, `12.0 …` right with a right-axis `0.0` format). - The tallest label's baseline minus the label font is ≥ 0 for bar, line, area and combo; the plot top stays at 5px with labels off. `pnpm --dir ui exec vitest run packages/plugin-charts/src/AdvancedChartImpl.dataLabels-27372.test.tsx` → 11 passed. </details> <details><summary>Ablations (each fix reverted through <code>scripts/ablation-replace.mjs</code>, restored blob = HEAD)</summary> - Formatter dropped from the `LabelList` (raw labels): 10 of 11 red (`expected [ '174000', … ] to deeply equal [ '174K', … ]`); only the labels-off margin case stays green. - Top margin set back to 5px: exactly the 4 headroom cases red (`"400K" is drawn above the chart surface: expected -11 to be greater than or equal to 0`), the 7 others green. </details> <details><summary>Package and gates</summary> - `pnpm --filter @object-ui/plugin-charts type-check` and `pnpm --dir ui exec vitest run packages/plugin-charts/` → exit 0; Test Files 100 passed (100), Tests 1292 passed | 9 skipped (1301), on 27cff9d. - `eslint` on both touched files: 0 errors; warning count equal to `c4c515ad` (41). - `pnpm --dir ui run check:new-line-citations` → 0 new citations. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 35 derived, 35 run on 27cff9d; 34 exit 0 first pass. `pnpm --dir ui exec vitest run scripts/` exit 1 on one 15 s timeout in `check-vi-mock-inherit.test.ts` (tree-sweep population test, a load timeout on a shared box; this diff touches no `ui/scripts` input); that file re-run alone under the lock: 83/83 passed, exit 0. `--ran` reconciliation: 35 accounted, 0 NOT-MEASURED, 0 UNRUN. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (touched files linted; repo-wide lint is CI's). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset (`@object-ui/plugin-charts` patch). - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the dispatch order's required Chinese summary). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbY75sPQWcfUg8cZQhg --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbY75sPQWcfUg8cZQhg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b10fd65 commit 45c9be5

3 files changed

Lines changed: 220 additions & 11 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/plugin-charts': patch
3+
---
4+
5+
fix(plugin-charts): a chart's data labels print in their value axis's units, and the tallest mark keeps its label on the chart surface
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Units.** With `showDataLabels` on, each value label now prints with the tick formatter of the value axis its series is drawn against. An axis with no authored `format` labels its marks in the same compact units as its ticks (`174K` beside a `400K` tick, where the label used to print `174000`); an authored `yAxis[].format` still wins, per axis on a dual-axis or combo chart. Dashboard charts, whose `chartConfig` carries no axis format, are the common case.
11+
- **Headroom.** A vertical bar, line, area or combo chart with data labels on takes a top margin that fits one label above the plot, so a mark on the top edge of the domain (the tallest bar under a nice top tick) no longer draws its label outside the chart. Charts without data labels, and horizontal bars, keep recharts' default margin.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
* objectstack#27372 — a chart's data labels (`showDataLabels`) read in the
9+
* units of their value axis, and the tallest mark keeps its label on the
10+
* chart surface.
11+
*
12+
* Two defects, one label:
13+
* - The value axis falls back to the compact formatter (`400K`) when no
14+
* `yAxis.format` is authored; the data label had no fallback and printed
15+
* the raw number (`174000` beside a `400K` tick).
16+
* - The label sits above its mark. With recharts' default 5px top margin, a
17+
* mark on the plot's top edge (the domain's nice max) had its label drawn
18+
* wholly above the chart surface.
19+
*
20+
* happy-dom paints no text, but given a fixed box recharts computes every mark
21+
* and label position arithmetically (the same property
22+
* `AdvancedChartImpl.pointAxisEdgeLabels-13390.test.tsx` relies on), so these
23+
* pins read the label's text and its `y` from the DOM.
24+
*/
25+
import React from 'react';
26+
import { describe, it, expect, afterEach, vi } from 'vitest';
27+
import { render, cleanup } from '@testing-library/react';
28+
29+
const WIDTH = 520;
30+
const HEIGHT = 300;
31+
32+
vi.mock('recharts', async () => {
33+
const actual = await vi.importActual<typeof import('recharts')>('recharts');
34+
return {
35+
...actual,
36+
ResponsiveContainer: ({ children }: { children: React.ReactElement<{ width: number; height: number }> }) =>
37+
React.cloneElement(children, { width: WIDTH, height: HEIGHT }),
38+
};
39+
});
40+
41+
import AdvancedChartImpl from './AdvancedChartImpl';
42+
43+
afterEach(cleanup);
44+
45+
/** The card's shape: the tallest value lands on a nice top tick (400K). */
46+
const PIPELINE = [
47+
{ stage: 'Prospecting', amount: 174000, won: 12 },
48+
{ stage: 'Qualification', amount: 400000, won: 30 },
49+
{ stage: 'Proposal', amount: 251000, won: 18 },
50+
{ stage: 'Negotiation', amount: 92500, won: 7 },
51+
];
52+
53+
/** The font size the chart prints data labels in. */
54+
const LABEL_FONT_PX = 11;
55+
56+
type Family = NonNullable<React.ComponentProps<typeof AdvancedChartImpl>['chartType']>;
57+
58+
function draw(chartType: Family, extra: Record<string, unknown> = {}) {
59+
const { container } = render(
60+
<AdvancedChartImpl
61+
chartType={chartType}
62+
data={PIPELINE}
63+
xAxisKey="stage"
64+
series={[{ dataKey: 'amount', label: 'Amount' }]}
65+
showLegend={false}
66+
showDataLabels
67+
isAnimationActive={false}
68+
{...extra}
69+
/>,
70+
);
71+
return container;
72+
}
73+
74+
/** Each data label: its text and the `y` its baseline is drawn at. */
75+
function labelsOf(container: HTMLElement) {
76+
const texts = [...container.querySelectorAll('.recharts-label-list text')];
77+
expect(texts.length, 'the chart drew no data labels, so this measures nothing').toBeGreaterThan(0);
78+
return texts.map((t) => ({ text: (t.textContent ?? '').trim(), y: Number(t.getAttribute('y')) }));
79+
}
80+
81+
/** The y-axis tick texts, so a label can be read against its own axis. */
82+
function yTicksOf(container: HTMLElement, axis = '.recharts-yAxis-tick-labels') {
83+
return [...container.querySelectorAll(`${axis} .recharts-cartesian-axis-tick-value`)].map((t) =>
84+
(t.textContent ?? '').trim(),
85+
);
86+
}
87+
88+
describe('objectstack#27372 — a data label reads in its value axis’s units', () => {
89+
for (const chartType of ['bar', 'line', 'area'] as const) {
90+
it(`prints the compact default beside a compact ${chartType} axis when no format is authored`, () => {
91+
const container = draw(chartType);
92+
// The axis is the reference: it must itself be compact, or the label
93+
// matching it would prove nothing about units.
94+
expect(yTicksOf(container)).toContain('400K');
95+
expect(labelsOf(container).map((l) => l.text)).toEqual(['174K', '400K', '251K', '92.5K']);
96+
});
97+
}
98+
99+
it('lets an authored axis format win over the compact default', () => {
100+
const container = draw('bar', { yAxes: [{ field: 'amount', format: '$0,0' }] });
101+
expect(labelsOf(container).map((l) => l.text)).toEqual(['$174,000', '$400,000', '$251,000', '$92,500']);
102+
});
103+
104+
it('prints each series in the units of the axis it is bound to on a dual-axis chart', () => {
105+
const container = draw('bar', {
106+
series: [
107+
{ dataKey: 'amount', label: 'Amount' },
108+
{ dataKey: 'won', label: 'Won', yAxis: 'right' },
109+
],
110+
yAxes: [{ field: 'amount' }, { field: 'won', position: 'right', format: '0.0' }],
111+
});
112+
expect(labelsOf(container).map((l) => l.text)).toEqual([
113+
'174K', '400K', '251K', '92.5K',
114+
'12.0', '30.0', '18.0', '7.0',
115+
]);
116+
});
117+
118+
it('prints a combo series in the units of the axis it is drawn against', () => {
119+
const container = draw('combo', {
120+
series: [
121+
{ dataKey: 'amount', label: 'Amount', chartType: 'bar' },
122+
{ dataKey: 'won', label: 'Won', chartType: 'line', yAxis: 'right' },
123+
],
124+
yAxes: [{ field: 'amount' }, { field: 'won', position: 'right', format: '0.0' }],
125+
});
126+
expect(labelsOf(container).map((l) => l.text)).toEqual([
127+
'174K', '400K', '251K', '92.5K',
128+
'12.0', '30.0', '18.0', '7.0',
129+
]);
130+
});
131+
});
132+
133+
describe('objectstack#27372 — the tallest mark keeps its data label on the chart surface', () => {
134+
for (const chartType of ['bar', 'line', 'area', 'combo'] as const) {
135+
it(`draws the top label of a ${chartType} chart inside the surface`, () => {
136+
const container = draw(chartType);
137+
const labels = labelsOf(container);
138+
const top = labels.reduce((a, b) => (b.y < a.y ? b : a));
139+
expect(top.text).toBe('400K');
140+
// The label's baseline is at `y`; its glyphs rise at most one font size
141+
// above it. Below that, the label is (partly) off the top of the svg.
142+
expect(top.y - LABEL_FONT_PX, `"${top.text}" is drawn above the chart surface`).toBeGreaterThanOrEqual(0);
143+
});
144+
}
145+
146+
it('keeps recharts’ default top margin when data labels are off', () => {
147+
const { container } = render(
148+
<AdvancedChartImpl
149+
chartType="bar"
150+
data={PIPELINE}
151+
xAxisKey="stage"
152+
series={[{ dataKey: 'amount', label: 'Amount' }]}
153+
showLegend={false}
154+
isAnimationActive={false}
155+
/>,
156+
);
157+
const plotTop = Number(container.querySelector('defs clipPath rect')?.getAttribute('y'));
158+
expect(plotTop).toBe(5);
159+
});
160+
});

‎ui/packages/plugin-charts/src/AdvancedChartImpl.tsx‎

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,36 @@ function estimateLabelWidthPx(label: string): number {
482482
/** recharts' default chart margin, left plus right (5px each side). */
483483
const CHART_MARGIN_X_PX = 10;
484484

485+
/** The font size `showDataLabels` prints each point's value in, in px. */
486+
const DATA_LABEL_FONT_PX = 11;
487+
/** recharts' `LabelList` default `offset`: the gap between a mark and its label. */
488+
const DATA_LABEL_OFFSET_PX = 5;
489+
490+
/**
491+
* The chart margin a vertical cartesian chart (bar, line, area, combo) takes
492+
* while `showDataLabels` is on (objectstack#27372).
493+
*
494+
* A data label sits ABOVE its mark: its baseline is `DATA_LABEL_OFFSET_PX`
495+
* over the mark's top. recharts' default margin leaves 5px between the plot's
496+
* top edge and the end of the chart surface, and the value axis's nice domain
497+
* routinely puts the tallest mark exactly on that edge (a 400,000 bar under a
498+
* 400K top tick). Its label was then drawn wholly outside the surface: the
499+
* tallest bar, the one a reader looks at first, was the one with no value.
500+
*
501+
* The top margin is that 5px plus the label's own height (offset and font),
502+
* so a mark ON the top edge still has its label inside the surface. A margin
503+
* rather than a padded domain: an author's `yAxis.max` pins the domain and
504+
* must keep doing so, and the margin holds whatever the domain is. The other
505+
* three sides stay recharts' default 5px, which `CHART_MARGIN_X_PX` counts on.
506+
* Horizontal bars are left alone: their value runs across the plot.
507+
*/
508+
const DATA_LABEL_CHART_MARGIN = {
509+
top: 5 + DATA_LABEL_OFFSET_PX + DATA_LABEL_FONT_PX,
510+
right: 5,
511+
bottom: 5,
512+
left: 5,
513+
} as const;
514+
485515
/**
486516
* The inset, in px, a line or area chart's categorical x axis keeps at BOTH
487517
* ends of the plot (objectui#13390).
@@ -2128,10 +2158,6 @@ function AdvancedChartImplInner({
21282158
yAxisPositionNotes(yPlacement, categoriesRunDown),
21292159
yAxisUndrawnNotes(yPlacement, yAxes, categoriesRunDown),
21302160
);
2131-
/** The entry a series' marks are measured against — for its value labels. */
2132-
const axisOfSeries = (s: { yAxis?: string }): NormalizedAxis | undefined =>
2133-
hasDualAxis ? (s.yAxis === 'right' ? rightY : leftY) : primaryY;
2134-
21352161
const xTickFormatter = React.useMemo(
21362162
() => formatterFor(xAxisSpec?.format, displayLocale) ?? formatTick,
21372163
[xAxisSpec?.format, displayLocale, formatTick],
@@ -2196,6 +2222,12 @@ function AdvancedChartImplInner({
21962222
() => formatterFor(rightY?.format, displayLocale) ?? formatYTick,
21972223
[rightY?.format, displayLocale, formatYTick],
21982224
);
2225+
/**
2226+
* The tick formatter of the value axis a series' marks are measured
2227+
* against — the one its value labels print with (objectstack#27372).
2228+
*/
2229+
const valueTickFormatterOf = (s: { yAxis?: string }): typeof yTickFormatter =>
2230+
hasDualAxis ? (s.yAxis === 'right' ? rightYTickFormatter : leftYTickFormatter) : yTickFormatter;
21992231

22002232
/**
22012233
* Every number plotted in one slot of a dual axis (or on the only axis,
@@ -2317,10 +2349,16 @@ function AdvancedChartImplInner({
23172349
? <Brush dataKey={xAxisKey} height={24} travellerWidth={8} stroke="hsl(var(--muted-foreground))" />
23182350
: null;
23192351

2320-
/** `showDataLabels` prints each point's value on the mark itself. */
2321-
const dataLabel = (formatter?: (v: any) => string) =>
2352+
/**
2353+
* `showDataLabels` prints each point's value on the mark itself, in the
2354+
* units of the value axis the mark is measured against (objectstack#27372):
2355+
* the caller passes that axis's own tick formatter, so an authored
2356+
* `yAxis.format` wins and an axis without one gives its label the compact
2357+
* default (`174K` beside a `400K` tick, never a raw `174000`).
2358+
*/
2359+
const dataLabel = (formatter: (v: any) => string) =>
23222360
showDataLabels
2323-
? <LabelList position="top" className="fill-foreground" fontSize={11} {...(formatter ? { formatter } : {})} />
2361+
? <LabelList position="top" className="fill-foreground" fontSize={DATA_LABEL_FONT_PX} offset={DATA_LABEL_OFFSET_PX} formatter={formatter} />
23242362
: null;
23252363

23262364
// Shared X-axis props for time/categorical axes, in two branches.
@@ -3034,7 +3072,7 @@ function AdvancedChartImplInner({
30343072
happens at chart level because that is the only place the CATEGORY
30353073
is known — a line/area item handler is handed the curve's props and
30363074
no datum. */}
3037-
<ComposedChart ref={setChartSurface} data={data} {...comboClickProps}>
3075+
<ComposedChart ref={setChartSurface} data={data} {...(showDataLabels ? { margin: DATA_LABEL_CHART_MARGIN } : {})} {...comboClickProps}>
30383076
<CartesianGrid {...gridProps} {...valueAxisIdFor(valueAxesHaveIds, categoriesRunDown)} />
30393077
<XAxis dataKey={xAxisKey} {...xAxisCommonProps} />
30403078
{/* A combo always draws both value axes (an authored combo binds an
@@ -3072,7 +3110,7 @@ function AdvancedChartImplInner({
30723110
: (seriesType === 'bar' ? 'left' : 'right');
30733111
const pres = seriesStyle(s, seriesType as any);
30743112
const stackProps = s.stack ? { stackId: String(s.stack) } : {};
3075-
const valueFormatter = formatterFor((yAxisId === 'right' ? rightY : leftY)?.format, displayLocale);
3113+
const valueFormatter = yAxisId === 'right' ? rightYTickFormatter : leftYTickFormatter;
30763114

30773115
if (seriesType === 'line') {
30783116
return (
@@ -3122,7 +3160,7 @@ function AdvancedChartImplInner({
31223160
<ChartFrame title={title} subtitle={subtitle}>
31233161
<ChartFootnote note={joinNotes(xPositionNote, yPositionNote)}>
31243162
<ChartContainer config={config} className={className} {...containerProps}>
3125-
<ChartComponent ref={setChartSurface} data={data} layout={isHorizontal ? 'vertical' : 'horizontal'} {...cartesianClickProps}>
3163+
<ChartComponent ref={setChartSurface} data={data} layout={isHorizontal ? 'vertical' : 'horizontal'} {...(showDataLabels && !isHorizontal ? { margin: DATA_LABEL_CHART_MARGIN } : {})} {...cartesianClickProps}>
31263164
<defs>
31273165
{gradColors.map((c) => (
31283166
<React.Fragment key={gslug(c)}>
@@ -3263,7 +3301,7 @@ function AdvancedChartImplInner({
32633301
// Recharts keys stacking off `stackId`, so the author's group name
32643302
// passes through unchanged.
32653303
const stackProps = s.stack ? { stackId: String(s.stack) } : {};
3266-
const valueFormatter = formatterFor(axisOfSeries(s)?.format, displayLocale);
3304+
const valueFormatter = valueTickFormatterOf(s);
32673305

32683306
if (chartType === 'bar' || chartType === 'horizontal-bar') {
32693307
// One series, one colour: a bar's colour names its SERIES, so a

0 commit comments

Comments
 (0)