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.
- package/package.json +1 -1
- package/src/component-pool/charts/area-chart-demo.tsx +110 -0
- package/src/component-pool/charts/area-chart.tsx +251 -0
- package/src/component-pool/charts/bar-chart-demo.tsx +110 -0
- package/src/component-pool/charts/bar-chart.tsx +220 -0
- package/src/component-pool/charts/chart-utils.ts +181 -0
- package/src/component-pool/charts/column-chart-demo.tsx +114 -0
- package/src/component-pool/charts/column-chart.tsx +227 -0
- package/src/component-pool/charts/donut-chart-demo.tsx +115 -0
- package/src/component-pool/charts/donut-chart.tsx +28 -0
- package/src/component-pool/charts/gauge-chart-demo.tsx +105 -0
- package/src/component-pool/charts/gauge-chart.tsx +113 -0
- package/src/component-pool/charts/index.ts +19 -0
- package/src/component-pool/charts/line-chart-demo.tsx +113 -0
- package/src/component-pool/charts/line-chart.tsx +230 -0
- package/src/component-pool/charts/pie-chart-demo.tsx +139 -0
- package/src/component-pool/charts/pie-chart.tsx +125 -0
- package/src/component-pool/charts/radar-chart-demo.tsx +91 -0
- package/src/component-pool/charts/radar-chart.tsx +196 -0
- package/src/component-pool/charts/scatter-chart-demo.tsx +91 -0
- package/src/component-pool/charts/scatter-chart.tsx +234 -0
- package/src/component-pool/radial-progress/radial-progress-demo.tsx +0 -1
- package/src/component-pool/tooltip/tooltip.tsx +1 -3
- package/src/components/action-sheet-date.tsx +133 -20
- package/src/components/action-sheet-demo.tsx +37 -0
- package/src/components/action-sheet.tsx +63 -0
- package/src/components/button-demo.tsx +101 -98
- package/src/components/button-push-animation-demo.tsx +71 -83
- package/src/components/editable-label-demo.tsx +39 -36
- package/src/components/input-number-demo.tsx +131 -163
- package/src/components/menu-item-props.tsx +3 -0
- package/src/components/mobile-components/mobile-side-menu.tsx +21 -5
- package/src/components/mobile-components/mobile-top-sys-icon.tsx +1 -1
- package/src/components/paging-link-demo.tsx +74 -90
- package/src/components/popup-menu-demo.tsx +128 -24
- package/src/components/popup-menu.tsx +454 -207
- package/src/components/toggle-switch-demo.tsx +227 -224
- package/src/demo/demo-frame-helper.tsx +238 -151
- package/src/demo/demo-registry.ts +20 -0
- package/src/demo/demo-render-page.tsx +2 -1
- package/src/demo/mock/side-menu-mock.tsx +1 -1
- package/src/frames/index.ts +1 -1
- package/src/frames/responsive-frame.tsx +26 -6
- package/src/frames/slider-frame.tsx +9 -3
package/package.json
CHANGED
|
@@ -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
|
+
};
|