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.
- package/dist/assets/ChartRenderer-B1mYF_kk.js +80 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MermaidRenderer-Dhffx5mc.js +260 -0
- package/dist/assets/MusicRenderer-DbXJ8e4-.js +18 -0
- package/dist/assets/SVGRenderer-EhamRX_K.js +6 -0
- package/dist/assets/_basePickBy-C2AibBqo.js +1 -0
- package/dist/assets/_baseUniq-D52pUSvn.js +1 -0
- package/dist/assets/arc-BSnyvXPh.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-BLMvVsZu.js +36 -0
- package/dist/assets/band-CquvqAHh.js +1 -0
- package/dist/assets/blockDiagram-VD42YOAC-CTz0dv5u.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-BjbN-GGh.js +10 -0
- package/dist/assets/channel-ryTtfXZE.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-DEMWGIsU.js +1 -0
- package/dist/assets/chunk-55IACEB6-BYaauHLT.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-D3_GFcfd.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-Dy2EuzfA.js +220 -0
- package/dist/assets/{chunk-FMBD7UC4-dPK7Boav-Cz7OoDLR.js → chunk-FMBD7UC4-C9XHmwza.js} +2 -2
- package/dist/assets/chunk-QN33PNHL-BOCltIi9.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CvA964eQ.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-B-3rZz90.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-VbShFIzz.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-VbShFIzz.js +1 -0
- package/dist/assets/clone-BlhdKVDQ.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CzPip3he.js +1 -0
- package/dist/assets/cytoscape.esm-CyJtwmzi.js +331 -0
- package/dist/assets/dagre-6UL2VRFP-CfCXYus3.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DyI2eDqp.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DuakIh40.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-C3-4IORM.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-Dt-nZEaD.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-B-2wjyTQ.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-CHTvjPq0.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-d5GWmgY4.js +65 -0
- package/dist/assets/graph-COgkbiU8.js +1 -0
- package/dist/assets/index-DN2bBcCs.js +134 -0
- package/dist/assets/index-DtztdW2a.js +643 -0
- package/dist/assets/index-lGqyWNWb.css +1 -0
- package/dist/assets/infoDiagram-HS3SLOUP-30Hn0iLj.js +2 -0
- package/dist/assets/init-Dmth1JHB.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CMOYXQ79.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-dt3376cq.js +89 -0
- package/dist/assets/layout-DH97sy5g.js +1 -0
- package/dist/assets/linear-CKzVTQ0r.js +1 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BcZHPAlA.js +68 -0
- package/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-DXjvgcgl.js +30 -0
- package/dist/assets/purify.es-A66Cw1IH.js +2 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CICk2FdS.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-BdtxpQRA.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-BVNS9BQJ.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-DF4fJTM7.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-C8H8HujX.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DwtU033b.js +1 -0
- package/dist/assets/step-EjIQ8UIn.js +1 -0
- package/dist/assets/time-h5EapSZu.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DUSG5Vfy.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-1PI2PY4S.js +160 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-CFPJj8WK.js +7 -0
- package/dist/favicon.svg +3 -1
- package/dist/index.html +2 -2
- package/markdown-renderer/LICENSE +201 -0
- package/markdown-renderer/src/CodeBlock.tsx +332 -0
- package/markdown-renderer/src/MarkdownComponents.tsx +233 -0
- package/markdown-renderer/src/MarkdownStyles.css +668 -0
- package/markdown-renderer/src/css.d.ts +4 -0
- package/markdown-renderer/src/index.ts +32 -0
- package/markdown-renderer/src/preprocessing.ts +519 -0
- package/markdown-renderer/src/renderers/ChartRenderer.tsx +1434 -0
- package/markdown-renderer/src/renderers/MermaidRenderer.tsx +474 -0
- package/markdown-renderer/src/renderers/MusicRenderer.tsx +394 -0
- package/markdown-renderer/src/renderers/SVGRenderer.tsx +307 -0
- package/markdown-renderer/src/types.ts +174 -0
- package/package.json +25 -3
- package/dist/assets/_baseUniq-BRKsqoH--68FUaYxk.js +0 -1
- package/dist/assets/arc-pab_su9s-BuY-VRZt.js +0 -1
- package/dist/assets/architectureDiagram-VXUJARFQ-DqQ8r_6g-C5b5VsO8.js +0 -36
- package/dist/assets/blockDiagram-VD42YOAC-B-dKfcH3-DVjqMJ-3.js +0 -122
- package/dist/assets/c4Diagram-YG6GDRKO-DMUPaBEl-o6ghjFV9.js +0 -10
- package/dist/assets/channel-HKsfPa5q-rOME8XF8.js +0 -1
- package/dist/assets/chunk-4BX2VUAB-CX67kh_B-Dl0Loq_e.js +0 -1
- package/dist/assets/chunk-55IACEB6-BocSyyvr-DYW83vZR.js +0 -1
- package/dist/assets/chunk-B4BG7PRW-CO8QAyfE-CsF1OX4R.js +0 -165
- package/dist/assets/chunk-DI55MBZ5-Dw1L6Eos-Dl1shJzL.js +0 -220
- package/dist/assets/chunk-QN33PNHL-vP2PqfVG-BuQNN8G2.js +0 -1
- package/dist/assets/chunk-QZHKN3VN-Bcidzu63-Bud1NKOb.js +0 -1
- package/dist/assets/chunk-TZMSLE5B-BtljMjlg-BKLxPJ-x.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-C7cYN9hv-BHg-LSxQ.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-C7cYN9hv-BHg-LSxQ.js +0 -1
- package/dist/assets/clone-DoPb9X13-DejjKfXe.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-BGzO4EsH-rofdToAx.js +0 -1
- package/dist/assets/cytoscape.esm-CjI2IsL8-Da6dFVsf.js +0 -331
- package/dist/assets/dagre-6UL2VRFP-TzNvXCds-Ls1mVyc7.js +0 -4
- package/dist/assets/diagram-PSM6KHXK-BqY4RpUg-BJmYXUei.js +0 -24
- package/dist/assets/diagram-QEK2KX5R-CTjgBsne-dz-VLadE.js +0 -43
- package/dist/assets/diagram-S2PKOQOG-BqrhTIpA-ikxUE9Dj.js +0 -24
- package/dist/assets/erDiagram-Q2GNP2WA-B2hsi_Tl-B5gu6Jrx.js +0 -60
- package/dist/assets/flowDiagram-NV44I4VS-C03vtt_F-C9LVdVvn.js +0 -162
- package/dist/assets/ganttDiagram-JELNMOA3-B3hAg964-CYO6ZcQ8.js +0 -267
- package/dist/assets/gitGraphDiagram-NY62KEGX-ByhMH0yZ-BvO0WGzP.js +0 -65
- package/dist/assets/graph-BmNkcFEM-Bl2fiTgr.js +0 -1
- package/dist/assets/index-BXexqYFc-CFPIFV8r.js +0 -134
- package/dist/assets/index-KsGsuMGp.css +0 -1
- package/dist/assets/index-nqhgVFEP.js +0 -1010
- package/dist/assets/infoDiagram-WHAUD3N6-is6Ho4-T-D6tHdi3J.js +0 -2
- package/dist/assets/journeyDiagram-XKPGCS4Q-CRTOL26C-CL4DFaQP.js +0 -139
- package/dist/assets/kanban-definition-3W4ZIXB7-CNnO_t6O-DZBlsSao.js +0 -89
- package/dist/assets/layout-C0kZPebx-DiUdszUx.js +0 -1
- package/dist/assets/min-7Gb0pNxh-DsojA8pm.js +0 -1
- package/dist/assets/mindmap-definition-VGOIOE7T-CJZ2wTTa-Cd46k2jT.js +0 -68
- package/dist/assets/pieDiagram-ADFJNKIX-C9OSknjr-BGVcHmss.js +0 -30
- package/dist/assets/quadrantDiagram-AYHSOK5B-CW8yuAqv-CrCYjmJT.js +0 -7
- package/dist/assets/requirementDiagram-UZGBJVZJ-nGPhruO1-BzOGHXGi.js +0 -64
- package/dist/assets/sankeyDiagram-TZEHDZUN-CmL90u-m-BWp9STLO.js +0 -10
- package/dist/assets/sequenceDiagram-WL72ISMW-B02VRcnM-DB4Adljk.js +0 -145
- package/dist/assets/stateDiagram-FKZM4ZOC-DjoyLUdz-pDyQ50aU.js +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Bq76BTB7-C3j4gDpN.js +0 -1
- package/dist/assets/timeline-definition-IT6M3QCI-CTn0Gm3T-O1FhSixZ.js +0 -61
- package/dist/assets/treemap-KMMF4GRG-BjgLKKyi-DG8IQefJ.js +0 -128
- 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
|
+
};
|