@agent-native/toolkit 0.213.1 → 0.216.0

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.
Files changed (222) hide show
  1. package/agent-native.eject.json +9 -0
  2. package/dist/app/agentkit/react/chat.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/chat.js +2 -2
  4. package/dist/app/agentkit/react/chat.js.map +1 -1
  5. package/dist/app/agentkit/react/components.d.ts +4 -1
  6. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/components.js +73 -13
  8. package/dist/app/agentkit/react/components.js.map +1 -1
  9. package/dist/app/agentkit/react/context.d.ts +19 -1
  10. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  11. package/dist/app/agentkit/react/context.js +7 -1
  12. package/dist/app/agentkit/react/context.js.map +1 -1
  13. package/dist/app/agentkit/react/index.d.ts +2 -0
  14. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  15. package/dist/app/agentkit/react/index.js +2 -0
  16. package/dist/app/agentkit/react/index.js.map +1 -1
  17. package/dist/app/agentkit/react/message-content-order.d.ts +21 -0
  18. package/dist/app/agentkit/react/message-content-order.d.ts.map +1 -0
  19. package/dist/app/agentkit/react/message-content-order.js +173 -0
  20. package/dist/app/agentkit/react/message-content-order.js.map +1 -0
  21. package/dist/app/agentkit/react/root.d.ts +6 -1
  22. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  23. package/dist/app/agentkit/react/root.js +2 -2
  24. package/dist/app/agentkit/react/root.js.map +1 -1
  25. package/dist/app/agentkit/react/styles.css +45 -8
  26. package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
  27. package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
  28. package/dist/app/agentkit/react/transcript-layout.js +34 -0
  29. package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
  30. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  31. package/dist/app/chat/AgentKitAssistantChat.js +9 -4
  32. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  33. package/dist/app/chat/AgentPanel.d.ts +16 -2
  34. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  35. package/dist/app/chat/AgentPanel.js +26 -5
  36. package/dist/app/chat/AgentPanel.js.map +1 -1
  37. package/dist/app/chat/chat/index.d.ts +2 -0
  38. package/dist/app/chat/chat/index.d.ts.map +1 -1
  39. package/dist/app/chat/chat/index.js.map +1 -1
  40. package/dist/app/chat/chat/surface-types.d.ts +15 -0
  41. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  42. package/dist/app/chat/chat/surface-types.js.map +1 -1
  43. package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
  44. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  45. package/dist/app/chat/chat/tool-call-display.js +10 -9
  46. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  47. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  48. package/dist/app/chat/chat/widgets/DataChartRenderer.js +300 -33
  49. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  50. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts +3 -2
  51. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  52. package/dist/app/chat/chat/widgets/DataChartWidget.js +18 -9
  53. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  54. package/dist/app/chat/chat/widgets/DataInsightsWidget.js +1 -1
  55. package/dist/app/chat/chat/widgets/DataInsightsWidget.js.map +1 -1
  56. package/dist/app/chat/chat/widgets/DataTableWidget.d.ts +3 -2
  57. package/dist/app/chat/chat/widgets/DataTableWidget.d.ts.map +1 -1
  58. package/dist/app/chat/chat/widgets/DataTableWidget.js +10 -66
  59. package/dist/app/chat/chat/widgets/DataTableWidget.js.map +1 -1
  60. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +11 -0
  61. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -0
  62. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +133 -0
  63. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -0
  64. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js +2 -2
  65. package/dist/app/chat/chat/widgets/builtin-tool-renderers.js.map +1 -1
  66. package/dist/app/chat/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  67. package/dist/app/chat/mcp-apps/McpAppRenderer.js +6 -5
  68. package/dist/app/chat/mcp-apps/McpAppRenderer.js.map +1 -1
  69. package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts +2 -0
  70. package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts.map +1 -0
  71. package/dist/app/chat/mcp-apps/mcp-app-theme.js +11 -0
  72. package/dist/app/chat/mcp-apps/mcp-app-theme.js.map +1 -0
  73. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  74. package/dist/app/i18n/catalogs/ar-SA.js +9 -0
  75. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  76. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  77. package/dist/app/i18n/catalogs/de-DE.js +9 -0
  78. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  79. package/dist/app/i18n/catalogs/en-US.d.ts +9 -0
  80. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  81. package/dist/app/i18n/catalogs/en-US.js +9 -0
  82. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  83. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  84. package/dist/app/i18n/catalogs/es-ES.js +9 -0
  85. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  86. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  87. package/dist/app/i18n/catalogs/fr-FR.js +9 -0
  88. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  89. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  90. package/dist/app/i18n/catalogs/hi-IN.js +9 -0
  91. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  92. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  93. package/dist/app/i18n/catalogs/ja-JP.js +9 -0
  94. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  95. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  96. package/dist/app/i18n/catalogs/ko-KR.js +9 -0
  97. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  98. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  99. package/dist/app/i18n/catalogs/pt-BR.js +9 -0
  100. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  101. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  102. package/dist/app/i18n/catalogs/zh-CN.js +9 -0
  103. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  104. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  105. package/dist/app/i18n/catalogs/zh-TW.js +9 -0
  106. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  107. package/dist/app/providers/AppProviders.d.ts +2 -1
  108. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  109. package/dist/app/providers/AppProviders.js +4 -3
  110. package/dist/app/providers/AppProviders.js.map +1 -1
  111. package/dist/app/providers/ThemeFamilySync.d.ts +2 -0
  112. package/dist/app/providers/ThemeFamilySync.d.ts.map +1 -0
  113. package/dist/app/providers/ThemeFamilySync.js +6 -0
  114. package/dist/app/providers/ThemeFamilySync.js.map +1 -0
  115. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  116. package/dist/app/settings/account/PreferencesSettings.js +2 -1
  117. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  118. package/dist/app/settings/account/ThemeFamilySettings.d.ts +2 -0
  119. package/dist/app/settings/account/ThemeFamilySettings.d.ts.map +1 -0
  120. package/dist/app/settings/account/ThemeFamilySettings.js +31 -0
  121. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -0
  122. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  123. package/dist/app/settings/account/search-entries.js +6 -0
  124. package/dist/app/settings/account/search-entries.js.map +1 -1
  125. package/dist/app/shared/DefaultSpinner.d.ts.map +1 -1
  126. package/dist/app/shared/DefaultSpinner.js +4 -5
  127. package/dist/app/shared/DefaultSpinner.js.map +1 -1
  128. package/dist/app/shared/ErrorBoundary.d.ts +1 -0
  129. package/dist/app/shared/ErrorBoundary.d.ts.map +1 -1
  130. package/dist/app/shared/ErrorBoundary.js +27 -12
  131. package/dist/app/shared/ErrorBoundary.js.map +1 -1
  132. package/dist/app/shared/PoweredByBadge.d.ts.map +1 -1
  133. package/dist/app/shared/PoweredByBadge.js +8 -49
  134. package/dist/app/shared/PoweredByBadge.js.map +1 -1
  135. package/dist/app/styles/agent-conversation.css +0 -23
  136. package/dist/app/styles/agent-native.css +1 -1
  137. package/dist/app/styles/theme-families.css +66 -0
  138. package/dist/composer/ComposerContextBanner.d.ts +24 -0
  139. package/dist/composer/ComposerContextBanner.d.ts.map +1 -0
  140. package/dist/composer/ComposerContextBanner.js +9 -0
  141. package/dist/composer/ComposerContextBanner.js.map +1 -0
  142. package/dist/composer/ComposerModePicker.d.ts +26 -0
  143. package/dist/composer/ComposerModePicker.d.ts.map +1 -0
  144. package/dist/composer/ComposerModePicker.js +14 -0
  145. package/dist/composer/ComposerModePicker.js.map +1 -0
  146. package/dist/composer/index.d.ts +2 -0
  147. package/dist/composer/index.d.ts.map +1 -1
  148. package/dist/composer/index.js +2 -0
  149. package/dist/composer/index.js.map +1 -1
  150. package/dist/styles.css +1 -0
  151. package/package.json +3 -3
  152. package/src/app/agentkit/react/chat.tsx +2 -0
  153. package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
  154. package/src/app/agentkit/react/components.spec.tsx +40 -1
  155. package/src/app/agentkit/react/components.tsx +186 -31
  156. package/src/app/agentkit/react/context.tsx +29 -0
  157. package/src/app/agentkit/react/index.ts +2 -0
  158. package/src/app/agentkit/react/lifecycle.spec.tsx +289 -4
  159. package/src/app/agentkit/react/message-content-order.spec.ts +190 -0
  160. package/src/app/agentkit/react/message-content-order.ts +246 -0
  161. package/src/app/agentkit/react/root.tsx +11 -0
  162. package/src/app/agentkit/react/styles.css +45 -8
  163. package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
  164. package/src/app/agentkit/react/transcript-layout.ts +97 -0
  165. package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
  166. package/src/app/chat/AgentKitAssistantChat.spec.tsx +58 -0
  167. package/src/app/chat/AgentKitAssistantChat.tsx +15 -1
  168. package/src/app/chat/AgentPanel.header.spec.ts +72 -14
  169. package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
  170. package/src/app/chat/AgentPanel.tsx +111 -55
  171. package/src/app/chat/chat/index.ts +2 -0
  172. package/src/app/chat/chat/surface-types.ts +18 -0
  173. package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
  174. package/src/app/chat/chat/tool-call-display.tsx +40 -8
  175. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +849 -0
  176. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +567 -72
  177. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +432 -0
  178. package/src/app/chat/chat/widgets/DataChartWidget.ssr.spec.tsx +25 -0
  179. package/src/app/chat/chat/widgets/DataChartWidget.tsx +58 -33
  180. package/src/app/chat/chat/widgets/DataInsightsWidget.tsx +2 -2
  181. package/src/app/chat/chat/widgets/DataTableWidget.spec.tsx +37 -3
  182. package/src/app/chat/chat/widgets/DataTableWidget.tsx +22 -86
  183. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +321 -0
  184. package/src/app/chat/chat/widgets/builtin-tool-renderers.tsx +5 -5
  185. package/src/app/chat/mcp-apps/McpAppRenderer.tsx +10 -5
  186. package/src/app/chat/mcp-apps/mcp-app-theme.spec.ts +30 -0
  187. package/src/app/chat/mcp-apps/mcp-app-theme.ts +11 -0
  188. package/src/app/i18n/catalogs/ar-SA.ts +10 -0
  189. package/src/app/i18n/catalogs/de-DE.ts +11 -0
  190. package/src/app/i18n/catalogs/en-US.ts +11 -0
  191. package/src/app/i18n/catalogs/es-ES.ts +12 -0
  192. package/src/app/i18n/catalogs/fr-FR.ts +12 -0
  193. package/src/app/i18n/catalogs/hi-IN.ts +10 -0
  194. package/src/app/i18n/catalogs/ja-JP.ts +11 -0
  195. package/src/app/i18n/catalogs/ko-KR.ts +10 -0
  196. package/src/app/i18n/catalogs/pt-BR.ts +11 -0
  197. package/src/app/i18n/catalogs/zh-CN.ts +9 -0
  198. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  199. package/src/app/providers/AppProviders.spec.tsx +71 -0
  200. package/src/app/providers/AppProviders.tsx +10 -0
  201. package/src/app/providers/ThemeFamilySync.tsx +6 -0
  202. package/src/app/settings/account/PreferencesSettings.spec.tsx +13 -2
  203. package/src/app/settings/account/PreferencesSettings.tsx +8 -0
  204. package/src/app/settings/account/ThemeFamilySettings.tsx +61 -0
  205. package/src/app/settings/account/search-entries.ts +6 -0
  206. package/src/app/shared/DefaultSpinner.spec.tsx +14 -0
  207. package/src/app/shared/DefaultSpinner.tsx +4 -5
  208. package/src/app/shared/ErrorBoundary.spec.tsx +45 -2
  209. package/src/app/shared/ErrorBoundary.tsx +37 -12
  210. package/src/app/shared/PoweredByBadge.spec.tsx +16 -0
  211. package/src/app/shared/PoweredByBadge.tsx +8 -50
  212. package/src/app/styles/agent-conversation.css +0 -23
  213. package/src/app/styles/agent-native.css +1 -1
  214. package/src/app/styles/agent-native.spec.ts +18 -0
  215. package/src/app/styles/theme-families.css +66 -0
  216. package/src/app/styles/theme-families.spec.ts +29 -0
  217. package/src/composer/ComposerContextBanner.spec.tsx +100 -0
  218. package/src/composer/ComposerContextBanner.tsx +77 -0
  219. package/src/composer/ComposerModePicker.spec.tsx +101 -0
  220. package/src/composer/ComposerModePicker.tsx +98 -0
  221. package/src/composer/index.ts +9 -0
  222. package/src/styles.css +1 -0
