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.
Files changed (36) hide show
  1. package/dist/ChatMessage.cjs.js +15 -0
  2. package/dist/ChatMessage.cjs.js.map +1 -0
  3. package/dist/ChatMessage.es.js +915 -0
  4. package/dist/ChatMessage.es.js.map +1 -0
  5. package/dist/SideDrawer.cjs.js +2 -0
  6. package/dist/SideDrawer.cjs.js.map +1 -0
  7. package/dist/SideDrawer.es.js +358 -0
  8. package/dist/SideDrawer.es.js.map +1 -0
  9. package/dist/Toast.cjs.js +2 -2
  10. package/dist/Toast.cjs.js.map +1 -1
  11. package/dist/Toast.es.js +301 -671
  12. package/dist/Toast.es.js.map +1 -1
  13. package/dist/components/UpdatedInteractionDetails.cjs.js +4 -4
  14. package/dist/components/UpdatedInteractionDetails.cjs.js.map +1 -1
  15. package/dist/components/UpdatedInteractionDetails.es.js +652 -603
  16. package/dist/components/UpdatedInteractionDetails.es.js.map +1 -1
  17. package/dist/components/chat.cjs.js +5 -7
  18. package/dist/components/chat.cjs.js.map +1 -1
  19. package/dist/components/chat.es.js +393 -849
  20. package/dist/components/chat.es.js.map +1 -1
  21. package/dist/components/common.cjs.js +1 -1
  22. package/dist/components/common.es.js +28 -27
  23. package/dist/components/common.es.js.map +1 -1
  24. package/dist/index.cjs.js +1 -1
  25. package/dist/index.es.js +73 -72
  26. package/dist/index.es.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/UpdatedInteractionDetails/UpdatedInteractionContext.jsx +6 -2
  29. package/src/components/UpdatedInteractionDetails/UpdatedInteractionDetails.jsx +190 -139
  30. package/src/components/chat/ChartRenderer.jsx +300 -49
  31. package/src/components/common/AskCompass.jsx +91 -87
  32. package/src/components/common/SideDrawer.jsx +7 -3
  33. package/dist/ChartRenderer.cjs.js +0 -3
  34. package/dist/ChartRenderer.cjs.js.map +0 -1
  35. package/dist/ChartRenderer.es.js +0 -304
  36. 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
- contentStyle: {
29
- backgroundColor: 'var(--paper-elevated, #fff)',
30
- border: '1px solid var(--border, #e0e0e0)',
31
- borderRadius: 'var(--radius-sm, 4px)',
32
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
33
- color: 'var(--text-ink, #1e2125)',
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 downloadPng = async (containerEl, title) => {
50
- const svg = containerEl.querySelector('svg');
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, '&amp;')
188
+ .replace(/</g, '&lt;')
189
+ .replace(/>/g, '&gt;')
190
+ .replace(/"/g, '&quot;')
191
+ .replace(/'/g, '&apos;');
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 w = svg.clientWidth || 600;
55
- const h = svg.clientHeight || 300;
56
- clone.setAttribute('width', w);
57
- clone.setAttribute('height', h);
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 = ({ onDownload, onCopy, copied }) => (
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={onDownload} title="Download as PNG" style={{
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: { value: xLabel, position: 'insideBottom', offset: -10, style: { textAnchor: 'middle', fill: 'var(--text-muted, #666)' } }
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
- label: { value: yLabel, angle: -90, position: 'insideLeft', style: { textAnchor: 'middle', fill: 'var(--text-muted, #666)' } }
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={300}>
192
- <BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: rotateXTicks ? 40 : bottomMargin }}>
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
- <Tooltip {...TOOLTIP_STYLE} />
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
- <Tooltip {...TOOLTIP_STYLE} />
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={300}>
236
- <LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: rotateXTicks ? 40 : bottomMargin }}>
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
- <Tooltip {...TOOLTIP_STYLE} />
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={300}>
257
- <AreaChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: rotateXTicks ? 40 : bottomMargin }}>
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
- <Tooltip {...TOOLTIP_STYLE} />
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="50%" cy="50%" outerRadius={100}
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 {...TOOLTIP_STYLE} />
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 handleDownload = useCallback(() => {
371
- if (chartRef.current) downloadPng(chartRef.current, title);
372
- }, [title]);
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 onDownload={handleDownload} onCopy={handleCopy} copied={copied} />}
636
+ {showActions && <ActionBar onDownloadPng={handleDownloadPng} onCopy={handleCopy} copied={copied} />}
386
637
  <div ref={chartRef}>
387
638
  {renderChart()}
388
639
  </div>