orbitchat 2.14.0 → 3.0.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 (179) hide show
  1. package/dist/assets/ChartRenderer-B1mYF_kk.js +80 -0
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/MermaidRenderer-Dhffx5mc.js +260 -0
  62. package/dist/assets/MusicRenderer-DbXJ8e4-.js +18 -0
  63. package/dist/assets/SVGRenderer-EhamRX_K.js +6 -0
  64. package/dist/assets/_basePickBy-C2AibBqo.js +1 -0
  65. package/dist/assets/_baseUniq-D52pUSvn.js +1 -0
  66. package/dist/assets/arc-BSnyvXPh.js +1 -0
  67. package/dist/assets/architectureDiagram-VXUJARFQ-BLMvVsZu.js +36 -0
  68. package/dist/assets/band-CquvqAHh.js +1 -0
  69. package/dist/assets/blockDiagram-VD42YOAC-CTz0dv5u.js +122 -0
  70. package/dist/assets/c4Diagram-YG6GDRKO-BjbN-GGh.js +10 -0
  71. package/dist/assets/channel-ryTtfXZE.js +1 -0
  72. package/dist/assets/chunk-4BX2VUAB-DEMWGIsU.js +1 -0
  73. package/dist/assets/chunk-55IACEB6-BYaauHLT.js +1 -0
  74. package/dist/assets/chunk-B4BG7PRW-D3_GFcfd.js +165 -0
  75. package/dist/assets/chunk-DI55MBZ5-Dy2EuzfA.js +220 -0
  76. package/dist/assets/{chunk-FMBD7UC4-dPK7Boav-Cz7OoDLR.js → chunk-FMBD7UC4-C9XHmwza.js} +2 -2
  77. package/dist/assets/chunk-QN33PNHL-BOCltIi9.js +1 -0
  78. package/dist/assets/chunk-QZHKN3VN-CvA964eQ.js +1 -0
  79. package/dist/assets/chunk-TZMSLE5B-B-3rZz90.js +1 -0
  80. package/dist/assets/classDiagram-2ON5EDUG-VbShFIzz.js +1 -0
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-VbShFIzz.js +1 -0
  82. package/dist/assets/clone-BlhdKVDQ.js +1 -0
  83. package/dist/assets/cose-bilkent-S5V4N54A-CzPip3he.js +1 -0
  84. package/dist/assets/cytoscape.esm-CyJtwmzi.js +331 -0
  85. package/dist/assets/dagre-6UL2VRFP-CfCXYus3.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-PSM6KHXK-DyI2eDqp.js +24 -0
  88. package/dist/assets/diagram-QEK2KX5R-DuakIh40.js +43 -0
  89. package/dist/assets/diagram-S2PKOQOG-C3-4IORM.js +24 -0
  90. package/dist/assets/erDiagram-Q2GNP2WA-Dt-nZEaD.js +60 -0
  91. package/dist/assets/flowDiagram-NV44I4VS-B-2wjyTQ.js +162 -0
  92. package/dist/assets/ganttDiagram-JELNMOA3-CHTvjPq0.js +267 -0
  93. package/dist/assets/gitGraphDiagram-V2S2FVAM-d5GWmgY4.js +65 -0
  94. package/dist/assets/graph-COgkbiU8.js +1 -0
  95. package/dist/assets/index-DN2bBcCs.js +134 -0
  96. package/dist/assets/index-DtztdW2a.js +643 -0
  97. package/dist/assets/index-lGqyWNWb.css +1 -0
  98. package/dist/assets/infoDiagram-HS3SLOUP-30Hn0iLj.js +2 -0
  99. package/dist/assets/init-Dmth1JHB.js +1 -0
  100. package/dist/assets/journeyDiagram-XKPGCS4Q-CMOYXQ79.js +139 -0
  101. package/dist/assets/kanban-definition-3W4ZIXB7-dt3376cq.js +89 -0
  102. package/dist/assets/layout-DH97sy5g.js +1 -0
  103. package/dist/assets/linear-CKzVTQ0r.js +1 -0
  104. package/dist/assets/mindmap-definition-VGOIOE7T-BcZHPAlA.js +68 -0
  105. package/dist/assets/ordinal-DILIJJjt.js +1 -0
  106. package/dist/assets/pieDiagram-ADFJNKIX-DXjvgcgl.js +30 -0
  107. package/dist/assets/purify.es-A66Cw1IH.js +2 -0
  108. package/dist/assets/quadrantDiagram-AYHSOK5B-CICk2FdS.js +7 -0
  109. package/dist/assets/requirementDiagram-UZGBJVZJ-BdtxpQRA.js +64 -0
  110. package/dist/assets/sankeyDiagram-TZEHDZUN-BVNS9BQJ.js +10 -0
  111. package/dist/assets/sequenceDiagram-WL72ISMW-DF4fJTM7.js +145 -0
  112. package/dist/assets/stateDiagram-FKZM4ZOC-C8H8HujX.js +1 -0
  113. package/dist/assets/stateDiagram-v2-4FDKWEC3-DwtU033b.js +1 -0
  114. package/dist/assets/step-EjIQ8UIn.js +1 -0
  115. package/dist/assets/time-h5EapSZu.js +1 -0
  116. package/dist/assets/timeline-definition-IT6M3QCI-DUSG5Vfy.js +61 -0
  117. package/dist/assets/treemap-GDKQZRPO-1PI2PY4S.js +160 -0
  118. package/dist/assets/xychartDiagram-PRI3JC2R-CFPJj8WK.js +7 -0
  119. package/dist/favicon.svg +3 -1
  120. package/dist/index.html +2 -2
  121. package/markdown-renderer/LICENSE +201 -0
  122. package/markdown-renderer/src/CodeBlock.tsx +332 -0
  123. package/markdown-renderer/src/MarkdownComponents.tsx +233 -0
  124. package/markdown-renderer/src/MarkdownStyles.css +668 -0
  125. package/markdown-renderer/src/css.d.ts +4 -0
  126. package/markdown-renderer/src/index.ts +32 -0
  127. package/markdown-renderer/src/preprocessing.ts +519 -0
  128. package/markdown-renderer/src/renderers/ChartRenderer.tsx +1434 -0
  129. package/markdown-renderer/src/renderers/MermaidRenderer.tsx +474 -0
  130. package/markdown-renderer/src/renderers/MusicRenderer.tsx +394 -0
  131. package/markdown-renderer/src/renderers/SVGRenderer.tsx +307 -0
  132. package/markdown-renderer/src/types.ts +174 -0
  133. package/package.json +25 -3
  134. package/dist/assets/_baseUniq-BRKsqoH--68FUaYxk.js +0 -1
  135. package/dist/assets/arc-pab_su9s-BuY-VRZt.js +0 -1
  136. package/dist/assets/architectureDiagram-VXUJARFQ-DqQ8r_6g-C5b5VsO8.js +0 -36
  137. package/dist/assets/blockDiagram-VD42YOAC-B-dKfcH3-DVjqMJ-3.js +0 -122
  138. package/dist/assets/c4Diagram-YG6GDRKO-DMUPaBEl-o6ghjFV9.js +0 -10
  139. package/dist/assets/channel-HKsfPa5q-rOME8XF8.js +0 -1
  140. package/dist/assets/chunk-4BX2VUAB-CX67kh_B-Dl0Loq_e.js +0 -1
  141. package/dist/assets/chunk-55IACEB6-BocSyyvr-DYW83vZR.js +0 -1
  142. package/dist/assets/chunk-B4BG7PRW-CO8QAyfE-CsF1OX4R.js +0 -165
  143. package/dist/assets/chunk-DI55MBZ5-Dw1L6Eos-Dl1shJzL.js +0 -220
  144. package/dist/assets/chunk-QN33PNHL-vP2PqfVG-BuQNN8G2.js +0 -1
  145. package/dist/assets/chunk-QZHKN3VN-Bcidzu63-Bud1NKOb.js +0 -1
  146. package/dist/assets/chunk-TZMSLE5B-BtljMjlg-BKLxPJ-x.js +0 -1
  147. package/dist/assets/classDiagram-2ON5EDUG-C7cYN9hv-BHg-LSxQ.js +0 -1
  148. package/dist/assets/classDiagram-v2-WZHVMYZB-C7cYN9hv-BHg-LSxQ.js +0 -1
  149. package/dist/assets/clone-DoPb9X13-DejjKfXe.js +0 -1
  150. package/dist/assets/cose-bilkent-S5V4N54A-BGzO4EsH-rofdToAx.js +0 -1
  151. package/dist/assets/cytoscape.esm-CjI2IsL8-Da6dFVsf.js +0 -331
  152. package/dist/assets/dagre-6UL2VRFP-TzNvXCds-Ls1mVyc7.js +0 -4
  153. package/dist/assets/diagram-PSM6KHXK-BqY4RpUg-BJmYXUei.js +0 -24
  154. package/dist/assets/diagram-QEK2KX5R-CTjgBsne-dz-VLadE.js +0 -43
  155. package/dist/assets/diagram-S2PKOQOG-BqrhTIpA-ikxUE9Dj.js +0 -24
  156. package/dist/assets/erDiagram-Q2GNP2WA-B2hsi_Tl-B5gu6Jrx.js +0 -60
  157. package/dist/assets/flowDiagram-NV44I4VS-C03vtt_F-C9LVdVvn.js +0 -162
  158. package/dist/assets/ganttDiagram-JELNMOA3-B3hAg964-CYO6ZcQ8.js +0 -267
  159. package/dist/assets/gitGraphDiagram-NY62KEGX-ByhMH0yZ-BvO0WGzP.js +0 -65
  160. package/dist/assets/graph-BmNkcFEM-Bl2fiTgr.js +0 -1
  161. package/dist/assets/index-BXexqYFc-CFPIFV8r.js +0 -134
  162. package/dist/assets/index-KsGsuMGp.css +0 -1
  163. package/dist/assets/index-nqhgVFEP.js +0 -1010
  164. package/dist/assets/infoDiagram-WHAUD3N6-is6Ho4-T-D6tHdi3J.js +0 -2
  165. package/dist/assets/journeyDiagram-XKPGCS4Q-CRTOL26C-CL4DFaQP.js +0 -139
  166. package/dist/assets/kanban-definition-3W4ZIXB7-CNnO_t6O-DZBlsSao.js +0 -89
  167. package/dist/assets/layout-C0kZPebx-DiUdszUx.js +0 -1
  168. package/dist/assets/min-7Gb0pNxh-DsojA8pm.js +0 -1
  169. package/dist/assets/mindmap-definition-VGOIOE7T-CJZ2wTTa-Cd46k2jT.js +0 -68
  170. package/dist/assets/pieDiagram-ADFJNKIX-C9OSknjr-BGVcHmss.js +0 -30
  171. package/dist/assets/quadrantDiagram-AYHSOK5B-CW8yuAqv-CrCYjmJT.js +0 -7
  172. package/dist/assets/requirementDiagram-UZGBJVZJ-nGPhruO1-BzOGHXGi.js +0 -64
  173. package/dist/assets/sankeyDiagram-TZEHDZUN-CmL90u-m-BWp9STLO.js +0 -10
  174. package/dist/assets/sequenceDiagram-WL72ISMW-B02VRcnM-DB4Adljk.js +0 -145
  175. package/dist/assets/stateDiagram-FKZM4ZOC-DjoyLUdz-pDyQ50aU.js +0 -1
  176. package/dist/assets/stateDiagram-v2-4FDKWEC3-Bq76BTB7-C3j4gDpN.js +0 -1
  177. package/dist/assets/timeline-definition-IT6M3QCI-CTn0Gm3T-O1FhSixZ.js +0 -61
  178. package/dist/assets/treemap-KMMF4GRG-BjgLKKyi-DG8IQefJ.js +0 -128
  179. package/dist/assets/xychartDiagram-PRI3JC2R-Cgg6Uija-CPVch5-l.js +0 -7