@@ -0,0 +1,849 @@
1
+ import React from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import type { DataChartWidget as DataChartWidgetData } from "./data-widget-types.js";
6
+
7
+ vi.mock("@agent-native/core/client/i18n", () => ({
8
+ useT: () => (key: string) => key,
9
+ }));
10
+
11
+ import { DataChartRenderer } from "./DataChartRenderer.js";
12
+
13
+ const chart: DataChartWidgetData = {
14
+ type: "bar",
15
+ xKey: "day",
16
+ series: [
17
+ { key: "first", label: "First" },
18
+ { key: "second", label: "Second" },
19
+ { key: "third", label: "Third" },
20
+ { key: "fourth", label: "Fourth" },
21
+ ],
22
+ data: [{ day: "Monday", first: 1, second: 2, third: 3, fourth: 4 }],
23
+ };
24
+
25
+ describe("DataChartRenderer theme colors", () => {
26
+ it("uses the validated row range when rendering a capped chart", () => {
27
+ const markup = renderToStaticMarkup(
28
+ <DataChartRenderer
29
+ chart={{
30
+ type: "bar",
31
+ xKey: "day",
32
+ series: [{ key: "value", label: "Value" }],
33
+ data: Array.from({ length: 201 }, (_, index) => ({
34
+ day: `Day ${index}`,
35
+ value: index === 200 ? 1000 : 1,
36
+ })),
37
+ valueAxisTickInterval: 1,
38
+ }}
39
+ />,
40
+ );
41
+
42
+ expect(Array.from(markup.matchAll(/<rect /g))).toHaveLength(200);
43
+ expect(markup).toContain(">Day 199</text>");
44
+ expect(markup).not.toContain(">Day 200</text>");
45
+ });
46
+
47
+ it("resolves its series colors through app chart tokens with legacy fallbacks", () => {
48
+ const markup = renderToStaticMarkup(<DataChartRenderer chart={chart} />);
49
+
50
+ expect(markup).toContain('viewBox="0 0 640 360"');
51
+ expect(markup).toContain("block w-full h-auto");
52
+ expect(markup).not.toContain("style=");
53
+ expect(markup).not.toContain("preserveAspectRatio");
54
+ expect(markup).not.toContain("h-60");
55
+ expect(markup).toContain('fill="hsl(var(--chart-1, var(--primary)))"');
56
+ expect(markup).toContain('fill="hsl(var(--chart-2, 199 89% 48%))"');
57
+ expect(markup).toContain('fill="hsl(var(--chart-3, 151 55% 42%))"');
58
+ expect(markup).toContain('fill="hsl(var(--chart-4, 35 92% 50%))"');
59
+ expect(markup).toContain('stroke="hsl(var(--chart-grid, var(--border)))"');
60
+ });
61
+
62
+ it("uses the requested chart content height", () => {
63
+ const markup = renderToStaticMarkup(
64
+ <DataChartRenderer chart={{ ...chart, contentHeight: 400 }} />,
65
+ );
66
+
67
+ expect(markup).toContain('style="height:400px;padding:0px"');
68
+ expect(markup).toContain(
69
+ 'class="agent-data-chart-renderer block w-full h-full"',
70
+ );
71
+ expect(markup).toContain('viewBox="0 0 640 360"');
72
+ expect(markup).toContain('preserveAspectRatio="none"');
73
+ });
74
+
75
+ it("fits labels and the chart canvas inside a padded body box", () => {
76
+ const markup = renderToStaticMarkup(
77
+ <DataChartRenderer
78
+ chart={{
79
+ ...chart,
80
+ contentHeight: 400,
81
+ contentPadding: 16,
82
+ labelFontFamily: "sans",
83
+ labelFontSize: 14,
84
+ }}
85
+ />,
86
+ );
87
+
88
+ expect(markup).toContain('style="height:400px;padding:16px"');
89
+ expect(markup).toContain(
90
+ 'font-family="var(--font-sans, ui-sans-serif, system-ui, sans-serif)"',
91
+ );
92
+ expect(markup).toContain('font-size="14"');
93
+ });
94
+
95
+ it("renders optional horizontal titles and value labels with their font roles", () => {
96
+ const markup = renderToStaticMarkup(
97
+ <DataChartRenderer
98
+ chart={{
99
+ ...chart,
100
+ layout: "horizontal",
101
+ xAxisTitle: "Value",
102
+ yAxisTitle: "Category",
103
+ showValueLabels: true,
104
+ labelFontSize: 10,
105
+ axisTitleFontSize: 11,
106
+ axisTitleFontWeight: 500,
107
+ valueLabelFontSize: 11,
108
+ valueLabelFontWeight: 400,
109
+ }}
110
+ />,
111
+ );
112
+
113
+ expect(markup).toContain(">Value</text>");
114
+ expect(markup).toContain(">Category</text>");
115
+ expect(markup).toContain('font-size="10"');
116
+ expect(markup).toContain('font-size="11"');
117
+ expect(markup).toContain('font-weight="500"');
118
+ expect(markup).toContain('font-weight="400"');
119
+ expect(markup).toContain('text-anchor="start"');
120
+ expect(markup).toContain('viewBox="0 0 640 360"');
121
+ });
122
+
123
+ it("renders bounded numeric value-axis steps with matching grid labels", () => {
124
+ const markup = renderToStaticMarkup(
125
+ <DataChartRenderer
126
+ chart={{
127
+ type: "bar",
128
+ layout: "horizontal",
129
+ xKey: "day",
130
+ series: [{ key: "value", label: "Value" }],
131
+ data: [{ day: "Monday", value: 3 }],
132
+ valueAxisTickInterval: 0.2,
133
+ }}
134
+ />,
135
+ );
136
+ const numericLabels = Array.from(
137
+ markup.matchAll(/<text[^>]*>([^<]+)<\/text>/g),
138
+ (match) => match[1],
139
+ ).filter((label) => /^\d+\.\d$/.test(label ?? ""));
140
+
141
+ expect(numericLabels).toEqual(
142
+ Array.from({ length: 16 }, (_, index) => (index * 0.2).toFixed(1)),
143
+ );
144
+ expect(markup).not.toContain('stroke-dasharray="3 3"');
145
+ });
146
+
147
+ it("keeps horizontal-bar spacing by default and accepts an 8% slot gap", () => {
148
+ const renderBar = (horizontalCategoryGapRatio?: number) =>
149
+ renderToStaticMarkup(
150
+ <DataChartRenderer
151
+ chart={{
152
+ type: "bar",
153
+ layout: "horizontal",
154
+ xKey: "day",
155
+ series: [{ key: "value", label: "Value" }],
156
+ data: [{ day: "Monday", value: 3 }],
157
+ horizontalCategoryGapRatio,
158
+ }}
159
+ />,
160
+ );
161
+ const firstBarHeight = (markup: string) =>
162
+ Number(markup.match(/<rect[^>]*height="([^"]+)"/)?.[1]);
163
+
164
+ expect(firstBarHeight(renderBar(0.08))).toBeGreaterThan(
165
+ firstBarHeight(renderBar()),
166
+ );
167
+ expect(renderBar()).toContain('stroke-dasharray="3 3"');
168
+ expect(() => renderBar(0.91)).toThrow(
169
+ "Horizontal category gap ratio must be between 0 and 0.9",
170
+ );
171
+ });
172
+
173
+ it("aligns custom value-axis ticks to the interval within the chart range", () => {
174
+ const markup = renderToStaticMarkup(
175
+ <DataChartRenderer
176
+ chart={{
177
+ type: "bar",
178
+ layout: "horizontal",
179
+ xKey: "day",
180
+ series: [{ key: "value", label: "Value" }],
181
+ data: [{ day: "Monday", value: 0.8 }],
182
+ valueAxisTickInterval: 0.3,
183
+ }}
184
+ />,
185
+ );
186
+ const numericLabels = Array.from(
187
+ markup.matchAll(/<text[^>]*>([^<]+)<\/text>/g),
188
+ (match) => match[1],
189
+ ).filter((label) => /^\d+\.\d$/.test(label ?? ""));
190
+
191
+ expect(numericLabels).toEqual(["0.0", "0.3", "0.6"]);
192
+ });
193
+
194
+ it("limits custom tick-label density without dropping grid lines", () => {
195
+ const markup = renderToStaticMarkup(
196
+ <DataChartRenderer
197
+ chart={{
198
+ type: "bar",
199
+ layout: "horizontal",
200
+ xKey: "day",
201
+ series: [{ key: "value", label: "Value" }],
202
+ data: [
203
+ { day: "First", value: 0 },
204
+ { day: "Last", value: 99 },
205
+ ],
206
+ valueAxisTickInterval: 1,
207
+ }}
208
+ />,
209
+ );
210
+ const numericLabels = Array.from(
211
+ markup.matchAll(/<text[^>]*>([^<]+)<\/text>/g),
212
+ (match) => match[1],
213
+ ).filter((label) => /^\d+$/.test(label ?? ""));
214
+
215
+ expect(numericLabels.length).toBeLessThanOrEqual(30);
216
+ expect(numericLabels[0]).toBe("0");
217
+ expect(numericLabels.at(-1)).toBe("99");
218
+ const tickLabelPositions = Array.from(
219
+ markup.matchAll(
220
+ /<text x="([\d.]+)"[^>]*text-anchor="middle">\d+<\/text>/g,
221
+ ),
222
+ (match) => Number(match[1]),
223
+ );
224
+ expect(
225
+ tickLabelPositions.at(-1)! - tickLabelPositions.at(-2)!,
226
+ ).toBeGreaterThanOrEqual(22);
227
+ expect(markup.match(/<line /g)).toHaveLength(101);
228
+
229
+ const smallRangeMarkup = renderToStaticMarkup(
230
+ <DataChartRenderer
231
+ chart={{
232
+ type: "bar",
233
+ layout: "horizontal",
234
+ xKey: "day",
235
+ series: [{ key: "value", label: "Value" }],
236
+ data: [
237
+ { day: "First", value: 0 },
238
+ { day: "Last", value: 3 },
239
+ ],
240
+ valueAxisTickInterval: 0.2,
241
+ }}
242
+ />,
243
+ );
244
+ const smallRangeLabels = Array.from(
245
+ smallRangeMarkup.matchAll(/<text[^>]*>([^<]+)<\/text>/g),
246
+ (match) => match[1],
247
+ ).filter((label) => /^\d+\.\d$/.test(label ?? ""));
248
+
249
+ expect(smallRangeLabels).toEqual(
250
+ Array.from({ length: 16 }, (_, index) => (index * 0.2).toFixed(1)),
251
+ );
252
+ });
253
+
254
+ it("keeps a single value label when the axis cannot fit two", () => {
255
+ const markup = renderToStaticMarkup(
256
+ <DataChartRenderer
257
+ chart={{
258
+ type: "bar",
259
+ layout: "horizontal",
260
+ xKey: "day",
261
+ series: [{ key: "value", label: "Value" }],
262
+ data: [{ day: "Only", value: 9.9e60 }],
263
+ valueAxisTickInterval: 1e59,
264
+ }}
265
+ />,
266
+ );
267
+ const numericLabels = Array.from(
268
+ markup.matchAll(/<text[^>]*text-anchor="middle">([^<]+)<\/text>/g),
269
+ (match) => match[1],
270
+ ).filter((label) => /^\d[\d,]*$/.test(label ?? ""));
271
+
272
+ expect(numericLabels).toHaveLength(1);
273
+ });
274
+
275
+ it("keeps long horizontal value labels inside the chart canvas", () => {
276
+ const markup = renderToStaticMarkup(
277
+ <DataChartRenderer
278
+ chart={{
279
+ type: "bar",
280
+ layout: "horizontal",
281
+ xKey: "day",
282
+ series: [{ key: "value", label: "Value" }],
283
+ data: [
284
+ { day: "Low", value: -1.9e50 },
285
+ { day: "High", value: 1.9e50 },
286
+ ],
287
+ valueAxisTickInterval: 1e50,
288
+ }}
289
+ />,
290
+ );
291
+ const valueTickLabels = Array.from(
292
+ markup.matchAll(
293
+ /<text x="([\d.]+)" y="340"[^>]*text-anchor="middle">([^<]+)<\/text>/g,
294
+ ),
295
+ (match) => ({ x: Number(match[1]), label: match[2] ?? "" }),
296
+ );
297
+
298
+ expect(valueTickLabels).toHaveLength(1);
299
+ for (const tick of valueTickLabels) {
300
+ const estimatedWidth = tick.label.length * 11 * 0.65;
301
+ expect(tick.x - estimatedWidth / 2).toBeGreaterThanOrEqual(0);
302
+ expect(tick.x + estimatedWidth / 2).toBeLessThanOrEqual(640);
303
+ }
304
+ });
305
+
306
+ it("thins labels using actual tick positions across uneven ranges", () => {
307
+ const markup = renderToStaticMarkup(
308
+ <DataChartRenderer
309
+ chart={{
310
+ type: "bar",
311
+ layout: "horizontal",
312
+ xKey: "day",
313
+ series: [{ key: "value", label: "Value" }],
314
+ data: [
315
+ { day: "Low", value: -5.1e28 },
316
+ { day: "High", value: 1.49e29 },
317
+ ],
318
+ valueAxisTickInterval: 5e28,
319
+ }}
320
+ />,
321
+ );
322
+ const valueTickLabels = Array.from(
323
+ markup.matchAll(
324
+ /<text x="([\d.]+)" y="340"[^>]*text-anchor="middle">([^<]+)<\/text>/g,
325
+ ),
326
+ (match) => ({ x: Number(match[1]), label: match[2] ?? "" }),
327
+ );
328
+
329
+ expect(valueTickLabels).toHaveLength(1);
330
+ });
331
+
332
+ it("compacts horizontal tick labels that would exceed the chart width", () => {
333
+ const markup = renderToStaticMarkup(
334
+ <DataChartRenderer
335
+ chart={{
336
+ type: "bar",
337
+ layout: "horizontal",
338
+ xKey: "day",
339
+ series: [{ key: "value", label: "Value" }],
340
+ data: [{ day: "High", value: 1e300 }],
341
+ valueAxisTickInterval: 1e299,
342
+ }}
343
+ />,
344
+ );
345
+ const valueTickLabels = Array.from(
346
+ markup.matchAll(
347
+ /<text x="([\d.]+)" y="340"[^>]*text-anchor="middle">([^<]+)<\/text>/g,
348
+ ),
349
+ (match) => ({ x: Number(match[1]), label: match[2] ?? "" }),
350
+ );
351
+
352
+ expect(valueTickLabels).toHaveLength(11);
353
+ expect(valueTickLabels[1]?.label).toBe("1e+299");
354
+ for (const tick of valueTickLabels) {
355
+ const estimatedWidth = tick.label.length * 11 * 0.65;
356
+ expect(tick.x - estimatedWidth / 2).toBeGreaterThanOrEqual(0);
357
+ expect(tick.x + estimatedWidth / 2).toBeLessThanOrEqual(640);
358
+ }
359
+ });
360
+
361
+ it("keeps extreme horizontal bar labels within the chart at large font sizes", () => {
362
+ const markup = renderToStaticMarkup(
363
+ <DataChartRenderer
364
+ chart={{
365
+ type: "bar",
366
+ layout: "horizontal",
367
+ xKey: "day",
368
+ series: [{ key: "value", label: "Value" }],
369
+ data: [
370
+ { day: "Low", value: -1.2345678901234567e300 },
371
+ { day: "Middle", value: 6.172839450617284e299 },
372
+ { day: "High", value: 1.2345678901234567e300 },
373
+ ],
374
+ showValueLabels: true,
375
+ valueLabelFontSize: 24,
376
+ valueAxisTickInterval: 1e299,
377
+ }}
378
+ />,
379
+ );
380
+ const valueLabels = Array.from(
381
+ markup.matchAll(
382
+ /<text x="([\d.-]+)" y="([\d.]+)"[^>]*font-size="24"[^>]*text-anchor="(start|end)">([^<]+)<\/text>/g,
383
+ ),
384
+ (match) => ({
385
+ x: Number(match[1]),
386
+ anchor: match[3],
387
+ label: match[4] ?? "",
388
+ }),
389
+ );
390
+
391
+ expect(valueLabels).toHaveLength(3);
392
+ for (const label of valueLabels) {
393
+ const estimatedWidth = label.label.length * 24 * 0.65;
394
+ const leftEdge =
395
+ label.anchor === "end" ? label.x - estimatedWidth : label.x;
396
+ const rightEdge =
397
+ label.anchor === "end" ? label.x : label.x + estimatedWidth;
398
+ expect(leftEdge).toBeGreaterThanOrEqual(0);
399
+ expect(rightEdge).toBeLessThanOrEqual(640);
400
+ }
401
+
402
+ const bars = Array.from(
403
+ markup.matchAll(/<rect x="([\d.-]+)" y="[\d.-]+" width="([\d.-]+)"/g),
404
+ (match) => ({ x: Number(match[1]), width: Number(match[2]) }),
405
+ );
406
+ expect(bars).toHaveLength(3);
407
+ for (const bar of bars) {
408
+ expect(bar.x).toBeGreaterThanOrEqual(0);
409
+ expect(bar.x + bar.width).toBeLessThanOrEqual(640);
410
+ }
411
+ });
412
+
413
+ it("formats the minimum supported interval without collapsing ticks", () => {
414
+ const markup = renderToStaticMarkup(
415
+ <DataChartRenderer
416
+ chart={{
417
+ type: "line",
418
+ xKey: "day",
419
+ series: [{ key: "value", label: "Value" }],
420
+ data: [
421
+ { day: "Monday", value: 0 },
422
+ { day: "Tuesday", value: 3e-12 },
423
+ ],
424
+ valueAxisTickInterval: 1e-12,
425
+ }}
426
+ />,
427
+ );
428
+ const tickLabels = Array.from(
429
+ markup.matchAll(/<text[^>]*text-anchor="end">([^<]+)<\/text>/g),
430
+ (match) => match[1],
431
+ );
432
+
433
+ expect(tickLabels).toEqual([
434
+ "0.000000000000",
435
+ "0.000000000001",
436
+ "0.000000000002",
437
+ "0.000000000003",
438
+ ]);
439
+ });
440
+
441
+ it("preserves precision for allowed intervals with fractional coefficients", () => {
442
+ const markup = renderToStaticMarkup(
443
+ <DataChartRenderer
444
+ chart={{
445
+ type: "line",
446
+ xKey: "day",
447
+ series: [{ key: "value", label: "Value" }],
448
+ data: [
449
+ { day: "Monday", value: 0 },
450
+ { day: "Tuesday", value: 3.6e-12 },
451
+ ],
452
+ valueAxisTickInterval: 1.2e-12,
453
+ }}
454
+ />,
455
+ );
456
+ const tickLabels = Array.from(
457
+ markup.matchAll(/<text[^>]*text-anchor="end">([^<]+)<\/text>/g),
458
+ (match) => match[1],
459
+ );
460
+
461
+ expect(tickLabels).toEqual([
462
+ "0.0000000000000",
463
+ "0.0000000000012",
464
+ "0.0000000000024",
465
+ "0.0000000000036",
466
+ ]);
467
+ });
468
+
469
+ it("compacts wide vertical ticks while keeping them clear of the axis title", () => {
470
+ const markup = renderToStaticMarkup(
471
+ <DataChartRenderer
472
+ chart={{
473
+ type: "line",
474
+ xKey: "day",
475
+ yAxisTitle: "Revenue",
476
+ series: [{ key: "value", label: "Value" }],
477
+ data: [
478
+ { day: "Low", value: 0 },
479
+ { day: "High", value: 1e300 },
480
+ ],
481
+ valueAxisTickInterval: 1e299,
482
+ }}
483
+ />,
484
+ );
485
+ const tickLabels = Array.from(
486
+ markup.matchAll(
487
+ /<text x="([\d.]+)" y="([\d.]+)"[^>]*text-anchor="end">([^<]+)<\/text>/g,
488
+ ),
489
+ (match) => ({
490
+ x: Number(match[1]),
491
+ y: Number(match[2]),
492
+ label: match[3] ?? "",
493
+ }),
494
+ );
495
+ const plotBounds = markup.match(
496
+ /<line x1="([^"]+)" x2="([^"]+)" y1="[^"]+" y2="[^"]+"/,
497
+ );
498
+
499
+ expect(tickLabels).toHaveLength(11);
500
+ expect(tickLabels[1]?.label).toBe("1e+299");
501
+ expect(plotBounds).not.toBeNull();
502
+ expect(
503
+ Number(plotBounds?.[2]) - Number(plotBounds?.[1]),
504
+ ).toBeGreaterThanOrEqual(32);
505
+ const title = markup.match(
506
+ /<text x="([\d.]+)" y="[\d.]+" transform="rotate\(-90 [\d.]+ [\d.]+\)"[^>]*font-size="([\d.]+)"[^>]*>Revenue<\/text>/,
507
+ );
508
+ expect(title).not.toBeNull();
509
+ const titleRight = Number(title?.[1]) + Number(title?.[2]) / 2;
510
+ for (const tick of tickLabels) {
511
+ const estimatedWidth = tick.label.length * 11 * 0.65;
512
+ expect(tick.x - estimatedWidth).toBeGreaterThanOrEqual(0);
513
+ expect(tick.x - estimatedWidth).toBeGreaterThanOrEqual(titleRight + 8);
514
+ expect(tick.x).toBeLessThanOrEqual(640);
515
+ expect(tick.y).toBeGreaterThanOrEqual(0);
516
+ expect(tick.y).toBeLessThanOrEqual(360);
517
+ }
518
+ });
519
+
520
+ it("keeps a usable plot width with large ticks and a large axis title", () => {
521
+ const markup = renderToStaticMarkup(
522
+ <DataChartRenderer
523
+ chart={{
524
+ type: "line",
525
+ xKey: "day",
526
+ yAxisTitle: "Revenue",
527
+ labelFontSize: 24,
528
+ axisTitleFontSize: 24,
529
+ series: [{ key: "value", label: "Value" }],
530
+ data: [
531
+ { day: "Low", value: -1e26 },
532
+ { day: "High", value: 0 },
533
+ ],
534
+ valueAxisTickInterval: 1e25,
535
+ }}
536
+ />,
537
+ );
538
+ const plotBounds = markup.match(
539
+ /<line x1="([^\"]+)" x2="([^\"]+)" y1="[^\"]+" y2="[^\"]+"/,
540
+ );
541
+
542
+ expect(plotBounds).not.toBeNull();
543
+ expect(
544
+ Number(plotBounds?.[2]) - Number(plotBounds?.[1]),
545
+ ).toBeGreaterThanOrEqual(32);
546
+ });
547
+
548
+ it("thins dense value labels without dropping grid lines or crowding endpoints", () => {
549
+ const horizontalMarkup = renderToStaticMarkup(
550
+ <DataChartRenderer
551
+ chart={{
552
+ type: "bar",
553
+ layout: "horizontal",
554
+ xKey: "day",
555
+ series: [{ key: "value", label: "Value" }],
556
+ data: [
557
+ { day: "First", value: 0 },
558
+ { day: "Last", value: 3 },
559
+ ],
560
+ valueAxisTickInterval: 0.04,
561
+ }}
562
+ />,
563
+ );
564
+ const horizontalLabels = Array.from(
565
+ horizontalMarkup.matchAll(
566
+ /<text x="([\d.]+)" y="340"[^>]*text-anchor="middle">([^<]+)<\/text>/g,
567
+ ),
568
+ (match) => ({ x: Number(match[1]), label: match[2] ?? "" }),
569
+ );
570
+ const horizontalBoxes = horizontalLabels
571
+ .map(({ x, label }) => ({
572
+ left: x - (label.length * 11 * 0.65) / 2,
573
+ right: x + (label.length * 11 * 0.65) / 2,
574
+ label,
575
+ }))
576
+ .sort((first, second) => first.left - second.left);
577
+
578
+ expect(horizontalLabels.length).toBeLessThan(76);
579
+ expect(horizontalLabels.map(({ label }) => label)).toContain("0.00");
580
+ expect(horizontalLabels.map(({ label }) => label)).toContain("3.00");
581
+ expect(horizontalMarkup.match(/<line /g)).toHaveLength(77);
582
+ expect(
583
+ horizontalBoxes.every(
584
+ (box, index) =>
585
+ index === 0 || horizontalBoxes[index - 1]!.right + 8 <= box.left,
586
+ ),
587
+ ).toBe(true);
588
+
589
+ const verticalMarkup = renderToStaticMarkup(
590
+ <DataChartRenderer
591
+ chart={{
592
+ type: "line",
593
+ xKey: "day",
594
+ series: [{ key: "value", label: "Value" }],
595
+ data: [
596
+ { day: "First", value: 0 },
597
+ { day: "Last", value: 3 },
598
+ ],
599
+ valueAxisTickInterval: 0.04,
600
+ }}
601
+ />,
602
+ );
603
+ const verticalLabels = Array.from(
604
+ verticalMarkup.matchAll(
605
+ /<text x="[\d.]+" y="([\d.]+)"[^>]*text-anchor="end">([^<]+)<\/text>/g,
606
+ ),
607
+ (match) => ({ y: Number(match[1]), label: match[2] ?? "" }),
608
+ ).sort((first, second) => first.y - second.y);
609
+
610
+ expect(verticalLabels.length).toBeLessThan(76);
611
+ expect(verticalLabels.map(({ label }) => label)).toContain("0.00");
612
+ expect(verticalLabels.map(({ label }) => label)).toContain("3.00");
613
+ expect(verticalMarkup.match(/<line /g)).toHaveLength(77);
614
+ expect(
615
+ verticalLabels.every(
616
+ (label, index) =>
617
+ index === 0 || label.y - verticalLabels[index - 1]!.y >= 15,
618
+ ),
619
+ ).toBe(true);
620
+ });
621
+
622
+ it("reserves room for custom decimal value-axis labels", () => {
623
+ const markup = renderToStaticMarkup(
624
+ <DataChartRenderer
625
+ chart={{
626
+ type: "bar",
627
+ xKey: "day",
628
+ series: [{ key: "value", label: "Value" }],
629
+ data: [{ day: "Monday", value: 0.0003 }],
630
+ valueAxisTickInterval: 0.0001,
631
+ }}
632
+ />,
633
+ );
634
+ const tickLabels = Array.from(
635
+ markup.matchAll(/<text x="([\d.]+)"[^>]*>(0\.000[123])<\/text>/g),
636
+ (match) => ({ x: Number(match[1]), label: match[2] }),
637
+ );
638
+
639
+ expect(tickLabels).toHaveLength(3);
640
+ expect(tickLabels.every(({ x }) => x >= 50)).toBe(true);
641
+ });
642
+
643
+ it("keeps grouped horizontal bars within slots at the maximum category gap", () => {
644
+ const markup = renderToStaticMarkup(
645
+ <DataChartRenderer
646
+ chart={{
647
+ type: "bar",
648
+ layout: "horizontal",
649
+ xKey: "day",
650
+ series: [
651
+ { key: "first", label: "First" },
652
+ { key: "second", label: "Second" },
653
+ ],
654
+ data: Array.from({ length: 10 }, (_, index) => ({
655
+ day: `Day ${index}`,
656
+ first: 1,
657
+ second: 2,
658
+ })),
659
+ horizontalCategoryGapRatio: 0.9,
660
+ }}
661
+ />,
662
+ );
663
+ const bars = Array.from(
664
+ markup.matchAll(
665
+ /<rect x="[^"]+" y="([\d.]+)" width="[^"]+" height="([\d.]+)"/g,
666
+ ),
667
+ (match) => ({ y: Number(match[1]), height: Number(match[2]) }),
668
+ );
669
+
670
+ expect(bars).toHaveLength(20);
671
+ expect(
672
+ bars.every(
673
+ (bar, index) =>
674
+ index === 0 || bars[index - 1].y + bars[index - 1].height <= bar.y,
675
+ ),
676
+ ).toBe(true);
677
+ });
678
+
679
+ it("keeps four automatic value-axis ticks when no step is set", () => {
680
+ const markup = renderToStaticMarkup(
681
+ <DataChartRenderer
682
+ chart={{
683
+ type: "bar",
684
+ layout: "horizontal",
685
+ xKey: "day",
686
+ series: [{ key: "value", label: "Value" }],
687
+ data: [{ day: "Monday", value: 3 }],
688
+ }}
689
+ />,
690
+ );
691
+ const numericLabels = Array.from(
692
+ markup.matchAll(/<text[^>]*>([^<]+)<\/text>/g),
693
+ (match) => match[1],
694
+ ).filter((label) => /^\d+(?:\.\d)?$/.test(label ?? ""));
695
+
696
+ expect(numericLabels).toEqual(["0", "1", "2", "3"]);
697
+ });
698
+
699
+ it("rejects intervals that would create an excessive number of ticks", () => {
700
+ expect(() =>
701
+ renderToStaticMarkup(
702
+ <DataChartRenderer
703
+ chart={{
704
+ type: "bar",
705
+ layout: "horizontal",
706
+ xKey: "day",
707
+ series: [{ key: "value", label: "Value" }],
708
+ data: [{ day: "Monday", value: 3 }],
709
+ valueAxisTickInterval: 0.001,
710
+ }}
711
+ />,
712
+ ),
713
+ ).toThrow("Chart value-axis interval exceeds the tick limit");
714
+ });
715
+
716
+ it("rejects non-finite value-axis intervals", () => {
717
+ expect(() =>
718
+ renderToStaticMarkup(
719
+ <DataChartRenderer
720
+ chart={{
721
+ type: "bar",
722
+ layout: "horizontal",
723
+ xKey: "day",
724
+ series: [{ key: "value", label: "Value" }],
725
+ data: [{ day: "Monday", value: 3 }],
726
+ valueAxisTickInterval: Number.POSITIVE_INFINITY,
727
+ }}
728
+ />,
729
+ ),
730
+ ).toThrow("Chart value-axis interval must be finite and positive");
731
+ });
732
+
733
+ it("reserves space for long horizontal category and value labels", () => {
734
+ const markup = renderToStaticMarkup(
735
+ <DataChartRenderer
736
+ chart={{
737
+ ...chart,
738
+ layout: "horizontal",
739
+ showValueLabels: true,
740
+ labelFontSize: 10,
741
+ valueLabelFontSize: 11,
742
+ data: [
743
+ {
744
+ day: "A very long category label",
745
+ first: 123456,
746
+ second: 2,
747
+ third: 3,
748
+ fourth: 4,
749
+ },
750
+ ],
751
+ }}
752
+ />,
753
+ );
754
+ const categoryLabel = markup.match(
755
+ /<text x="([\d.]+)" y="[^"]+" fill="hsl\(var\(--muted-foreground\)\)" font-size="10" text-anchor="end">A very long c\.\.\.<\/text>/,
756
+ );
757
+ const largestValueLabel = markup.match(
758
+ /<text x="([\d.]+)" y="[^"]+" fill="hsl\(var\(--muted-foreground\)\)" font-size="11" font-weight="400" text-anchor="start">123,456<\/text>/,
759
+ );
760
+
761
+ expect(categoryLabel).not.toBeNull();
762
+ expect(Number(categoryLabel?.[1])).toBeGreaterThan(100);
763
+ expect(largestValueLabel).not.toBeNull();
764
+ expect(Number(largestValueLabel?.[1])).toBeLessThan(590);
765
+ });
766
+
767
+ it("places negative horizontal value labels outside the bar's left edge", () => {
768
+ const markup = renderToStaticMarkup(
769
+ <DataChartRenderer
770
+ chart={{
771
+ type: "bar",
772
+ layout: "horizontal",
773
+ xKey: "day",
774
+ series: [{ key: "value", label: "Value" }],
775
+ data: [{ day: "Monday", value: -123456 }],
776
+ showValueLabels: true,
777
+ }}
778
+ />,
779
+ );
780
+ const negativeBarX = markup.match(/<rect x="([\d.]+)"/);
781
+ const negativeBarLabel = markup.match(
782
+ /<text x="([\d.]+)"[^>]*text-anchor="end">-123,456<\/text>/,
783
+ );
784
+ const categoryLabel = markup.match(
785
+ /<text x="([\d.]+)" y="[^"]+" fill="hsl\(var\(--muted-foreground\)\)" font-size="11" text-anchor="end">Monday<\/text>/,
786
+ );
787
+
788
+ expect(negativeBarX).not.toBeNull();
789
+ expect(negativeBarLabel).not.toBeNull();
790
+ expect(Number(negativeBarLabel?.[1])).toBeLessThan(
791
+ Number(negativeBarX?.[1]),
792
+ );
793
+ expect(categoryLabel).not.toBeNull();
794
+ expect(
795
+ Number(negativeBarLabel?.[1]) - Number(categoryLabel?.[1]),
796
+ ).toBeGreaterThan(60);
797
+ });
798
+
799
+ it.each(["line", "area"] as const)(
800
+ "renders requested value labels for %s charts",
801
+ (type) => {
802
+ const markup = renderToStaticMarkup(
803
+ <DataChartRenderer
804
+ chart={{
805
+ type,
806
+ xKey: "day",
807
+ series: [{ key: "value", label: "Value" }],
808
+ data: [
809
+ { day: "Monday", value: 4 },
810
+ { day: "Tuesday", value: 11 },
811
+ { day: "Wednesday", value: 7 },
812
+ ],
813
+ showValueLabels: true,
814
+ }}
815
+ />,
816
+ );
817
+
818
+ expect(markup).toContain(">7</text>");
819
+ expect(markup).toContain('font-weight="400"');
820
+ },
821
+ );
822
+
823
+ it.each(["line", "area"] as const)(
824
+ "keeps the final %s value label inside the chart bounds",
825
+ (type) => {
826
+ const markup = renderToStaticMarkup(
827
+ <DataChartRenderer
828
+ chart={{
829
+ type,
830
+ xKey: "day",
831
+ series: [{ key: "value", label: "Value" }],
832
+ data: [
833
+ { day: "Monday", value: 4 },
834
+ { day: "Tuesday", value: 11 },
835
+ { day: "Wednesday", value: 123456789 },
836
+ ],
837
+ showValueLabels: true,
838
+ }}
839
+ />,
840
+ );
841
+ const finalValueLabel = markup.match(
842
+ /<text x="([\d.]+)" y="[\d.]+" fill="hsl\(var\(--muted-foreground\)\)" font-size="11" font-weight="400" text-anchor="end">123,456,789<\/text>/,
843
+ );
844
+
845
+ expect(finalValueLabel).not.toBeNull();
846
+ expect(Number(finalValueLabel?.[1])).toBeLessThan(640);
847
+ },
848
+ );
849
+ });