lupine.components 1.1.30 → 1.1.32

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/component-pool/charts/area-chart-demo.tsx +110 -0
  3. package/src/component-pool/charts/area-chart.tsx +251 -0
  4. package/src/component-pool/charts/bar-chart-demo.tsx +110 -0
  5. package/src/component-pool/charts/bar-chart.tsx +220 -0
  6. package/src/component-pool/charts/chart-utils.ts +181 -0
  7. package/src/component-pool/charts/column-chart-demo.tsx +114 -0
  8. package/src/component-pool/charts/column-chart.tsx +227 -0
  9. package/src/component-pool/charts/donut-chart-demo.tsx +115 -0
  10. package/src/component-pool/charts/donut-chart.tsx +28 -0
  11. package/src/component-pool/charts/gauge-chart-demo.tsx +105 -0
  12. package/src/component-pool/charts/gauge-chart.tsx +113 -0
  13. package/src/component-pool/charts/index.ts +19 -0
  14. package/src/component-pool/charts/line-chart-demo.tsx +113 -0
  15. package/src/component-pool/charts/line-chart.tsx +230 -0
  16. package/src/component-pool/charts/pie-chart-demo.tsx +139 -0
  17. package/src/component-pool/charts/pie-chart.tsx +125 -0
  18. package/src/component-pool/charts/radar-chart-demo.tsx +91 -0
  19. package/src/component-pool/charts/radar-chart.tsx +196 -0
  20. package/src/component-pool/charts/scatter-chart-demo.tsx +91 -0
  21. package/src/component-pool/charts/scatter-chart.tsx +234 -0
  22. package/src/component-pool/radial-progress/radial-progress-demo.tsx +0 -1
  23. package/src/component-pool/tooltip/tooltip.tsx +1 -3
  24. package/src/components/action-sheet-date.tsx +133 -20
  25. package/src/components/action-sheet-demo.tsx +37 -0
  26. package/src/components/action-sheet.tsx +63 -0
  27. package/src/components/button-demo.tsx +101 -98
  28. package/src/components/button-push-animation-demo.tsx +71 -83
  29. package/src/components/editable-label-demo.tsx +39 -36
  30. package/src/components/input-number-demo.tsx +131 -163
  31. package/src/components/menu-item-props.tsx +3 -0
  32. package/src/components/mobile-components/mobile-side-menu.tsx +21 -5
  33. package/src/components/mobile-components/mobile-top-sys-icon.tsx +1 -1
  34. package/src/components/paging-link-demo.tsx +74 -90
  35. package/src/components/popup-menu-demo.tsx +128 -24
  36. package/src/components/popup-menu.tsx +454 -207
  37. package/src/components/toggle-switch-demo.tsx +227 -224
  38. package/src/demo/demo-frame-helper.tsx +238 -151
  39. package/src/demo/demo-registry.ts +20 -0
  40. package/src/demo/demo-render-page.tsx +2 -1
  41. package/src/demo/mock/side-menu-mock.tsx +1 -1
  42. package/src/frames/index.ts +1 -1
  43. package/src/frames/responsive-frame.tsx +26 -6
  44. package/src/frames/slider-frame.tsx +9 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.components",
