orbitchat 3.3.8 → 3.5.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 (90) hide show
  1. package/dist/assets/ChartRenderer-C5FkaI18.js +80 -0
  2. package/dist/assets/MermaidRenderer-CghpQL29.js +259 -0
  3. package/dist/assets/{MusicRenderer-DZhuX52M.js → MusicRenderer-Bsg1uCOO.js} +2 -2
  4. package/dist/assets/{SVGRenderer-CB5j7ekx.js → SVGRenderer-BkbsVjFt.js} +1 -1
  5. package/dist/assets/_basePickBy-BmphHeNv.js +1 -0
  6. package/dist/assets/{_baseUniq-BFwLbgVF.js → _baseUniq-DcPuVRoH.js} +1 -1
  7. package/dist/assets/{architectureDiagram-VXUJARFQ-Dhht8baZ.js → architectureDiagram-2XIMDMQ5-ejVJFzXd.js} +3 -3
  8. package/dist/assets/blockDiagram-WCTKOSBZ-In1L1uKu.js +132 -0
  9. package/dist/assets/c4Diagram-IC4MRINW-DUOWrv1E.js +10 -0
  10. package/dist/assets/channel-Cgb_NwCj.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-FoAnG2DD.js → chunk-4BX2VUAB-CEKQJtxy.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-Cj902k47.js → chunk-55IACEB6-CZtwN-ba.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-D8TmopkR.js → chunk-FMBD7UC4-DC7v8pYp.js} +1 -1
  14. package/dist/assets/chunk-JSJVCQXG-BImJsuxH.js +1 -0
  15. package/dist/assets/{chunk-QN33PNHL-DDhJT2OP.js → chunk-KX2RTZJC-Bn6iA2gB.js} +1 -1
  16. package/dist/assets/{chunk-DI55MBZ5-YO8RmHLs.js → chunk-NQ4KR5QH-CuypZAVn.js} +4 -4
  17. package/dist/assets/{chunk-QZHKN3VN-CCSqXR9J.js → chunk-QZHKN3VN-BWpcmxTA.js} +1 -1
  18. package/dist/assets/chunk-WL4C6EOR-DYuNq0mZ.js +189 -0
  19. package/dist/assets/classDiagram-VBA2DB6C-fyPj1cvz.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-fyPj1cvz.js +1 -0
  21. package/dist/assets/clone-Cw0uzdEo.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-Dp7UKE-6.js → cose-bilkent-S5V4N54A-CrYky9tE.js} +1 -1
  23. package/dist/assets/dagre-KLK3FWXG-Bu-DLCK7.js +4 -0
  24. package/dist/assets/diagram-E7M64L7V-aAXWrxTM.js +24 -0
  25. package/dist/assets/diagram-IFDJBPK2-Dw4zd9rB.js +43 -0
  26. package/dist/assets/{diagram-S2PKOQOG-COTWuuiM.js → diagram-P4PSJMXO-45WsYDhT.js} +1 -1
  27. package/dist/assets/erDiagram-INFDFZHY-DOTnUTaB.js +70 -0
  28. package/dist/assets/flowDiagram-PKNHOUZH-DG_tQtcp.js +162 -0
  29. package/dist/assets/{ganttDiagram-JELNMOA3-nAhYUBJC.js → ganttDiagram-A5KZAMGK-BzsGIuEd.js} +30 -5
  30. package/dist/assets/gitGraphDiagram-K3NZZRJ6-Cj6jckyn.js +65 -0
  31. package/dist/assets/{graph-CVfZ5ZRD.js → graph-DTIKcgiu.js} +1 -1
  32. package/dist/assets/index-C3vuLth5.js +620 -0
  33. package/dist/assets/{index-DuEkeKcS.js → index-C4cdR4BU.js} +1 -1
  34. package/dist/assets/index-DMCMxyfd.css +1 -0
  35. package/dist/assets/infoDiagram-LFFYTUFH-CCtedtHp.js +2 -0
  36. package/dist/assets/ishikawaDiagram-PHBUUO56-CLGCPNak.js +70 -0
  37. package/dist/assets/{journeyDiagram-XKPGCS4Q-DzPJjseD.js → journeyDiagram-4ABVD52K-BXpAIS18.js} +3 -3
  38. package/dist/assets/{kanban-definition-3W4ZIXB7-BMPHlnkL.js → kanban-definition-K7BYSVSG-Dz6JMJ6P.js} +5 -5
  39. package/dist/assets/{layout-Cdwf2_35.js → layout-1zKkDoXK.js} +1 -1
  40. package/dist/assets/{mindmap-definition-VGOIOE7T-76g77fcj.js → mindmap-definition-YRQLILUH-Bk0RuY2l.js} +7 -7
  41. package/dist/assets/{pieDiagram-ADFJNKIX-_2bZQVhp.js → pieDiagram-SKSYHLDU-BRxRa9wV.js} +2 -2
  42. package/dist/assets/purify.es-DIZLy5JB.js +2 -0
  43. package/dist/assets/{quadrantDiagram-AYHSOK5B-DHidw6CG.js → quadrantDiagram-337W2JSQ-0xXACG3E.js} +1 -1
  44. package/dist/assets/{requirementDiagram-UZGBJVZJ-BosyfqW6.js → requirementDiagram-Z7DCOOCP-Cbizpsr2.js} +14 -5
  45. package/dist/assets/{sankeyDiagram-TZEHDZUN-kXQhuPRq.js → sankeyDiagram-WA2Y5GQK-C-2SmZmM.js} +1 -1
  46. package/dist/assets/sequenceDiagram-2WXFIKYE-BIt_HBTk.js +145 -0
  47. package/dist/assets/{stateDiagram-FKZM4ZOC-DUwTvhKc.js → stateDiagram-RAJIS63D-q_Cz5sxe.js} +1 -1
  48. package/dist/assets/stateDiagram-v2-FVOUBMTO-DNYb0Gsx.js +1 -0
  49. package/dist/assets/{timeline-definition-IT6M3QCI-tUdTwV48.js → timeline-definition-YZTLITO2-BwLu_3Bg.js} +1 -1
  50. package/dist/assets/treemap-KZPCXAKY-DUv4YaOt.js +162 -0
  51. package/dist/assets/vennDiagram-LZ73GAT5-CfA8AUEM.js +34 -0
  52. package/dist/assets/{xychartDiagram-PRI3JC2R-2ndTjhZS.js → xychartDiagram-JWTSCODW-ByOeXycF.js} +2 -2
  53. package/dist/index.html +2 -2
  54. package/package.json +1 -9
  55. package/dist/assets/ChartRenderer-BtX7_jv5.js +0 -80
  56. package/dist/assets/MermaidRenderer-DLpT9XPj.js +0 -260
  57. package/dist/assets/_basePickBy-KeSLCJM0.js +0 -1
  58. package/dist/assets/blockDiagram-VD42YOAC-C0uY9SKW.js +0 -122
  59. package/dist/assets/c4Diagram-YG6GDRKO-AGMRXqhN.js +0 -10
  60. package/dist/assets/channel-D-yx-ubr.js +0 -1
  61. package/dist/assets/chunk-B4BG7PRW-DZisX-Yn.js +0 -165
  62. package/dist/assets/chunk-TZMSLE5B-DtWrsAau.js +0 -1
  63. package/dist/assets/classDiagram-2ON5EDUG-BOVDq9sH.js +0 -1
  64. package/dist/assets/classDiagram-v2-WZHVMYZB-BOVDq9sH.js +0 -1
  65. package/dist/assets/clone-DOmxAX3a.js +0 -1
  66. package/dist/assets/dagre-6UL2VRFP-NEctntTO.js +0 -4
  67. package/dist/assets/diagram-PSM6KHXK-Bdb-Z7Rq.js +0 -24
  68. package/dist/assets/diagram-QEK2KX5R-Cxi1cnQw.js +0 -43
  69. package/dist/assets/erDiagram-Q2GNP2WA-CgLq4-Sy.js +0 -60
  70. package/dist/assets/flowDiagram-NV44I4VS-CVdT6vYV.js +0 -162
  71. package/dist/assets/gitGraphDiagram-V2S2FVAM-cZq9sWZG.js +0 -65
  72. package/dist/assets/index-Baf0NBsK.css +0 -1
  73. package/dist/assets/index-CmDt8-sd.js +0 -621
  74. package/dist/assets/infoDiagram-HS3SLOUP-DFQtcUA2.js +0 -2
  75. package/dist/assets/purify.es-A66Cw1IH.js +0 -2
  76. package/dist/assets/sequenceDiagram-WL72ISMW-CQCG4z68.js +0 -145
  77. package/dist/assets/stateDiagram-v2-4FDKWEC3-Ccelj_jo.js +0 -1
  78. package/dist/assets/treemap-GDKQZRPO-AkKmBZRv.js +0 -160
  79. package/markdown-renderer/LICENSE +0 -201
  80. package/markdown-renderer/src/CodeBlock.tsx +0 -332
  81. package/markdown-renderer/src/MarkdownComponents.tsx +0 -233
  82. package/markdown-renderer/src/MarkdownStyles.css +0 -732
  83. package/markdown-renderer/src/css.d.ts +0 -4
  84. package/markdown-renderer/src/index.ts +0 -32
  85. package/markdown-renderer/src/preprocessing.ts +0 -519
  86. package/markdown-renderer/src/renderers/ChartRenderer.tsx +0 -1464
  87. package/markdown-renderer/src/renderers/MermaidRenderer.tsx +0 -474
  88. package/markdown-renderer/src/renderers/MusicRenderer.tsx +0 -394
  89. package/markdown-renderer/src/renderers/SVGRenderer.tsx +0 -307
  90. package/markdown-renderer/src/types.ts +0 -174
