lupine.components 1.1.41 → 1.1.42
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 +0 -4
- package/src/component-pool/charts/area-chart.tsx +5 -2
- package/src/component-pool/charts/bar-chart-demo.tsx +1 -4
- package/src/component-pool/charts/bar-chart.tsx +5 -2
- package/src/component-pool/charts/chart-utils.ts +1 -1
- package/src/component-pool/charts/column-chart-demo.tsx +1 -4
- package/src/component-pool/charts/column-chart.tsx +5 -2
- package/src/component-pool/charts/donut-chart-demo.tsx +2 -5
- package/src/component-pool/charts/donut-chart.tsx +1 -23
- package/src/component-pool/charts/gauge-chart-demo.tsx +0 -4
- package/src/component-pool/charts/gauge-chart.tsx +5 -2
- package/src/component-pool/charts/line-chart-demo.tsx +0 -4
- package/src/component-pool/charts/line-chart.tsx +5 -2
- package/src/component-pool/charts/pie-chart-demo.tsx +4 -10
- package/src/component-pool/charts/pie-chart.tsx +78 -61
- package/src/component-pool/charts/radar-chart-demo.tsx +0 -3
- package/src/component-pool/charts/radar-chart.tsx +40 -5
- package/src/component-pool/charts/scatter-chart-demo.tsx +0 -3
- package/src/component-pool/charts/scatter-chart.tsx +5 -2
- package/src/component-pool/index.ts +1 -0
- package/src/component-pool/svg-graph/index.ts +1 -0
- package/src/component-pool/svg-graph/svg-graph-demo.tsx +59 -0
- package/src/component-pool/svg-graph/svg-graph.ts +166 -0
- package/src/component-pool/youtube-player/youtube-player-demo.tsx +4 -4
- package/src/component-pool/youtube-player/youtube-player.tsx +89 -13
- package/src/components/menu-bar.tsx +15 -4
- package/src/components/menu-sidebar.tsx +3 -1
- package/src/components/tabs.tsx +9 -6
- package/src/demo/demo-about.tsx +1 -1
- package/src/demo/demo-frame-helper.tsx +8 -1
- package/src/demo/demo-registry.ts +2 -0
package/package.json
CHANGED
|
@@ -16,14 +16,12 @@ export const areaChartDemo: DemoStory<any> = {
|
|
|
16
16
|
args: {
|
|
17
17
|
title: 'Company Revenues',
|
|
18
18
|
width: '100%',
|
|
19
|
-
height: '350px',
|
|
20
19
|
showLegend: true,
|
|
21
20
|
curved: true,
|
|
22
21
|
},
|
|
23
22
|
argTypes: {
|
|
24
23
|
title: { control: 'text' },
|
|
25
24
|
width: { control: 'text' },
|
|
26
|
-
height: { control: 'text' },
|
|
27
25
|
showLegend: { control: 'boolean' },
|
|
28
26
|
curved: { control: 'boolean' },
|
|
29
27
|
},
|
|
@@ -70,7 +68,6 @@ export const areaChartDemo: DemoStory<any> = {
|
|
|
70
68
|
data={multiSeriesData}
|
|
71
69
|
title={args.title}
|
|
72
70
|
width={args.width}
|
|
73
|
-
height={args.height}
|
|
74
71
|
showLegend={args.showLegend}
|
|
75
72
|
curved={args.curved}
|
|
76
73
|
yAxisFormatter={(val) => '$' + val + 'M'}
|
|
@@ -84,7 +81,6 @@ export const areaChartDemo: DemoStory<any> = {
|
|
|
84
81
|
<AreaChart
|
|
85
82
|
data={multiSeriesData}
|
|
86
83
|
title='Straight Area Comparison'
|
|
87
|
-
height='300px'
|
|
88
84
|
showLegend={true}
|
|
89
85
|
curved={false}
|
|
90
86
|
yAxisFormatter={(val) => '$' + val + 'M'}
|
|
@@ -228,13 +228,16 @@ export const AreaChart = (props: AreaChartProps) => {
|
|
|
228
228
|
},
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
const
|
|
231
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
232
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
233
|
+
|
|
234
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
232
235
|
|
|
233
236
|
return (
|
|
234
237
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
235
238
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
236
239
|
|
|
237
|
-
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
|
|
240
|
+
<div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
|
|
238
241
|
|
|
239
242
|
{showLegend && (
|
|
240
243
|
<div class='chart-legend'>
|
|
@@ -26,13 +26,11 @@ export const barChartDemo: DemoStory<any> = {
|
|
|
26
26
|
args: {
|
|
27
27
|
title: 'Top Economies',
|
|
28
28
|
width: '100%',
|
|
29
|
-
height: '350px',
|
|
30
29
|
showLegend: true,
|
|
31
30
|
},
|
|
32
31
|
argTypes: {
|
|
33
32
|
title: { control: 'text' },
|
|
34
33
|
width: { control: 'text' },
|
|
35
|
-
height: { control: 'text' },
|
|
36
34
|
showLegend: { control: 'boolean' },
|
|
37
35
|
},
|
|
38
36
|
render: (args: any) => {
|
|
@@ -78,7 +76,6 @@ export const barChartDemo: DemoStory<any> = {
|
|
|
78
76
|
data={singleSeriesData}
|
|
79
77
|
title={args.title}
|
|
80
78
|
width={args.width}
|
|
81
|
-
height={args.height}
|
|
82
79
|
showLegend={args.showLegend}
|
|
83
80
|
xAxisFormatter={(val) => '$' + val + 'T'}
|
|
84
81
|
/>
|
|
@@ -88,7 +85,7 @@ export const barChartDemo: DemoStory<any> = {
|
|
|
88
85
|
<section class='demo-section'>
|
|
89
86
|
<div class='section-title'>Multi-Series (Grouped Horizontal)</div>
|
|
90
87
|
<div class='chart-box'>
|
|
91
|
-
<BarChart data={multiSeriesData} title='Quarterly Comparison'
|
|
88
|
+
<BarChart data={multiSeriesData} title='Quarterly Comparison' showLegend={true} />
|
|
92
89
|
</div>
|
|
93
90
|
</section>
|
|
94
91
|
</div>
|
|
@@ -197,13 +197,16 @@ export const BarChart = (props: BarChartProps) => {
|
|
|
197
197
|
},
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
const
|
|
200
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
201
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
202
|
+
|
|
203
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
201
204
|
|
|
202
205
|
return (
|
|
203
206
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
204
207
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
205
208
|
|
|
206
|
-
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
|
|
209
|
+
<div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
|
|
207
210
|
|
|
208
211
|
{showLegend && (
|
|
209
212
|
<div class='chart-legend'>
|
|
@@ -27,13 +27,11 @@ export const columnChartDemo: DemoStory<any> = {
|
|
|
27
27
|
args: {
|
|
28
28
|
title: 'Monthly Revenue',
|
|
29
29
|
width: '100%',
|
|
30
|
-
height: '350px',
|
|
31
30
|
showLegend: true,
|
|
32
31
|
},
|
|
33
32
|
argTypes: {
|
|
34
33
|
title: { control: 'text' },
|
|
35
34
|
width: { control: 'text' },
|
|
36
|
-
height: { control: 'text' },
|
|
37
35
|
showLegend: { control: 'boolean' },
|
|
38
36
|
},
|
|
39
37
|
render: (args: any) => {
|
|
@@ -79,7 +77,6 @@ export const columnChartDemo: DemoStory<any> = {
|
|
|
79
77
|
data={singleSeriesData}
|
|
80
78
|
title={args.title}
|
|
81
79
|
width={args.width}
|
|
82
|
-
height={args.height}
|
|
83
80
|
showLegend={args.showLegend}
|
|
84
81
|
yAxisFormatter={(val) => '$' + val}
|
|
85
82
|
/>
|
|
@@ -89,7 +86,7 @@ export const columnChartDemo: DemoStory<any> = {
|
|
|
89
86
|
<section class='demo-section'>
|
|
90
87
|
<div class='section-title'>Multi-Series (Grouped)</div>
|
|
91
88
|
<div class='chart-box'>
|
|
92
|
-
<ColumnChart data={multiSeriesData} title='Quarterly Product Sales'
|
|
89
|
+
<ColumnChart data={multiSeriesData} title='Quarterly Product Sales' showLegend={true} />
|
|
93
90
|
</div>
|
|
94
91
|
</section>
|
|
95
92
|
</div>
|
|
@@ -204,13 +204,16 @@ export const ColumnChart = (props: ColumnChartProps) => {
|
|
|
204
204
|
},
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
-
const
|
|
207
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
208
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
209
|
+
|
|
210
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
208
211
|
|
|
209
212
|
return (
|
|
210
213
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
211
214
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
212
215
|
|
|
213
|
-
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
|
|
216
|
+
<div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
|
|
214
217
|
|
|
215
218
|
{showLegend && (
|
|
216
219
|
<div class='chart-legend'>
|
|
@@ -18,14 +18,12 @@ export const donutChartDemo: DemoStory<any> = {
|
|
|
18
18
|
args: {
|
|
19
19
|
title: 'Traffic Sources',
|
|
20
20
|
width: '100%',
|
|
21
|
-
height: '300px',
|
|
22
21
|
showLegend: true,
|
|
23
22
|
innerRadiusRatio: 0.6,
|
|
24
23
|
},
|
|
25
24
|
argTypes: {
|
|
26
25
|
title: { control: 'text' },
|
|
27
26
|
width: { control: 'text' },
|
|
28
|
-
height: { control: 'text' },
|
|
29
27
|
showLegend: { control: 'boolean' },
|
|
30
28
|
innerRadiusRatio: { control: 'number' },
|
|
31
29
|
},
|
|
@@ -78,7 +76,6 @@ export const donutChartDemo: DemoStory<any> = {
|
|
|
78
76
|
data={sampleData}
|
|
79
77
|
title={args.title}
|
|
80
78
|
width={args.width}
|
|
81
|
-
height={args.height}
|
|
82
79
|
showLegend={args.showLegend}
|
|
83
80
|
innerRadiusRatio={args.innerRadiusRatio}
|
|
84
81
|
/>
|
|
@@ -89,10 +86,10 @@ export const donutChartDemo: DemoStory<any> = {
|
|
|
89
86
|
<div class='section-title'>Variations</div>
|
|
90
87
|
<div class='grid'>
|
|
91
88
|
<div class='chart-box'>
|
|
92
|
-
<DonutChart data={sampleData} title='Thin Ring' innerRadiusRatio={0.8}
|
|
89
|
+
<DonutChart data={sampleData} title='Thin Ring' innerRadiusRatio={0.8} />
|
|
93
90
|
</div>
|
|
94
91
|
<div class='chart-box'>
|
|
95
|
-
<DonutChart data={sampleData} title='Thick Ring' innerRadiusRatio={0.3}
|
|
92
|
+
<DonutChart data={sampleData} title='Thick Ring' innerRadiusRatio={0.3} />
|
|
96
93
|
</div>
|
|
97
94
|
</div>
|
|
98
95
|
</section>
|
|
@@ -2,27 +2,5 @@ import { PieChartProps, PieChart } from './pie-chart';
|
|
|
2
2
|
|
|
3
3
|
// Donut Chart is just a Pie Chart with an inner radius wrapper
|
|
4
4
|
export const DonutChart = (props: Omit<PieChartProps, 'innerRadiusRatio'> & { innerRadiusRatio?: number }) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<div
|
|
9
|
-
style={{
|
|
10
|
-
flex: 1,
|
|
11
|
-
position: 'relative',
|
|
12
|
-
display: 'flex',
|
|
13
|
-
justifyContent: 'center',
|
|
14
|
-
alignItems: 'center',
|
|
15
|
-
minHeight: '0',
|
|
16
|
-
}}
|
|
17
|
-
>
|
|
18
|
-
<svg
|
|
19
|
-
className='chart-svg' // Changed 'class' to 'className' for React
|
|
20
|
-
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible' }}
|
|
21
|
-
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
22
|
-
preserveAspectRatio='xMidYMid meet'
|
|
23
|
-
>
|
|
24
|
-
<PieChart {...props} innerRadiusRatio={props.innerRadiusRatio ?? 0.6} />
|
|
25
|
-
</svg>
|
|
26
|
-
</div>
|
|
27
|
-
);
|
|
5
|
+
return <PieChart {...props} innerRadiusRatio={props.innerRadiusRatio ?? 0.6} />;
|
|
28
6
|
};
|
|
@@ -8,13 +8,11 @@ export const gaugeChartDemo: DemoStory<any> = {
|
|
|
8
8
|
args: {
|
|
9
9
|
title: 'Server Load',
|
|
10
10
|
width: '100%',
|
|
11
|
-
height: '350px',
|
|
12
11
|
value: 65,
|
|
13
12
|
},
|
|
14
13
|
argTypes: {
|
|
15
14
|
title: { control: 'text' },
|
|
16
15
|
width: { control: 'text' },
|
|
17
|
-
height: { control: 'text' },
|
|
18
16
|
value: { control: 'number' },
|
|
19
17
|
},
|
|
20
18
|
render: (args: any) => {
|
|
@@ -60,7 +58,6 @@ export const gaugeChartDemo: DemoStory<any> = {
|
|
|
60
58
|
liveValue.value = (
|
|
61
59
|
<GaugeChart
|
|
62
60
|
title='CPU Usage'
|
|
63
|
-
height='300px'
|
|
64
61
|
value={Math.round(currentValue)}
|
|
65
62
|
color='#e74c3c'
|
|
66
63
|
valueFormatter={(v) => v + '%'}
|
|
@@ -79,7 +76,6 @@ export const gaugeChartDemo: DemoStory<any> = {
|
|
|
79
76
|
<GaugeChart
|
|
80
77
|
title={args.title}
|
|
81
78
|
width={args.width}
|
|
82
|
-
height={args.height}
|
|
83
79
|
value={args.value}
|
|
84
80
|
valueFormatter={(v) => v + '%'}
|
|
85
81
|
/>
|
|
@@ -63,13 +63,16 @@ export const GaugeChart = (props: GaugeChartProps) => {
|
|
|
63
63
|
globalCssId,
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
const
|
|
66
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
67
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
68
|
+
|
|
69
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
67
70
|
|
|
68
71
|
return (
|
|
69
72
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
70
73
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
71
74
|
|
|
72
|
-
<div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0 }}>
|
|
75
|
+
<div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0, paddingTop }}>
|
|
73
76
|
<svg
|
|
74
77
|
class='chart-svg'
|
|
75
78
|
style={{
|
|
@@ -21,14 +21,12 @@ export const lineChartDemo: DemoStory<any> = {
|
|
|
21
21
|
args: {
|
|
22
22
|
title: 'Exchange Rates over Week',
|
|
23
23
|
width: '100%',
|
|
24
|
-
height: '350px',
|
|
25
24
|
showLegend: true,
|
|
26
25
|
curved: false,
|
|
27
26
|
},
|
|
28
27
|
argTypes: {
|
|
29
28
|
title: { control: 'text' },
|
|
30
29
|
width: { control: 'text' },
|
|
31
|
-
height: { control: 'text' },
|
|
32
30
|
showLegend: { control: 'boolean' },
|
|
33
31
|
curved: { control: 'boolean' },
|
|
34
32
|
},
|
|
@@ -75,7 +73,6 @@ export const lineChartDemo: DemoStory<any> = {
|
|
|
75
73
|
data={multiSeriesData}
|
|
76
74
|
title={args.title}
|
|
77
75
|
width={args.width}
|
|
78
|
-
height={args.height}
|
|
79
76
|
showLegend={args.showLegend}
|
|
80
77
|
curved={args.curved}
|
|
81
78
|
/>
|
|
@@ -88,7 +85,6 @@ export const lineChartDemo: DemoStory<any> = {
|
|
|
88
85
|
<LineChart
|
|
89
86
|
data={exchangeRatesData}
|
|
90
87
|
title='Crypto Value 2024'
|
|
91
|
-
height='300px'
|
|
92
88
|
showLegend={true}
|
|
93
89
|
curved={true}
|
|
94
90
|
/>
|
|
@@ -207,13 +207,16 @@ export const LineChart = (props: LineChartProps) => {
|
|
|
207
207
|
},
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
-
const
|
|
210
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
211
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
212
|
+
|
|
213
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
211
214
|
|
|
212
215
|
return (
|
|
213
216
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
214
217
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
215
218
|
|
|
216
|
-
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
|
|
219
|
+
<div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
|
|
217
220
|
|
|
218
221
|
{showLegend && (
|
|
219
222
|
<div class='chart-legend'>
|
|
@@ -23,13 +23,11 @@ export const pieChartDemo: DemoStory<any> = {
|
|
|
23
23
|
args: {
|
|
24
24
|
title: 'Fruit Sales',
|
|
25
25
|
width: '100%',
|
|
26
|
-
height: '300px',
|
|
27
26
|
showLegend: true,
|
|
28
27
|
},
|
|
29
28
|
argTypes: {
|
|
30
29
|
title: { control: 'text' },
|
|
31
30
|
width: { control: 'text' },
|
|
32
|
-
height: { control: 'text' },
|
|
33
31
|
showLegend: { control: 'boolean' },
|
|
34
32
|
},
|
|
35
33
|
render: (args: any) => {
|
|
@@ -81,7 +79,6 @@ export const pieChartDemo: DemoStory<any> = {
|
|
|
81
79
|
data={sampleData}
|
|
82
80
|
title={args.title}
|
|
83
81
|
width={args.width}
|
|
84
|
-
height={args.height}
|
|
85
82
|
showLegend={args.showLegend}
|
|
86
83
|
/>
|
|
87
84
|
</div>
|
|
@@ -91,10 +88,10 @@ export const pieChartDemo: DemoStory<any> = {
|
|
|
91
88
|
<div class='section-title'>Variations</div>
|
|
92
89
|
<div class='grid'>
|
|
93
90
|
<div class='chart-box'>
|
|
94
|
-
<PieChart data={sampleData} title='No Legend' showLegend={false}
|
|
91
|
+
<PieChart data={sampleData} title='No Legend' showLegend={false} />
|
|
95
92
|
</div>
|
|
96
93
|
<div class='chart-box'>
|
|
97
|
-
<PieChart data={emptyData} title='Empty Data'
|
|
94
|
+
<PieChart data={emptyData} title='Empty Data' />
|
|
98
95
|
</div>
|
|
99
96
|
<div class='chart-box'>
|
|
100
97
|
<PieChart
|
|
@@ -103,8 +100,7 @@ export const pieChartDemo: DemoStory<any> = {
|
|
|
103
100
|
series: [{ name: 'Lone', data: [100] }],
|
|
104
101
|
}}
|
|
105
102
|
title='100% Value'
|
|
106
|
-
|
|
107
|
-
/>
|
|
103
|
+
/>
|
|
108
104
|
</div>
|
|
109
105
|
<div class='chart-box'>
|
|
110
106
|
<PieChart
|
|
@@ -113,8 +109,7 @@ export const pieChartDemo: DemoStory<any> = {
|
|
|
113
109
|
series: [{ name: 'Half', data: [50, 50] }],
|
|
114
110
|
}}
|
|
115
111
|
title='50/50 Split'
|
|
116
|
-
|
|
117
|
-
/>
|
|
112
|
+
/>
|
|
118
113
|
</div>
|
|
119
114
|
</div>
|
|
120
115
|
</section>
|
|
@@ -132,7 +127,6 @@ const data = {
|
|
|
132
127
|
data={data}
|
|
133
128
|
title="Fruit Sales"
|
|
134
129
|
width="100%"
|
|
135
|
-
height="300px"
|
|
136
130
|
showLegend={true}
|
|
137
131
|
/>
|
|
138
132
|
`,
|
|
@@ -13,78 +13,94 @@ export const PieChart = (props: PieChartProps) => {
|
|
|
13
13
|
const showLegend = props.showLegend !== false;
|
|
14
14
|
const innerRadiusRatio = props.innerRadiusRatio ?? 0;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!series || !series.data || series.data.length === 0) {
|
|
16
|
+
const seriesList = props.data.series;
|
|
17
|
+
if (!seriesList || seriesList.length === 0) {
|
|
19
18
|
return <div class='&-container'>No data</div>;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
const values = series.data;
|
|
23
|
-
const total = values.reduce((sum, val) => sum + val, 0);
|
|
24
|
-
|
|
25
21
|
// SVG viewBox settings
|
|
26
22
|
const viewBoxSize = 200;
|
|
27
23
|
const center = viewBoxSize / 2;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
24
|
+
const maxRadius = (viewBoxSize / 2) * 0.9; // 90% of container to leave margin for stroke/anti-aliasing
|
|
25
|
+
|
|
26
|
+
const globalInnerRadius = maxRadius * innerRadiusRatio;
|
|
27
|
+
const numSeries = seriesList.length;
|
|
28
|
+
// If only 1 series and innerRadiusRatio is 0, ringThickness is maxRadius.
|
|
29
|
+
// If multiple series, we divide the available space (maxRadius - globalInnerRadius) among them.
|
|
30
|
+
const ringThickness = (maxRadius - globalInnerRadius) / numSeries;
|
|
31
|
+
|
|
32
|
+
const allSlices: any[] = [];
|
|
33
|
+
|
|
34
|
+
seriesList.forEach((series, seriesIndex) => {
|
|
35
|
+
const values = series.data;
|
|
36
|
+
if (!values || values.length === 0) return;
|
|
37
|
+
|
|
38
|
+
const total = values.reduce((sum, val) => sum + val, 0);
|
|
39
|
+
let currentAngle = 0;
|
|
40
|
+
|
|
41
|
+
// Outermost ring is series 0
|
|
42
|
+
const outerR = maxRadius - seriesIndex * ringThickness;
|
|
43
|
+
const innerR = outerR - ringThickness;
|
|
44
|
+
|
|
45
|
+
values.forEach((val, index) => {
|
|
46
|
+
// Prevent 0 area slices. Also handle case where 1 val is 100%
|
|
47
|
+
const ratio = total > 0 ? val / total : 0;
|
|
48
|
+
const angleDelta = ratio * 360;
|
|
49
|
+
|
|
50
|
+
// Fix for 100% slice (SVG arcs don't draw well at exactly 360)
|
|
51
|
+
const isFullCircle = angleDelta === 360;
|
|
52
|
+
const adjustedAngleDelta = isFullCircle ? 359.99 : angleDelta;
|
|
53
|
+
|
|
54
|
+
const startAngle = currentAngle;
|
|
55
|
+
const endAngle = currentAngle + adjustedAngleDelta;
|
|
56
|
+
currentAngle += adjustedAngleDelta;
|
|
57
|
+
|
|
58
|
+
const pathD = describeArc(center, center, outerR, startAngle, endAngle, innerR);
|
|
59
|
+
const color = getChartColor(index);
|
|
60
|
+
const label = props.data.labels[index] || `Item ${index + 1}`;
|
|
61
|
+
|
|
62
|
+
const handleMouseEnter = (e: any) => {
|
|
63
|
+
const percentage = (ratio * 100).toFixed(1) + '%';
|
|
64
|
+
Tooltip.show(
|
|
65
|
+
e,
|
|
66
|
+
<div>
|
|
67
|
+
<div style={{ fontWeight: 'bold' }}>{series.name ? `${series.name} - ` : ''}{label}</div>
|
|
68
|
+
<div>Value: {val}</div>
|
|
69
|
+
<div>Share: {percentage}</div>
|
|
70
|
+
</div>,
|
|
71
|
+
{ position: 'auto' }
|
|
72
|
+
);
|
|
73
|
+
e.target.style.opacity = 0.8;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleMouseLeave = (e: any) => {
|
|
77
|
+
Tooltip.hide();
|
|
78
|
+
e.target.style.opacity = 1;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
allSlices.push(
|
|
82
|
+
<path
|
|
83
|
+
class='chart-element'
|
|
84
|
+
d={pathD}
|
|
85
|
+
fill={color}
|
|
86
|
+
stroke='var(--primary-bg-color)' // border color between slices
|
|
87
|
+
stroke-width={isFullCircle ? '0' : '1'}
|
|
88
|
+
onMouseEnter={handleMouseEnter}
|
|
89
|
+
onMouseLeave={handleMouseLeave}
|
|
90
|
+
style={{ transition: 'opacity 0.2s' }}
|
|
91
|
+
/>
|
|
60
92
|
);
|
|
61
|
-
|
|
62
|
-
e.target.style.opacity = 0.8;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const handleMouseLeave = (e: any) => {
|
|
66
|
-
Tooltip.hide();
|
|
67
|
-
e.target.style.opacity = 1;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<path
|
|
72
|
-
class='chart-element'
|
|
73
|
-
d={pathD}
|
|
74
|
-
fill={color}
|
|
75
|
-
stroke='var(--primary-bg-color)' // border color between slices
|
|
76
|
-
stroke-width={isFullCircle ? '0' : '1'}
|
|
77
|
-
onMouseEnter={handleMouseEnter}
|
|
78
|
-
onMouseLeave={handleMouseLeave}
|
|
79
|
-
/>
|
|
80
|
-
);
|
|
93
|
+
});
|
|
81
94
|
});
|
|
82
95
|
|
|
83
96
|
const ref: RefProps = {
|
|
84
97
|
globalCssId,
|
|
85
98
|
};
|
|
86
99
|
|
|
87
|
-
const
|
|
100
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
101
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
102
|
+
|
|
103
|
+
const styleStr = `width: ${props.width || '100%'}; min-height: 200px;`;
|
|
88
104
|
|
|
89
105
|
return (
|
|
90
106
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
@@ -98,6 +114,7 @@ export const PieChart = (props: PieChartProps) => {
|
|
|
98
114
|
justifyContent: 'center',
|
|
99
115
|
alignItems: 'center',
|
|
100
116
|
minHeight: '0',
|
|
117
|
+
paddingTop,
|
|
101
118
|
}}
|
|
102
119
|
>
|
|
103
120
|
<svg
|
|
@@ -106,13 +123,13 @@ export const PieChart = (props: PieChartProps) => {
|
|
|
106
123
|
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
107
124
|
preserveAspectRatio='xMidYMid meet'
|
|
108
125
|
>
|
|
109
|
-
<g>{
|
|
126
|
+
<g>{allSlices}</g>
|
|
110
127
|
</svg>
|
|
111
128
|
</div>
|
|
112
129
|
|
|
113
130
|
{showLegend && (
|
|
114
131
|
<div class='chart-legend'>
|
|
115
|
-
{
|
|
132
|
+
{props.data.labels.map((_, i) => (
|
|
116
133
|
<div class='legend-item'>
|
|
117
134
|
<div class='legend-color' style={{ backgroundColor: getChartColor(i) }} />
|
|
118
135
|
<div>{props.data.labels[i] || `Item ${i + 1}`}</div>
|
|
@@ -16,13 +16,11 @@ export const radarChartDemo: DemoStory<any> = {
|
|
|
16
16
|
args: {
|
|
17
17
|
title: 'Character Stats',
|
|
18
18
|
width: '100%',
|
|
19
|
-
height: '400px',
|
|
20
19
|
showLegend: true,
|
|
21
20
|
},
|
|
22
21
|
argTypes: {
|
|
23
22
|
title: { control: 'text' },
|
|
24
23
|
width: { control: 'text' },
|
|
25
|
-
height: { control: 'text' },
|
|
26
24
|
showLegend: { control: 'boolean' },
|
|
27
25
|
},
|
|
28
26
|
render: (args: any) => {
|
|
@@ -68,7 +66,6 @@ export const radarChartDemo: DemoStory<any> = {
|
|
|
68
66
|
data={statsData}
|
|
69
67
|
title={args.title}
|
|
70
68
|
width={args.width}
|
|
71
|
-
height={args.height}
|
|
72
69
|
showLegend={args.showLegend}
|
|
73
70
|
/>
|
|
74
71
|
</div>
|
|
@@ -105,8 +105,20 @@ export const RadarChart = (props: RadarChartProps) => {
|
|
|
105
105
|
|
|
106
106
|
const pointsStr = coordinates.map((c) => `${c.x},${c.y}`).join(' ');
|
|
107
107
|
|
|
108
|
+
const seriesGroupElements: any[] = [];
|
|
109
|
+
|
|
108
110
|
// Fill area
|
|
109
|
-
|
|
111
|
+
seriesGroupElements.push(
|
|
112
|
+
<polygon
|
|
113
|
+
class="radar-polygon"
|
|
114
|
+
points={pointsStr}
|
|
115
|
+
fill={color}
|
|
116
|
+
fillOpacity='0.15'
|
|
117
|
+
stroke={color}
|
|
118
|
+
strokeWidth='2'
|
|
119
|
+
style={{ transition: 'fill-opacity 0.2s ease' }}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
110
122
|
|
|
111
123
|
// Points and Tooltips
|
|
112
124
|
coordinates.forEach((c) => {
|
|
@@ -132,9 +144,9 @@ export const RadarChart = (props: RadarChartProps) => {
|
|
|
132
144
|
e.target.setAttribute('r', '4');
|
|
133
145
|
};
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
seriesGroupElements.push(
|
|
136
148
|
<circle
|
|
137
|
-
class='chart-element'
|
|
149
|
+
class='chart-element radar-point'
|
|
138
150
|
cx={c.x}
|
|
139
151
|
cy={c.y}
|
|
140
152
|
r='4'
|
|
@@ -147,6 +159,26 @@ export const RadarChart = (props: RadarChartProps) => {
|
|
|
147
159
|
/>
|
|
148
160
|
);
|
|
149
161
|
});
|
|
162
|
+
|
|
163
|
+
const handleGroupMouseEnter = (e: any) => {
|
|
164
|
+
const polygon = e.currentTarget.querySelector('.radar-polygon');
|
|
165
|
+
if (polygon) polygon.setAttribute('fill-opacity', '0.6');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleGroupMouseLeave = (e: any) => {
|
|
169
|
+
const polygon = e.currentTarget.querySelector('.radar-polygon');
|
|
170
|
+
if (polygon) polygon.setAttribute('fill-opacity', '0.15');
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
dataElements.push(
|
|
174
|
+
<g
|
|
175
|
+
class="radar-series"
|
|
176
|
+
onMouseEnter={handleGroupMouseEnter}
|
|
177
|
+
onMouseLeave={handleGroupMouseLeave}
|
|
178
|
+
>
|
|
179
|
+
{seriesGroupElements}
|
|
180
|
+
</g>
|
|
181
|
+
);
|
|
150
182
|
});
|
|
151
183
|
|
|
152
184
|
return dataElements;
|
|
@@ -156,13 +188,16 @@ export const RadarChart = (props: RadarChartProps) => {
|
|
|
156
188
|
globalCssId,
|
|
157
189
|
};
|
|
158
190
|
|
|
159
|
-
const
|
|
191
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
192
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
193
|
+
|
|
194
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
160
195
|
|
|
161
196
|
return (
|
|
162
197
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
163
198
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
164
199
|
|
|
165
|
-
<div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0 }}>
|
|
200
|
+
<div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0, paddingTop }}>
|
|
166
201
|
<svg
|
|
167
202
|
class='chart-svg'
|
|
168
203
|
style={{
|
|
@@ -16,13 +16,11 @@ export const scatterChartDemo: DemoStory<any> = {
|
|
|
16
16
|
args: {
|
|
17
17
|
title: 'Value Correlation',
|
|
18
18
|
width: '100%',
|
|
19
|
-
height: '350px',
|
|
20
19
|
showLegend: true,
|
|
21
20
|
},
|
|
22
21
|
argTypes: {
|
|
23
22
|
title: { control: 'text' },
|
|
24
23
|
width: { control: 'text' },
|
|
25
|
-
height: { control: 'text' },
|
|
26
24
|
showLegend: { control: 'boolean' },
|
|
27
25
|
},
|
|
28
26
|
render: (args: any) => {
|
|
@@ -68,7 +66,6 @@ export const scatterChartDemo: DemoStory<any> = {
|
|
|
68
66
|
data={correlationData}
|
|
69
67
|
title={args.title}
|
|
70
68
|
width={args.width}
|
|
71
|
-
height={args.height}
|
|
72
69
|
showLegend={args.showLegend}
|
|
73
70
|
/>
|
|
74
71
|
</div>
|
|
@@ -211,13 +211,16 @@ export const ScatterChart = (props: ScatterChartProps) => {
|
|
|
211
211
|
},
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
-
const
|
|
214
|
+
const ratio = props.aspectRatio ?? 16 / 9;
|
|
215
|
+
const paddingTop = `${(1 / ratio) * 100}%`;
|
|
216
|
+
|
|
217
|
+
const styleStr = `width: ${props.width || '100%'};`;
|
|
215
218
|
|
|
216
219
|
return (
|
|
217
220
|
<div ref={ref} class='&-container' style={styleStr}>
|
|
218
221
|
{props.title && <div class='chart-title'>{props.title}</div>}
|
|
219
222
|
|
|
220
|
-
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
|
|
223
|
+
<div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
|
|
221
224
|
|
|
222
225
|
{showLegend && (
|
|
223
226
|
<div class='chart-legend'>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './svg-graph';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RefProps } from 'lupine.web';
|
|
2
|
+
import { SvgGraph } from './svg-graph';
|
|
3
|
+
import { DemoStory } from '../../demo/demo-types';
|
|
4
|
+
|
|
5
|
+
const SvgGraphDemo = () => {
|
|
6
|
+
const ref: RefProps = {
|
|
7
|
+
onLoad: async (el) => {
|
|
8
|
+
const graph = new SvgGraph(el as HTMLElement);
|
|
9
|
+
|
|
10
|
+
// Draw a line
|
|
11
|
+
graph.drawLine('red', '2px', 50, 50, 250, 50, 'line1');
|
|
12
|
+
|
|
13
|
+
// Draw a rect
|
|
14
|
+
graph.draw('rect', '#e0f7fa', '#00acc1', '2px', 50, 100, 100, 50, 'rect1');
|
|
15
|
+
|
|
16
|
+
// Draw an ellipse
|
|
17
|
+
graph.draw('ellipse', '#fce4ec', '#d81b60', '2px', 200, 100, 100, 50, 'ellipse1');
|
|
18
|
+
|
|
19
|
+
// Draw a rounded rect
|
|
20
|
+
graph.draw('roundrect', '#e8f5e9', '#43a047', '2px', 50, 200, 150, 60, 'roundrect1');
|
|
21
|
+
|
|
22
|
+
// Test resize after 1s
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
graph.resize('line1', 50, 50, 300, 100);
|
|
25
|
+
}, 1000);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div style={{ padding: '16px' }}>
|
|
31
|
+
<h3>SvgGraph Component Demo</h3>
|
|
32
|
+
<p>This demonstrates the imperative SVG Graph API modernized from legacy JGraph.</p>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
ref={ref}
|
|
36
|
+
style={{
|
|
37
|
+
position: 'relative',
|
|
38
|
+
width: '100%',
|
|
39
|
+
height: '400px',
|
|
40
|
+
border: '1px solid #ccc',
|
|
41
|
+
marginTop: '16px',
|
|
42
|
+
backgroundColor: '#fafafa',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{/* SVG will be injected here */}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const svgGraphDemo: DemoStory<any> = {
|
|
52
|
+
id: 'svg-graph-demo',
|
|
53
|
+
text: 'Svg Graph',
|
|
54
|
+
args: {},
|
|
55
|
+
render: (args: any) => {
|
|
56
|
+
return <SvgGraphDemo />;
|
|
57
|
+
},
|
|
58
|
+
code: 'import { SvgGraph } from "lupine.components/src/component-pool/svg-graph";\n// Check source code for usage.',
|
|
59
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modernized SVG Graph Library
|
|
3
|
+
* Replaces the legacy JGraph library.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class SvgGraph {
|
|
7
|
+
public svgRoot: SVGSVGElement;
|
|
8
|
+
private elements: Map<string, SVGElement> = new Map();
|
|
9
|
+
|
|
10
|
+
constructor(container: HTMLElement | string) {
|
|
11
|
+
let el: HTMLElement | null = null;
|
|
12
|
+
if (typeof container === 'string') {
|
|
13
|
+
el = document.getElementById(container);
|
|
14
|
+
} else {
|
|
15
|
+
el = container;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!el) {
|
|
19
|
+
throw new Error('SvgGraph: container not found');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
el.style.userSelect = 'none';
|
|
23
|
+
|
|
24
|
+
this.svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
25
|
+
this.svgRoot.style.width = '100%';
|
|
26
|
+
this.svgRoot.style.height = '100%';
|
|
27
|
+
this.svgRoot.style.position = 'absolute';
|
|
28
|
+
this.svgRoot.style.left = '0';
|
|
29
|
+
this.svgRoot.style.top = '0';
|
|
30
|
+
this.svgRoot.style.pointerEvents = 'none'; // usually good for overlays
|
|
31
|
+
|
|
32
|
+
el.appendChild(this.svgRoot);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
draw(
|
|
36
|
+
shape: 'rect' | 'ellipse' | 'roundrect' | 'line',
|
|
37
|
+
fillColor: string,
|
|
38
|
+
lineColor: string,
|
|
39
|
+
lineWidth: string,
|
|
40
|
+
left: number,
|
|
41
|
+
top: number,
|
|
42
|
+
width: number,
|
|
43
|
+
height: number,
|
|
44
|
+
id?: string
|
|
45
|
+
): SVGElement {
|
|
46
|
+
const svgNamespace = 'http://www.w3.org/2000/svg';
|
|
47
|
+
let svg: SVGElement;
|
|
48
|
+
|
|
49
|
+
if (shape === 'rect') {
|
|
50
|
+
svg = document.createElementNS(svgNamespace, 'rect');
|
|
51
|
+
svg.setAttributeNS(null, 'x', left + 'px');
|
|
52
|
+
svg.setAttributeNS(null, 'y', top + 'px');
|
|
53
|
+
svg.setAttributeNS(null, 'width', width + 'px');
|
|
54
|
+
svg.setAttributeNS(null, 'height', height + 'px');
|
|
55
|
+
} else if (shape === 'ellipse') {
|
|
56
|
+
svg = document.createElementNS(svgNamespace, 'ellipse');
|
|
57
|
+
svg.setAttributeNS(null, 'cx', left + width / 2 + 'px');
|
|
58
|
+
svg.setAttributeNS(null, 'cy', top + height / 2 + 'px');
|
|
59
|
+
svg.setAttributeNS(null, 'rx', width / 2 + 'px');
|
|
60
|
+
svg.setAttributeNS(null, 'ry', height / 2 + 'px');
|
|
61
|
+
} else if (shape === 'roundrect') {
|
|
62
|
+
svg = document.createElementNS(svgNamespace, 'rect');
|
|
63
|
+
svg.setAttributeNS(null, 'x', left + 'px');
|
|
64
|
+
svg.setAttributeNS(null, 'y', top + 'px');
|
|
65
|
+
svg.setAttributeNS(null, 'rx', '20px');
|
|
66
|
+
svg.setAttributeNS(null, 'ry', '20px');
|
|
67
|
+
svg.setAttributeNS(null, 'width', width + 'px');
|
|
68
|
+
svg.setAttributeNS(null, 'height', height + 'px');
|
|
69
|
+
} else if (shape === 'line') {
|
|
70
|
+
svg = document.createElementNS(svgNamespace, 'line');
|
|
71
|
+
svg.setAttributeNS(null, 'x1', left + 'px');
|
|
72
|
+
svg.setAttributeNS(null, 'y1', top + 'px');
|
|
73
|
+
svg.setAttributeNS(null, 'x2', width + 'px');
|
|
74
|
+
svg.setAttributeNS(null, 'y2', height + 'px');
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(`Unsupported shape: ${shape}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
svg.style.position = 'absolute';
|
|
80
|
+
svg.setAttributeNS(null, 'fill', fillColor || 'none');
|
|
81
|
+
svg.setAttributeNS(null, 'stroke', lineColor || 'none');
|
|
82
|
+
svg.setAttributeNS(null, 'stroke-width', lineWidth || '1px');
|
|
83
|
+
svg.style.pointerEvents = 'auto'; // allow interaction with shape
|
|
84
|
+
|
|
85
|
+
if (id) {
|
|
86
|
+
svg.id = id;
|
|
87
|
+
this.elements.set(id, svg);
|
|
88
|
+
}
|
|
89
|
+
this.svgRoot.appendChild(svg);
|
|
90
|
+
return svg;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
drawLine(
|
|
94
|
+
lineColor: string,
|
|
95
|
+
lineWidth: string,
|
|
96
|
+
left: number,
|
|
97
|
+
top: number,
|
|
98
|
+
width: number,
|
|
99
|
+
height: number,
|
|
100
|
+
id?: string
|
|
101
|
+
): SVGElement {
|
|
102
|
+
return this.draw('line', '', lineColor, lineWidth, left, top, width, height, id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
resize(idOrObj: string | SVGElement, left: number, top: number, width: number, height: number): SVGElement | void {
|
|
106
|
+
const svg =
|
|
107
|
+
typeof idOrObj === 'string' ? this.elements.get(idOrObj) || (document.getElementById(idOrObj) as any) : idOrObj;
|
|
108
|
+
if (!svg) return;
|
|
109
|
+
|
|
110
|
+
if (svg.tagName === 'rect') {
|
|
111
|
+
svg.setAttributeNS(null, 'x', left + 'px');
|
|
112
|
+
svg.setAttributeNS(null, 'y', top + 'px');
|
|
113
|
+
svg.setAttributeNS(null, 'width', width + 'px');
|
|
114
|
+
svg.setAttributeNS(null, 'height', height + 'px');
|
|
115
|
+
} else if (svg.tagName === 'ellipse') {
|
|
116
|
+
svg.setAttributeNS(null, 'cx', left + width / 2 + 'px');
|
|
117
|
+
svg.setAttributeNS(null, 'cy', top + height / 2 + 'px');
|
|
118
|
+
svg.setAttributeNS(null, 'rx', width / 2 + 'px');
|
|
119
|
+
svg.setAttributeNS(null, 'ry', height / 2 + 'px');
|
|
120
|
+
} else if (svg.tagName === 'line') {
|
|
121
|
+
svg.setAttributeNS(null, 'x1', left + 'px');
|
|
122
|
+
svg.setAttributeNS(null, 'y1', top + 'px');
|
|
123
|
+
svg.setAttributeNS(null, 'x2', width + 'px');
|
|
124
|
+
svg.setAttributeNS(null, 'y2', height + 'px');
|
|
125
|
+
}
|
|
126
|
+
return svg;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
remove(idOrObj: string | SVGElement): void {
|
|
130
|
+
const svg = typeof idOrObj === 'string' ? this.elements.get(idOrObj) || document.getElementById(idOrObj) : idOrObj;
|
|
131
|
+
if (svg && svg.parentNode) {
|
|
132
|
+
svg.parentNode.removeChild(svg);
|
|
133
|
+
if (typeof idOrObj === 'string') this.elements.delete(idOrObj);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setAttribute(shape: SVGElement, cmd: string, value: string): void {
|
|
138
|
+
if (shape != null) {
|
|
139
|
+
if (cmd === 'fillcolor') {
|
|
140
|
+
shape.setAttributeNS(null, 'fill', value || 'none');
|
|
141
|
+
} else if (cmd === 'linecolor') {
|
|
142
|
+
shape.setAttributeNS(null, 'stroke', value || 'none');
|
|
143
|
+
} else if (cmd === 'linewidth') {
|
|
144
|
+
shape.setAttributeNS(null, 'stroke-width', parseInt(value) + 'px');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getAttribute(shape: SVGElement, cmd: string): string {
|
|
150
|
+
let result = '';
|
|
151
|
+
if (shape != null) {
|
|
152
|
+
if (cmd === 'fillcolor') {
|
|
153
|
+
result = shape.getAttributeNS(null, 'fill') || '';
|
|
154
|
+
if (result === 'none') result = '';
|
|
155
|
+
} else if (cmd === 'linecolor') {
|
|
156
|
+
result = shape.getAttributeNS(null, 'stroke') || '';
|
|
157
|
+
if (result === 'none') result = '';
|
|
158
|
+
} else if (cmd === 'linewidth') {
|
|
159
|
+
result = shape.getAttributeNS(null, 'stroke') || '';
|
|
160
|
+
if (result === 'none') result = '';
|
|
161
|
+
else result = shape.getAttributeNS(null, 'stroke-width') || '';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -5,13 +5,13 @@ export const youtubePlayerDemo: DemoStory<YouTubePlayerProps> = {
|
|
|
5
5
|
id: 'youtubePlayerDemo',
|
|
6
6
|
text: 'YouTube Player',
|
|
7
7
|
args: {
|
|
8
|
-
|
|
8
|
+
srcOrVideoId: 'dQw4w9WgXcQ',
|
|
9
9
|
width: '100%',
|
|
10
10
|
height: '400px',
|
|
11
11
|
autoplay: false,
|
|
12
12
|
},
|
|
13
13
|
argTypes: {
|
|
14
|
-
|
|
14
|
+
srcOrVideoId: { control: 'text', description: 'YouTube Video ID' },
|
|
15
15
|
width: { control: 'text' },
|
|
16
16
|
height: { control: 'text' },
|
|
17
17
|
autoplay: { control: 'boolean' },
|
|
@@ -39,8 +39,8 @@ export const youtubePlayerDemo: DemoStory<YouTubePlayerProps> = {
|
|
|
39
39
|
<section>
|
|
40
40
|
<div class='section-title'>Custom Size</div>
|
|
41
41
|
<div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
|
|
42
|
-
<YouTubePlayer
|
|
43
|
-
<YouTubePlayer
|
|
42
|
+
<YouTubePlayer srcOrVideoId='jNQXAC9IVRw' width='300px' height='200px' />
|
|
43
|
+
<YouTubePlayer srcOrVideoId='M7lc1UVf-VE' width='400px' height='250px' />
|
|
44
44
|
</div>
|
|
45
45
|
</section>
|
|
46
46
|
</div>
|
|
@@ -3,38 +3,114 @@ import { CssProps } from 'lupine.web';
|
|
|
3
3
|
export type YouTubePlayerProps = {
|
|
4
4
|
class?: string;
|
|
5
5
|
style?: CssProps;
|
|
6
|
-
|
|
6
|
+
srcOrVideoId: string;
|
|
7
7
|
width?: string | number;
|
|
8
8
|
height?: string | number;
|
|
9
9
|
autoplay?: boolean;
|
|
10
10
|
allowFullScreen?: boolean;
|
|
11
|
+
controls?: boolean;
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
muted?: boolean;
|
|
14
|
+
rel?: boolean;
|
|
15
|
+
modestbranding?: boolean;
|
|
11
16
|
};
|
|
12
17
|
|
|
18
|
+
function parseYouTubeUrl(urlOrId: string) {
|
|
19
|
+
if (!urlOrId) return { videoId: '' };
|
|
20
|
+
if (/^[a-zA-Z0-9_-]{11}$/.test(urlOrId)) return { videoId: urlOrId };
|
|
21
|
+
|
|
22
|
+
let videoId = '';
|
|
23
|
+
let list = '';
|
|
24
|
+
let start = '';
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const urlObj = new URL(urlOrId);
|
|
28
|
+
if (urlObj.hostname.includes('youtube.com')) {
|
|
29
|
+
if (urlObj.pathname === '/watch') {
|
|
30
|
+
videoId = urlObj.searchParams.get('v') || '';
|
|
31
|
+
} else if (urlObj.pathname.startsWith('/embed/')) {
|
|
32
|
+
videoId = urlObj.pathname.split('/')[2] || '';
|
|
33
|
+
} else if (urlObj.pathname.startsWith('/v/')) {
|
|
34
|
+
videoId = urlObj.pathname.split('/')[2] || '';
|
|
35
|
+
}
|
|
36
|
+
list = urlObj.searchParams.get('list') || '';
|
|
37
|
+
start = urlObj.searchParams.get('t') || urlObj.searchParams.get('start') || '';
|
|
38
|
+
} else if (urlObj.hostname.includes('youtu.be')) {
|
|
39
|
+
videoId = urlObj.pathname.slice(1);
|
|
40
|
+
list = urlObj.searchParams.get('list') || '';
|
|
41
|
+
start = urlObj.searchParams.get('t') || urlObj.searchParams.get('start') || '';
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
const match = urlOrId.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w-]{11})/);
|
|
45
|
+
if (match) videoId = match[1];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (start && isNaN(Number(start))) {
|
|
49
|
+
const timeMatch = start.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
|
|
50
|
+
if (timeMatch && timeMatch[0]) {
|
|
51
|
+
let seconds = 0;
|
|
52
|
+
if (timeMatch[1]) seconds += parseInt(timeMatch[1]) * 3600;
|
|
53
|
+
if (timeMatch[2]) seconds += parseInt(timeMatch[2]) * 60;
|
|
54
|
+
if (timeMatch[3]) seconds += parseInt(timeMatch[3]);
|
|
55
|
+
if (seconds > 0) start = seconds.toString();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { videoId, list, start };
|
|
60
|
+
}
|
|
61
|
+
|
|
13
62
|
export const YouTubePlayer = (props: YouTubePlayerProps) => {
|
|
14
63
|
const {
|
|
15
|
-
|
|
64
|
+
srcOrVideoId,
|
|
16
65
|
width = '100%',
|
|
17
66
|
height = '100%',
|
|
18
67
|
autoplay = false,
|
|
19
68
|
allowFullScreen = true,
|
|
69
|
+
controls = true,
|
|
70
|
+
loop = false,
|
|
71
|
+
muted = false,
|
|
72
|
+
rel = false,
|
|
73
|
+
modestbranding = true,
|
|
20
74
|
style,
|
|
21
75
|
class: className,
|
|
22
76
|
} = props;
|
|
23
77
|
|
|
24
|
-
const
|
|
78
|
+
const { videoId, list, start } = parseYouTubeUrl(srcOrVideoId);
|
|
79
|
+
|
|
80
|
+
if (typeof document === 'undefined') {
|
|
81
|
+
return <div class={className} css={{ position: 'relative', width, height, ...style }}></div>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let src = '';
|
|
85
|
+
if (videoId || list) {
|
|
86
|
+
src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}`;
|
|
87
|
+
if (!controls) src += '&controls=0';
|
|
88
|
+
if (loop) src += `&loop=1&playlist=${videoId}`; // YouTube requires playlist=VIDEO_ID for loop to work
|
|
89
|
+
if (muted) src += '&mute=1';
|
|
90
|
+
if (!rel) src += '&rel=0';
|
|
91
|
+
if (modestbranding) src += '&modestbranding=1';
|
|
92
|
+
if (list) src += `&list=${list}`;
|
|
93
|
+
if (start) src += `&start=${start}`;
|
|
94
|
+
}
|
|
25
95
|
|
|
26
96
|
return (
|
|
27
97
|
<div class={className} css={{ position: 'relative', width, height, ...style }}>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
98
|
+
{src ? (
|
|
99
|
+
<iframe
|
|
100
|
+
width='100%'
|
|
101
|
+
height='100%'
|
|
102
|
+
src={src}
|
|
103
|
+
title='YouTube video player'
|
|
104
|
+
frameBorder='0'
|
|
105
|
+
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
|
|
106
|
+
allowFullScreen={allowFullScreen}
|
|
107
|
+
style={{ border: 'none', display: 'block' }}
|
|
108
|
+
></iframe>
|
|
109
|
+
) : (
|
|
110
|
+
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0f0f0', color: '#999', border: '1px solid #ddd' }}>
|
|
111
|
+
Invalid YouTube URL
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
38
114
|
</div>
|
|
39
115
|
);
|
|
40
116
|
};
|
|
@@ -23,13 +23,17 @@ export const MenuBar = ({
|
|
|
23
23
|
menuId,
|
|
24
24
|
items,
|
|
25
25
|
className,
|
|
26
|
-
textColor
|
|
27
|
-
backgroundColor
|
|
28
|
-
hoverColor
|
|
29
|
-
hoverBgColor
|
|
26
|
+
textColor,
|
|
27
|
+
backgroundColor,
|
|
28
|
+
hoverColor,
|
|
29
|
+
hoverBgColor,
|
|
30
30
|
maxWidth = '100%',
|
|
31
31
|
maxWidthMobileMenu = MediaQueryMaxWidth.TabletMax,
|
|
32
32
|
}: MenuBarProps) => {
|
|
33
|
+
backgroundColor = backgroundColor || 'var(--menubar-bg-color)';
|
|
34
|
+
textColor = textColor || 'var(--menubar-color)';
|
|
35
|
+
hoverColor = hoverColor || 'var(--activatable-color-hover)';
|
|
36
|
+
hoverBgColor = hoverBgColor || 'var(--activatable-bg-color-hover)';
|
|
33
37
|
const css: any = {
|
|
34
38
|
width: '100%',
|
|
35
39
|
maxWidth: maxWidth,
|
|
@@ -140,6 +144,13 @@ export const MenuBar = ({
|
|
|
140
144
|
'.menu-bar-top.open': {
|
|
141
145
|
display: 'flex',
|
|
142
146
|
flexDirection: 'column',
|
|
147
|
+
position: 'absolute',
|
|
148
|
+
top: '100%',
|
|
149
|
+
left: 0,
|
|
150
|
+
right: 0,
|
|
151
|
+
zIndex: 100,
|
|
152
|
+
backgroundColor: backgroundColor,
|
|
153
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
143
154
|
},
|
|
144
155
|
'.menu-bar-top.open .menu-bar-sub-box > .menu-bar-sub': {
|
|
145
156
|
display: 'flex',
|
|
@@ -19,6 +19,7 @@ export type MenuSidebarProps = {
|
|
|
19
19
|
color?: string;
|
|
20
20
|
backgroundColor?: string;
|
|
21
21
|
isDevAdmin?: boolean;
|
|
22
|
+
defaultOpenAll?: boolean;
|
|
22
23
|
};
|
|
23
24
|
export const MenuSidebar = ({
|
|
24
25
|
mobileMenu,
|
|
@@ -31,6 +32,7 @@ export const MenuSidebar = ({
|
|
|
31
32
|
maxWidth = '100%',
|
|
32
33
|
maxWidthMobileMenu = MediaQueryMaxWidth.TabletMax,
|
|
33
34
|
isDevAdmin = false,
|
|
35
|
+
defaultOpenAll = false,
|
|
34
36
|
}: MenuSidebarProps) => {
|
|
35
37
|
const css: CssProps = {
|
|
36
38
|
// backgroundColor,
|
|
@@ -228,7 +230,7 @@ export const MenuSidebar = ({
|
|
|
228
230
|
}
|
|
229
231
|
let ref: RefProps = {};
|
|
230
232
|
return item.items ? (
|
|
231
|
-
<div ref={ref} class=
|
|
233
|
+
<div ref={ref} class={`menu-sidebar-sub-box ${defaultOpenAll ? 'open' : ''}`} onClick={() => onItemToggleClick(ref)}>
|
|
232
234
|
<div class='menu-sidebar-item'>{item.text}</div>
|
|
233
235
|
{renderItems(item.items, 'menu-sidebar-sub')}
|
|
234
236
|
</div>
|
package/src/components/tabs.tsx
CHANGED
|
@@ -124,12 +124,6 @@ export const Tabs = ({ pages, defaultIndex, topClassName, pagePadding, hook: ref
|
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
126
|
if (refUpdate) {
|
|
127
|
-
refUpdate.updateTitle = (index: number, title: string) => {
|
|
128
|
-
const doms = ref.$all(`.&tabs > div > .tab`);
|
|
129
|
-
if (index >= 0 && index < doms.length) {
|
|
130
|
-
doms[index].innerHTML = title;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
127
|
refUpdate.updateIndex = updateIndex;
|
|
134
128
|
refUpdate.removePage = removePage;
|
|
135
129
|
refUpdate.newPage = newPage;
|
|
@@ -141,6 +135,15 @@ export const Tabs = ({ pages, defaultIndex, topClassName, pagePadding, hook: ref
|
|
|
141
135
|
const doms = ref.$all(`.&tabs > div > .tab`);
|
|
142
136
|
return doms.length;
|
|
143
137
|
};
|
|
138
|
+
refUpdate.updateTitle = (index: number, title: string) => {
|
|
139
|
+
const doms = ref.$all(`.&tabs > div > .tab`);
|
|
140
|
+
if (index === -1) {
|
|
141
|
+
index = refUpdate.getIndex!();
|
|
142
|
+
}
|
|
143
|
+
if (index >= 0 && index < doms.length) {
|
|
144
|
+
doms[index].innerHTML = title;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
144
147
|
refUpdate.findAndActivate = (title: string) => {
|
|
145
148
|
const doms = ref.$all(`.&tabs > div > .tab`);
|
|
146
149
|
for (let i = 0; i < doms.length; i++) {
|
package/src/demo/demo-about.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import { redirectDemo } from '../components/redirect-demo';
|
|
|
24
24
|
import { textWaveDemo } from '../components/text-wave-demo';
|
|
25
25
|
import { textScaleDemo } from '../components/text-scale-demo';
|
|
26
26
|
import { textGlowDemo } from '../components/text-glow-demo';
|
|
27
|
+
import { svgGraphDemo } from '../component-pool/svg-graph/svg-graph-demo';
|
|
27
28
|
import { toggleButtonDemo } from '../components/toggle-button-demo';
|
|
28
29
|
import { messageBoxDemo } from '../components/message-box-demo';
|
|
29
30
|
import { loadingSpinDemo } from '../components/loading-spin-demo';
|
|
@@ -611,6 +612,12 @@ export class DemoFrameHelper {
|
|
|
611
612
|
url: '',
|
|
612
613
|
js: () => this.addPanel(redirectDemo.text, <DemoPage story={redirectDemo} />),
|
|
613
614
|
},
|
|
615
|
+
{
|
|
616
|
+
id: svgGraphDemo.id,
|
|
617
|
+
text: svgGraphDemo.text,
|
|
618
|
+
url: '',
|
|
619
|
+
js: () => this.addPanel(svgGraphDemo.text, <DemoPage story={svgGraphDemo} />),
|
|
620
|
+
},
|
|
614
621
|
],
|
|
615
622
|
},
|
|
616
623
|
{
|
|
@@ -622,7 +629,7 @@ export class DemoFrameHelper {
|
|
|
622
629
|
id: 'about',
|
|
623
630
|
text: 'About',
|
|
624
631
|
url: '',
|
|
625
|
-
js: () => this.addPanel('About', DemoAboutPage
|
|
632
|
+
js: () => this.addPanel('About', <DemoAboutPage />),
|
|
626
633
|
},
|
|
627
634
|
],
|
|
628
635
|
},
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
gaugeChartDemo,
|
|
69
69
|
scatterChartDemo,
|
|
70
70
|
} from '../component-pool/charts';
|
|
71
|
+
import { svgGraphDemo } from '../component-pool/svg-graph/svg-graph-demo';
|
|
71
72
|
|
|
72
73
|
export const demoRegistry: Record<string, DemoStory<any>> = {
|
|
73
74
|
[buttonDemo.id]: buttonDemo,
|
|
@@ -138,4 +139,5 @@ export const demoRegistry: Record<string, DemoStory<any>> = {
|
|
|
138
139
|
[radarChartDemo.id]: radarChartDemo,
|
|
139
140
|
[gaugeChartDemo.id]: gaugeChartDemo,
|
|
140
141
|
[scatterChartDemo.id]: scatterChartDemo,
|
|
142
|
+
[svgGraphDemo.id]: svgGraphDemo,
|
|
141
143
|
};
|