3
- "version": "1.1.30",
3
+ "version": "1.1.32",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -0,0 +1,110 @@
1
+ import { AreaChart } from './area-chart';
2
+ import { DemoStory } from '../../demo/demo-types';
3
+ import { ChartData } from './chart-utils';
4
+
5
+ const multiSeriesData: ChartData = {
6
+ labels: ['2019', '2020', '2021', '2022', '2023', '2024'],
7
+ series: [
8
+ { name: 'Cloud Revenue', data: [15, 22, 35, 48, 65, 80] },
9
+ { name: 'Hardware Sales', data: [50, 48, 45, 40, 35, 30] },
10
+ ],
11
+ };
12
+
13
+ export const areaChartDemo: DemoStory<any> = {
14
+ id: 'areaChartDemo',
15
+ text: 'Area Chart',
16
+ args: {
17
+ title: 'Company Revenues',
18
+ width: '100%',
19
+ height: '350px',
20
+ showLegend: true,
21
+ curved: true,
22
+ },
23
+ argTypes: {
24
+ title: { control: 'text' },
25
+ width: { control: 'text' },
26
+ height: { control: 'text' },
27
+ showLegend: { control: 'boolean' },
28
+ curved: { control: 'boolean' },
29
+ },
30
+ render: (args: any) => {
31
+ const css = {
32
+ display: 'flex',
33
+ flexDirection: 'column',
34
+ gap: '48px',
35
+ padding: '24px',
36
+ alignItems: 'center',
37
+
38
+ '.demo-section': {
39
+ display: 'flex',
40
+ flexDirection: 'column',
41
+ alignItems: 'center',
42
+ gap: '24px',
43
+ width: '100%',
44
+ maxWidth: '800px',
45
+ },
46
+ '.section-title': {
47
+ fontSize: '20px',
48
+ fontWeight: 'bold',
49
+ color: 'var(--primary-color)',
50
+ marginBottom: '16px',
51
+ borderBottom: '2px solid var(--secondary-border-color)',
52
+ width: '100%',
53
+ paddingBottom: '8px',
54
+ },
55
+ '.chart-box': {
56
+ padding: '20px',
57
+ border: '1px solid var(--secondary-border-color)',
58
+ borderRadius: '8px',
59
+ backgroundColor: 'var(--primary-bg-color)',
60
+ width: '100%',
61
+ },
62
+ };
63
+
64
+ return (
65
+ <div css={css}>
66
+ <section class='demo-section'>
67
+ <div class='section-title'>Interactive Control</div>
68
+ <div class='chart-box'>
69
+ <AreaChart
70
+ data={multiSeriesData}
71
+ title={args.title}
72
+ width={args.width}
73
+ height={args.height}
74
+ showLegend={args.showLegend}
75
+ curved={args.curved}
76
+ yAxisFormatter={(val) => '$' + val + 'M'}
77
+ />
78
+ </div>
79
+ </section>
80
+
81
+ <section class='demo-section'>
82
+ <div class='section-title'>Straight Area Chart</div>
83
+ <div class='chart-box'>
84
+ <AreaChart
85
+ data={multiSeriesData}
86
+ title='Straight Area Comparison'
87
+ height='300px'
88
+ showLegend={true}
89
+ curved={false}
90
+ yAxisFormatter={(val) => '$' + val + 'M'}
91
+ />
92
+ </div>
93
+ </section>
94
+ </div>
95
+ );
96
+ },
97
+ code: `import { AreaChart } from 'lupine.components/component-pool/charts';
98
+
99
+ const data = {
100
+ labels: ['Q1', 'Q2', 'Q3'],
101
+ series: [{ name: 'Growth', data: [10, 25, 45] }]
102
+ };
103
+
104
+ <AreaChart
105
+ data={data}
106
+ title="Growth Area"
107
+ curved={true}
108
+ />
109
+ `,
110
+ };
@@ -0,0 +1,251 @@
1
+ import { bindGlobalStyle, getGlobalStylesId, HtmlVar } from 'lupine.components';
2
+ import { chartCommonCss, getChartColor, BasicChartProps } from './chart-utils';
3
+ import { Tooltip } from '../tooltip';
4
+
5
+ export type AreaChartProps = BasicChartProps & {
6
+ yAxisFormatter?: (value: number) => string;
7
+ curved?: boolean;
8
+ };
9
+
10
+ export const AreaChart = (props: AreaChartProps) => {
11
+ const globalCssId = getGlobalStylesId(chartCommonCss);
12
+ bindGlobalStyle(globalCssId, chartCommonCss);
13
+
14
+ const showLegend = props.showLegend !== false;
15
+ const series = props.data.series;
16
+ const labels = props.data.labels;
17
+
18
+ if (!series || series.length === 0 || labels.length === 0) {
19
+ return <div class='&-container'>No data</div>;
20
+ }
21
+
22
+ // Find max value for Y axis scale
23
+ let maxVal = 0;
24
+ series.forEach((s) => {
25
+ s.data.forEach((val) => {
26
+ if (val > maxVal) maxVal = val;
27
+ });
28
+ });
29
+
30
+ const tickCount = 5;
31
+ const order = Math.floor(Math.log10(maxVal || 1));
32
+ const magnitude = Math.pow(10, order);
33
+ const niceStep = Math.ceil(maxVal / magnitude / tickCount) * magnitude;
34
+ const niceMax = niceStep * tickCount;
35
+
36
+ const yTicks = Array.from({ length: tickCount + 1 }, (_, i) => parseFloat((i * niceStep).toPrecision(12)));
37
+
38
+ const renderChart = (viewBoxWidth: number, viewBoxHeight: number) => {
39
+ // Layout parameters
40
+ const padding = { top: 20, right: 80, bottom: 40, left: 60 };
41
+
42
+ const chartWidth = viewBoxWidth - padding.left - padding.right;
43
+ const chartHeight = viewBoxHeight - padding.top - padding.bottom;
44
+
45
+ // Calculate points considering a half step padding on left and right for area/line
46
+ const pointStep = chartWidth / Math.max(1, labels.length);
47
+ const dataLeftPadding = pointStep / 2;
48
+
49
+ const formatY = props.yAxisFormatter || ((val) => val.toString());
50
+
51
+ // Render Y Axis
52
+ const renderYAxis = () => {
53
+ return yTicks.map((val) => {
54
+ const y = padding.top + chartHeight - (val / niceMax) * chartHeight;
55
+ return (
56
+ <g>
57
+ <line
58
+ x1={padding.left}
59
+ y1={y}
60
+ x2={viewBoxWidth - padding.right}
61
+ y2={y}
62
+ stroke='var(--secondary-border-color)'
63
+ stroke-width='1'
64
+ stroke-dasharray='4 4'
65
+ />
66
+ <text x={padding.left - 15} y={y + 4} fill='var(--primary-color)' fontSize='12' text-anchor='end'>
67
+ {formatY(val)}
68
+ </text>
69
+ </g>
70
+ );
71
+ });
72
+ };
73
+
74
+ // Render X Axis
75
+ const renderXAxis = () => {
76
+ return labels.map((label, index) => {
77
+ const x = padding.left + dataLeftPadding + index * pointStep;
78
+ const y = viewBoxHeight - padding.bottom + 25;
79
+ return (
80
+ <text x={x} y={y} fill='var(--primary-color)' fontSize='12' text-anchor='middle'>
81
+ {label}
82
+ </text>
83
+ );
84
+ });
85
+ };
86
+
87
+ // Render Areas, Lines & Points
88
+ const renderAreasLinesAndPoints = () => {
89
+ const areas: any[] = [];
90
+ const lines: any[] = [];
91
+ const points: any[] = [];
92
+
93
+ // Render area in reverse order so first series is drawn last (on top) if they overlap
94
+ // Note: Better visualization is sometimes stacking, but basic is overlapping
95
+ series
96
+ .slice()
97
+ .reverse()
98
+ .forEach((s, revIndex) => {
99
+ const sIndex = series.length - 1 - revIndex;
100
+ const color = s.color || getChartColor(sIndex);
101
+
102
+ const coordinates = s.data.map((val, lIndex) => {
103
+ const x = padding.left + dataLeftPadding + lIndex * pointStep;
104
+ const y = padding.top + chartHeight - (val / niceMax) * chartHeight;
105
+ return { x, y, val, label: labels[lIndex] };
106
+ });
107
+
108
+ // Draw Path Base
109
+ let linePath = '';
110
+ if (props.curved && coordinates.length > 2) {
111
+ linePath = `M ${coordinates[0].x} ${coordinates[0].y} `;
112
+ for (let i = 0; i < coordinates.length - 1; i++) {
113
+ const current = coordinates[i];
114
+ const next = coordinates[i + 1];
115
+ const mx = (current.x + next.x) / 2;
116
+ linePath += `C ${mx} ${current.y}, ${mx} ${next.y}, ${next.x} ${next.y} `;
117
+ }
118
+ } else {
119
+ linePath = 'M ' + coordinates.map((c) => `${c.x} ${c.y}`).join(' L ');
120
+ }
121
+
122
+ const endX = coordinates[coordinates.length - 1].x;
123
+ const startX = coordinates[0].x;
124
+ const baseY = padding.top + chartHeight;
125
+
126
+ const areaPath = `${linePath} L ${endX} ${baseY} L ${startX} ${baseY} Z`;
127
+
128
+ areas.push(
129
+ <path
130
+ d={areaPath}
131
+ fill={color}
132
+ opacity='0.3' // Transparency for area
133
+ />
134
+ );
135
+
136
+ lines.push(<path d={linePath} fill='none' stroke={color} stroke-width='3' />);
137
+
138
+ // Draw interactive points overlay
139
+ coordinates.forEach((c) => {
140
+ const handleMouseEnter = (e: any) => {
141
+ Tooltip.show(
142
+ e,
143
+ <div>
144
+ <div style={{ fontWeight: 'bold' }}>{c.label}</div>
145
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
146
+ <div style={{ width: '10px', height: '10px', backgroundColor: color, borderRadius: '2px' }} />
147
+ <span>
148
+ {s.name}: {formatY(c.val)}
149
+ </span>
150
+ </div>
151
+ </div>,
152
+ { position: 'auto' }
153
+ );
154
+ e.target.setAttribute('r', '6');
155
+ };
156
+
157
+ const handleMouseLeave = (e: any) => {
158
+ Tooltip.hide();
159
+ e.target.setAttribute('r', '4');
160
+ };
161
+
162
+ points.push(
163
+ <circle
164
+ class='chart-element'
165
+ cx={c.x}
166
+ cy={c.y}
167
+ r='4'
168
+ fill='var(--primary-bg-color)'
169
+ stroke={color}
170
+ stroke-width='2'
171
+ onMouseEnter={handleMouseEnter}
172
+ onMouseLeave={handleMouseLeave}
173
+ style={{ transition: 'r 0.2s ease' }}
174
+ />
175
+ );
176
+ });
177
+ });
178
+
179
+ return { areas, lines, points };
180
+ };
181
+
182
+ const { areas, lines, points } = renderAreasLinesAndPoints();
183
+
184
+ return (
185
+ <svg
186
+ class='chart-svg'
187
+ style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible' }}
188
+ viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
189
+ preserveAspectRatio='none'
190
+ >
191
+ {/* Base Axis line */}
192
+ <line
193
+ x1={padding.left}
194
+ y1={padding.top + chartHeight}
195
+ x2={viewBoxWidth - padding.right}
196
+ y2={padding.top + chartHeight}
197
+ stroke='var(--secondary-color)'
198
+ stroke-width='2'
199
+ />
200
+
201
+ <g class='y-axis'>{renderYAxis()}</g>
202
+ <g class='x-axis'>{renderXAxis()}</g>
203
+ <g class='areas'>{areas}</g>
204
+ <g class='lines'>{lines}</g>
205
+ <g class='points'>{points}</g>
206
+ </svg>
207
+ );
208
+ };
209
+
210
+ const chartVar = new HtmlVar(renderChart(1000, 300));
211
+
212
+ const ref = {
213
+ globalCssId,
214
+ onLoad: async (el: Element) => {
215
+ const ro = new ResizeObserver((entries) => {
216
+ const { width, height } = entries[0].contentRect;
217
+ if (width > 50 && height > 50) {
218
+ chartVar.value = renderChart(width, height);
219
+ }
220
+ });
221
+ ro.observe(el);
222
+ (el as any)._ro = ro;
223
+ },
224
+ onUnload: async (el: Element) => {
225
+ if ((el as any)._ro) {
226
+ (el as any)._ro.disconnect();
227
+ }
228
+ },
229
+ };
230
+
231
+ const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
232
+
233
+ return (
234
+ <div ref={ref} class='&-container' style={styleStr}>
235
+ {props.title && <div class='chart-title'>{props.title}</div>}
236
+
237
+ <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
238
+
239
+ {showLegend && (
240
+ <div class='chart-legend'>
241
+ {series.map((s, i) => (
242
+ <div class='legend-item'>
243
+ <div class='legend-color' style={{ backgroundColor: s.color || getChartColor(i) }} />
244
+ <div>{s.name}</div>
245
+ </div>
246
+ ))}
247
+ </div>
248
+ )}
249
+ </div>
250
+ );
251
+ };
@@ -0,0 +1,110 @@
1
+ import { BarChart } from './bar-chart';
2
+ import { DemoStory } from '../../demo/demo-types';
3
+ import { ChartData } from './chart-utils';
4
+
5
+ const singleSeriesData: ChartData = {
6
+ labels: ['United States', 'China', 'Japan', 'Germany', 'United Kingdom'],
7
+ series: [
8
+ {
9
+ name: 'GDP (Trillions)',
10
+ data: [25.4, 17.9, 4.2, 4.0, 3.0],
11
+ },
12
+ ],
13
+ };
14
+
15
+ const multiSeriesData: ChartData = {
16
+ labels: ['Q1', 'Q2', 'Q3', 'Q4'],
17
+ series: [
18
+ { name: 'Product A', data: [40, 30, 50, 70] },
19
+ { name: 'Product B', data: [20, 40, 30, 60] },
20
+ ],
21
+ };
22
+
23
+ export const barChartDemo: DemoStory<any> = {
24
+ id: 'barChartDemo',
25
+ text: 'Bar Chart',
26
+ args: {
27
+ title: 'Top Economies',
28
+ width: '100%',
29
+ height: '350px',
30
+ showLegend: true,
31
+ },
32
+ argTypes: {
33
+ title: { control: 'text' },
34
+ width: { control: 'text' },
35
+ height: { control: 'text' },
36
+ showLegend: { control: 'boolean' },
37
+ },
38
+ render: (args: any) => {
39
+ const css = {
40
+ display: 'flex',
41
+ flexDirection: 'column',
42
+ gap: '48px',
43
+ padding: '24px',
44
+ alignItems: 'center',
45
+
46
+ '.demo-section': {
47
+ display: 'flex',
48
+ flexDirection: 'column',
49
+ alignItems: 'center',
50
+ gap: '24px',
51
+ width: '100%',
52
+ maxWidth: '800px',
53
+ },
54
+ '.section-title': {
55
+ fontSize: '20px',
56
+ fontWeight: 'bold',
57
+ color: 'var(--primary-color)',
58
+ marginBottom: '16px',
59
+ borderBottom: '2px solid var(--secondary-border-color)',
60
+ width: '100%',
61
+ paddingBottom: '8px',
62
+ },
63
+ '.chart-box': {
64
+ padding: '20px',
65
+ border: '1px solid var(--secondary-border-color)',
66
+ borderRadius: '8px',
67
+ backgroundColor: 'var(--primary-bg-color)',
68
+ width: '100%',
69
+ },
70
+ };
71
+
72
+ return (
73
+ <div css={css}>
74
+ <section class='demo-section'>
75
+ <div class='section-title'>Interactive Control</div>
76
+ <div class='chart-box'>
77
+ <BarChart
78
+ data={singleSeriesData}
79
+ title={args.title}
80
+ width={args.width}
81
+ height={args.height}
82
+ showLegend={args.showLegend}
83
+ xAxisFormatter={(val) => '$' + val + 'T'}
84
+ />
85
+ </div>
86
+ </section>
87
+
88
+ <section class='demo-section'>
89
+ <div class='section-title'>Multi-Series (Grouped Horizontal)</div>
90
+ <div class='chart-box'>
91
+ <BarChart data={multiSeriesData} title='Quarterly Comparison' height='300px' showLegend={true} />
92
+ </div>
93
+ </section>
94
+ </div>
95
+ );
96
+ },
97
+ code: `import { BarChart } from 'lupine.components/component-pool/charts';
98
+
99
+ const data = {
100
+ labels: ['US', 'CN', 'JP'],
101
+ series: [{ name: 'GDP', data: [25, 18, 4] }]
102
+ };
103
+
104
+ <BarChart
105
+ data={data}
106
+ title="Top Economies"
107
+ xAxisFormatter={val => '$' + val + 'T'}
108
+ />
109
+ `,
110
+ };
@@ -0,0 +1,220 @@
1
+ import { bindGlobalStyle, getGlobalStylesId, HtmlVar } from 'lupine.components';
2
+ import { chartCommonCss, getChartColor, BasicChartProps } from './chart-utils';
3
+ import { Tooltip } from '../tooltip';
4
+
5
+ export type BarChartProps = BasicChartProps & {
6
+ xAxisFormatter?: (value: number) => string;
7
+ };
8
+
9
+ export const BarChart = (props: BarChartProps) => {
10
+ const globalCssId = getGlobalStylesId(chartCommonCss);
11
+ bindGlobalStyle(globalCssId, chartCommonCss);
12
+
13
+ const showLegend = props.showLegend !== false;
14
+ const series = props.data.series;
15
+ const labels = props.data.labels;
16
+
17
+ if (!series || series.length === 0 || labels.length === 0) {
18
+ return <div class='&-container'>No data</div>;
19
+ }
20
+
21
+ // Find max value for X axis scale
22
+ let maxVal = 0;
23
+ series.forEach((s) => {
24
+ s.data.forEach((val) => {
25
+ if (val > maxVal) maxVal = val;
26
+ });
27
+ });
28
+
29
+ const tickCount = 5;
30
+ const order = Math.floor(Math.log10(maxVal || 1));
31
+ const magnitude = Math.pow(10, order);
32
+ const niceStep = Math.ceil(maxVal / magnitude / tickCount) * magnitude;
33
+ const niceMax = niceStep * tickCount;
34
+
35
+ const xTicks = Array.from({ length: tickCount + 1 }, (_, i) => parseFloat((i * niceStep).toPrecision(12)));
36
+
37
+ const renderChart = (viewBoxWidth: number, viewBoxHeight: number) => {
38
+ // Layout parameters for Bar Chart (flipped)
39
+ const padding = { top: 20, right: 60, bottom: 40, left: 150 }; // larger left padding for labels
40
+
41
+ const chartWidth = viewBoxWidth - padding.left - padding.right;
42
+ const chartHeight = viewBoxHeight - padding.top - padding.bottom;
43
+
44
+ const groupHeight = chartHeight / Math.max(1, labels.length);
45
+ // Reduce barPadding a bit, ensure the whole group fits inside the allocated groupHeight height.
46
+ const barPadding = 0.2 * groupHeight;
47
+ const barHeight = (groupHeight - barPadding) / Math.max(1, series.length);
48
+
49
+ const formatX = props.xAxisFormatter || ((val) => val.toString());
50
+
51
+ // Render X Axis Ticks & Grid (Vertical lines now)
52
+ const renderXAxis = () => {
53
+ return xTicks.map((val) => {
54
+ const x = padding.left + (val / niceMax) * chartWidth;
55
+ return (
56
+ <g>
57
+ <line
58
+ x1={x}
59
+ y1={padding.top}
60
+ x2={x}
61
+ y2={viewBoxHeight - padding.bottom}
62
+ stroke='var(--secondary-border-color)'
63
+ stroke-width='1'
64
+ stroke-dasharray='4 4'
65
+ />
66
+ <text
67
+ x={x}
68
+ y={viewBoxHeight - padding.bottom + 25}
69
+ fill='var(--primary-color)'
70
+ fontSize='12'
71
+ text-anchor='middle'
72
+ >
73
+ {formatX(val)}
74
+ </text>
75
+ </g>
76
+ );
77
+ });
78
+ };
79
+
80
+ // Render Y Axis Labels (Horizontal text now)
81
+ const renderYAxis = () => {
82
+ return labels.map((label, index) => {
83
+ const y = padding.top + index * groupHeight + groupHeight / 2;
84
+ return (
85
+ <text
86
+ x={padding.left - 15}
87
+ y={y + 4} // slight alignment offset
88
+ fill='var(--primary-color)'
89
+ fontSize='12'
90
+ text-anchor='end'
91
+ >
92
+ {label}
93
+ </text>
94
+ );
95
+ });
96
+ };
97
+
98
+ // Render Bars (Horizontal rectangles now)
99
+ const renderBars = () => {
100
+ const bars: any[] = [];
101
+
102
+ labels.forEach((label, lIndex) => {
103
+ const groupY = padding.top + lIndex * groupHeight + barPadding / 2;
104
+
105
+ series.forEach((s, sIndex) => {
106
+ const val = s.data[lIndex] || 0;
107
+ const color = s.color || getChartColor(sIndex);
108
+
109
+ const barWidth = (val / niceMax) * chartWidth;
110
+ const x = padding.left;
111
+ const y = groupY + sIndex * barHeight;
112
+
113
+ const handleMouseEnter = (e: any) => {
114
+ Tooltip.show(
115
+ e,
116
+ <div>
117
+ <div style={{ fontWeight: 'bold' }}>{label}</div>
118
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
119
+ <div style={{ width: '10px', height: '10px', backgroundColor: color, borderRadius: '2px' }} />
120
+ <span>
121
+ {s.name}: {formatX(val)}
122
+ </span>
123
+ </div>
124
+ </div>,
125
+ { position: 'auto' }
126
+ );
127
+ e.target.style.opacity = 0.8;
128
+ };
129
+
130
+ const handleMouseLeave = (e: any) => {
131
+ Tooltip.hide();
132
+ e.target.style.opacity = 1;
133
+ };
134
+
135
+ bars.push(
136
+ <rect
137
+ class='chart-element'
138
+ x={x}
139
+ y={y}
140
+ width={barWidth}
141
+ height={barHeight - 2} // -2 for slight individual bar padding
142
+ fill={color}
143
+ rx='2'
144
+ ry='2'
145
+ onMouseEnter={handleMouseEnter}
146
+ onMouseLeave={handleMouseLeave}
147
+ />
148
+ );
149
+ });
150
+ });
151
+
152
+ return bars;
153
+ };
154
+
155
+ return (
156
+ <svg
157
+ class='chart-svg'
158
+ style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible' }}
159
+ viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
160
+ preserveAspectRatio='none'
161
+ >
162
+ {/* Base Axis line (Vertical now) */}
163
+ <line
164
+ x1={padding.left}
165
+ y1={padding.top}
166
+ x2={padding.left}
167
+ y2={viewBoxHeight - padding.bottom}
168
+ stroke='var(--secondary-border-color)'
169
+ stroke-width='2'
170
+ />
171
+
172
+ <g class='x-axis'>{renderXAxis()}</g>
173
+ <g class='y-axis'>{renderYAxis()}</g>
174
+ <g class='bars'>{renderBars()}</g>
175
+ </svg>
176
+ );
177
+ };
178
+
179
+ const chartVar = new HtmlVar(renderChart(1000, Math.max(300, labels.length * 40 + 60)));
180
+
181
+ const ref = {
182
+ globalCssId,
183
+ onLoad: async (el: Element) => {
184
+ const ro = new ResizeObserver((entries) => {
185
+ const { width } = entries[0].contentRect;
186
+ if (width > 50) {
187
+ chartVar.value = renderChart(width, Math.max(300, labels.length * 40 + 60));
188
+ }
189
+ });
190
+ ro.observe(el);
191
+ (el as any)._ro = ro;
192
+ },
193
+ onUnload: async (el: Element) => {
194
+ if ((el as any)._ro) {
195
+ (el as any)._ro.disconnect();
196
+ }
197
+ },
198
+ };
199
+
200
+ const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
201
+
202
+ return (
203
+ <div ref={ref} class='&-container' style={styleStr}>
204
+ {props.title && <div class='chart-title'>{props.title}</div>}
205
+
206
+ <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
207
+
208
+ {showLegend && (
209
+ <div class='chart-legend'>
210
+ {series.map((s, i) => (
211
+ <div class='legend-item'>
212
+ <div class='legend-color' style={{ backgroundColor: s.color || getChartColor(i) }} />
213
+ <div>{s.name}</div>
214
+ </div>
215
+ ))}
216
+ </div>
217
+ )}
218
+ </div>
219
+ );
220
+ };