chordia-ui 3.7.0 → 3.7.2
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/ChatMessage.cjs.js +15 -0
- package/dist/ChatMessage.cjs.js.map +1 -0
- package/dist/ChatMessage.es.js +915 -0
- package/dist/ChatMessage.es.js.map +1 -0
- package/dist/SideDrawer.cjs.js +2 -0
- package/dist/SideDrawer.cjs.js.map +1 -0
- package/dist/SideDrawer.es.js +358 -0
- package/dist/SideDrawer.es.js.map +1 -0
- package/dist/Toast.cjs.js +2 -2
- package/dist/Toast.cjs.js.map +1 -1
- package/dist/Toast.es.js +301 -671
- package/dist/Toast.es.js.map +1 -1
- package/dist/components/UpdatedInteractionDetails.cjs.js +4 -4
- package/dist/components/UpdatedInteractionDetails.cjs.js.map +1 -1
- package/dist/components/UpdatedInteractionDetails.es.js +652 -603
- package/dist/components/UpdatedInteractionDetails.es.js.map +1 -1
- package/dist/components/chat.cjs.js +5 -7
- package/dist/components/chat.cjs.js.map +1 -1
- package/dist/components/chat.es.js +393 -849
- package/dist/components/chat.es.js.map +1 -1
- package/dist/components/common.cjs.js +1 -1
- package/dist/components/common.es.js +28 -27
- package/dist/components/common.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +73 -72
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/components/UpdatedInteractionDetails/UpdatedInteractionContext.jsx +6 -2
- package/src/components/UpdatedInteractionDetails/UpdatedInteractionDetails.jsx +190 -139
- package/src/components/chat/ChartRenderer.jsx +300 -49
- package/src/components/common/AskCompass.jsx +91 -87
- package/src/components/common/SideDrawer.jsx +7 -3
- package/dist/ChartRenderer.cjs.js +0 -3
- package/dist/ChartRenderer.cjs.js.map +0 -1
- package/dist/ChartRenderer.es.js +0 -304
- package/dist/ChartRenderer.es.js.map +0 -1
|
@@ -25,17 +25,116 @@ const AXIS_TICK = { fill: 'var(--text-muted, #666)', fontSize: 12 };
|
|
|
25
25
|
const AXIS_LINE = { stroke: 'var(--border, #e0e0e0)' };
|
|
26
26
|
const GRID_STROKE = 'var(--border, #e0e0e0)';
|
|
27
27
|
const TOOLTIP_STYLE = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
// pointerEvents: 'none' is critical — without it, the tooltip steals hover from chart elements
|
|
29
|
+
// and flickers (especially noticeable on pie slices).
|
|
30
|
+
// transition: 'none' disables Recharts' default position-slide animation, so the tooltip
|
|
31
|
+
// snaps to the active point instead of flying in from its previous location.
|
|
32
|
+
wrapperStyle: {
|
|
33
|
+
zIndex: 9999,
|
|
34
|
+
pointerEvents: 'none',
|
|
35
|
+
outline: 'none',
|
|
36
|
+
transition: 'none',
|
|
34
37
|
},
|
|
35
|
-
labelStyle: { color: 'var(--text-strong, #1e2125)' },
|
|
36
38
|
};
|
|
37
39
|
const LEGEND_STYLE = { color: 'var(--text-ink, #1e2125)', fontSize: '12px' };
|
|
38
40
|
|
|
41
|
+
// Custom tooltip with a clean light-bg card layout.
|
|
42
|
+
// Each row has a colored dot (the series color) + dark text — always readable, no contrast tricks.
|
|
43
|
+
const ChartTooltip = ({ active, payload, label }) => {
|
|
44
|
+
if (!active || !payload || !payload.length) return null;
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className="chordia-chart-tooltip"
|
|
48
|
+
style={{
|
|
49
|
+
background: '#ffffff',
|
|
50
|
+
border: '1px solid rgba(52, 58, 64, 0.12)',
|
|
51
|
+
borderRadius: 8,
|
|
52
|
+
boxShadow: '0 8px 24px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15, 23, 42, 0.06)',
|
|
53
|
+
padding: '10px 12px',
|
|
54
|
+
minWidth: 140,
|
|
55
|
+
maxWidth: 280,
|
|
56
|
+
maxHeight: 280,
|
|
57
|
+
overflowY: 'auto',
|
|
58
|
+
fontSize: 12,
|
|
59
|
+
lineHeight: 1.5,
|
|
60
|
+
color: '#1E2125',
|
|
61
|
+
// Re-enable pointer events on the inner content so the user can scroll with the wheel
|
|
62
|
+
// even though the wrapper above has pointerEvents: 'none' (which prevents hover flicker).
|
|
63
|
+
pointerEvents: 'auto',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<style>{`
|
|
67
|
+
.chordia-chart-tooltip::-webkit-scrollbar { width: 8px; }
|
|
68
|
+
.chordia-chart-tooltip::-webkit-scrollbar-track { background: transparent; }
|
|
69
|
+
.chordia-chart-tooltip::-webkit-scrollbar-thumb {
|
|
70
|
+
background: rgba(52, 58, 64, 0.25);
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
}
|
|
73
|
+
.chordia-chart-tooltip::-webkit-scrollbar-thumb:hover {
|
|
74
|
+
background: rgba(52, 58, 64, 0.4);
|
|
75
|
+
}
|
|
76
|
+
`}</style>
|
|
77
|
+
{label !== undefined && label !== '' && (
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
fontSize: 11,
|
|
81
|
+
fontWeight: 600,
|
|
82
|
+
letterSpacing: '0.02em',
|
|
83
|
+
color: '#6B7280',
|
|
84
|
+
textTransform: 'uppercase',
|
|
85
|
+
marginBottom: 6,
|
|
86
|
+
paddingBottom: 6,
|
|
87
|
+
borderBottom: '1px solid rgba(52, 58, 64, 0.08)',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{label}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
94
|
+
{payload.map((entry, i) => {
|
|
95
|
+
const seriesColor = entry.color || entry.payload?.fill || '#9CA3AF';
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
key={i}
|
|
99
|
+
style={{
|
|
100
|
+
display: 'flex',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
gap: 8,
|
|
103
|
+
justifyContent: 'space-between',
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
|
107
|
+
<span
|
|
108
|
+
style={{
|
|
109
|
+
width: 8,
|
|
110
|
+
height: 8,
|
|
111
|
+
borderRadius: '50%',
|
|
112
|
+
background: seriesColor,
|
|
113
|
+
flexShrink: 0,
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
<span
|
|
117
|
+
style={{
|
|
118
|
+
color: '#374151',
|
|
119
|
+
overflow: 'hidden',
|
|
120
|
+
textOverflow: 'ellipsis',
|
|
121
|
+
whiteSpace: 'nowrap',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{entry.name}
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
<span style={{ color: '#111827', fontWeight: 600, marginLeft: 8 }}>
|
|
128
|
+
{entry.value}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
39
138
|
// Smart label: skip x_label if tick values already contain the label text
|
|
40
139
|
const shouldShowAxisLabel = (label, tickValues) => {
|
|
41
140
|
if (!label) return false;
|
|
@@ -46,21 +145,112 @@ const shouldShowAxisLabel = (label, tickValues) => {
|
|
|
46
145
|
};
|
|
47
146
|
|
|
48
147
|
// --- Export utilities ---
|
|
49
|
-
const
|
|
50
|
-
const
|
|
148
|
+
const getChartSvg = (containerEl) => {
|
|
149
|
+
const wrappers = Array.from(containerEl.querySelectorAll('.recharts-wrapper'));
|
|
150
|
+
const largestWrapper = wrappers.length
|
|
151
|
+
? wrappers.reduce((largest, current) => {
|
|
152
|
+
const largestRect = largest.getBoundingClientRect();
|
|
153
|
+
const currentRect = current.getBoundingClientRect();
|
|
154
|
+
return (currentRect.width * currentRect.height) > (largestRect.width * largestRect.height)
|
|
155
|
+
? current
|
|
156
|
+
: largest;
|
|
157
|
+
})
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
if (largestWrapper) {
|
|
161
|
+
const surface = largestWrapper.querySelector('svg.recharts-surface');
|
|
162
|
+
if (surface) {
|
|
163
|
+
const rect = surface.getBoundingClientRect();
|
|
164
|
+
if (rect.width >= 100 && rect.height >= 100) return surface;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fallback: pick the largest SVG so we don't accidentally export icon SVGs.
|
|
169
|
+
const candidates = Array.from(containerEl.querySelectorAll('svg'));
|
|
170
|
+
if (!candidates.length) return null;
|
|
171
|
+
|
|
172
|
+
const largestSvg = candidates.reduce((largest, current) => {
|
|
173
|
+
const largestRect = largest.getBoundingClientRect();
|
|
174
|
+
const currentRect = current.getBoundingClientRect();
|
|
175
|
+
return (currentRect.width * currentRect.height) > (largestRect.width * largestRect.height)
|
|
176
|
+
? current
|
|
177
|
+
: largest;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const largestRect = largestSvg.getBoundingClientRect();
|
|
181
|
+
if (largestRect.width < 100 || largestRect.height < 100) return null;
|
|
182
|
+
|
|
183
|
+
return largestSvg;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const escapeXml = (value) => String(value)
|
|
187
|
+
.replace(/&/g, '&')
|
|
188
|
+
.replace(/</g, '<')
|
|
189
|
+
.replace(/>/g, '>')
|
|
190
|
+
.replace(/"/g, '"')
|
|
191
|
+
.replace(/'/g, ''');
|
|
192
|
+
|
|
193
|
+
const buildExportSvg = ({ chartSvg, width, height, legendItems = [] }) => {
|
|
194
|
+
const baseSvg = chartSvg.cloneNode(true);
|
|
195
|
+
const legendLineHeight = 20;
|
|
196
|
+
const legendStartX = 16;
|
|
197
|
+
const legendRightPadding = 16;
|
|
198
|
+
const maxLegendX = width - legendRightPadding;
|
|
199
|
+
const legendRows = [];
|
|
200
|
+
let currentRow = [];
|
|
201
|
+
let currentRowWidth = 0;
|
|
202
|
+
|
|
203
|
+
legendItems.forEach(({ label, color }) => {
|
|
204
|
+
const itemWidth = Math.max(40, (String(label).length * 7) + 34);
|
|
205
|
+
if (currentRow.length && (legendStartX + currentRowWidth + itemWidth > maxLegendX)) {
|
|
206
|
+
legendRows.push(currentRow);
|
|
207
|
+
currentRow = [];
|
|
208
|
+
currentRowWidth = 0;
|
|
209
|
+
}
|
|
210
|
+
currentRow.push({ label, color, itemWidth });
|
|
211
|
+
currentRowWidth += itemWidth;
|
|
212
|
+
});
|
|
213
|
+
if (currentRow.length) legendRows.push(currentRow);
|
|
214
|
+
|
|
215
|
+
const exportLegendHeight = legendRows.length ? (legendRows.length * legendLineHeight) + 20 : 0;
|
|
216
|
+
const exportHeight = height + exportLegendHeight;
|
|
217
|
+
|
|
218
|
+
baseSvg.setAttribute('width', width);
|
|
219
|
+
baseSvg.setAttribute('height', height);
|
|
220
|
+
baseSvg.setAttribute('x', 0);
|
|
221
|
+
baseSvg.setAttribute('y', 0);
|
|
222
|
+
|
|
223
|
+
const legendMarkup = legendRows.map((row, rowIndex) => {
|
|
224
|
+
let cursorX = legendStartX;
|
|
225
|
+
const legendLineY = height + 20 + (rowIndex * legendLineHeight);
|
|
226
|
+
const rowMarkup = row.map(({ label, color, itemWidth }) => {
|
|
227
|
+
const line = `<line x1="${cursorX}" y1="${legendLineY}" x2="${cursorX + 10}" y2="${legendLineY}" stroke="${color}" stroke-width="2" />`;
|
|
228
|
+
const dot = `<circle cx="${cursorX + 5}" cy="${legendLineY}" r="2.5" fill="#fff" stroke="${color}" stroke-width="1.5" />`;
|
|
229
|
+
const text = `<text x="${cursorX + 14}" y="${legendLineY + 4}" fill="#666" font-size="12">${escapeXml(label)}</text>`;
|
|
230
|
+
cursorX += itemWidth;
|
|
231
|
+
return `${line}${dot}${text}`;
|
|
232
|
+
}).join('');
|
|
233
|
+
return rowMarkup;
|
|
234
|
+
}).join('');
|
|
235
|
+
|
|
236
|
+
return [
|
|
237
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${exportHeight}" viewBox="0 0 ${width} ${exportHeight}">`,
|
|
238
|
+
'<style>text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }</style>',
|
|
239
|
+
'<rect x="0" y="0" width="100%" height="100%" fill="#ffffff" />',
|
|
240
|
+
new XMLSerializer().serializeToString(baseSvg),
|
|
241
|
+
legendMarkup,
|
|
242
|
+
'</svg>',
|
|
243
|
+
].join('');
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const downloadPng = async (containerEl, title, legendItems = []) => {
|
|
247
|
+
const svg = getChartSvg(containerEl);
|
|
51
248
|
if (!svg) return;
|
|
52
|
-
const clone = svg.cloneNode(true);
|
|
53
249
|
// Ensure dimensions
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
59
|
-
// Inline computed styles for export fidelity
|
|
60
|
-
const styleEl = document.createElement('style');
|
|
61
|
-
styleEl.textContent = `text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }`;
|
|
62
|
-
clone.insertBefore(styleEl, clone.firstChild);
|
|
63
|
-
const xml = new XMLSerializer().serializeToString(clone);
|
|
250
|
+
const svgRect = svg.getBoundingClientRect();
|
|
251
|
+
const w = svgRect.width || svg.clientWidth || Number(svg.getAttribute('width')) || 600;
|
|
252
|
+
const h = svgRect.height || svg.clientHeight || Number(svg.getAttribute('height')) || 300;
|
|
253
|
+
const xml = buildExportSvg({ chartSvg: svg, width: w, height: h, legendItems });
|
|
64
254
|
const blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' });
|
|
65
255
|
const url = URL.createObjectURL(blob);
|
|
66
256
|
const img = new Image();
|
|
@@ -83,6 +273,23 @@ const downloadPng = async (containerEl, title) => {
|
|
|
83
273
|
img.src = url;
|
|
84
274
|
};
|
|
85
275
|
|
|
276
|
+
const downloadSvg = (containerEl, title, legendItems = []) => {
|
|
277
|
+
const svg = getChartSvg(containerEl);
|
|
278
|
+
if (!svg) return;
|
|
279
|
+
const svgRect = svg.getBoundingClientRect();
|
|
280
|
+
const w = svgRect.width || svg.clientWidth || Number(svg.getAttribute('width')) || 600;
|
|
281
|
+
const h = svgRect.height || svg.clientHeight || Number(svg.getAttribute('height')) || 300;
|
|
282
|
+
|
|
283
|
+
const xml = buildExportSvg({ chartSvg: svg, width: w, height: h, legendItems });
|
|
284
|
+
const blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' });
|
|
285
|
+
const url = URL.createObjectURL(blob);
|
|
286
|
+
const link = document.createElement('a');
|
|
287
|
+
link.download = `${(title || 'chart').replace(/[^a-z0-9]+/gi, '_')}.svg`;
|
|
288
|
+
link.href = url;
|
|
289
|
+
link.click();
|
|
290
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
291
|
+
};
|
|
292
|
+
|
|
86
293
|
const copyAsCsv = (series, allXValues) => {
|
|
87
294
|
const header = ['', ...series.map(s => s.name)].join(',');
|
|
88
295
|
const rows = allXValues.map(x => {
|
|
@@ -96,7 +303,7 @@ const copyAsCsv = (series, allXValues) => {
|
|
|
96
303
|
navigator.clipboard.writeText(csv);
|
|
97
304
|
};
|
|
98
305
|
|
|
99
|
-
const ActionBar = ({
|
|
306
|
+
const ActionBar = ({ onDownloadPng, onCopy, copied }) => (
|
|
100
307
|
<div style={{
|
|
101
308
|
display: 'flex', gap: 6, justifyContent: 'flex-end',
|
|
102
309
|
marginBottom: 8, opacity: 0.7, transition: 'opacity 0.15s',
|
|
@@ -109,7 +316,7 @@ const ActionBar = ({ onDownload, onCopy, copied }) => (
|
|
|
109
316
|
}}>
|
|
110
317
|
{copied ? '✓ Copied' : '📋 CSV'}
|
|
111
318
|
</button>
|
|
112
|
-
<button onClick={
|
|
319
|
+
<button onClick={onDownloadPng} title="Download as PNG" style={{
|
|
113
320
|
background: 'none', border: '1px solid var(--border, #e0e0e0)',
|
|
114
321
|
borderRadius: 'var(--radius-sm, 4px)', padding: '3px 8px',
|
|
115
322
|
fontSize: 11, color: 'var(--text-muted, #666)', cursor: 'pointer',
|
|
@@ -120,7 +327,7 @@ const ActionBar = ({ onDownload, onCopy, copied }) => (
|
|
|
120
327
|
</div>
|
|
121
328
|
);
|
|
122
329
|
|
|
123
|
-
const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
330
|
+
const ChartRenderer = ({ chartType, title, xLabel, yLabel, series, compact = false }) => {
|
|
124
331
|
const chartRef = useRef(null);
|
|
125
332
|
const [copied, setCopied] = React.useState(false);
|
|
126
333
|
|
|
@@ -142,12 +349,35 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
142
349
|
|
|
143
350
|
// Determine if axis labels are redundant
|
|
144
351
|
const showXLabel = shouldShowAxisLabel(xLabel, allXValues);
|
|
145
|
-
const bottomMargin = showXLabel ? 20 : 5;
|
|
146
352
|
|
|
147
353
|
// Check if x-axis labels are long (rotate them)
|
|
148
354
|
const maxTickLen = Math.max(...allXValues.map(v => String(v).length));
|
|
149
355
|
const rotateXTicks = maxTickLen > 12 || allXValues.length > 8;
|
|
150
356
|
|
|
357
|
+
// Bottom margin: stack space for tick labels + the axis label so they don't overlap
|
|
358
|
+
const xTickSpace = rotateXTicks ? 40 : 5;
|
|
359
|
+
const xLabelSpace = showXLabel ? 25 : 0;
|
|
360
|
+
const bottomMargin = xTickSpace + xLabelSpace;
|
|
361
|
+
|
|
362
|
+
// Estimate legend rows so we can reserve enough vertical space for it. Recharts
|
|
363
|
+
// doesn't auto-detect wrapping, so a multi-row legend collides with the x-axis label.
|
|
364
|
+
// Conservative estimate: ~4 items per row.
|
|
365
|
+
const legendRows = isSingleSeries ? 0 : Math.max(1, Math.ceil(seriesNames.length / 4));
|
|
366
|
+
const legendHeight = legendRows > 0 ? legendRows * 24 + (showXLabel ? 24 : 12) : 0;
|
|
367
|
+
|
|
368
|
+
// Bump chart container height so the wrapped legend gets its own space below the chart
|
|
369
|
+
// SVG instead of overlapping the x-axis label.
|
|
370
|
+
// In compact mode (e.g. AskCompass panel), shrink the base height so charts feel proportional
|
|
371
|
+
// to the smaller surface they're rendered in.
|
|
372
|
+
const baseChartHeight = compact ? 240 : 300;
|
|
373
|
+
const chartHeight = baseChartHeight + Math.max(0, (legendRows - 1) * 28);
|
|
374
|
+
|
|
375
|
+
// Always keep a gap between the x-axis (or its label) and the legend.
|
|
376
|
+
// Extra padding when an x-label is also present so all three layers (ticks → label → legend) breathe.
|
|
377
|
+
// zIndex: 0 ensures the legend never paints over the tooltip (which is zIndex: 1000).
|
|
378
|
+
const legendPaddingTop = showXLabel ? 24 : 12;
|
|
379
|
+
const legendWrapperStyle = { ...LEGEND_STYLE, paddingTop: legendPaddingTop, zIndex: 0 };
|
|
380
|
+
|
|
151
381
|
const containerStyle = {
|
|
152
382
|
background: 'var(--paper-elevated, #fff)',
|
|
153
383
|
border: '1px solid var(--border, #e0e0e0)',
|
|
@@ -171,7 +401,13 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
171
401
|
axisLine: AXIS_LINE,
|
|
172
402
|
tickLine: AXIS_LINE,
|
|
173
403
|
...(showXLabel ? {
|
|
174
|
-
label: {
|
|
404
|
+
label: {
|
|
405
|
+
value: xLabel,
|
|
406
|
+
position: 'insideBottom',
|
|
407
|
+
// Push label below rotated ticks (which take ~40px) when present
|
|
408
|
+
offset: rotateXTicks ? -45 : -10,
|
|
409
|
+
style: { textAnchor: 'middle', fill: 'var(--text-muted, #666)' },
|
|
410
|
+
},
|
|
175
411
|
} : {}),
|
|
176
412
|
};
|
|
177
413
|
|
|
@@ -180,7 +416,14 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
180
416
|
axisLine: AXIS_LINE,
|
|
181
417
|
tickLine: AXIS_LINE,
|
|
182
418
|
...(yLabel ? {
|
|
183
|
-
|
|
419
|
+
width: 72,
|
|
420
|
+
label: {
|
|
421
|
+
value: yLabel,
|
|
422
|
+
angle: -90,
|
|
423
|
+
position: 'insideLeft',
|
|
424
|
+
offset: 10,
|
|
425
|
+
style: { textAnchor: 'middle', fill: 'var(--text-muted, #666)' },
|
|
426
|
+
},
|
|
184
427
|
} : {}),
|
|
185
428
|
};
|
|
186
429
|
|
|
@@ -188,13 +431,12 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
188
431
|
switch (chartType) {
|
|
189
432
|
case 'bar':
|
|
190
433
|
return (
|
|
191
|
-
<ResponsiveContainer width="100%" height={
|
|
192
|
-
<BarChart data={chartData} margin={{ top: 5, right: 30, left:
|
|
434
|
+
<ResponsiveContainer width="100%" height={chartHeight}>
|
|
435
|
+
<BarChart data={chartData} margin={{ top: 5, right: 30, left: yLabel ? 10 : 20, bottom: bottomMargin }}>
|
|
193
436
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_STROKE} />
|
|
194
437
|
<XAxis {...xAxisProps} />
|
|
195
438
|
<YAxis {...yAxisProps} />
|
|
196
|
-
<
|
|
197
|
-
{!isSingleSeries && <Legend wrapperStyle={LEGEND_STYLE} />}
|
|
439
|
+
{!isSingleSeries && <Legend wrapperStyle={legendWrapperStyle} height={legendHeight} />}
|
|
198
440
|
{seriesNames.map((name, i) => (
|
|
199
441
|
<Bar key={name} dataKey={name} fill={COLORS[i % COLORS.length]} radius={[2, 2, 0, 0]} isAnimationActive={false}>
|
|
200
442
|
{isSingleSeries && chartData.map((_, idx) => (
|
|
@@ -202,6 +444,7 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
202
444
|
))}
|
|
203
445
|
</Bar>
|
|
204
446
|
))}
|
|
447
|
+
<Tooltip wrapperStyle={TOOLTIP_STYLE.wrapperStyle} content={<ChartTooltip />} cursor={{ fill: 'rgba(94, 136, 176, 0.06)', stroke: 'rgba(52, 58, 64, 0.18)', strokeDasharray: '3 3' }} isAnimationActive={false} animationDuration={0} />
|
|
205
448
|
</BarChart>
|
|
206
449
|
</ResponsiveContainer>
|
|
207
450
|
);
|
|
@@ -209,15 +452,14 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
209
452
|
case 'horizontal_bar': {
|
|
210
453
|
// Horizontal bar: swap X/Y, use YAxis for categories
|
|
211
454
|
return (
|
|
212
|
-
<ResponsiveContainer width="100%" height={Math.max(300, chartData.length * 40)}>
|
|
455
|
+
<ResponsiveContainer width="100%" height={Math.max(compact ? 240 : 300, chartData.length * (compact ? 32 : 40))}>
|
|
213
456
|
<BarChart data={chartData} layout="vertical" margin={{ top: 5, right: 30, left: 100, bottom: 5 }}>
|
|
214
457
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_STROKE} />
|
|
215
458
|
<XAxis type="number" tick={AXIS_TICK} axisLine={AXIS_LINE} tickLine={AXIS_LINE}
|
|
216
459
|
{...(yLabel ? { label: { value: yLabel, position: 'insideBottom', offset: -5, style: { textAnchor: 'middle', fill: 'var(--text-muted, #666)' } } } : {})}
|
|
217
460
|
/>
|
|
218
461
|
<YAxis type="category" dataKey="x" tick={{ fill: 'var(--text-muted, #666)', fontSize: 11 }} width={90} axisLine={AXIS_LINE} tickLine={AXIS_LINE} />
|
|
219
|
-
<
|
|
220
|
-
{!isSingleSeries && <Legend wrapperStyle={LEGEND_STYLE} />}
|
|
462
|
+
{!isSingleSeries && <Legend wrapperStyle={legendWrapperStyle} height={legendHeight} />}
|
|
221
463
|
{seriesNames.map((name, i) => (
|
|
222
464
|
<Bar key={name} dataKey={name} fill={COLORS[i % COLORS.length]} radius={[0, 2, 2, 0]} isAnimationActive={false}>
|
|
223
465
|
{isSingleSeries && chartData.map((_, idx) => (
|
|
@@ -225,6 +467,7 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
225
467
|
))}
|
|
226
468
|
</Bar>
|
|
227
469
|
))}
|
|
470
|
+
<Tooltip wrapperStyle={TOOLTIP_STYLE.wrapperStyle} content={<ChartTooltip />} cursor={{ fill: 'rgba(94, 136, 176, 0.06)', stroke: 'rgba(52, 58, 64, 0.18)', strokeDasharray: '3 3' }} isAnimationActive={false} animationDuration={0} />
|
|
228
471
|
</BarChart>
|
|
229
472
|
</ResponsiveContainer>
|
|
230
473
|
);
|
|
@@ -232,13 +475,12 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
232
475
|
|
|
233
476
|
case 'line':
|
|
234
477
|
return (
|
|
235
|
-
<ResponsiveContainer width="100%" height={
|
|
236
|
-
<LineChart data={chartData} margin={{ top: 5, right: 30, left:
|
|
478
|
+
<ResponsiveContainer width="100%" height={chartHeight}>
|
|
479
|
+
<LineChart data={chartData} margin={{ top: 5, right: 30, left: yLabel ? 10 : 20, bottom: bottomMargin }}>
|
|
237
480
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_STROKE} />
|
|
238
481
|
<XAxis {...xAxisProps} />
|
|
239
482
|
<YAxis {...yAxisProps} />
|
|
240
|
-
<
|
|
241
|
-
<Legend wrapperStyle={LEGEND_STYLE} />
|
|
483
|
+
<Legend wrapperStyle={legendWrapperStyle} height={legendHeight} />
|
|
242
484
|
{seriesNames.map((name, i) => (
|
|
243
485
|
<Line key={name} type="monotone" dataKey={name}
|
|
244
486
|
stroke={COLORS[i % COLORS.length]} strokeWidth={2}
|
|
@@ -247,19 +489,19 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
247
489
|
isAnimationActive={false}
|
|
248
490
|
/>
|
|
249
491
|
))}
|
|
492
|
+
<Tooltip wrapperStyle={TOOLTIP_STYLE.wrapperStyle} content={<ChartTooltip />} cursor={{ fill: 'rgba(94, 136, 176, 0.06)', stroke: 'rgba(52, 58, 64, 0.18)', strokeDasharray: '3 3' }} isAnimationActive={false} animationDuration={0} />
|
|
250
493
|
</LineChart>
|
|
251
494
|
</ResponsiveContainer>
|
|
252
495
|
);
|
|
253
496
|
|
|
254
497
|
case 'area':
|
|
255
498
|
return (
|
|
256
|
-
<ResponsiveContainer width="100%" height={
|
|
257
|
-
<AreaChart data={chartData} margin={{ top: 5, right: 30, left:
|
|
499
|
+
<ResponsiveContainer width="100%" height={chartHeight}>
|
|
500
|
+
<AreaChart data={chartData} margin={{ top: 5, right: 30, left: yLabel ? 10 : 20, bottom: bottomMargin }}>
|
|
258
501
|
<CartesianGrid strokeDasharray="3 3" stroke={GRID_STROKE} />
|
|
259
502
|
<XAxis {...xAxisProps} />
|
|
260
503
|
<YAxis {...yAxisProps} />
|
|
261
|
-
<
|
|
262
|
-
<Legend wrapperStyle={LEGEND_STYLE} />
|
|
504
|
+
<Legend wrapperStyle={legendWrapperStyle} height={legendHeight} />
|
|
263
505
|
{seriesNames.map((name, i) => (
|
|
264
506
|
<Area key={name} type="monotone" dataKey={name}
|
|
265
507
|
stroke={COLORS[i % COLORS.length]} strokeWidth={2}
|
|
@@ -267,6 +509,7 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
267
509
|
isAnimationActive={false}
|
|
268
510
|
/>
|
|
269
511
|
))}
|
|
512
|
+
<Tooltip wrapperStyle={TOOLTIP_STYLE.wrapperStyle} content={<ChartTooltip />} cursor={{ fill: 'rgba(94, 136, 176, 0.06)', stroke: 'rgba(52, 58, 64, 0.18)', strokeDasharray: '3 3' }} isAnimationActive={false} animationDuration={0} />
|
|
270
513
|
</AreaChart>
|
|
271
514
|
</ResponsiveContainer>
|
|
272
515
|
);
|
|
@@ -292,16 +535,16 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
292
535
|
);
|
|
293
536
|
};
|
|
294
537
|
return (
|
|
295
|
-
<ResponsiveContainer width="100%" height={320}>
|
|
296
|
-
<PieChart>
|
|
297
|
-
<Pie data={pieData} cx="
|
|
538
|
+
<ResponsiveContainer width="100%" height={compact ? 260 : 320}>
|
|
539
|
+
<PieChart margin={{ top: 16, right: 16, bottom: 16, left: 16 }}>
|
|
540
|
+
<Pie data={pieData} cx={compact ? "42%" : "38%"} cy="50%" outerRadius={compact ? 88 : 110}
|
|
298
541
|
dataKey="value" label={renderLabel} labelLine={{ stroke: 'var(--text-muted, #666)' }}
|
|
299
542
|
isAnimationActive={false}>
|
|
300
543
|
{pieData.map((entry, i) => (
|
|
301
544
|
<Cell key={i} fill={entry.fill} />
|
|
302
545
|
))}
|
|
303
546
|
</Pie>
|
|
304
|
-
<Tooltip {
|
|
547
|
+
<Tooltip wrapperStyle={TOOLTIP_STYLE.wrapperStyle} content={<ChartTooltip />} cursor={{ fill: 'rgba(94, 136, 176, 0.06)', stroke: 'rgba(52, 58, 64, 0.18)', strokeDasharray: '3 3' }} isAnimationActive={false} animationDuration={0} />
|
|
305
548
|
</PieChart>
|
|
306
549
|
</ResponsiveContainer>
|
|
307
550
|
);
|
|
@@ -367,9 +610,17 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
367
610
|
}
|
|
368
611
|
};
|
|
369
612
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
613
|
+
const showLegendInExport = chartType === 'line'
|
|
614
|
+
|| chartType === 'area'
|
|
615
|
+
|| ((chartType === 'bar' || chartType === 'horizontal_bar') && !isSingleSeries);
|
|
616
|
+
|
|
617
|
+
const exportLegendItems = showLegendInExport
|
|
618
|
+
? seriesNames.map((name, i) => ({ label: name, color: COLORS[i % COLORS.length] }))
|
|
619
|
+
: [];
|
|
620
|
+
|
|
621
|
+
const handleDownloadPng = useCallback(() => {
|
|
622
|
+
if (chartRef.current) downloadPng(chartRef.current, title, exportLegendItems);
|
|
623
|
+
}, [title, exportLegendItems]);
|
|
373
624
|
|
|
374
625
|
const handleCopy = useCallback(() => {
|
|
375
626
|
copyAsCsv(series, allXValues);
|
|
@@ -382,7 +633,7 @@ const ChartRenderer = ({ chartType, title, xLabel, yLabel, series }) => {
|
|
|
382
633
|
return (
|
|
383
634
|
<div style={containerStyle}>
|
|
384
635
|
{title && <div style={titleStyle}>{title}</div>}
|
|
385
|
-
{showActions && <ActionBar
|
|
636
|
+
{showActions && <ActionBar onDownloadPng={handleDownloadPng} onCopy={handleCopy} copied={copied} />}
|
|
386
637
|
<div ref={chartRef}>
|
|
387
638
|
{renderChart()}
|
|
388
639
|
</div>
|