@@ -0,0 +1,1434 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import {
3
+ Area,
4
+ AreaChart,
5
+ Bar,
6
+ BarChart,
7
+ CartesianGrid,
8
+ Cell,
9
+ ComposedChart,
10
+ Label,
11
+ Legend,
12
+ Line,
13
+ LineChart,
14
+ Pie,
15
+ PieChart,
16
+ ReferenceLine,
17
+ ResponsiveContainer,
18
+ Scatter,
19
+ ScatterChart,
20
+ Tooltip,
21
+ XAxis,
22
+ YAxis,
23
+ } from 'recharts';
24
+ import type { Formatter, NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
25
+ import type {
26
+ ChartConfig,
27
+ ChartDataItem,
28
+ ChartFormatterConfig,
29
+ ChartReferenceLineConfig,
30
+ ChartRendererProps,
31
+ ChartSeriesConfig,
32
+ } from '../types';
33
+
34
+ // Default color palette for charts
35
+ export const DEFAULT_COLORS = [
36
+ '#3b82f6',
37
+ '#8b5cf6',
38
+ '#ec4899',
39
+ '#f59e0b',
40
+ '#10b981',
41
+ '#06b6d4',
42
+ '#6366f1',
43
+ '#ef4444',
44
+ ];
45
+
46
+ const MONTH_ABBREVIATIONS: Record<string, string> = {
47
+ january: 'Jan',
48
+ february: 'Feb',
49
+ march: 'Mar',
50
+ april: 'Apr',
51
+ may: 'May',
52
+ june: 'Jun',
53
+ july: 'Jul',
54
+ august: 'Aug',
55
+ september: 'Sep',
56
+ october: 'Oct',
57
+ november: 'Nov',
58
+ december: 'Dec',
59
+ };
60
+
61
+ const CHART_THEME_VARS = {
62
+ axis: 'var(--md-chart-axis, #374151)',
63
+ grid: 'var(--md-chart-grid, #e5e7eb)',
64
+ text: 'var(--md-chart-text, #000000)',
65
+ tooltipBg: 'var(--md-chart-tooltip-bg, #ffffff)',
66
+ tooltipBorder: 'var(--md-chart-tooltip-border, #ccc)',
67
+ tooltipText: 'var(--md-chart-tooltip-text, #000000)',
68
+ secondaryText: 'var(--md-text-secondary, #4b5563)',
69
+ } as const;
70
+
71
+ type PartialChartConfig = Partial<ChartConfig> & {
72
+ labels?: string[];
73
+ };
74
+
75
+ interface NormalizedSeries extends ChartSeriesConfig {
76
+ color: string;
77
+ name: string;
78
+ type: 'bar' | 'line' | 'area' | 'scatter';
79
+ yAxisId: 'left' | 'right';
80
+ opacity: number;
81
+ stackId?: string;
82
+ strokeWidth?: number;
83
+ dot?: boolean;
84
+ }
85
+
86
+ const tryParseJSON = <T,>(value: string): T | null => {
87
+ try {
88
+ return JSON.parse(value);
89
+ } catch {
90
+ return null;
91
+ }
92
+ };
93
+
94
+ const stripQuotes = (value: string) => value.replace(/^['"]|['"]$/g, '');
95
+
96
+ const parseListValue = (value: string): string[] => {
97
+ const trimmed = value.trim();
98
+ if (!trimmed) return [];
99
+
100
+ const parsed = tryParseJSON<string[]>(trimmed);
101
+ if (parsed) return parsed;
102
+
103
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
104
+ const inner = trimmed.slice(1, -1);
105
+ return inner
106
+ .split(',')
107
+ .map((item) => stripQuotes(item.trim()))
108
+ .filter(Boolean);
109
+ }
110
+
111
+ return trimmed
112
+ .split(',')
113
+ .map((item) => stripQuotes(item.trim()))
114
+ .filter(Boolean);
115
+ };
116
+
117
+ const parseNumericValue = (value: string): number | undefined => {
118
+ const normalized = value.trim().replace(/px$/i, '');
119
+ if (!normalized) return undefined;
120
+ const parsed = Number(normalized);
121
+ return Number.isNaN(parsed) ? undefined : parsed;
122
+ };
123
+
124
+ const parseFormatterValue = (value: string): ChartFormatterConfig | undefined => {
125
+ const trimmed = value.trim();
126
+ if (!trimmed) return undefined;
127
+
128
+ const parsed = tryParseJSON<ChartFormatterConfig>(trimmed);
129
+ if (parsed) return parsed;
130
+
131
+ return { format: trimmed as ChartFormatterConfig['format'] };
132
+ };
133
+
134
+ const applyConfigLine = (config: PartialChartConfig, key: string, rawValue: string) => {
135
+ const trimmedKey = key.trim();
136
+ if (!trimmedKey) return;
137
+
138
+ const value = rawValue.trim();
139
+ if (!value.length) return;
140
+
141
+ const lowerKey = trimmedKey.toLowerCase();
142
+ const boolValue = value.toLowerCase();
143
+
144
+ const ensureFormatter = () => {
145
+ if (!config.formatter) config.formatter = {};
146
+ return config.formatter;
147
+ };
148
+
149
+ switch (lowerKey) {
150
+ case 'type':
151
+ config.type = value as ChartConfig['type'];
152
+ return;
153
+ case 'title':
154
+ config.title = value;
155
+ return;
156
+ case 'description':
157
+ config.description = value;
158
+ return;
159
+ case 'xaxislabel':
160
+ config.xAxisLabel = value;
161
+ return;
162
+ case 'yaxislabel':
163
+ config.yAxisLabel = value;
164
+ return;
165
+ case 'yaxisrightlabel':
166
+ config.yAxisRightLabel = value;
167
+ return;
168
+ case 'stacked':
169
+ config.stacked = boolValue === 'true';
170
+ return;
171
+ case 'showlegend':
172
+ config.showLegend = boolValue === 'true';
173
+ return;
174
+ case 'showgrid':
175
+ config.showGrid = boolValue === 'true';
176
+ return;
177
+ case 'height': {
178
+ const parsed = parseNumericValue(value);
179
+ if (typeof parsed === 'number') config.height = parsed;
180
+ return;
181
+ }
182
+ case 'width': {
183
+ const parsed = parseNumericValue(value);
184
+ if (typeof parsed === 'number') config.width = parsed;
185
+ return;
186
+ }
187
+ case 'xkey':
188
+ config.xKey = value;
189
+ return;
190
+ case 'xaxistype':
191
+ config.xAxisType = value === 'number' ? 'number' : 'category';
192
+ return;
193
+ case 'valueformat': {
194
+ const formatter = ensureFormatter();
195
+ formatter.format = value as ChartFormatterConfig['format'];
196
+ return;
197
+ }
198
+ case 'valueprefix': {
199
+ const formatter = ensureFormatter();
200
+ formatter.prefix = value;
201
+ return;
202
+ }
203
+ case 'valuesuffix': {
204
+ const formatter = ensureFormatter();
205
+ formatter.suffix = value;
206
+ return;
207
+ }
208
+ case 'valuecurrency': {
209
+ const formatter = ensureFormatter();
210
+ formatter.currency = value;
211
+ return;
212
+ }
213
+ case 'valuedecimals': {
214
+ const parsed = parseNumericValue(value);
215
+ if (typeof parsed === 'number') {
216
+ const formatter = ensureFormatter();
217
+ formatter.decimals = parsed;
218
+ }
219
+ return;
220
+ }
221
+ case 'formatter': {
222
+ const parsed = parseFormatterValue(value);
223
+ if (parsed) {
224
+ config.formatter = { ...config.formatter, ...parsed };
225
+ }
226
+ return;
227
+ }
228
+ case 'colors':
229
+ config.colors = parseListValue(value);
230
+ return;
231
+ case 'labels':
232
+ config.labels = parseListValue(value);
233
+ return;
234
+ case 'datakeys':
235
+ config.dataKeys = parseListValue(value);
236
+ return;
237
+ case 'data': {
238
+ const parsed = tryParseJSON<ChartDataItem[]>(value);
239
+ if (parsed) config.data = parsed;
240
+ return;
241
+ }
242
+ case 'series': {
243
+ const parsed = tryParseJSON<ChartSeriesConfig[]>(value);
244
+ if (parsed) config.series = parsed;
245
+ return;
246
+ }
247
+ case 'referencelines': {
248
+ const parsed = tryParseJSON<ChartReferenceLineConfig[]>(value);
249
+ if (parsed) config.referenceLines = parsed;
250
+ return;
251
+ }
252
+ default:
253
+ return;
254
+ }
255
+ };
256
+
257
+ export const parseChartConfig = (code: string, language: string): ChartConfig | null => {
258
+ try {
259
+ // Parse JSON format
260
+ if (language === 'chart-json') {
261
+ return JSON.parse(code) as ChartConfig;
262
+ }
263
+
264
+ const lines = code.trim().split('\n');
265
+ const config: PartialChartConfig = { colors: DEFAULT_COLORS };
266
+
267
+ // Check if it's table format
268
+ const hasTable = lines.some((line) => line.includes('|'));
269
+
270
+ if (hasTable) {
271
+ const tableLines = lines.filter((line) => line.includes('|'));
272
+
273
+ // More robust separator detection
274
+ const isSeparatorLine = (line: string) => {
275
+ const trimmed = line.trim();
276
+ if (!trimmed.includes('|')) return false;
277
+ const withoutPipesAndSpaces = trimmed.replace(/[|\s]/g, '');
278
+ const dashCount = (withoutPipesAndSpaces.match(/-/g) || []).length;
279
+ return dashCount > 0 && dashCount >= withoutPipesAndSpaces.length * 0.5;
280
+ };
281
+
282
+ // Find separator index to properly split header from data
283
+ const separatorIndex = tableLines.findIndex(isSeparatorLine);
284
+
285
+ // If no separator found yet, we're still streaming - return partial config
286
+ if (separatorIndex === -1) {
287
+ // Still set headers if we have them
288
+ if (tableLines.length > 0) {
289
+ const headers = tableLines[0]
290
+ .split('|')
291
+ .map((h) => h.trim())
292
+ .filter(Boolean);
293
+ if (headers.length > 0) {
294
+ config.xKey = headers[0];
295
+ config.dataKeys = headers.slice(1);
296
+ }
297
+ }
298
+ config.data = [];
299
+ } else {
300
+ const headers = tableLines[0]
301
+ .split('|')
302
+ .map((h) => h.trim())
303
+ .filter(Boolean);
304
+ const dataRows = tableLines.slice(separatorIndex + 1); // Skip everything up to and including separator
305
+
306
+ config.data = dataRows
307
+ .map((row) => {
308
+ // Skip separator-like rows that might appear in data
309
+ if (isSeparatorLine(row)) return null;
310
+
311
+ const values = row
312
+ .split('|')
313
+ .map((v) => v.trim())
314
+ .filter(Boolean);
315
+ if (!values.length) return null;
316
+
317
+ const obj: ChartDataItem = {};
318
+ headers.forEach((header, idx) => {
319
+ const value = values[idx];
320
+ if (typeof value === 'undefined') return;
321
+ obj[header] = value !== '' && !Number.isNaN(Number(value)) ? Number(value) : value;
322
+ });
323
+ return obj;
324
+ })
325
+ .filter(Boolean) as ChartDataItem[];
326
+
327
+ config.xKey = headers[0];
328
+ config.dataKeys = headers.slice(1);
329
+ }
330
+
331
+ for (const line of lines) {
332
+ if (line.includes('|')) break;
333
+ const [key, ...valueParts] = line.split(':');
334
+ if (!key || !valueParts.length) continue;
335
+ applyConfigLine(config, key, valueParts.join(':'));
336
+ }
337
+ } else {
338
+ for (const line of lines) {
339
+ const [key, ...valueParts] = line.split(':');
340
+ if (!key || !valueParts.length) continue;
341
+ applyConfigLine(config, key, valueParts.join(':'));
342
+ }
343
+
344
+ if (Array.isArray(config.data) && typeof config.data[0] === 'number') {
345
+ // Handle array of numbers - convert to objects with name/value pairs
346
+ const numericData = config.data as unknown as number[];
347
+ const labels =
348
+ config.labels || numericData.map((_, idx) => `Item ${idx + 1}`);
349
+ config.data = numericData.map((value, idx) => ({
350
+ name: labels[idx],
351
+ value,
352
+ }));
353
+ config.xKey = 'name';
354
+ config.dataKeys = ['value'];
355
+ }
356
+ }
357
+
358
+ config.data = config.data ?? [];
359
+ if (!config.colors || config.colors.length === 0) {
360
+ config.colors = DEFAULT_COLORS;
361
+ }
362
+
363
+ return config as ChartConfig;
364
+ } catch (err) {
365
+ console.error('Failed to parse chart config:', err);
366
+ return null;
367
+ }
368
+ };
369
+
370
+ const inferXAxisTypeFromData = (data: ChartDataItem[], xKey?: string): 'category' | 'number' => {
371
+ if (!Array.isArray(data) || !data.length) return 'category';
372
+ const key = xKey || 'name';
373
+ const values = data
374
+ .map((item) => (item && typeof item === 'object' ? item[key] : undefined))
375
+ .filter((value) => typeof value !== 'undefined');
376
+ if (!values.length) return 'category';
377
+ const allNumbers = values.every((value) => typeof value === 'number' && !Number.isNaN(value));
378
+ return allNumbers ? 'number' : 'category';
379
+ };
380
+
381
+ const formatValue = (value: unknown, formatter?: ChartFormatterConfig) => {
382
+ if (typeof value !== 'number' || Number.isNaN(value)) {
383
+ return value ?? '';
384
+ }
385
+
386
+ if (!formatter) {
387
+ return new Intl.NumberFormat().format(value);
388
+ }
389
+
390
+ const {
391
+ format = 'number',
392
+ currency = 'USD',
393
+ decimals,
394
+ minimumFractionDigits,
395
+ maximumFractionDigits,
396
+ prefix = '',
397
+ suffix = '',
398
+ } = formatter;
399
+
400
+ const options: Intl.NumberFormatOptions = {};
401
+
402
+ if (typeof decimals === 'number' && !Number.isNaN(decimals)) {
403
+ options.minimumFractionDigits = decimals;
404
+ options.maximumFractionDigits = decimals;
405
+ } else {
406
+ if (typeof minimumFractionDigits === 'number') {
407
+ options.minimumFractionDigits = minimumFractionDigits;
408
+ }
409
+ if (typeof maximumFractionDigits === 'number') {
410
+ options.maximumFractionDigits = maximumFractionDigits;
411
+ }
412
+ }
413
+
414
+ if (format === 'currency') {
415
+ options.style = 'currency';
416
+ options.currency = currency || 'USD';
417
+ } else if (format === 'percent') {
418
+ options.style = 'percent';
419
+ } else if (format === 'compact') {
420
+ options.notation = 'compact';
421
+ } else if (!options.maximumFractionDigits) {
422
+ options.maximumFractionDigits = 2;
423
+ }
424
+
425
+ const formatted = new Intl.NumberFormat(undefined, options).format(value);
426
+ return `${prefix}${formatted}${suffix}`;
427
+ };
428
+
429
+ const buildSeries = (config: ChartConfig, colors: string[]): NormalizedSeries[] => {
430
+ const fallbackSeries: ChartSeriesConfig[] =
431
+ config.dataKeys && config.dataKeys.length
432
+ ? config.dataKeys.map((key) => ({ key }))
433
+ : [{ key: 'value' }];
434
+
435
+ const baseSeries: ChartSeriesConfig[] = (config.series && config.series.length ? config.series : fallbackSeries).filter(
436
+ (series): series is ChartSeriesConfig => Boolean(series.key),
437
+ );
438
+
439
+ const defaultType: NormalizedSeries['type'] =
440
+ config.type === 'line'
441
+ ? 'line'
442
+ : config.type === 'area'
443
+ ? 'area'
444
+ : config.type === 'scatter'
445
+ ? 'scatter'
446
+ : 'bar';
447
+
448
+ return baseSeries.map((series, idx) => {
449
+ const resolvedType = config.type === 'composed' ? series.type || defaultType : defaultType;
450
+ return {
451
+ ...series,
452
+ type: resolvedType,
453
+ key: series.key as string,
454
+ name: series.name ?? (series.key as string),
455
+ color: series.color ?? colors[idx % colors.length],
456
+ yAxisId: (series.yAxisId ?? 'left') as 'left' | 'right',
457
+ stackId:
458
+ typeof series.stackId !== 'undefined'
459
+ ? series.stackId
460
+ : config.stacked
461
+ ? 'stack'
462
+ : undefined,
463
+ strokeWidth: series.strokeWidth ?? (resolvedType === 'line' ? 2 : 1),
464
+ dot: typeof series.dot === 'boolean' ? series.dot : true,
465
+ opacity: series.opacity ?? (resolvedType === 'area' ? 0.55 : 1),
466
+ };
467
+ });
468
+ };
469
+
470
+ const renderReferenceLines = (referenceLines?: ChartReferenceLineConfig[]) => {
471
+ if (!referenceLines?.length) return null;
472
+ return referenceLines
473
+ .filter((line) => typeof line.y !== 'undefined' || typeof line.x !== 'undefined')
474
+ .map((line, idx) => {
475
+ // Map position values to valid Recharts LabelPosition values
476
+ const mapPosition = (pos?: 'start' | 'middle' | 'end'): 'insideStart' | 'middle' | 'end' => {
477
+ if (pos === 'start') return 'insideStart';
478
+ if (pos === 'middle') return 'middle';
479
+ if (pos === 'end') return 'end';
480
+ return 'end';
481
+ };
482
+
483
+ return (
484
+ <ReferenceLine
485
+ key={`reference-${idx}`}
486
+ y={line.y}
487
+ x={line.x}
488
+ stroke={line.color || '#9ca3af'}
489
+ strokeDasharray={line.strokeDasharray || '4 4'}
490
+ label={
491
+ line.label
492
+ ? {
493
+ value: line.label,
494
+ position: mapPosition(line.position),
495
+ fill: line.color || '#4b5563',
496
+ }
497
+ : undefined
498
+ }
499
+ />
500
+ );
501
+ });
502
+ };
503
+
504
+ const inferAxisLabel = (series: NormalizedSeries[], axis: 'left' | 'right'): string | undefined => {
505
+ const axisSeries = series.filter((item) => item.yAxisId === axis);
506
+ if (!axisSeries.length) return undefined;
507
+ const labels = axisSeries
508
+ .map((item) => item.name || item.key)
509
+ .filter((name): name is string => Boolean(name));
510
+ if (!labels.length) return undefined;
511
+ const unique = Array.from(new Set(labels));
512
+ return unique.join(' / ');
513
+ };
514
+
515
+ const renderYAxisLabel = (value: string | undefined, orientation: 'left' | 'right') => {
516
+ if (!value) return null;
517
+ const offset = orientation === 'right' ? 20 : -20;
518
+ const position = orientation === 'right' ? 'insideRight' : 'insideLeft';
519
+ return (
520
+ <Label
521
+ value={value}
522
+ angle={orientation === 'right' ? 90 : -90}
523
+ position={position}
524
+ style={{ textAnchor: 'middle', fill: CHART_THEME_VARS.axis, fontSize: 12, fontWeight: 500 }}
525
+ offset={offset}
526
+ />
527
+ );
528
+ };
529
+
530
+ // Heuristics to detect if chart data appears incomplete/streaming
531
+ const isLikelyIncomplete = (code: string): boolean => {
532
+ // Check for incomplete JSON structures
533
+ const openBraces = (code.match(/\{/g) || []).length;
534
+ const closeBraces = (code.match(/\}/g) || []).length;
535
+ const openBrackets = (code.match(/\[/g) || []).length;
536
+ const closeBrackets = (code.match(/\]/g) || []).length;
537
+
538
+ if (openBraces !== closeBraces || openBrackets !== closeBrackets) {
539
+ return true;
540
+ }
541
+
542
+ const lines = code.split('\n');
543
+ const tableLines = lines.filter(line => line.trim().includes('|'));
544
+
545
+ if (tableLines.length > 0) {
546
+ // Check for table with header but no data rows yet
547
+ // A valid table needs: header row, separator row (---|---), and at least one data row
548
+ // More lenient separator detection - any line with mostly dashes and pipes
549
+ const isSeparatorLine = (line: string) => {
550
+ const trimmed = line.trim();
551
+ if (!trimmed.includes('|')) return false;
552
+ // Count dashes vs other chars (excluding pipes and spaces)
553
+ const withoutPipesAndSpaces = trimmed.replace(/[|\s]/g, '');
554
+ const dashCount = (withoutPipesAndSpaces.match(/-/g) || []).length;
555
+ // If more than 50% are dashes (and has some dashes), it's likely a separator
556
+ return dashCount > 0 && dashCount >= withoutPipesAndSpaces.length * 0.5;
557
+ };
558
+
559
+ const separatorLines = tableLines.filter(isSeparatorLine);
560
+ const nonSeparatorLines = tableLines.filter(line => !isSeparatorLine(line));
561
+
562
+ // If we have table content but no separator row yet, it's incomplete
563
+ if (nonSeparatorLines.length > 0 && separatorLines.length === 0) {
564
+ return true;
565
+ }
566
+
567
+ // If we have header + separator but no data rows, it's incomplete
568
+ if (separatorLines.length > 0 && nonSeparatorLines.length <= 1) {
569
+ return true;
570
+ }
571
+
572
+ // Check if last line ends with | but has fewer columns (incomplete row being typed)
573
+ const lastTableLine = tableLines[tableLines.length - 1].trim();
574
+ if (lastTableLine && !isSeparatorLine(lastTableLine)) {
575
+ const headerLine = nonSeparatorLines[0];
576
+ if (headerLine) {
577
+ const headerCols = headerLine.split('|').filter(s => s.trim()).length;
578
+ const lastRowCols = lastTableLine.split('|').filter(s => s.trim()).length;
579
+
580
+ // If last row has fewer columns than header, it's incomplete
581
+ if (lastRowCols > 0 && lastRowCols < headerCols) {
582
+ return true;
583
+ }
584
+ }
585
+ }
586
+ }
587
+
588
+ // Check for trailing incomplete key-value pairs (key: with nothing after)
589
+ const lastNonEmptyLine = lines.filter(l => l.trim()).pop() || '';
590
+ if (lastNonEmptyLine.match(/^\w+:\s*$/) && !lastNonEmptyLine.includes('|')) {
591
+ return true;
592
+ }
593
+
594
+ // Check if the last line looks like it's mid-typing (ends with partial content)
595
+ // e.g., "| January | 156 |" when more columns are expected
596
+ if (lastNonEmptyLine.endsWith('|') && tableLines.length > 0) {
597
+ // Could be mid-row, check if it's likely incomplete
598
+ const pipeCount = (lastNonEmptyLine.match(/\|/g) || []).length;
599
+ const headerPipes = tableLines[0] ? (tableLines[0].match(/\|/g) || []).length : 0;
600
+ if (pipeCount > 0 && pipeCount < headerPipes) {
601
+ return true;
602
+ }
603
+ }
604
+
605
+ return false;
606
+ };
607
+
608
+ export const ChartRenderer: React.FC<ChartRendererProps> = ({ code, language }) => {
609
+ const [error, setError] = useState<string | null>(null);
610
+ const [config, setConfig] = useState<ChartConfig | null>(null);
611
+ const [isStreaming, setIsStreaming] = useState(false);
612
+ const [isWaitingForData, setIsWaitingForData] = useState(false);
613
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
614
+ const lastCodeRef = useRef<string>('');
615
+ const lastUpdateTimeRef = useRef<number>(0);
616
+ const streamingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
617
+
618
+ useEffect(() => {
619
+ const now = Date.now();
620
+ const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
621
+ const codeChanged = code !== lastCodeRef.current;
622
+
623
+ // Update refs
624
+ lastCodeRef.current = code;
625
+ lastUpdateTimeRef.current = now;
626
+
627
+ // Check if data appears incomplete (streaming in progress)
628
+ const incomplete = isLikelyIncomplete(code);
629
+
630
+ // Detect rapid updates (streaming) - updates faster than 500ms apart
631
+ // LLM streaming can have variable timing, so we use a more generous threshold
632
+ const rapidUpdate = codeChanged && timeSinceLastUpdate < 500 && timeSinceLastUpdate > 0;
633
+ const likelyStreaming = incomplete || rapidUpdate;
634
+
635
+ // Parse the current code
636
+ const parsed = parseChartConfig(code, language);
637
+
638
+ // Validation - but handle differently if we're streaming
639
+ if (!parsed) {
640
+ if (likelyStreaming) {
641
+ // During streaming, show waiting state instead of error
642
+ setIsWaitingForData(true);
643
+ setIsStreaming(true);
644
+ setError(null);
645
+ setConfig(null);
646
+ } else {
647
+ setConfig(null);
648
+ setError('Failed to parse chart configuration');
649
+ setIsStreaming(false);
650
+ setIsWaitingForData(false);
651
+ }
652
+ return;
653
+ }
654
+
655
+ if (!Array.isArray(parsed.data) || parsed.data.length === 0) {
656
+ if (likelyStreaming) {
657
+ // During streaming with no data yet, show waiting state
658
+ setIsWaitingForData(true);
659
+ setIsStreaming(true);
660
+ setError(null);
661
+ setConfig(null);
662
+
663
+ // Clear any existing streaming timeout
664
+ if (streamingTimeoutRef.current) {
665
+ clearTimeout(streamingTimeoutRef.current);
666
+ }
667
+
668
+ // After 5 seconds of no valid data, show error (streaming likely failed)
669
+ streamingTimeoutRef.current = setTimeout(() => {
670
+ const currentParsed = parseChartConfig(lastCodeRef.current, language);
671
+ if (!currentParsed || !Array.isArray(currentParsed.data) || currentParsed.data.length === 0) {
672
+ setError('Chart data is empty');
673
+ setIsStreaming(false);
674
+ setIsWaitingForData(false);
675
+ }
676
+ }, 5000);
677
+ } else {
678
+ setConfig(null);
679
+ setError('Chart data is empty');
680
+ setIsStreaming(false);
681
+ setIsWaitingForData(false);
682
+ }
683
+ return;
684
+ }
685
+
686
+ if (!parsed.type) {
687
+ if (likelyStreaming) {
688
+ // Type not yet received during streaming
689
+ setIsWaitingForData(true);
690
+ setIsStreaming(true);
691
+ setError(null);
692
+ setConfig(null);
693
+ } else {
694
+ setConfig(null);
695
+ setError('Chart type is required (bar, line, pie, area, scatter, composed)');
696
+ setIsStreaming(false);
697
+ setIsWaitingForData(false);
698
+ }
699
+ return;
700
+ }
701
+
702
+ // Clear waiting state - we have valid data now
703
+ setIsWaitingForData(false);
704
+
705
+ // Clear streaming timeout if we got valid data
706
+ if (streamingTimeoutRef.current) {
707
+ clearTimeout(streamingTimeoutRef.current);
708
+ streamingTimeoutRef.current = null;
709
+ }
710
+
711
+ if (likelyStreaming) {
712
+ setIsStreaming(true);
713
+
714
+ // Clear any existing debounce timer
715
+ if (debounceTimerRef.current) {
716
+ clearTimeout(debounceTimerRef.current);
717
+ }
718
+
719
+ // Debounce: wait for data to stabilize before final render
720
+ // Use longer debounce (400ms) to handle LLM streaming variability
721
+ debounceTimerRef.current = setTimeout(() => {
722
+ setIsStreaming(false);
723
+ // Re-parse in case code changed during debounce
724
+ const finalParsed = parseChartConfig(code, language);
725
+ if (finalParsed && Array.isArray(finalParsed.data) && finalParsed.data.length > 0) {
726
+ setError(null);
727
+ setConfig(finalParsed);
728
+ }
729
+ }, 400);
730
+
731
+ // Show partial data while streaming (but still set it)
732
+ setError(null);
733
+ setConfig(parsed);
734
+ } else {
735
+ // Data is complete and not rapidly updating - render immediately
736
+ setIsStreaming(false);
737
+ setError(null);
738
+ setConfig(parsed);
739
+ }
740
+
741
+ // Cleanup
742
+ return () => {
743
+ if (debounceTimerRef.current) {
744
+ clearTimeout(debounceTimerRef.current);
745
+ }
746
+ if (streamingTimeoutRef.current) {
747
+ clearTimeout(streamingTimeoutRef.current);
748
+ }
749
+ };
750
+ }, [code, language]);
751
+
752
+ if (error) {
753
+ return (
754
+ <div className="graph-error">
755
+ <div className="graph-error-title">Chart Rendering Error</div>
756
+ <div className="graph-error-message">{error}</div>
757
+ <pre style={{ marginTop: '8px', fontSize: '0.8em', opacity: 0.7 }}>
758
+ <code>{code}</code>
759
+ </pre>
760
+ </div>
761
+ );
762
+ }
763
+
764
+ if (!config) {
765
+ return (
766
+ <div className="graph-container chart-container">
767
+ <div style={{
768
+ display: 'flex',
769
+ flexDirection: 'column',
770
+ alignItems: 'center',
771
+ justifyContent: 'center',
772
+ padding: '40px 20px',
773
+ color: 'var(--md-text-secondary, #6b7280)',
774
+ minHeight: '200px',
775
+ }}>
776
+ <svg
777
+ style={{
778
+ animation: 'spin 1s linear infinite',
779
+ marginBottom: '12px',
780
+ width: '32px',
781
+ height: '32px',
782
+ color: isWaitingForData ? '#3b82f6' : 'currentColor',
783
+ }}
784
+ viewBox="0 0 24 24"
785
+ fill="none"
786
+ >
787
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
788
+ </svg>
789
+ <span style={{ fontWeight: 500 }}>
790
+ {isWaitingForData ? 'Receiving chart data...' : 'Loading chart...'}
791
+ </span>
792
+ {isWaitingForData && (
793
+ <span style={{ fontSize: '0.85em', marginTop: '4px', opacity: 0.7 }}>
794
+ Waiting for complete data from stream
795
+ </span>
796
+ )}
797
+ </div>
798
+ <style>{`
799
+ @keyframes spin {
800
+ from { transform: rotate(0deg); }
801
+ to { transform: rotate(360deg); }
802
+ }
803
+ `}</style>
804
+ </div>
805
+ );
806
+ }
807
+
808
+ const colors = config.colors && config.colors.length ? config.colors : DEFAULT_COLORS;
809
+ const height = config.height || 320;
810
+ const derivedSeries = buildSeries(config, colors);
811
+
812
+ const hasRightAxis = derivedSeries.some((series) => series.yAxisId === 'right');
813
+ const showLegend = config.showLegend ?? (config.type === 'pie' || derivedSeries.length > 1);
814
+ const showGrid = config.showGrid ?? true;
815
+
816
+ const referenceLineElements = renderReferenceLines(config.referenceLines);
817
+ const inferredLeftLabel = inferAxisLabel(derivedSeries, 'left');
818
+ const inferredRightLabel = inferAxisLabel(derivedSeries, 'right');
819
+ const leftAxisLabelText = config.yAxisLabel ?? inferredLeftLabel ?? 'Value';
820
+ const rightAxisLabelText = hasRightAxis
821
+ ? config.yAxisRightLabel ?? inferredRightLabel ?? 'Value'
822
+ : undefined;
823
+
824
+ // Use CSS custom properties for theme-aware styling
825
+ // These will be read from the computed styles of the container
826
+ const axisColor = CHART_THEME_VARS.axis;
827
+ const gridColor = CHART_THEME_VARS.grid;
828
+ const textColor = CHART_THEME_VARS.text;
829
+ const tooltipTextColor = CHART_THEME_VARS.tooltipText;
830
+ const tooltipBgColor = CHART_THEME_VARS.tooltipBg;
831
+ const tooltipBorderColor = CHART_THEME_VARS.tooltipBorder;
832
+ const secondaryTextColor = CHART_THEME_VARS.secondaryText;
833
+ const axisStylingProps = {
834
+ tick: { fill: axisColor },
835
+ axisLine: { stroke: axisColor },
836
+ tickLine: { stroke: axisColor },
837
+ };
838
+ const xAxisType =
839
+ config.xAxisType ??
840
+ (config.type === 'scatter' ? 'number' : inferXAxisTypeFromData(config.data, config.xKey));
841
+ const isCategoryXAxis = xAxisType === 'category';
842
+
843
+ // Calculate data point count and determine label rotation/truncation needs
844
+ const dataPointCount = config.data?.length || 0;
845
+
846
+ // Analyze actual label lengths for smarter truncation decisions
847
+ const analyzeLabelLengths = (): { maxLength: number; avgLength: number } => {
848
+ if (!isCategoryXAxis || !config.data || dataPointCount === 0) {
849
+ return { maxLength: 0, avgLength: 0 };
850
+ }
851
+ const xKey = config.xKey || 'name';
852
+ const lengths = config.data
853
+ .map((item: ChartDataItem) => {
854
+ const value = item && typeof item === 'object' ? item[xKey] : undefined;
855
+ return value ? String(value).length : 0;
856
+ })
857
+ .filter((len: number) => len > 0);
858
+
859
+ if (lengths.length === 0) return { maxLength: 0, avgLength: 0 };
860
+
861
+ const maxLength = Math.max(...lengths);
862
+ const avgLength = lengths.reduce((sum: number, len: number) => sum + len, 0) / lengths.length;
863
+ return { maxLength, avgLength };
864
+ };
865
+
866
+ const labelAnalysis = analyzeLabelLengths();
867
+ const shouldRotateLabels = isCategoryXAxis && dataPointCount > 6;
868
+ // Only truncate if we have many data points AND labels are actually long
869
+ const shouldTruncateLabels = isCategoryXAxis && dataPointCount > 4 && labelAnalysis.avgLength > 15;
870
+ const maxLabelLength = shouldTruncateLabels ? (dataPointCount > 10 ? 12 : 20) : Infinity;
871
+ const labelLengthForSizing = shouldTruncateLabels
872
+ ? (dataPointCount > 10 ? 12 : 20)
873
+ : labelAnalysis.maxLength || 0;
874
+
875
+ // Calculate interval to show fewer labels when there are many
876
+ // With rotated labels, we can show more labels before needing intervals
877
+ const calculateInterval = (count: number): number => {
878
+ if (count <= 12) return 0; // Show all labels (common case like 12 months)
879
+ if (count <= 16) return 1; // Show every other label
880
+ if (count <= 24) return Math.floor(count / 12); // Show ~12 labels
881
+ return Math.floor(count / 15); // Show ~15 labels max for very large datasets
882
+ };
883
+
884
+ const labelInterval = isCategoryXAxis ? calculateInterval(dataPointCount) : 0;
885
+
886
+ const estimateTickLabelHeight = () => {
887
+ if (!isCategoryXAxis || dataPointCount === 0) return 24;
888
+ const baseHeight = 24;
889
+ if (!shouldRotateLabels) {
890
+ const longLabelBonus = Math.max(labelLengthForSizing - 12, 0) * 1.5;
891
+ return Math.min(baseHeight + longLabelBonus, 48);
892
+ }
893
+ const approxCharWidth = 6.5;
894
+ const rotationRadians = (45 * Math.PI) / 180;
895
+ const approxWidth = labelLengthForSizing * approxCharWidth;
896
+ const rotatedHeight = Math.sin(rotationRadians) * approxWidth;
897
+ return Math.min(Math.max(baseHeight, rotatedHeight + 12), 140);
898
+ };
899
+
900
+ const estimatedTickLabelHeight = estimateTickLabelHeight();
901
+ const xAxisHeight = shouldRotateLabels
902
+ ? Math.min(Math.max(estimatedTickLabelHeight + 16, 80), 160)
903
+ : Math.max(estimatedTickLabelHeight + 12, 36);
904
+
905
+ // Note: Don't set scale: 'band' explicitly - Recharts handles this automatically
906
+ // for bar charts, and setting it can interfere with rendering in some cases
907
+ const categoricalXAxisProps = {
908
+ type: 'category' as const,
909
+ interval: labelInterval as number,
910
+ allowDuplicatedCategory: false,
911
+ padding: { left: 16, right: 16 },
912
+ angle: shouldRotateLabels ? -45 : 0,
913
+ textAnchor: shouldRotateLabels ? 'end' as const : 'middle' as const,
914
+ height: xAxisHeight,
915
+ };
916
+ const numericXAxisProps = {
917
+ type: 'number' as const,
918
+ domain: ['dataMin', 'dataMax'] as const,
919
+ allowDuplicatedCategory: true,
920
+ };
921
+ const xAxisProps = isCategoryXAxis ? categoricalXAxisProps : numericXAxisProps;
922
+
923
+ const tooltipStyle = {
924
+ backgroundColor: tooltipBgColor,
925
+ border: `1px solid ${tooltipBorderColor}`,
926
+ borderRadius: '4px',
927
+ padding: '8px',
928
+ color: tooltipTextColor,
929
+ };
930
+
931
+ const tooltipLabelStyle = {
932
+ color: tooltipTextColor,
933
+ fontWeight: 600,
934
+ marginBottom: '4px',
935
+ };
936
+
937
+ const tooltipItemStyle = {
938
+ color: tooltipTextColor,
939
+ };
940
+
941
+ // Cursor style for hover highlight - semi-transparent for better UX
942
+ const tooltipCursor = {
943
+ fill: gridColor,
944
+ fillOpacity: 0.3,
945
+ };
946
+
947
+ const tooltipFormatter: Formatter<ValueType, NameType> = (value, name) => {
948
+ const safeName = (name ?? '') as NameType;
949
+ if (typeof value === 'number') {
950
+ const formatted = formatValue(value, config.formatter);
951
+ return [String(formatted ?? value), safeName];
952
+ }
953
+ if (Array.isArray(value)) {
954
+ return [value.join(', '), safeName];
955
+ }
956
+ if (typeof value === 'string') {
957
+ return [value, safeName];
958
+ }
959
+ return ['', safeName];
960
+ };
961
+
962
+ const axisTickFormatter = (value: unknown) => {
963
+ if (typeof value !== 'number') return String(value ?? '');
964
+ const formatted = formatValue(value, config.formatter);
965
+ return String(formatted ?? '');
966
+ };
967
+
968
+ const formatCategoryLabel = (value: unknown) => {
969
+ if (typeof value !== 'string') return String(value ?? '');
970
+ const normalized = value.trim().toLowerCase();
971
+ const monthAbbrev = MONTH_ABBREVIATIONS[normalized];
972
+ if (monthAbbrev) return monthAbbrev;
973
+ return value;
974
+ };
975
+
976
+ const truncateLabel = (label: string, maxLength: number): string => {
977
+ if (!label || typeof label !== 'string') return String(label ?? '');
978
+ if (label.length <= maxLength) return label;
979
+ // Truncate at word boundary when possible for better readability
980
+ const truncated = label.substring(0, maxLength - 3);
981
+ const lastSpace = truncated.lastIndexOf(' ');
982
+ if (lastSpace > maxLength * 0.6) {
983
+ // If we can truncate at a word boundary without losing too much, do it
984
+ return truncated.substring(0, lastSpace) + '...';
985
+ }
986
+ return truncated + '...';
987
+ };
988
+
989
+ const xAxisTickFormatter = (value: unknown) => {
990
+ if (!isCategoryXAxis) return axisTickFormatter(value);
991
+ const formatted = formatCategoryLabel(value);
992
+ return shouldTruncateLabels ? truncateLabel(formatted, maxLabelLength) : formatted;
993
+ };
994
+
995
+ // Tooltip label formatter: always show full original label, even if axis label is truncated
996
+ const tooltipLabelFormatter = (label: unknown) => {
997
+ if (!isCategoryXAxis) return String(label ?? '');
998
+ // Return the original formatted label (before truncation) for tooltip
999
+ return formatCategoryLabel(label);
1000
+ };
1001
+
1002
+ const xAxisHasLabel = Boolean(config.xAxisLabel);
1003
+
1004
+ // Keep margins modest so the plotting area stays visible even with rotated labels
1005
+ const baseBottomMargin = 16;
1006
+ const axisLabelSpace = xAxisHasLabel ? 28 : 10;
1007
+ const legendSpace = showLegend ? 26 : 6;
1008
+ const rotatedPadding = shouldRotateLabels
1009
+ ? Math.min(Math.max(estimatedTickLabelHeight - 48, 0), 28)
1010
+ : 0;
1011
+ // Extra space when both rotated labels and axis label are present to prevent overlap
1012
+ const rotatedWithLabelExtra = shouldRotateLabels && xAxisHasLabel ? 16 : 0;
1013
+ const bottomMargin = baseBottomMargin + axisLabelSpace + legendSpace + rotatedPadding + rotatedWithLabelExtra;
1014
+
1015
+ const chartMargin = {
1016
+ left: leftAxisLabelText ? 80 : 10,
1017
+ right: rightAxisLabelText ? 80 : 10,
1018
+ top: 10,
1019
+ bottom: bottomMargin,
1020
+ };
1021
+
1022
+ // Calculate offset based on rotated label height to prevent overlap
1023
+ const xAxisLabelOffset = shouldRotateLabels
1024
+ ? Math.min(Math.max(estimatedTickLabelHeight - 60, 0), 36)
1025
+ : 0;
1026
+ const xAxisLabel = xAxisHasLabel
1027
+ ? {
1028
+ value: config.xAxisLabel,
1029
+ position: 'bottom' as const,
1030
+ offset: xAxisLabelOffset,
1031
+ }
1032
+ : undefined;
1033
+
1034
+ // When labels are long and rotated, they squeeze the legend horizontally
1035
+ // Use vertical layout for legend in those cases
1036
+ const useLegendVerticalLayout = shouldRotateLabels && labelAnalysis.avgLength > 20;
1037
+
1038
+ const legendWrapperStyle: React.CSSProperties = {
1039
+ color: textColor,
1040
+ marginTop: xAxisHasLabel ? 16 + xAxisLabelOffset : 12,
1041
+ paddingTop: shouldRotateLabels ? Math.min(Math.max(estimatedTickLabelHeight - 50, 8), 30) : 8,
1042
+ display: 'flex',
1043
+ justifyContent: 'center',
1044
+ gap: useLegendVerticalLayout ? '8px' : '16px',
1045
+ flexDirection: useLegendVerticalLayout ? 'column' : 'row',
1046
+ alignItems: useLegendVerticalLayout ? 'center' : undefined,
1047
+ };
1048
+
1049
+ return (
1050
+ <div
1051
+ className="graph-container chart-container"
1052
+ style={{ flexDirection: 'column', alignItems: 'stretch', position: 'relative' }}
1053
+ >
1054
+ {isStreaming && (
1055
+ <div
1056
+ style={{
1057
+ position: 'absolute',
1058
+ top: '8px',
1059
+ right: '8px',
1060
+ display: 'flex',
1061
+ alignItems: 'center',
1062
+ padding: '4px 8px',
1063
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
1064
+ borderRadius: '4px',
1065
+ fontSize: '12px',
1066
+ color: '#3b82f6',
1067
+ zIndex: 10,
1068
+ }}
1069
+ >
1070
+ <svg
1071
+ style={{
1072
+ animation: 'spin 1s linear infinite',
1073
+ marginRight: '4px',
1074
+ width: '12px',
1075
+ height: '12px'
1076
+ }}
1077
+ viewBox="0 0 24 24"
1078
+ fill="none"
1079
+ >
1080
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
1081
+ </svg>
1082
+ Updating...
1083
+ <style>{`
1084
+ @keyframes spin {
1085
+ from { transform: rotate(0deg); }
1086
+ to { transform: rotate(360deg); }
1087
+ }
1088
+ `}</style>
1089
+ </div>
1090
+ )}
1091
+ {config.title && (
1092
+ <h4
1093
+ style={{
1094
+ textAlign: 'center',
1095
+ marginBottom: config.description ? '4px' : '12px',
1096
+ marginTop: 0,
1097
+ color: textColor,
1098
+ fontWeight: 600,
1099
+ background: 'transparent',
1100
+ }}
1101
+ >
1102
+ {config.title}
1103
+ </h4>
1104
+ )}
1105
+ {config.description && (
1106
+ <p
1107
+ style={{
1108
+ textAlign: 'center',
1109
+ marginTop: 0,
1110
+ marginBottom: '12px',
1111
+ color: secondaryTextColor,
1112
+ fontSize: '0.9rem',
1113
+ }}
1114
+ >
1115
+ {config.description}
1116
+ </p>
1117
+ )}
1118
+ <ResponsiveContainer width="100%" height={height}>
1119
+ {config.type === 'bar' && (
1120
+ <BarChart data={config.data} margin={chartMargin} barCategoryGap="20%">
1121
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1122
+ <XAxis
1123
+ dataKey={config.xKey || 'name'}
1124
+ label={xAxisLabel}
1125
+ {...axisStylingProps}
1126
+ {...xAxisProps}
1127
+ tickFormatter={xAxisTickFormatter}
1128
+ />
1129
+ <YAxis
1130
+ yAxisId="left"
1131
+ width={80}
1132
+ tickFormatter={axisTickFormatter}
1133
+ {...axisStylingProps}
1134
+ >
1135
+ {renderYAxisLabel(leftAxisLabelText, 'left')}
1136
+ </YAxis>
1137
+ {hasRightAxis && (
1138
+ <YAxis
1139
+ yAxisId="right"
1140
+ orientation="right"
1141
+ width={80}
1142
+ tickFormatter={axisTickFormatter}
1143
+ {...axisStylingProps}
1144
+ >
1145
+ {renderYAxisLabel(rightAxisLabelText, 'right')}
1146
+ </YAxis>
1147
+ )}
1148
+ <Tooltip
1149
+ contentStyle={tooltipStyle}
1150
+ labelStyle={tooltipLabelStyle}
1151
+ itemStyle={tooltipItemStyle}
1152
+ formatter={tooltipFormatter}
1153
+ labelFormatter={tooltipLabelFormatter}
1154
+ cursor={tooltipCursor}
1155
+ />
1156
+ {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1157
+ {referenceLineElements}
1158
+ {derivedSeries.map((series) => (
1159
+ <Bar
1160
+ name={series.name}
1161
+ key={series.key}
1162
+ dataKey={series.key}
1163
+ fill={series.color}
1164
+ stackId={series.stackId}
1165
+ yAxisId={series.yAxisId}
1166
+ radius={[4, 4, 0, 0]}
1167
+ />
1168
+ ))}
1169
+ </BarChart>
1170
+ )}
1171
+
1172
+ {config.type === 'line' && (
1173
+ <LineChart data={config.data} margin={chartMargin}>
1174
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1175
+ <XAxis
1176
+ dataKey={config.xKey || 'name'}
1177
+ label={xAxisLabel}
1178
+ {...axisStylingProps}
1179
+ {...xAxisProps}
1180
+ tickFormatter={xAxisTickFormatter}
1181
+ />
1182
+ <YAxis
1183
+ yAxisId="left"
1184
+ width={80}
1185
+ tickFormatter={axisTickFormatter}
1186
+ {...axisStylingProps}
1187
+ >
1188
+ {renderYAxisLabel(leftAxisLabelText, 'left')}
1189
+ </YAxis>
1190
+ {hasRightAxis && (
1191
+ <YAxis
1192
+ yAxisId="right"
1193
+ orientation="right"
1194
+ width={80}
1195
+ tickFormatter={axisTickFormatter}
1196
+ {...axisStylingProps}
1197
+ >
1198
+ {renderYAxisLabel(rightAxisLabelText, 'right')}
1199
+ </YAxis>
1200
+ )}
1201
+ <Tooltip
1202
+ contentStyle={tooltipStyle}
1203
+ labelStyle={tooltipLabelStyle}
1204
+ itemStyle={tooltipItemStyle}
1205
+ formatter={tooltipFormatter}
1206
+ labelFormatter={tooltipLabelFormatter}
1207
+ cursor={tooltipCursor}
1208
+ />
1209
+ {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1210
+ {referenceLineElements}
1211
+ {derivedSeries.map((series) => (
1212
+ <Line
1213
+ name={series.name}
1214
+ key={series.key}
1215
+ type="monotone"
1216
+ dataKey={series.key}
1217
+ stroke={series.color}
1218
+ yAxisId={series.yAxisId}
1219
+ strokeWidth={series.strokeWidth}
1220
+ dot={series.dot}
1221
+ />
1222
+ ))}
1223
+ </LineChart>
1224
+ )}
1225
+
1226
+ {config.type === 'area' && (
1227
+ <AreaChart data={config.data} margin={chartMargin}>
1228
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1229
+ <XAxis
1230
+ dataKey={config.xKey || 'name'}
1231
+ label={xAxisLabel}
1232
+ {...axisStylingProps}
1233
+ {...xAxisProps}
1234
+ tickFormatter={xAxisTickFormatter}
1235
+ />
1236
+ <YAxis
1237
+ yAxisId="left"
1238
+ width={80}
1239
+ tickFormatter={axisTickFormatter}
1240
+ {...axisStylingProps}
1241
+ >
1242
+ {renderYAxisLabel(leftAxisLabelText, 'left')}
1243
+ </YAxis>
1244
+ {hasRightAxis && (
1245
+ <YAxis
1246
+ yAxisId="right"
1247
+ orientation="right"
1248
+ width={80}
1249
+ tickFormatter={axisTickFormatter}
1250
+ {...axisStylingProps}
1251
+ >
1252
+ {renderYAxisLabel(rightAxisLabelText, 'right')}
1253
+ </YAxis>
1254
+ )}
1255
+ <Tooltip
1256
+ contentStyle={tooltipStyle}
1257
+ labelStyle={tooltipLabelStyle}
1258
+ itemStyle={tooltipItemStyle}
1259
+ formatter={tooltipFormatter}
1260
+ labelFormatter={tooltipLabelFormatter}
1261
+ cursor={tooltipCursor}
1262
+ />
1263
+ {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1264
+ {referenceLineElements}
1265
+ {derivedSeries.map((series) => (
1266
+ <Area
1267
+ name={series.name}
1268
+ key={series.key}
1269
+ type="monotone"
1270
+ dataKey={series.key}
1271
+ stroke={series.color}
1272
+ yAxisId={series.yAxisId}
1273
+ fill={series.color}
1274
+ fillOpacity={series.opacity}
1275
+ stackId={series.stackId}
1276
+ />
1277
+ ))}
1278
+ </AreaChart>
1279
+ )}
1280
+
1281
+ {config.type === 'composed' && (
1282
+ <ComposedChart data={config.data} margin={chartMargin}>
1283
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1284
+ <XAxis
1285
+ dataKey={config.xKey || 'name'}
1286
+ label={xAxisLabel}
1287
+ {...axisStylingProps}
1288
+ {...xAxisProps}
1289
+ tickFormatter={xAxisTickFormatter}
1290
+ />
1291
+ <YAxis
1292
+ yAxisId="left"
1293
+ width={80}
1294
+ tickFormatter={axisTickFormatter}
1295
+ {...axisStylingProps}
1296
+ >
1297
+ {renderYAxisLabel(leftAxisLabelText, 'left')}
1298
+ </YAxis>
1299
+ {hasRightAxis && (
1300
+ <YAxis
1301
+ yAxisId="right"
1302
+ orientation="right"
1303
+ width={80}
1304
+ tickFormatter={axisTickFormatter}
1305
+ {...axisStylingProps}
1306
+ >
1307
+ {renderYAxisLabel(rightAxisLabelText, 'right')}
1308
+ </YAxis>
1309
+ )}
1310
+ <Tooltip
1311
+ contentStyle={tooltipStyle}
1312
+ labelStyle={tooltipLabelStyle}
1313
+ itemStyle={tooltipItemStyle}
1314
+ formatter={tooltipFormatter}
1315
+ labelFormatter={tooltipLabelFormatter}
1316
+ cursor={tooltipCursor}
1317
+ />
1318
+ {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1319
+ {referenceLineElements}
1320
+ {derivedSeries.map((series) => {
1321
+ switch (series.type) {
1322
+ case 'line':
1323
+ return (
1324
+ <Line
1325
+ name={series.name}
1326
+ key={series.key}
1327
+ type="monotone"
1328
+ dataKey={series.key}
1329
+ stroke={series.color}
1330
+ yAxisId={series.yAxisId}
1331
+ strokeWidth={series.strokeWidth}
1332
+ dot={series.dot}
1333
+ />
1334
+ );
1335
+ case 'area':
1336
+ return (
1337
+ <Area
1338
+ name={series.name}
1339
+ key={series.key}
1340
+ type="monotone"
1341
+ dataKey={series.key}
1342
+ stroke={series.color}
1343
+ yAxisId={series.yAxisId}
1344
+ fill={series.color}
1345
+ fillOpacity={series.opacity}
1346
+ stackId={series.stackId}
1347
+ />
1348
+ );
1349
+ case 'scatter':
1350
+ return (
1351
+ <Scatter
1352
+ name={series.name}
1353
+ key={series.key}
1354
+ dataKey={series.key}
1355
+ fill={series.color}
1356
+ yAxisId={series.yAxisId}
1357
+ />
1358
+ );
1359
+ default:
1360
+ return (
1361
+ <Bar
1362
+ name={series.name}
1363
+ key={series.key}
1364
+ dataKey={series.key}
1365
+ fill={series.color}
1366
+ stackId={series.stackId}
1367
+ yAxisId={series.yAxisId}
1368
+ radius={[4, 4, 0, 0]}
1369
+ />
1370
+ );
1371
+ }
1372
+ })}
1373
+ </ComposedChart>
1374
+ )}
1375
+
1376
+ {config.type === 'pie' && (
1377
+ <PieChart>
1378
+ <Pie
1379
+ data={config.data}
1380
+ dataKey={config.dataKeys?.[0] || 'value'}
1381
+ nameKey={config.xKey || 'name'}
1382
+ cx="50%"
1383
+ cy="50%"
1384
+ outerRadius={height / 3}
1385
+ label={{ fill: textColor }}
1386
+ >
1387
+ {config.data.map((_: unknown, index: number) => (
1388
+ <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
1389
+ ))}
1390
+ </Pie>
1391
+ <Tooltip
1392
+ contentStyle={tooltipStyle}
1393
+ itemStyle={tooltipItemStyle}
1394
+ formatter={tooltipFormatter}
1395
+ />
1396
+ {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1397
+ </PieChart>
1398
+ )}
1399
+
1400
+ {config.type === 'scatter' && (
1401
+ <ScatterChart margin={chartMargin}>
1402
+ {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1403
+ <XAxis
1404
+ dataKey={config.xKey || 'x'}
1405
+ label={xAxisLabel}
1406
+ {...axisStylingProps}
1407
+ {...xAxisProps}
1408
+ tickFormatter={xAxisTickFormatter}
1409
+ />
1410
+ <YAxis
1411
+ dataKey={config.dataKeys?.[0] || 'y'}
1412
+ width={80}
1413
+ tickFormatter={axisTickFormatter}
1414
+ {...axisStylingProps}
1415
+ >
1416
+ {renderYAxisLabel(leftAxisLabelText, 'left')}
1417
+ </YAxis>
1418
+ <Tooltip
1419
+ contentStyle={tooltipStyle}
1420
+ labelStyle={tooltipLabelStyle}
1421
+ itemStyle={tooltipItemStyle}
1422
+ formatter={tooltipFormatter}
1423
+ labelFormatter={tooltipLabelFormatter}
1424
+ cursor={tooltipCursor}
1425
+ />
1426
+ {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1427
+ {referenceLineElements}
1428
+ <Scatter name="Data" data={config.data} fill={colors[0]} />
1429
+ </ScatterChart>
1430
+ )}
1431
+ </ResponsiveContainer>
1432
+ </div>
1433
+ );
1434
+ };