@takaro/lib-components 0.3.3 → 0.4.0
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/components/actions/Dropdown/useDropdown.tsx +8 -1
- package/src/components/charts/echarts/EChartsArea.stories.tsx +139 -0
- package/src/components/charts/echarts/EChartsArea.tsx +139 -0
- package/src/components/charts/echarts/EChartsBar.stories.tsx +141 -0
- package/src/components/charts/echarts/EChartsBar.tsx +133 -0
- package/src/components/charts/echarts/EChartsBase.tsx +265 -0
- package/src/components/charts/echarts/EChartsFunnel.stories.tsx +164 -0
- package/src/components/charts/echarts/EChartsFunnel.tsx +114 -0
- package/src/components/charts/echarts/EChartsHeatmap.stories.tsx +168 -0
- package/src/components/charts/echarts/EChartsHeatmap.tsx +141 -0
- package/src/components/charts/echarts/EChartsLine.stories.tsx +132 -0
- package/src/components/charts/echarts/EChartsLine.tsx +111 -0
- package/src/components/charts/echarts/EChartsPie.stories.tsx +131 -0
- package/src/components/charts/echarts/EChartsPie.tsx +124 -0
- package/src/components/charts/echarts/EChartsRadialBar.stories.tsx +124 -0
- package/src/components/charts/echarts/EChartsRadialBar.tsx +118 -0
- package/src/components/charts/echarts/EChartsScatter.stories.tsx +166 -0
- package/src/components/charts/echarts/EChartsScatter.tsx +135 -0
- package/src/components/charts/echarts/index.ts +26 -0
- package/src/components/charts/index.tsx +26 -0
- package/src/components/feedback/Alert/__snapshots__/Alert.test.tsx.snap +1 -3
- package/src/components/feedback/Alert/index.tsx +1 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { FC, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
|
3
|
+
import * as echarts from 'echarts/core';
|
|
4
|
+
import { CanvasRenderer } from 'echarts/renderers';
|
|
5
|
+
import {
|
|
6
|
+
TitleComponent,
|
|
7
|
+
TooltipComponent,
|
|
8
|
+
GridComponent,
|
|
9
|
+
LegendComponent,
|
|
10
|
+
ToolboxComponent,
|
|
11
|
+
DataZoomComponent,
|
|
12
|
+
VisualMapComponent,
|
|
13
|
+
MarkLineComponent,
|
|
14
|
+
MarkPointComponent,
|
|
15
|
+
MarkAreaComponent,
|
|
16
|
+
PolarComponent,
|
|
17
|
+
} from 'echarts/components';
|
|
18
|
+
import {
|
|
19
|
+
LineChart,
|
|
20
|
+
BarChart,
|
|
21
|
+
PieChart,
|
|
22
|
+
ScatterChart,
|
|
23
|
+
RadarChart,
|
|
24
|
+
MapChart,
|
|
25
|
+
TreeChart,
|
|
26
|
+
TreemapChart,
|
|
27
|
+
GraphChart,
|
|
28
|
+
GaugeChart,
|
|
29
|
+
FunnelChart,
|
|
30
|
+
ParallelChart,
|
|
31
|
+
SankeyChart,
|
|
32
|
+
BoxplotChart,
|
|
33
|
+
CandlestickChart,
|
|
34
|
+
EffectScatterChart,
|
|
35
|
+
LinesChart,
|
|
36
|
+
HeatmapChart,
|
|
37
|
+
PictorialBarChart,
|
|
38
|
+
ThemeRiverChart,
|
|
39
|
+
SunburstChart,
|
|
40
|
+
CustomChart,
|
|
41
|
+
} from 'echarts/charts';
|
|
42
|
+
import { useTheme } from '../../../hooks';
|
|
43
|
+
import { ParentSize } from '@visx/responsive';
|
|
44
|
+
import { EChartsOption } from 'echarts';
|
|
45
|
+
|
|
46
|
+
// Register all components
|
|
47
|
+
echarts.use([
|
|
48
|
+
CanvasRenderer,
|
|
49
|
+
TitleComponent,
|
|
50
|
+
TooltipComponent,
|
|
51
|
+
GridComponent,
|
|
52
|
+
LegendComponent,
|
|
53
|
+
ToolboxComponent,
|
|
54
|
+
DataZoomComponent,
|
|
55
|
+
VisualMapComponent,
|
|
56
|
+
MarkLineComponent,
|
|
57
|
+
MarkPointComponent,
|
|
58
|
+
MarkAreaComponent,
|
|
59
|
+
PolarComponent,
|
|
60
|
+
LineChart,
|
|
61
|
+
BarChart,
|
|
62
|
+
PieChart,
|
|
63
|
+
ScatterChart,
|
|
64
|
+
RadarChart,
|
|
65
|
+
MapChart,
|
|
66
|
+
TreeChart,
|
|
67
|
+
TreemapChart,
|
|
68
|
+
GraphChart,
|
|
69
|
+
GaugeChart,
|
|
70
|
+
FunnelChart,
|
|
71
|
+
ParallelChart,
|
|
72
|
+
SankeyChart,
|
|
73
|
+
BoxplotChart,
|
|
74
|
+
CandlestickChart,
|
|
75
|
+
EffectScatterChart,
|
|
76
|
+
LinesChart,
|
|
77
|
+
HeatmapChart,
|
|
78
|
+
PictorialBarChart,
|
|
79
|
+
ThemeRiverChart,
|
|
80
|
+
SunburstChart,
|
|
81
|
+
CustomChart,
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
export interface EChartsBaseProps {
|
|
85
|
+
option: EChartsOption;
|
|
86
|
+
height?: string | number;
|
|
87
|
+
width?: string | number;
|
|
88
|
+
loading?: boolean;
|
|
89
|
+
onChartReady?: (instance: any) => void;
|
|
90
|
+
onEvents?: Record<string, (params: any) => void>;
|
|
91
|
+
style?: React.CSSProperties;
|
|
92
|
+
className?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const EChartsBase: FC<EChartsBaseProps> = ({
|
|
96
|
+
option,
|
|
97
|
+
height = '100%',
|
|
98
|
+
width = '100%',
|
|
99
|
+
loading = false,
|
|
100
|
+
onChartReady,
|
|
101
|
+
onEvents,
|
|
102
|
+
style,
|
|
103
|
+
className,
|
|
104
|
+
}) => {
|
|
105
|
+
const theme = useTheme();
|
|
106
|
+
const chartRef = useRef<ReactEChartsCore>(null);
|
|
107
|
+
|
|
108
|
+
// Create theme-aware default options
|
|
109
|
+
const themeOptions = useMemo(() => {
|
|
110
|
+
return {
|
|
111
|
+
backgroundColor: 'transparent',
|
|
112
|
+
textStyle: {
|
|
113
|
+
color: theme.colors.text,
|
|
114
|
+
fontFamily: 'inherit',
|
|
115
|
+
},
|
|
116
|
+
title: {
|
|
117
|
+
textStyle: {
|
|
118
|
+
color: theme.colors.text,
|
|
119
|
+
fontSize: 16,
|
|
120
|
+
fontWeight: 'bold',
|
|
121
|
+
},
|
|
122
|
+
subtextStyle: {
|
|
123
|
+
color: theme.colors.textAlt,
|
|
124
|
+
fontSize: 12,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
legend: {
|
|
128
|
+
textStyle: {
|
|
129
|
+
color: theme.colors.text,
|
|
130
|
+
},
|
|
131
|
+
pageTextStyle: {
|
|
132
|
+
color: theme.colors.text,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
tooltip: {
|
|
136
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
137
|
+
borderColor: theme.colors.backgroundAccent,
|
|
138
|
+
borderWidth: 1,
|
|
139
|
+
textStyle: {
|
|
140
|
+
color: theme.colors.text,
|
|
141
|
+
},
|
|
142
|
+
extraCssText: `box-shadow: ${theme.elevation[200]};`,
|
|
143
|
+
},
|
|
144
|
+
grid: {
|
|
145
|
+
left: '3%',
|
|
146
|
+
right: '4%',
|
|
147
|
+
bottom: '3%',
|
|
148
|
+
containLabel: true,
|
|
149
|
+
},
|
|
150
|
+
xAxis: {
|
|
151
|
+
axisLine: {
|
|
152
|
+
lineStyle: {
|
|
153
|
+
color: theme.colors.backgroundAccent,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
axisTick: {
|
|
157
|
+
lineStyle: {
|
|
158
|
+
color: theme.colors.backgroundAccent,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
axisLabel: {
|
|
162
|
+
color: theme.colors.textAlt,
|
|
163
|
+
},
|
|
164
|
+
splitLine: {
|
|
165
|
+
lineStyle: {
|
|
166
|
+
color: theme.colors.backgroundAccent,
|
|
167
|
+
type: 'dashed',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
yAxis: {
|
|
172
|
+
axisLine: {
|
|
173
|
+
lineStyle: {
|
|
174
|
+
color: theme.colors.backgroundAccent,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
axisTick: {
|
|
178
|
+
lineStyle: {
|
|
179
|
+
color: theme.colors.backgroundAccent,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
axisLabel: {
|
|
183
|
+
color: theme.colors.textAlt,
|
|
184
|
+
},
|
|
185
|
+
splitLine: {
|
|
186
|
+
lineStyle: {
|
|
187
|
+
color: theme.colors.backgroundAccent,
|
|
188
|
+
type: 'dashed',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
color: [
|
|
193
|
+
theme.colors.primary,
|
|
194
|
+
theme.colors.success,
|
|
195
|
+
theme.colors.warning,
|
|
196
|
+
theme.colors.error,
|
|
197
|
+
theme.colors.info,
|
|
198
|
+
'#8b5cf6',
|
|
199
|
+
'#06b6d4',
|
|
200
|
+
'#f59e0b',
|
|
201
|
+
'#ec4899',
|
|
202
|
+
'#10b981',
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}, [theme]);
|
|
206
|
+
|
|
207
|
+
// Merge theme options with provided options
|
|
208
|
+
const mergedOptions = useMemo(() => {
|
|
209
|
+
// Check if this is a chart type that doesn't use axes
|
|
210
|
+
const hasNonCartesianChart =
|
|
211
|
+
option.series &&
|
|
212
|
+
Array.isArray(option.series) &&
|
|
213
|
+
option.series.some((s: any) =>
|
|
214
|
+
['pie', 'radar', 'gauge', 'funnel', 'sankey', 'graph', 'tree', 'treemap', 'sunburst'].includes(s.type),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Create adjusted theme options based on chart type
|
|
218
|
+
const adjustedThemeOptions = hasNonCartesianChart
|
|
219
|
+
? {
|
|
220
|
+
backgroundColor: themeOptions.backgroundColor,
|
|
221
|
+
textStyle: themeOptions.textStyle,
|
|
222
|
+
title: themeOptions.title,
|
|
223
|
+
legend: themeOptions.legend,
|
|
224
|
+
tooltip: themeOptions.tooltip,
|
|
225
|
+
color: themeOptions.color,
|
|
226
|
+
}
|
|
227
|
+
: themeOptions;
|
|
228
|
+
|
|
229
|
+
return echarts.util.merge(adjustedThemeOptions, option);
|
|
230
|
+
}, [themeOptions, option]);
|
|
231
|
+
|
|
232
|
+
// Handle resize
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const handleResize = () => {
|
|
235
|
+
if (chartRef.current) {
|
|
236
|
+
const instance = chartRef.current.getEchartsInstance();
|
|
237
|
+
instance.resize();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
window.addEventListener('resize', handleResize);
|
|
242
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<ReactEChartsCore
|
|
247
|
+
ref={chartRef}
|
|
248
|
+
echarts={echarts}
|
|
249
|
+
option={mergedOptions}
|
|
250
|
+
style={{ height, width, ...style }}
|
|
251
|
+
className={className}
|
|
252
|
+
showLoading={loading}
|
|
253
|
+
onChartReady={onChartReady}
|
|
254
|
+
onEvents={onEvents}
|
|
255
|
+
opts={{ renderer: 'canvas' }}
|
|
256
|
+
notMerge={true}
|
|
257
|
+
lazyUpdate={true}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Wrapper with ParentSize for responsive sizing
|
|
263
|
+
export const ResponsiveECharts: FC<Omit<EChartsBaseProps, 'width' | 'height'>> = (props) => {
|
|
264
|
+
return <ParentSize>{({ width, height }) => <EChartsBase {...props} width={width} height={height} />}</ParentSize>;
|
|
265
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Meta, StoryFn } from '@storybook/react';
|
|
3
|
+
import { EChartsFunnel, EChartsFunnelProps } from './EChartsFunnel';
|
|
4
|
+
import { styled } from '../../../styled';
|
|
5
|
+
import { Card } from '../../../components';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Charts/ECharts/Funnel',
|
|
9
|
+
component: EChartsFunnel,
|
|
10
|
+
args: {
|
|
11
|
+
showLegend: true,
|
|
12
|
+
showLabel: true,
|
|
13
|
+
sort: 'descending',
|
|
14
|
+
title: 'Funnel Chart',
|
|
15
|
+
gap: 2,
|
|
16
|
+
},
|
|
17
|
+
} as Meta<EChartsFunnelProps>;
|
|
18
|
+
|
|
19
|
+
const Wrapper = styled.div`
|
|
20
|
+
height: 500px;
|
|
21
|
+
width: 100%;
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
interface FunnelData {
|
|
25
|
+
stage: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Generate sample funnel data
|
|
30
|
+
function generateFunnelData(): FunnelData[] {
|
|
31
|
+
return [
|
|
32
|
+
{ stage: 'Visits', count: 1000 },
|
|
33
|
+
{ stage: 'Registrations', count: 650 },
|
|
34
|
+
{ stage: 'First Purchase', count: 400 },
|
|
35
|
+
{ stage: 'Repeat Purchase', count: 250 },
|
|
36
|
+
{ stage: 'VIP Status', count: 100 },
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Default: StoryFn<EChartsFunnelProps<FunnelData>> = (args) => {
|
|
41
|
+
const data = generateFunnelData();
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Wrapper>
|
|
45
|
+
<EChartsFunnel<FunnelData>
|
|
46
|
+
{...args}
|
|
47
|
+
data={data}
|
|
48
|
+
nameAccessor={(d) => d.stage}
|
|
49
|
+
valueAccessor={(d) => d.count}
|
|
50
|
+
seriesName="Conversion"
|
|
51
|
+
/>
|
|
52
|
+
</Wrapper>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const PlayerConversion: StoryFn = () => {
|
|
57
|
+
const conversionData = [
|
|
58
|
+
{ stage: 'New Players', count: 5000 },
|
|
59
|
+
{ stage: 'Tutorial Completed', count: 3500 },
|
|
60
|
+
{ stage: 'First Mission', count: 2800 },
|
|
61
|
+
{ stage: 'Level 10 Reached', count: 1800 },
|
|
62
|
+
{ stage: 'First Purchase', count: 900 },
|
|
63
|
+
{ stage: 'Regular Player', count: 450 },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Wrapper>
|
|
68
|
+
<Card variant="outline">
|
|
69
|
+
<Card.Title label="Player Conversion Funnel" />
|
|
70
|
+
<Card.Body>
|
|
71
|
+
<div style={{ height: '400px' }}>
|
|
72
|
+
<EChartsFunnel
|
|
73
|
+
data={conversionData}
|
|
74
|
+
nameAccessor={(d) => d.stage}
|
|
75
|
+
valueAccessor={(d) => d.count}
|
|
76
|
+
seriesName="Players"
|
|
77
|
+
showLegend={false}
|
|
78
|
+
labelPosition="inside"
|
|
79
|
+
tooltipFormatter={(params: any) => {
|
|
80
|
+
return `${params.name}<br/>Players: ${params.value.toLocaleString()} (${params.percent}%)`;
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</Card.Body>
|
|
85
|
+
</Card>
|
|
86
|
+
</Wrapper>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const SalesProcess: StoryFn<EChartsFunnelProps<FunnelData>> = (args) => {
|
|
91
|
+
const salesData = [
|
|
92
|
+
{ stage: 'Leads', count: 100 },
|
|
93
|
+
{ stage: 'Qualified', count: 80 },
|
|
94
|
+
{ stage: 'Proposal', count: 60 },
|
|
95
|
+
{ stage: 'Negotiation', count: 40 },
|
|
96
|
+
{ stage: 'Closed', count: 30 },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Wrapper>
|
|
101
|
+
<EChartsFunnel<FunnelData>
|
|
102
|
+
{...args}
|
|
103
|
+
data={salesData}
|
|
104
|
+
nameAccessor={(d) => d.stage}
|
|
105
|
+
valueAccessor={(d) => d.count}
|
|
106
|
+
seriesName="Sales"
|
|
107
|
+
title="Sales Pipeline"
|
|
108
|
+
subtitle="Q4 2024"
|
|
109
|
+
labelPosition="right"
|
|
110
|
+
/>
|
|
111
|
+
</Wrapper>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const AscendingFunnel: StoryFn<EChartsFunnelProps<FunnelData>> = (args) => {
|
|
116
|
+
const growthData = [
|
|
117
|
+
{ stage: 'Bronze', count: 30 },
|
|
118
|
+
{ stage: 'Silver', count: 50 },
|
|
119
|
+
{ stage: 'Gold', count: 75 },
|
|
120
|
+
{ stage: 'Platinum', count: 90 },
|
|
121
|
+
{ stage: 'Diamond', count: 100 },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Wrapper>
|
|
126
|
+
<EChartsFunnel<FunnelData>
|
|
127
|
+
{...args}
|
|
128
|
+
data={growthData}
|
|
129
|
+
nameAccessor={(d) => d.stage}
|
|
130
|
+
valueAccessor={(d) => d.count}
|
|
131
|
+
seriesName="Tier Distribution"
|
|
132
|
+
sort="ascending"
|
|
133
|
+
title="Player Tier Progression"
|
|
134
|
+
subtitle="Inverted funnel showing growth"
|
|
135
|
+
gap={5}
|
|
136
|
+
/>
|
|
137
|
+
</Wrapper>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const NoSort: StoryFn<EChartsFunnelProps<FunnelData>> = (args) => {
|
|
142
|
+
const customData = [
|
|
143
|
+
{ stage: 'Start', count: 80 },
|
|
144
|
+
{ stage: 'Middle', count: 100 },
|
|
145
|
+
{ stage: 'Peak', count: 120 },
|
|
146
|
+
{ stage: 'Decline', count: 60 },
|
|
147
|
+
{ stage: 'End', count: 40 },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Wrapper>
|
|
152
|
+
<EChartsFunnel<FunnelData>
|
|
153
|
+
{...args}
|
|
154
|
+
data={customData}
|
|
155
|
+
nameAccessor={(d) => d.stage}
|
|
156
|
+
valueAccessor={(d) => d.count}
|
|
157
|
+
seriesName="Custom Flow"
|
|
158
|
+
sort="none"
|
|
159
|
+
title="Custom Process Flow"
|
|
160
|
+
subtitle="Maintains data order"
|
|
161
|
+
/>
|
|
162
|
+
</Wrapper>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FC, useMemo } from 'react';
|
|
2
|
+
import { EChartsOption } from 'echarts';
|
|
3
|
+
import { ResponsiveECharts, EChartsBaseProps } from './EChartsBase';
|
|
4
|
+
|
|
5
|
+
export interface EChartsFunnelProps<T = any> extends Omit<EChartsBaseProps, 'option'> {
|
|
6
|
+
data: T[];
|
|
7
|
+
nameAccessor: (d: T) => string;
|
|
8
|
+
valueAccessor: (d: T) => number;
|
|
9
|
+
seriesName?: string;
|
|
10
|
+
showLegend?: boolean;
|
|
11
|
+
showLabel?: boolean;
|
|
12
|
+
title?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
sort?: 'ascending' | 'descending' | 'none';
|
|
15
|
+
gap?: number;
|
|
16
|
+
tooltipFormatter?: (params: any) => string;
|
|
17
|
+
labelPosition?: 'left' | 'right' | 'inside';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const EChartsFunnel: FC<EChartsFunnelProps> = ({
|
|
21
|
+
data,
|
|
22
|
+
nameAccessor,
|
|
23
|
+
valueAccessor,
|
|
24
|
+
seriesName = 'Funnel',
|
|
25
|
+
showLegend = true,
|
|
26
|
+
showLabel = true,
|
|
27
|
+
title,
|
|
28
|
+
subtitle,
|
|
29
|
+
sort = 'descending',
|
|
30
|
+
gap = 2,
|
|
31
|
+
tooltipFormatter,
|
|
32
|
+
labelPosition = 'inside',
|
|
33
|
+
...chartProps
|
|
34
|
+
}) => {
|
|
35
|
+
const option: EChartsOption = useMemo(() => {
|
|
36
|
+
const funnelData = data.map((d) => ({
|
|
37
|
+
name: nameAccessor(d),
|
|
38
|
+
value: valueAccessor(d),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
title: title
|
|
43
|
+
? {
|
|
44
|
+
text: title,
|
|
45
|
+
subtext: subtitle,
|
|
46
|
+
left: 'center',
|
|
47
|
+
}
|
|
48
|
+
: undefined,
|
|
49
|
+
legend: showLegend
|
|
50
|
+
? {
|
|
51
|
+
orient: 'vertical',
|
|
52
|
+
left: 'left',
|
|
53
|
+
top: 'middle',
|
|
54
|
+
}
|
|
55
|
+
: undefined,
|
|
56
|
+
tooltip: {
|
|
57
|
+
trigger: 'item',
|
|
58
|
+
formatter: tooltipFormatter || '{a} <br/>{b}: {c} ({d}%)',
|
|
59
|
+
},
|
|
60
|
+
series: [
|
|
61
|
+
{
|
|
62
|
+
name: seriesName,
|
|
63
|
+
type: 'funnel',
|
|
64
|
+
left: '10%',
|
|
65
|
+
top: 60,
|
|
66
|
+
bottom: 60,
|
|
67
|
+
width: '80%',
|
|
68
|
+
min: 0,
|
|
69
|
+
max: 100,
|
|
70
|
+
minSize: '0%',
|
|
71
|
+
maxSize: '100%',
|
|
72
|
+
sort: sort,
|
|
73
|
+
gap: gap,
|
|
74
|
+
label: {
|
|
75
|
+
show: showLabel,
|
|
76
|
+
position: labelPosition,
|
|
77
|
+
formatter: '{b}: {c}',
|
|
78
|
+
},
|
|
79
|
+
labelLine: {
|
|
80
|
+
length: 10,
|
|
81
|
+
lineStyle: {
|
|
82
|
+
width: 1,
|
|
83
|
+
type: 'solid',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
itemStyle: {
|
|
87
|
+
borderWidth: 0,
|
|
88
|
+
},
|
|
89
|
+
emphasis: {
|
|
90
|
+
label: {
|
|
91
|
+
fontSize: 20,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
data: funnelData,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}, [
|
|
99
|
+
data,
|
|
100
|
+
nameAccessor,
|
|
101
|
+
valueAccessor,
|
|
102
|
+
seriesName,
|
|
103
|
+
showLegend,
|
|
104
|
+
showLabel,
|
|
105
|
+
title,
|
|
106
|
+
subtitle,
|
|
107
|
+
sort,
|
|
108
|
+
gap,
|
|
109
|
+
tooltipFormatter,
|
|
110
|
+
labelPosition,
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
return <ResponsiveECharts option={option} {...chartProps} />;
|
|
114
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Meta, StoryFn } from '@storybook/react';
|
|
3
|
+
import { EChartsHeatmap, EChartsHeatmapProps } from './EChartsHeatmap';
|
|
4
|
+
import { styled } from '../../../styled';
|
|
5
|
+
import { Card } from '../../../components';
|
|
6
|
+
import { faker } from '@faker-js/faker';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'Charts/ECharts/Heatmap',
|
|
10
|
+
component: EChartsHeatmap,
|
|
11
|
+
args: {
|
|
12
|
+
title: 'Heatmap Example',
|
|
13
|
+
xAxisLabel: 'Hour',
|
|
14
|
+
yAxisLabel: 'Day',
|
|
15
|
+
showLabel: false,
|
|
16
|
+
},
|
|
17
|
+
} as Meta<EChartsHeatmapProps>;
|
|
18
|
+
|
|
19
|
+
const Wrapper = styled.div`
|
|
20
|
+
height: 500px;
|
|
21
|
+
width: 100%;
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
interface HeatmapData {
|
|
25
|
+
hour: number;
|
|
26
|
+
day: number;
|
|
27
|
+
value: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generate sample heatmap data
|
|
31
|
+
function generateHeatmapData(): HeatmapData[] {
|
|
32
|
+
const data: HeatmapData[] = [];
|
|
33
|
+
const days = 7;
|
|
34
|
+
const hours = 24;
|
|
35
|
+
|
|
36
|
+
for (let day = 0; day < days; day++) {
|
|
37
|
+
for (let hour = 0; hour < hours; hour++) {
|
|
38
|
+
// Simulate peak hours (lunch and evening)
|
|
39
|
+
let baseValue = 10;
|
|
40
|
+
if (hour >= 11 && hour <= 13) baseValue = 40; // Lunch peak
|
|
41
|
+
if (hour >= 18 && hour <= 21) baseValue = 60; // Evening peak
|
|
42
|
+
if (hour >= 0 && hour <= 6) baseValue = 2; // Night low
|
|
43
|
+
|
|
44
|
+
// Weekend bonus
|
|
45
|
+
if (day === 5 || day === 6) baseValue *= 1.5;
|
|
46
|
+
|
|
47
|
+
data.push({
|
|
48
|
+
hour,
|
|
49
|
+
day,
|
|
50
|
+
value: baseValue + faker.number.int({ min: -5, max: 20 }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
59
|
+
const hourLabels = Array.from({ length: 24 }, (_, i) =>
|
|
60
|
+
i === 0 ? '12am' : i < 12 ? `${i}am` : i === 12 ? '12pm' : `${i - 12}pm`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export const Default: StoryFn<EChartsHeatmapProps<HeatmapData>> = (args) => {
|
|
64
|
+
const data = generateHeatmapData();
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Wrapper>
|
|
68
|
+
<EChartsHeatmap<HeatmapData>
|
|
69
|
+
{...args}
|
|
70
|
+
data={data}
|
|
71
|
+
xAccessor={(d) => d.hour}
|
|
72
|
+
yAccessor={(d) => d.day}
|
|
73
|
+
valueAccessor={(d) => d.value}
|
|
74
|
+
xCategories={hourLabels}
|
|
75
|
+
yCategories={dayNames}
|
|
76
|
+
/>
|
|
77
|
+
</Wrapper>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const PeakSalesHeatmap: StoryFn = () => {
|
|
82
|
+
const data = generateHeatmapData();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Wrapper>
|
|
86
|
+
<Card variant="outline">
|
|
87
|
+
<Card.Title label="Peak Sales Heatmap" />
|
|
88
|
+
<Card.Body>
|
|
89
|
+
<div style={{ height: '400px' }}>
|
|
90
|
+
<EChartsHeatmap
|
|
91
|
+
data={data}
|
|
92
|
+
xAccessor={(d) => d.hour}
|
|
93
|
+
yAccessor={(d) => d.day}
|
|
94
|
+
valueAccessor={(d) => d.value}
|
|
95
|
+
xCategories={hourLabels}
|
|
96
|
+
yCategories={dayNames}
|
|
97
|
+
xAxisLabel="Hour of Day"
|
|
98
|
+
yAxisLabel="Day of Week"
|
|
99
|
+
tooltipFormatter={(params: any) => {
|
|
100
|
+
const value = params.value;
|
|
101
|
+
return `${dayNames[value[1]]}, ${hourLabels[value[0]]}<br/>Sales: ${value[2]}`;
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</Card.Body>
|
|
106
|
+
</Card>
|
|
107
|
+
</Wrapper>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const PlayerActivityHeatmap: StoryFn<EChartsHeatmapProps<HeatmapData>> = (args) => {
|
|
112
|
+
const data = generateHeatmapData().map((d) => ({
|
|
113
|
+
...d,
|
|
114
|
+
value: d.value * 2, // Higher values for player activity
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Wrapper>
|
|
119
|
+
<EChartsHeatmap<HeatmapData>
|
|
120
|
+
{...args}
|
|
121
|
+
data={data}
|
|
122
|
+
xAccessor={(d) => d.hour}
|
|
123
|
+
yAccessor={(d) => d.day}
|
|
124
|
+
valueAccessor={(d) => d.value}
|
|
125
|
+
xCategories={hourLabels}
|
|
126
|
+
yCategories={dayNames}
|
|
127
|
+
title="Player Activity Patterns"
|
|
128
|
+
subtitle="Average players online by hour and day"
|
|
129
|
+
showLabel={false}
|
|
130
|
+
minValue={0}
|
|
131
|
+
maxValue={150}
|
|
132
|
+
/>
|
|
133
|
+
</Wrapper>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const WithLabels: StoryFn<EChartsHeatmapProps<HeatmapData>> = (args) => {
|
|
138
|
+
// Smaller dataset for label visibility
|
|
139
|
+
const data: HeatmapData[] = [];
|
|
140
|
+
for (let day = 0; day < 5; day++) {
|
|
141
|
+
for (let hour = 0; hour < 12; hour++) {
|
|
142
|
+
data.push({
|
|
143
|
+
hour: hour * 2, // Every 2 hours
|
|
144
|
+
day,
|
|
145
|
+
value: faker.number.int({ min: 10, max: 100 }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sparseHours = hourLabels.filter((_, i) => i % 2 === 0);
|
|
151
|
+
const weekdays = dayNames.slice(0, 5);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Wrapper>
|
|
155
|
+
<EChartsHeatmap<HeatmapData>
|
|
156
|
+
{...args}
|
|
157
|
+
data={data}
|
|
158
|
+
xAccessor={(d) => d.hour / 2}
|
|
159
|
+
yAccessor={(d) => d.day}
|
|
160
|
+
valueAccessor={(d) => d.value}
|
|
161
|
+
xCategories={sparseHours}
|
|
162
|
+
yCategories={weekdays}
|
|
163
|
+
title="Resource Usage"
|
|
164
|
+
showLabel={true}
|
|
165
|
+
/>
|
|
166
|
+
</Wrapper>
|
|
167
|
+
);
|
|
168
|
+
};
|