@@ -1,1464 +0,0 @@
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 [containerWidth, setContainerWidth] = useState(0);
614
- const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
615
- const lastCodeRef = useRef<string>('');
616
- const lastUpdateTimeRef = useRef<number>(0);
617
- const streamingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
618
- const chartViewportRef = useRef<HTMLDivElement>(null);
619
-
620
- useEffect(() => {
621
- const now = Date.now();
622
- const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
623
- const codeChanged = code !== lastCodeRef.current;
624
-
625
- // Update refs
626
- lastCodeRef.current = code;
627
- lastUpdateTimeRef.current = now;
628
-
629
- // Check if data appears incomplete (streaming in progress)
630
- const incomplete = isLikelyIncomplete(code);
631
-
632
- // Detect rapid updates (streaming) - updates faster than 500ms apart
633
- // LLM streaming can have variable timing, so we use a more generous threshold
634
- const rapidUpdate = codeChanged && timeSinceLastUpdate < 500 && timeSinceLastUpdate > 0;
635
- const likelyStreaming = incomplete || rapidUpdate;
636
-
637
- // Parse the current code
638
- const parsed = parseChartConfig(code, language);
639
-
640
- // Validation - but handle differently if we're streaming
641
- if (!parsed) {
642
- if (likelyStreaming) {
643
- // During streaming, show waiting state instead of error
644
- setIsWaitingForData(true);
645
- setIsStreaming(true);
646
- setError(null);
647
- setConfig(null);
648
- } else {
649
- setConfig(null);
650
- setError('Failed to parse chart configuration');
651
- setIsStreaming(false);
652
- setIsWaitingForData(false);
653
- }
654
- return;
655
- }
656
-
657
- if (!Array.isArray(parsed.data) || parsed.data.length === 0) {
658
- if (likelyStreaming) {
659
- // During streaming with no data yet, show waiting state
660
- setIsWaitingForData(true);
661
- setIsStreaming(true);
662
- setError(null);
663
- setConfig(null);
664
-
665
- // Clear any existing streaming timeout
666
- if (streamingTimeoutRef.current) {
667
- clearTimeout(streamingTimeoutRef.current);
668
- }
669
-
670
- // After 5 seconds of no valid data, show error (streaming likely failed)
671
- streamingTimeoutRef.current = setTimeout(() => {
672
- const currentParsed = parseChartConfig(lastCodeRef.current, language);
673
- if (!currentParsed || !Array.isArray(currentParsed.data) || currentParsed.data.length === 0) {
674
- setError('Chart data is empty');
675
- setIsStreaming(false);
676
- setIsWaitingForData(false);
677
- }
678
- }, 5000);
679
- } else {
680
- setConfig(null);
681
- setError('Chart data is empty');
682
- setIsStreaming(false);
683
- setIsWaitingForData(false);
684
- }
685
- return;
686
- }
687
-
688
- if (!parsed.type) {
689
- if (likelyStreaming) {
690
- // Type not yet received during streaming
691
- setIsWaitingForData(true);
692
- setIsStreaming(true);
693
- setError(null);
694
- setConfig(null);
695
- } else {
696
- setConfig(null);
697
- setError('Chart type is required (bar, line, pie, area, scatter, composed)');
698
- setIsStreaming(false);
699
- setIsWaitingForData(false);
700
- }
701
- return;
702
- }
703
-
704
- // Clear waiting state - we have valid data now
705
- setIsWaitingForData(false);
706
-
707
- // Clear streaming timeout if we got valid data
708
- if (streamingTimeoutRef.current) {
709
- clearTimeout(streamingTimeoutRef.current);
710
- streamingTimeoutRef.current = null;
711
- }
712
-
713
- if (likelyStreaming) {
714
- setIsStreaming(true);
715
-
716
- // Clear any existing debounce timer
717
- if (debounceTimerRef.current) {
718
- clearTimeout(debounceTimerRef.current);
719
- }
720
-
721
- // Debounce: wait for data to stabilize before final render
722
- // Use longer debounce (400ms) to handle LLM streaming variability
723
- debounceTimerRef.current = setTimeout(() => {
724
- setIsStreaming(false);
725
- // Re-parse in case code changed during debounce
726
- const finalParsed = parseChartConfig(code, language);
727
- if (finalParsed && Array.isArray(finalParsed.data) && finalParsed.data.length > 0) {
728
- setError(null);
729
- setConfig(finalParsed);
730
- }
731
- }, 400);
732
-
733
- // Show partial data while streaming (but still set it)
734
- setError(null);
735
- setConfig(parsed);
736
- } else {
737
- // Data is complete and not rapidly updating - render immediately
738
- setIsStreaming(false);
739
- setError(null);
740
- setConfig(parsed);
741
- }
742
-
743
- // Cleanup
744
- return () => {
745
- if (debounceTimerRef.current) {
746
- clearTimeout(debounceTimerRef.current);
747
- }
748
- if (streamingTimeoutRef.current) {
749
- clearTimeout(streamingTimeoutRef.current);
750
- }
751
- };
752
- }, [code, language]);
753
-
754
- useEffect(() => {
755
- const node = chartViewportRef.current;
756
- if (!node || typeof window === 'undefined' || typeof window.ResizeObserver === 'undefined') return;
757
-
758
- const observer = new window.ResizeObserver((entries) => {
759
- const width = entries[0]?.contentRect?.width ?? 0;
760
- if (width > 0) {
761
- setContainerWidth(width);
762
- }
763
- });
764
- observer.observe(node);
765
-
766
- return () => observer.disconnect();
767
- }, []);
768
-
769
- if (error) {
770
- return (
771
- <div className="graph-error">
772
- <div className="graph-error-title">Chart Rendering Error</div>
773
- <div className="graph-error-message">{error}</div>
774
- <pre style={{ marginTop: '8px', fontSize: '0.8em', opacity: 0.7 }}>
775
- <code>{code}</code>
776
- </pre>
777
- </div>
778
- );
779
- }
780
-
781
- if (!config) {
782
- return (
783
- <div className="graph-container chart-container">
784
- <div style={{
785
- display: 'flex',
786
- flexDirection: 'column',
787
- alignItems: 'center',
788
- justifyContent: 'center',
789
- padding: '40px 20px',
790
- color: 'var(--md-text-secondary, #6b7280)',
791
- minHeight: '200px',
792
- }}>
793
- <svg
794
- style={{
795
- animation: 'spin 1s linear infinite',
796
- marginBottom: '12px',
797
- width: '32px',
798
- height: '32px',
799
- color: isWaitingForData ? '#3b82f6' : 'currentColor',
800
- }}
801
- viewBox="0 0 24 24"
802
- fill="none"
803
- >
804
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
805
- </svg>
806
- <span style={{ fontWeight: 500 }}>
807
- {isWaitingForData ? 'Receiving chart data...' : 'Loading chart...'}
808
- </span>
809
- {isWaitingForData && (
810
- <span style={{ fontSize: '0.85em', marginTop: '4px', opacity: 0.7 }}>
811
- Waiting for complete data from stream
812
- </span>
813
- )}
814
- </div>
815
- <style>{`
816
- @keyframes spin {
817
- from { transform: rotate(0deg); }
818
- to { transform: rotate(360deg); }
819
- }
820
- `}</style>
821
- </div>
822
- );
823
- }
824
-
825
- const colors = config.colors && config.colors.length ? config.colors : DEFAULT_COLORS;
826
- const derivedSeries = buildSeries(config, colors);
827
-
828
- const hasRightAxis = derivedSeries.some((series) => series.yAxisId === 'right');
829
- const showLegend = config.showLegend ?? (config.type === 'pie' || derivedSeries.length > 1);
830
- const showGrid = config.showGrid ?? true;
831
-
832
- const referenceLineElements = renderReferenceLines(config.referenceLines);
833
- const inferredLeftLabel = inferAxisLabel(derivedSeries, 'left');
834
- const inferredRightLabel = inferAxisLabel(derivedSeries, 'right');
835
- const leftAxisLabelText = config.yAxisLabel ?? inferredLeftLabel ?? 'Value';
836
- const rightAxisLabelText = hasRightAxis
837
- ? config.yAxisRightLabel ?? inferredRightLabel ?? 'Value'
838
- : undefined;
839
-
840
- // Use CSS custom properties for theme-aware styling
841
- // These will be read from the computed styles of the container
842
- const axisColor = CHART_THEME_VARS.axis;
843
- const gridColor = CHART_THEME_VARS.grid;
844
- const textColor = CHART_THEME_VARS.text;
845
- const tooltipTextColor = CHART_THEME_VARS.tooltipText;
846
- const tooltipBgColor = CHART_THEME_VARS.tooltipBg;
847
- const tooltipBorderColor = CHART_THEME_VARS.tooltipBorder;
848
- const secondaryTextColor = CHART_THEME_VARS.secondaryText;
849
- const axisStylingProps = {
850
- tick: { fill: axisColor },
851
- axisLine: { stroke: axisColor },
852
- tickLine: { stroke: axisColor },
853
- };
854
- const xAxisType =
855
- config.xAxisType ??
856
- (config.type === 'scatter' ? 'number' : inferXAxisTypeFromData(config.data, config.xKey));
857
- const isCategoryXAxis = xAxisType === 'category';
858
- const isCompactViewport = containerWidth > 0 && containerWidth < 640;
859
- const height = config.height || (isCompactViewport ? 380 : 320);
860
-
861
- // Calculate data point count and determine label rotation/truncation needs
862
- const dataPointCount = config.data?.length || 0;
863
- const defaultChartWidth = isCompactViewport ? 520 : 720;
864
- const estimatedPerPointWidth =
865
- config.type === 'bar' || config.type === 'composed'
866
- ? (isCompactViewport ? 92 : 72)
867
- : config.type === 'line' || config.type === 'area'
868
- ? (isCompactViewport ? 84 : 64)
869
- : (isCompactViewport ? 64 : 56);
870
- const intrinsicChartWidth = config.width ?? (isCategoryXAxis ? Math.max(defaultChartWidth, dataPointCount * estimatedPerPointWidth) : defaultChartWidth);
871
-
872
- // Analyze actual label lengths for smarter truncation decisions
873
- const analyzeLabelLengths = (): { maxLength: number; avgLength: number } => {
874
- if (!isCategoryXAxis || !config.data || dataPointCount === 0) {
875
- return { maxLength: 0, avgLength: 0 };
876
- }
877
- const xKey = config.xKey || 'name';
878
- const lengths = config.data
879
- .map((item: ChartDataItem) => {
880
- const value = item && typeof item === 'object' ? item[xKey] : undefined;
881
- return value ? String(value).length : 0;
882
- })
883
- .filter((len: number) => len > 0);
884
-
885
- if (lengths.length === 0) return { maxLength: 0, avgLength: 0 };
886
-
887
- const maxLength = Math.max(...lengths);
888
- const avgLength = lengths.reduce((sum: number, len: number) => sum + len, 0) / lengths.length;
889
- return { maxLength, avgLength };
890
- };
891
-
892
- const labelAnalysis = analyzeLabelLengths();
893
- const shouldRotateLabels = isCategoryXAxis && dataPointCount > 6;
894
- // Only truncate if we have many data points AND labels are actually long
895
- const shouldTruncateLabels = isCategoryXAxis && dataPointCount > 4 && labelAnalysis.avgLength > 15;
896
- const maxLabelLength = shouldTruncateLabels ? (dataPointCount > 10 ? 12 : 20) : Infinity;
897
- const labelLengthForSizing = shouldTruncateLabels
898
- ? (dataPointCount > 10 ? 12 : 20)
899
- : labelAnalysis.maxLength || 0;
900
-
901
- // Calculate interval to show fewer labels when there are many
902
- // With rotated labels, we can show more labels before needing intervals
903
- const calculateInterval = (count: number): number => {
904
- if (count <= 12) return 0; // Show all labels (common case like 12 months)
905
- if (count <= 16) return 1; // Show every other label
906
- if (count <= 24) return Math.floor(count / 12); // Show ~12 labels
907
- return Math.floor(count / 15); // Show ~15 labels max for very large datasets
908
- };
909
-
910
- const labelInterval = isCategoryXAxis ? calculateInterval(dataPointCount) : 0;
911
-
912
- const estimateTickLabelHeight = () => {
913
- if (!isCategoryXAxis || dataPointCount === 0) return 24;
914
- const baseHeight = 24;
915
- if (!shouldRotateLabels) {
916
- const longLabelBonus = Math.max(labelLengthForSizing - 12, 0) * 1.5;
917
- return Math.min(baseHeight + longLabelBonus, 48);
918
- }
919
- const approxCharWidth = 6.5;
920
- const rotationRadians = (45 * Math.PI) / 180;
921
- const approxWidth = labelLengthForSizing * approxCharWidth;
922
- const rotatedHeight = Math.sin(rotationRadians) * approxWidth;
923
- return Math.min(Math.max(baseHeight, rotatedHeight + 12), 140);
924
- };
925
-
926
- const estimatedTickLabelHeight = estimateTickLabelHeight();
927
- const xAxisHeight = shouldRotateLabels
928
- ? Math.min(Math.max(estimatedTickLabelHeight + (isCompactViewport ? 12 : 16), isCompactViewport ? 64 : 80), isCompactViewport ? 140 : 160)
929
- : Math.max(estimatedTickLabelHeight + 12, isCompactViewport ? 32 : 36);
930
-
931
- // Note: Don't set scale: 'band' explicitly - Recharts handles this automatically
932
- // for bar charts, and setting it can interfere with rendering in some cases
933
- const categoricalXAxisProps = {
934
- type: 'category' as const,
935
- interval: labelInterval as number,
936
- allowDuplicatedCategory: false,
937
- padding: { left: 16, right: 16 },
938
- angle: shouldRotateLabels ? -45 : 0,
939
- textAnchor: shouldRotateLabels ? 'end' as const : 'middle' as const,
940
- height: xAxisHeight,
941
- };
942
- const numericXAxisProps = {
943
- type: 'number' as const,
944
- domain: ['dataMin', 'dataMax'] as const,
945
- allowDuplicatedCategory: true,
946
- };
947
- const xAxisProps = isCategoryXAxis ? categoricalXAxisProps : numericXAxisProps;
948
-
949
- const tooltipStyle = {
950
- backgroundColor: tooltipBgColor,
951
- border: `1px solid ${tooltipBorderColor}`,
952
- borderRadius: '4px',
953
- padding: '8px',
954
- color: tooltipTextColor,
955
- };
956
-
957
- const tooltipLabelStyle = {
958
- color: tooltipTextColor,
959
- fontWeight: 600,
960
- marginBottom: '4px',
961
- };
962
-
963
- const tooltipItemStyle = {
964
- color: tooltipTextColor,
965
- };
966
-
967
- // Cursor style for hover highlight - semi-transparent for better UX
968
- const tooltipCursor = {
969
- fill: gridColor,
970
- fillOpacity: 0.3,
971
- };
972
-
973
- const tooltipFormatter: Formatter<ValueType, NameType> = (value, name) => {
974
- const safeName = (name ?? '') as NameType;
975
- if (typeof value === 'number') {
976
- const formatted = formatValue(value, config.formatter);
977
- return [String(formatted ?? value), safeName];
978
- }
979
- if (Array.isArray(value)) {
980
- return [value.join(', '), safeName];
981
- }
982
- if (typeof value === 'string') {
983
- return [value, safeName];
984
- }
985
- return ['', safeName];
986
- };
987
-
988
- const axisTickFormatter = (value: unknown) => {
989
- if (typeof value !== 'number') return String(value ?? '');
990
- const formatted = formatValue(value, config.formatter);
991
- return String(formatted ?? '');
992
- };
993
-
994
- const formatCategoryLabel = (value: unknown) => {
995
- if (typeof value !== 'string') return String(value ?? '');
996
- const normalized = value.trim().toLowerCase();
997
- const monthAbbrev = MONTH_ABBREVIATIONS[normalized];
998
- if (monthAbbrev) return monthAbbrev;
999
- return value;
1000
- };
1001
-
1002
- const truncateLabel = (label: string, maxLength: number): string => {
1003
- if (!label || typeof label !== 'string') return String(label ?? '');
1004
- if (label.length <= maxLength) return label;
1005
- // Truncate at word boundary when possible for better readability
1006
- const truncated = label.substring(0, maxLength - 3);
1007
- const lastSpace = truncated.lastIndexOf(' ');
1008
- if (lastSpace > maxLength * 0.6) {
1009
- // If we can truncate at a word boundary without losing too much, do it
1010
- return truncated.substring(0, lastSpace) + '...';
1011
- }
1012
- return truncated + '...';
1013
- };
1014
-
1015
- const xAxisTickFormatter = (value: unknown) => {
1016
- if (!isCategoryXAxis) return axisTickFormatter(value);
1017
- const formatted = formatCategoryLabel(value);
1018
- return shouldTruncateLabels ? truncateLabel(formatted, maxLabelLength) : formatted;
1019
- };
1020
-
1021
- // Tooltip label formatter: always show full original label, even if axis label is truncated
1022
- const tooltipLabelFormatter = (label: unknown) => {
1023
- if (!isCategoryXAxis) return String(label ?? '');
1024
- // Return the original formatted label (before truncation) for tooltip
1025
- return formatCategoryLabel(label);
1026
- };
1027
-
1028
- const xAxisHasLabel = Boolean(config.xAxisLabel);
1029
-
1030
- // Keep margins modest so the plotting area stays visible even with rotated labels
1031
- const baseBottomMargin = 16;
1032
- const axisLabelSpace = xAxisHasLabel ? 28 : 10;
1033
- const legendSpace = showLegend ? 26 : 6;
1034
- const rotatedPadding = shouldRotateLabels
1035
- ? Math.min(Math.max(estimatedTickLabelHeight - 48, 0), 28)
1036
- : 0;
1037
- // Extra space when both rotated labels and axis label are present to prevent overlap
1038
- const rotatedWithLabelExtra = shouldRotateLabels && xAxisHasLabel ? 16 : 0;
1039
- const bottomMargin = baseBottomMargin + axisLabelSpace + legendSpace + rotatedPadding + rotatedWithLabelExtra;
1040
-
1041
- const chartMargin = {
1042
- left: leftAxisLabelText ? (isCompactViewport ? 68 : 80) : 10,
1043
- right: rightAxisLabelText ? (isCompactViewport ? 68 : 80) : 10,
1044
- top: 10,
1045
- bottom: bottomMargin,
1046
- };
1047
-
1048
- // Calculate offset based on rotated label height to prevent overlap
1049
- const xAxisLabelOffset = shouldRotateLabels
1050
- ? Math.min(Math.max(estimatedTickLabelHeight - 60, 0), 36)
1051
- : 0;
1052
- const xAxisLabel = xAxisHasLabel
1053
- ? {
1054
- value: config.xAxisLabel,
1055
- position: 'bottom' as const,
1056
- offset: xAxisLabelOffset,
1057
- }
1058
- : undefined;
1059
-
1060
- // When labels are long and rotated, they squeeze the legend horizontally
1061
- // Use vertical layout for legend in those cases
1062
- const useLegendVerticalLayout = shouldRotateLabels && labelAnalysis.avgLength > 20;
1063
-
1064
- const legendWrapperStyle: React.CSSProperties = {
1065
- color: textColor,
1066
- marginTop: xAxisHasLabel ? (isCompactViewport ? 12 : 16) + xAxisLabelOffset : (isCompactViewport ? 8 : 12),
1067
- paddingTop: shouldRotateLabels ? Math.min(Math.max(estimatedTickLabelHeight - 50, 8), isCompactViewport ? 22 : 30) : 8,
1068
- display: 'flex',
1069
- justifyContent: 'center',
1070
- gap: useLegendVerticalLayout ? '8px' : (isCompactViewport ? '10px' : '16px'),
1071
- flexDirection: useLegendVerticalLayout ? 'column' : 'row',
1072
- alignItems: useLegendVerticalLayout ? 'center' : undefined,
1073
- };
1074
-
1075
- return (
1076
- <>
1077
- <div
1078
- className="graph-container chart-container"
1079
- style={{ flexDirection: 'column', alignItems: 'stretch', position: 'relative' }}
1080
- >
1081
- {isStreaming && (
1082
- <div className="md-expandable-actions">
1083
- <div
1084
- style={{
1085
- display: 'flex',
1086
- alignItems: 'center',
1087
- padding: '4px 8px',
1088
- backgroundColor: 'rgba(59, 130, 246, 0.1)',
1089
- borderRadius: '4px',
1090
- fontSize: '12px',
1091
- color: '#3b82f6',
1092
- }}
1093
- >
1094
- <svg
1095
- style={{
1096
- animation: 'spin 1s linear infinite',
1097
- marginRight: '4px',
1098
- width: '12px',
1099
- height: '12px'
1100
- }}
1101
- viewBox="0 0 24 24"
1102
- fill="none"
1103
- >
1104
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
1105
- </svg>
1106
- Updating...
1107
- <style>{`
1108
- @keyframes spin {
1109
- from { transform: rotate(0deg); }
1110
- to { transform: rotate(360deg); }
1111
- }
1112
- `}</style>
1113
- </div>
1114
- </div>
1115
- )}
1116
- {config.title && (
1117
- <h4
1118
- style={{
1119
- textAlign: 'center',
1120
- marginBottom: config.description ? '4px' : '12px',
1121
- marginTop: 0,
1122
- color: textColor,
1123
- fontWeight: 600,
1124
- background: 'transparent',
1125
- }}
1126
- >
1127
- {config.title}
1128
- </h4>
1129
- )}
1130
- {config.description && (
1131
- <p
1132
- style={{
1133
- textAlign: 'center',
1134
- marginTop: 0,
1135
- marginBottom: '12px',
1136
- color: secondaryTextColor,
1137
- fontSize: '0.9rem',
1138
- }}
1139
- >
1140
- {config.description}
1141
- </p>
1142
- )}
1143
- <div ref={chartViewportRef} style={{ width: '100%', overflowX: 'auto', overflowY: 'hidden' }}>
1144
- <div style={{ width: `${intrinsicChartWidth}px`, minWidth: '100%', height: `${height}px` }}>
1145
- <ResponsiveContainer width="100%" height="100%">
1146
- {config.type === 'bar' && (
1147
- <BarChart data={config.data} margin={chartMargin} barCategoryGap="20%">
1148
- {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1149
- <XAxis
1150
- dataKey={config.xKey || 'name'}
1151
- label={xAxisLabel}
1152
- {...axisStylingProps}
1153
- {...xAxisProps}
1154
- tickFormatter={xAxisTickFormatter}
1155
- />
1156
- <YAxis
1157
- yAxisId="left"
1158
- width={isCompactViewport ? 68 : 80}
1159
- tickFormatter={axisTickFormatter}
1160
- {...axisStylingProps}
1161
- >
1162
- {renderYAxisLabel(leftAxisLabelText, 'left')}
1163
- </YAxis>
1164
- {hasRightAxis && (
1165
- <YAxis
1166
- yAxisId="right"
1167
- orientation="right"
1168
- width={isCompactViewport ? 68 : 80}
1169
- tickFormatter={axisTickFormatter}
1170
- {...axisStylingProps}
1171
- >
1172
- {renderYAxisLabel(rightAxisLabelText, 'right')}
1173
- </YAxis>
1174
- )}
1175
- <Tooltip
1176
- contentStyle={tooltipStyle}
1177
- labelStyle={tooltipLabelStyle}
1178
- itemStyle={tooltipItemStyle}
1179
- formatter={tooltipFormatter}
1180
- labelFormatter={tooltipLabelFormatter}
1181
- cursor={tooltipCursor}
1182
- />
1183
- {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1184
- {referenceLineElements}
1185
- {derivedSeries.map((series) => (
1186
- <Bar
1187
- name={series.name}
1188
- key={series.key}
1189
- dataKey={series.key}
1190
- fill={series.color}
1191
- stackId={series.stackId}
1192
- yAxisId={series.yAxisId}
1193
- radius={[4, 4, 0, 0]}
1194
- />
1195
- ))}
1196
- </BarChart>
1197
- )}
1198
-
1199
- {config.type === 'line' && (
1200
- <LineChart data={config.data} margin={chartMargin}>
1201
- {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1202
- <XAxis
1203
- dataKey={config.xKey || 'name'}
1204
- label={xAxisLabel}
1205
- {...axisStylingProps}
1206
- {...xAxisProps}
1207
- tickFormatter={xAxisTickFormatter}
1208
- />
1209
- <YAxis
1210
- yAxisId="left"
1211
- width={isCompactViewport ? 68 : 80}
1212
- tickFormatter={axisTickFormatter}
1213
- {...axisStylingProps}
1214
- >
1215
- {renderYAxisLabel(leftAxisLabelText, 'left')}
1216
- </YAxis>
1217
- {hasRightAxis && (
1218
- <YAxis
1219
- yAxisId="right"
1220
- orientation="right"
1221
- width={isCompactViewport ? 68 : 80}
1222
- tickFormatter={axisTickFormatter}
1223
- {...axisStylingProps}
1224
- >
1225
- {renderYAxisLabel(rightAxisLabelText, 'right')}
1226
- </YAxis>
1227
- )}
1228
- <Tooltip
1229
- contentStyle={tooltipStyle}
1230
- labelStyle={tooltipLabelStyle}
1231
- itemStyle={tooltipItemStyle}
1232
- formatter={tooltipFormatter}
1233
- labelFormatter={tooltipLabelFormatter}
1234
- cursor={tooltipCursor}
1235
- />
1236
- {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1237
- {referenceLineElements}
1238
- {derivedSeries.map((series) => (
1239
- <Line
1240
- name={series.name}
1241
- key={series.key}
1242
- type="monotone"
1243
- dataKey={series.key}
1244
- stroke={series.color}
1245
- yAxisId={series.yAxisId}
1246
- strokeWidth={series.strokeWidth}
1247
- dot={series.dot}
1248
- />
1249
- ))}
1250
- </LineChart>
1251
- )}
1252
-
1253
- {config.type === 'area' && (
1254
- <AreaChart data={config.data} margin={chartMargin}>
1255
- {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1256
- <XAxis
1257
- dataKey={config.xKey || 'name'}
1258
- label={xAxisLabel}
1259
- {...axisStylingProps}
1260
- {...xAxisProps}
1261
- tickFormatter={xAxisTickFormatter}
1262
- />
1263
- <YAxis
1264
- yAxisId="left"
1265
- width={isCompactViewport ? 68 : 80}
1266
- tickFormatter={axisTickFormatter}
1267
- {...axisStylingProps}
1268
- >
1269
- {renderYAxisLabel(leftAxisLabelText, 'left')}
1270
- </YAxis>
1271
- {hasRightAxis && (
1272
- <YAxis
1273
- yAxisId="right"
1274
- orientation="right"
1275
- width={isCompactViewport ? 68 : 80}
1276
- tickFormatter={axisTickFormatter}
1277
- {...axisStylingProps}
1278
- >
1279
- {renderYAxisLabel(rightAxisLabelText, 'right')}
1280
- </YAxis>
1281
- )}
1282
- <Tooltip
1283
- contentStyle={tooltipStyle}
1284
- labelStyle={tooltipLabelStyle}
1285
- itemStyle={tooltipItemStyle}
1286
- formatter={tooltipFormatter}
1287
- labelFormatter={tooltipLabelFormatter}
1288
- cursor={tooltipCursor}
1289
- />
1290
- {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1291
- {referenceLineElements}
1292
- {derivedSeries.map((series) => (
1293
- <Area
1294
- name={series.name}
1295
- key={series.key}
1296
- type="monotone"
1297
- dataKey={series.key}
1298
- stroke={series.color}
1299
- yAxisId={series.yAxisId}
1300
- fill={series.color}
1301
- fillOpacity={series.opacity}
1302
- stackId={series.stackId}
1303
- />
1304
- ))}
1305
- </AreaChart>
1306
- )}
1307
-
1308
- {config.type === 'composed' && (
1309
- <ComposedChart data={config.data} margin={chartMargin}>
1310
- {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1311
- <XAxis
1312
- dataKey={config.xKey || 'name'}
1313
- label={xAxisLabel}
1314
- {...axisStylingProps}
1315
- {...xAxisProps}
1316
- tickFormatter={xAxisTickFormatter}
1317
- />
1318
- <YAxis
1319
- yAxisId="left"
1320
- width={isCompactViewport ? 68 : 80}
1321
- tickFormatter={axisTickFormatter}
1322
- {...axisStylingProps}
1323
- >
1324
- {renderYAxisLabel(leftAxisLabelText, 'left')}
1325
- </YAxis>
1326
- {hasRightAxis && (
1327
- <YAxis
1328
- yAxisId="right"
1329
- orientation="right"
1330
- width={isCompactViewport ? 68 : 80}
1331
- tickFormatter={axisTickFormatter}
1332
- {...axisStylingProps}
1333
- >
1334
- {renderYAxisLabel(rightAxisLabelText, 'right')}
1335
- </YAxis>
1336
- )}
1337
- <Tooltip
1338
- contentStyle={tooltipStyle}
1339
- labelStyle={tooltipLabelStyle}
1340
- itemStyle={tooltipItemStyle}
1341
- formatter={tooltipFormatter}
1342
- labelFormatter={tooltipLabelFormatter}
1343
- cursor={tooltipCursor}
1344
- />
1345
- {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1346
- {referenceLineElements}
1347
- {derivedSeries.map((series) => {
1348
- switch (series.type) {
1349
- case 'line':
1350
- return (
1351
- <Line
1352
- name={series.name}
1353
- key={series.key}
1354
- type="monotone"
1355
- dataKey={series.key}
1356
- stroke={series.color}
1357
- yAxisId={series.yAxisId}
1358
- strokeWidth={series.strokeWidth}
1359
- dot={series.dot}
1360
- />
1361
- );
1362
- case 'area':
1363
- return (
1364
- <Area
1365
- name={series.name}
1366
- key={series.key}
1367
- type="monotone"
1368
- dataKey={series.key}
1369
- stroke={series.color}
1370
- yAxisId={series.yAxisId}
1371
- fill={series.color}
1372
- fillOpacity={series.opacity}
1373
- stackId={series.stackId}
1374
- />
1375
- );
1376
- case 'scatter':
1377
- return (
1378
- <Scatter
1379
- name={series.name}
1380
- key={series.key}
1381
- dataKey={series.key}
1382
- fill={series.color}
1383
- yAxisId={series.yAxisId}
1384
- />
1385
- );
1386
- default:
1387
- return (
1388
- <Bar
1389
- name={series.name}
1390
- key={series.key}
1391
- dataKey={series.key}
1392
- fill={series.color}
1393
- stackId={series.stackId}
1394
- yAxisId={series.yAxisId}
1395
- radius={[4, 4, 0, 0]}
1396
- />
1397
- );
1398
- }
1399
- })}
1400
- </ComposedChart>
1401
- )}
1402
-
1403
- {config.type === 'pie' && (
1404
- <PieChart>
1405
- <Pie
1406
- data={config.data}
1407
- dataKey={config.dataKeys?.[0] || 'value'}
1408
- nameKey={config.xKey || 'name'}
1409
- cx="50%"
1410
- cy="50%"
1411
- outerRadius={height / 3}
1412
- label={{ fill: textColor }}
1413
- >
1414
- {config.data.map((_: unknown, index: number) => (
1415
- <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
1416
- ))}
1417
- </Pie>
1418
- <Tooltip
1419
- contentStyle={tooltipStyle}
1420
- itemStyle={tooltipItemStyle}
1421
- formatter={tooltipFormatter}
1422
- />
1423
- {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1424
- </PieChart>
1425
- )}
1426
-
1427
- {config.type === 'scatter' && (
1428
- <ScatterChart margin={chartMargin}>
1429
- {showGrid && <CartesianGrid strokeDasharray="3 3" stroke={gridColor} />}
1430
- <XAxis
1431
- dataKey={config.xKey || 'x'}
1432
- label={xAxisLabel}
1433
- {...axisStylingProps}
1434
- {...xAxisProps}
1435
- tickFormatter={xAxisTickFormatter}
1436
- />
1437
- <YAxis
1438
- dataKey={config.dataKeys?.[0] || 'y'}
1439
- width={isCompactViewport ? 68 : 80}
1440
- tickFormatter={axisTickFormatter}
1441
- {...axisStylingProps}
1442
- >
1443
- {renderYAxisLabel(leftAxisLabelText, 'left')}
1444
- </YAxis>
1445
- <Tooltip
1446
- contentStyle={tooltipStyle}
1447
- labelStyle={tooltipLabelStyle}
1448
- itemStyle={tooltipItemStyle}
1449
- formatter={tooltipFormatter}
1450
- labelFormatter={tooltipLabelFormatter}
1451
- cursor={tooltipCursor}
1452
- />
1453
- {showLegend && <Legend wrapperStyle={legendWrapperStyle} iconSize={10} iconType="square" />}
1454
- {referenceLineElements}
1455
- <Scatter name="Data" data={config.data} fill={colors[0]} />
1456
- </ScatterChart>
1457
- )}
1458
- </ResponsiveContainer>
1459
- </div>
1460
- </div>
1461
- </div>
1462
- </>
1463
- );
1464
- };