@takaro/lib-components 0.4.9 → 0.4.11
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 +13 -6
- package/src/components/actions/Button/__snapshots__/Button.test.tsx.snap +1 -1
- package/src/components/actions/IconButton/__snapshots__/IconButton.test.tsx.snap +1 -1
- package/src/components/charts/AreaChart/AreaChart.stories.tsx +11 -7
- package/src/components/charts/AreaChart/index.tsx +114 -63
- package/src/components/charts/BarChart/BarChart.stories.tsx +33 -10
- package/src/components/charts/BarChart/index.tsx +280 -147
- package/src/components/charts/EmptyChart.tsx +45 -0
- package/src/components/charts/GeoMercator/GeoMercator.stories.tsx +15 -9
- package/src/components/charts/GeoMercator/index.tsx +15 -172
- package/src/components/charts/Heatmap/Heatmap.stories.tsx +167 -33
- package/src/components/charts/Heatmap/index.tsx +427 -193
- package/src/components/charts/LineChart/LineChart.stories.tsx +77 -3
- package/src/components/charts/LineChart/index.tsx +200 -79
- package/src/components/charts/PieChart/PieChart.stories.tsx +128 -20
- package/src/components/charts/PieChart/index.tsx +353 -59
- package/src/components/charts/PointHighlight.tsx +2 -2
- package/src/components/charts/RadarChart/RadarChart.stories.tsx +14 -5
- package/src/components/charts/RadarChart/index.tsx +94 -45
- package/src/components/charts/RadialBarChart/RadialBarChart.stories.tsx +26 -1
- package/src/components/charts/RadialBarChart/index.tsx +100 -34
- package/src/components/charts/RadialLineChart/RadialLineChart.stories.tsx +19 -2
- package/src/components/charts/RadialLineChart/index.tsx +116 -26
- package/src/components/charts/index.tsx +0 -26
- package/src/components/charts/util.ts +50 -12
- package/src/components/data/CountryList/index.tsx +146 -0
- package/src/components/data/Stats/Sparkline.tsx +48 -0
- package/src/components/data/Stats/Stat.tsx +15 -4
- package/src/components/data/Stats/context.tsx +1 -1
- package/src/components/data/Stats/index.tsx +8 -3
- package/src/components/data/index.ts +3 -0
- package/src/components/feedback/IconTooltip/index.tsx +9 -6
- package/src/components/feedback/ProgressBar/ProgressBar.stories.tsx +13 -14
- package/src/components/feedback/ProgressBar/index.tsx +1 -1
- package/src/components/inputs/DurationField/__tests__/Generic.test.tsx +12 -0
- package/src/components/visual/Card/CardTitle.tsx +7 -1
- package/src/components/visual/Card/index.tsx +0 -4
- package/src/helpers/formatNumber.ts +6 -0
- package/src/helpers/index.ts +1 -0
- package/vite.config.mts +4 -0
- package/src/components/charts/echarts/EChartsArea.stories.tsx +0 -139
- package/src/components/charts/echarts/EChartsArea.tsx +0 -139
- package/src/components/charts/echarts/EChartsBar.stories.tsx +0 -141
- package/src/components/charts/echarts/EChartsBar.tsx +0 -133
- package/src/components/charts/echarts/EChartsBase.tsx +0 -264
- package/src/components/charts/echarts/EChartsFunnel.stories.tsx +0 -164
- package/src/components/charts/echarts/EChartsFunnel.tsx +0 -114
- package/src/components/charts/echarts/EChartsHeatmap.stories.tsx +0 -168
- package/src/components/charts/echarts/EChartsHeatmap.tsx +0 -141
- package/src/components/charts/echarts/EChartsLine.stories.tsx +0 -132
- package/src/components/charts/echarts/EChartsLine.tsx +0 -111
- package/src/components/charts/echarts/EChartsPie.stories.tsx +0 -131
- package/src/components/charts/echarts/EChartsPie.tsx +0 -124
- package/src/components/charts/echarts/EChartsRadialBar.stories.tsx +0 -124
- package/src/components/charts/echarts/EChartsRadialBar.tsx +0 -118
- package/src/components/charts/echarts/EChartsScatter.stories.tsx +0 -166
- package/src/components/charts/echarts/EChartsScatter.tsx +0 -135
- package/src/components/charts/echarts/index.ts +0 -26
|
@@ -3,12 +3,38 @@ import { Meta, StoryFn } from '@storybook/react';
|
|
|
3
3
|
import appleStock, { AppleStock } from '@visx/mock-data/lib/mocks/appleStock';
|
|
4
4
|
import { LineChart, LineChartProps } from '.';
|
|
5
5
|
import { styled } from '../../../styled';
|
|
6
|
+
import * as allCurves from '@visx/curve';
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
title: 'Charts/LineChart',
|
|
9
10
|
component: LineChart,
|
|
10
11
|
args: {
|
|
11
12
|
curveType: 'curveBasis',
|
|
13
|
+
grid: 'none',
|
|
14
|
+
showAxisX: false,
|
|
15
|
+
showAxisY: false,
|
|
16
|
+
axisXLabel: 'Date',
|
|
17
|
+
axisYLabel: 'Stock Price',
|
|
18
|
+
numTicksX: undefined,
|
|
19
|
+
numTicksY: undefined,
|
|
20
|
+
showTooltip: true,
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
curveType: {
|
|
24
|
+
control: 'select',
|
|
25
|
+
options: [...Object.keys(allCurves)],
|
|
26
|
+
},
|
|
27
|
+
grid: {
|
|
28
|
+
control: 'select',
|
|
29
|
+
options: ['none', 'x', 'y', 'xy'],
|
|
30
|
+
description: 'Display grid lines',
|
|
31
|
+
},
|
|
32
|
+
showAxisX: { control: 'boolean' },
|
|
33
|
+
showAxisY: { control: 'boolean' },
|
|
34
|
+
showTooltip: { control: 'boolean' },
|
|
35
|
+
color: { control: 'color' },
|
|
36
|
+
numTicksX: { control: { type: 'number', min: 1, max: 20, step: 1 } },
|
|
37
|
+
numTicksY: { control: { type: 'number', min: 1, max: 20, step: 1 } },
|
|
12
38
|
},
|
|
13
39
|
} as Meta<LineChartProps<AppleStock>>;
|
|
14
40
|
|
|
@@ -25,12 +51,60 @@ export const Default: StoryFn<LineChartProps<AppleStock>> = (args) => {
|
|
|
25
51
|
return (
|
|
26
52
|
<Wrapper>
|
|
27
53
|
<LineChart<AppleStock>
|
|
54
|
+
{...args}
|
|
28
55
|
name="AppleStock"
|
|
29
56
|
xAccessor={getDate}
|
|
30
|
-
yAccessor={getStockValue}
|
|
31
|
-
tooltipAccessor={getTooltip}
|
|
32
57
|
data={appleStock}
|
|
33
|
-
|
|
58
|
+
lines={[
|
|
59
|
+
{
|
|
60
|
+
id: 'close',
|
|
61
|
+
yAccessor: getStockValue,
|
|
62
|
+
tooltipAccessor: getTooltip,
|
|
63
|
+
color: '#664de5',
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
</Wrapper>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const MultipleLines: StoryFn<LineChartProps<AppleStock>> = (args) => {
|
|
72
|
+
const getDate = (d: AppleStock) => new Date(d.date);
|
|
73
|
+
|
|
74
|
+
const lineConfigs = [
|
|
75
|
+
{
|
|
76
|
+
id: 'close',
|
|
77
|
+
yAccessor: (d: AppleStock) => d.close,
|
|
78
|
+
color: '#664de5',
|
|
79
|
+
label: 'Close',
|
|
80
|
+
tooltipAccessor: (d: AppleStock) => `Close: $${d.close.toFixed(2)}`,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'high-estimate',
|
|
84
|
+
yAccessor: (d: AppleStock) => d.close * 1.1,
|
|
85
|
+
color: '#32CD32',
|
|
86
|
+
label: 'High (+10%)',
|
|
87
|
+
tooltipAccessor: (d: AppleStock) => `High: $${(d.close * 1.1).toFixed(2)}`,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'low-estimate',
|
|
91
|
+
yAccessor: (d: AppleStock) => d.close * 0.9,
|
|
92
|
+
color: '#FF6347',
|
|
93
|
+
label: 'Low (-10%)',
|
|
94
|
+
tooltipAccessor: (d: AppleStock) => `Low: $${(d.close * 0.9).toFixed(2)}`,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Wrapper>
|
|
100
|
+
<LineChart<AppleStock>
|
|
101
|
+
{...args}
|
|
102
|
+
name="AppleStockMultiLine"
|
|
103
|
+
xAccessor={getDate}
|
|
104
|
+
data={appleStock}
|
|
105
|
+
lines={lineConfigs}
|
|
106
|
+
axisYLabel="Stock Price ($)"
|
|
107
|
+
axisXLabel="Date"
|
|
34
108
|
/>
|
|
35
109
|
</Wrapper>
|
|
36
110
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bisector, extent, max } from '@visx/vendor/d3-array';
|
|
1
|
+
import { bisector, extent, max, min } from '@visx/vendor/d3-array';
|
|
2
2
|
import * as allCurves from '@visx/curve';
|
|
3
3
|
import { Group } from '@visx/group';
|
|
4
4
|
import { LinePath } from '@visx/shape';
|
|
@@ -7,12 +7,15 @@ import { scaleTime, scaleLinear } from '@visx/scale';
|
|
|
7
7
|
import { AxisBottom, AxisLeft } from '@visx/axis';
|
|
8
8
|
import { useTooltip, Tooltip, TooltipWithBounds } from '@visx/tooltip';
|
|
9
9
|
import { timeFormat } from '@visx/vendor/d3-time-format';
|
|
10
|
+
import { GridRows, GridColumns } from '@visx/grid';
|
|
10
11
|
|
|
11
12
|
import { useTheme } from '../../../hooks';
|
|
12
13
|
import { useCallback, useMemo } from 'react';
|
|
13
|
-
import {
|
|
14
|
+
import { ChartProps, InnerChartProps, getDefaultTooltipStyles, TooltipConfig } from '../util';
|
|
14
15
|
import { localPoint } from '@visx/event';
|
|
15
16
|
import { PointHighlight } from '../PointHighlight';
|
|
17
|
+
import { motion } from 'framer-motion';
|
|
18
|
+
import { EmptyChart } from '../EmptyChart';
|
|
16
19
|
|
|
17
20
|
const defaultMargin = { top: 10, right: 0, bottom: 25, left: 40 };
|
|
18
21
|
const defaultShowAxisX = true;
|
|
@@ -21,51 +24,57 @@ const defaultShowAxisY = true;
|
|
|
21
24
|
type CurveType = keyof typeof allCurves;
|
|
22
25
|
const formatDate = timeFormat("%b %d, '%y");
|
|
23
26
|
|
|
27
|
+
export interface LineConfig<T> {
|
|
28
|
+
id: string;
|
|
29
|
+
yAccessor: (d: T) => number;
|
|
30
|
+
tooltipAccessor?: (d: T) => string;
|
|
31
|
+
color?: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
export interface LineChartProps<T> extends ChartProps {
|
|
25
36
|
data: T[];
|
|
26
37
|
xAccessor: (d: T) => Date;
|
|
27
|
-
yAccessor: (d: T) => number;
|
|
28
|
-
tooltipAccessor?: (d: T) => string;
|
|
29
|
-
margin?: Margin;
|
|
30
38
|
curveType?: CurveType;
|
|
39
|
+
lines: LineConfig<T>[];
|
|
40
|
+
/** Tooltip configuration */
|
|
41
|
+
tooltip?: TooltipConfig<T>;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export const LineChart = <T,>({
|
|
34
45
|
data,
|
|
35
46
|
xAccessor,
|
|
36
|
-
yAccessor,
|
|
37
|
-
tooltipAccessor,
|
|
38
|
-
margin = defaultMargin,
|
|
39
47
|
name,
|
|
40
|
-
|
|
41
|
-
axisXLabel,
|
|
42
|
-
showGrid,
|
|
43
|
-
showAxisX = defaultShowAxisX,
|
|
44
|
-
showAxisY = defaultShowAxisY,
|
|
48
|
+
grid = 'none',
|
|
45
49
|
curveType = 'curveBasis',
|
|
50
|
+
lines,
|
|
51
|
+
axis,
|
|
52
|
+
tooltip,
|
|
53
|
+
animate = true,
|
|
54
|
+
margin = defaultMargin,
|
|
46
55
|
}: LineChartProps<T>) => {
|
|
47
|
-
|
|
56
|
+
const hasData = data && data.length > 0;
|
|
48
57
|
|
|
49
58
|
return (
|
|
50
59
|
<ParentSize>
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
{hasData
|
|
61
|
+
? (parent) => (
|
|
62
|
+
<Chart<T>
|
|
63
|
+
name={name}
|
|
64
|
+
xAccessor={xAccessor}
|
|
65
|
+
data={data}
|
|
66
|
+
width={parent.width}
|
|
67
|
+
height={parent.height}
|
|
68
|
+
grid={grid}
|
|
69
|
+
margin={margin}
|
|
70
|
+
axis={axis}
|
|
71
|
+
tooltip={tooltip}
|
|
72
|
+
animate={animate}
|
|
73
|
+
curveType={curveType}
|
|
74
|
+
lines={lines}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
: () => <EmptyChart />}
|
|
69
78
|
</ParentSize>
|
|
70
79
|
);
|
|
71
80
|
};
|
|
@@ -79,45 +88,73 @@ const Chart = <T,>({
|
|
|
79
88
|
curveType = 'curveBasis',
|
|
80
89
|
data,
|
|
81
90
|
xAccessor,
|
|
82
|
-
yAccessor,
|
|
83
91
|
margin = defaultMargin,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
grid = 'none',
|
|
93
|
+
axis,
|
|
94
|
+
tooltip,
|
|
95
|
+
animate = true,
|
|
96
|
+
lines,
|
|
89
97
|
}: InnerLineChartProps<T>) => {
|
|
90
98
|
const theme = useTheme();
|
|
91
99
|
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
100
|
+
const showAxisX = axis?.showX ?? defaultShowAxisX;
|
|
101
|
+
const showAxisY = axis?.showY ?? defaultShowAxisY;
|
|
102
|
+
const axisXLabel = axis?.labelX;
|
|
103
|
+
const axisYLabel = axis?.labelY;
|
|
104
|
+
const numTicksX = axis?.numTicksX ?? 5;
|
|
105
|
+
const numTicksY = axis?.numTicksY ?? 5;
|
|
106
|
+
const includeZeroY = axis?.includeZeroY ?? false;
|
|
107
|
+
const showTooltip = tooltip?.enabled ?? true;
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
// Calculate inner dimensions (chart area within margins)
|
|
110
|
+
const innerWidth = width - margin.left - margin.right;
|
|
111
|
+
const innerHeight = height - margin.top - margin.bottom;
|
|
112
|
+
|
|
113
|
+
const {
|
|
114
|
+
hideTooltip,
|
|
115
|
+
showTooltip: showTooltipHandler,
|
|
116
|
+
tooltipData,
|
|
117
|
+
tooltipLeft = 0,
|
|
118
|
+
tooltipTop = 0,
|
|
119
|
+
} = useTooltip<{
|
|
120
|
+
data: T;
|
|
121
|
+
lineId?: string;
|
|
122
|
+
}>();
|
|
98
123
|
|
|
99
124
|
const xScale = useMemo(
|
|
100
125
|
() =>
|
|
101
126
|
scaleTime<number>({
|
|
102
|
-
range: [
|
|
127
|
+
range: [0, innerWidth],
|
|
103
128
|
domain: extent(data, xAccessor) as [Date, Date],
|
|
104
129
|
}),
|
|
105
|
-
[innerWidth],
|
|
130
|
+
[innerWidth, data, xAccessor],
|
|
106
131
|
);
|
|
107
132
|
|
|
133
|
+
const yDomain = useMemo(() => {
|
|
134
|
+
const allValues = lines.flatMap((line) => data.map(line.yAccessor));
|
|
135
|
+
const minValue = includeZeroY ? 0 : (min(allValues) as number);
|
|
136
|
+
const maxValue = max(allValues) as number;
|
|
137
|
+
return [minValue, maxValue];
|
|
138
|
+
}, [lines, data, includeZeroY]);
|
|
139
|
+
|
|
108
140
|
const yScale = useMemo(
|
|
109
141
|
() =>
|
|
110
142
|
scaleLinear<number>({
|
|
111
|
-
range: [innerHeight,
|
|
112
|
-
domain:
|
|
143
|
+
range: [innerHeight, 0],
|
|
144
|
+
domain: yDomain,
|
|
145
|
+
nice: true,
|
|
113
146
|
}),
|
|
114
|
-
[innerHeight],
|
|
147
|
+
[innerHeight, yDomain],
|
|
115
148
|
);
|
|
116
149
|
|
|
117
150
|
const handleTooltip = useCallback(
|
|
118
151
|
(event: React.TouchEvent<SVGElement> | React.MouseEvent<SVGElement>) => {
|
|
119
152
|
const { x } = localPoint(event) || { x: 0 };
|
|
120
|
-
|
|
153
|
+
|
|
154
|
+
// Subtract margin.left because localPoint gives coordinates relative to SVG,
|
|
155
|
+
// but xScale is 0-based (relative to the chart area)
|
|
156
|
+
const xRelativeToChart = x - margin.left;
|
|
157
|
+
const x0 = xScale.invert(xRelativeToChart);
|
|
121
158
|
let index = bisector<T, Date>(xAccessor).left(data, x0);
|
|
122
159
|
|
|
123
160
|
// Clamp the index to ensure it's within the range of data points
|
|
@@ -125,15 +162,17 @@ const Chart = <T,>({
|
|
|
125
162
|
|
|
126
163
|
const d = data[index];
|
|
127
164
|
const tooltipX = xScale(xAccessor(d));
|
|
128
|
-
const tooltipY = yScale(yAccessor(d));
|
|
129
165
|
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
const activeYAccessor = lines[0].yAccessor;
|
|
167
|
+
const tooltipY = yScale(activeYAccessor(d));
|
|
168
|
+
|
|
169
|
+
showTooltipHandler({
|
|
170
|
+
tooltipData: { data: d },
|
|
132
171
|
tooltipLeft: tooltipX,
|
|
133
172
|
tooltipTop: tooltipY,
|
|
134
173
|
});
|
|
135
174
|
},
|
|
136
|
-
[yScale, xScale, data,
|
|
175
|
+
[yScale, xScale, data, xAccessor, lines, margin.left],
|
|
137
176
|
);
|
|
138
177
|
|
|
139
178
|
return width === 10 ? null : (
|
|
@@ -141,43 +180,96 @@ const Chart = <T,>({
|
|
|
141
180
|
<svg width={width} height={height}>
|
|
142
181
|
<rect width={width} height={height} fill={theme.colors.background} rx={14} ry={14} />
|
|
143
182
|
<Group top={margin.top} left={margin.left}>
|
|
183
|
+
{(grid === 'y' || grid === 'xy') && (
|
|
184
|
+
<GridRows
|
|
185
|
+
scale={yScale}
|
|
186
|
+
width={innerWidth}
|
|
187
|
+
stroke={theme.colors.backgroundAccent}
|
|
188
|
+
strokeOpacity={1}
|
|
189
|
+
strokeDasharray="2,2"
|
|
190
|
+
pointerEvents="none"
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
{(grid === 'x' || grid === 'xy') && (
|
|
194
|
+
<GridColumns
|
|
195
|
+
scale={xScale}
|
|
196
|
+
height={innerHeight}
|
|
197
|
+
stroke={theme.colors.backgroundAccent}
|
|
198
|
+
strokeOpacity={1}
|
|
199
|
+
strokeDasharray="2,2"
|
|
200
|
+
pointerEvents="none"
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
144
203
|
<rect
|
|
145
|
-
x={
|
|
146
|
-
|
|
204
|
+
x={0}
|
|
205
|
+
y={0}
|
|
206
|
+
width={innerWidth}
|
|
147
207
|
height={innerHeight}
|
|
148
208
|
fill="transparent"
|
|
149
|
-
onTouchStart={handleTooltip}
|
|
150
|
-
onTouchMove={handleTooltip}
|
|
151
|
-
onMouseMove={handleTooltip}
|
|
152
|
-
onMouseLeave={hideTooltip}
|
|
153
|
-
/>
|
|
154
|
-
<LinePath<T>
|
|
155
|
-
curve={allCurves[curveType]}
|
|
156
|
-
data={data}
|
|
157
|
-
x={(d) => xScale(xAccessor(d)) ?? 0}
|
|
158
|
-
y={(d) => yScale(yAccessor(d)) ?? 0}
|
|
159
|
-
stroke={theme.colors.primary}
|
|
160
|
-
strokeWidth={1.2}
|
|
161
|
-
strokeOpacity={0.6}
|
|
162
|
-
shapeRendering="geometricPrecision"
|
|
209
|
+
onTouchStart={showTooltip ? handleTooltip : undefined}
|
|
210
|
+
onTouchMove={showTooltip ? handleTooltip : undefined}
|
|
211
|
+
onMouseMove={showTooltip ? handleTooltip : undefined}
|
|
212
|
+
onMouseLeave={showTooltip ? hideTooltip : undefined}
|
|
163
213
|
/>
|
|
214
|
+
{lines.map((lineConfig, index) => (
|
|
215
|
+
<LinePath<T>
|
|
216
|
+
key={lineConfig.id}
|
|
217
|
+
curve={allCurves[curveType]}
|
|
218
|
+
data={data}
|
|
219
|
+
x={(d) => xScale(xAccessor(d)) ?? 0}
|
|
220
|
+
y={(d) => yScale(lineConfig.yAccessor(d)) ?? 0}
|
|
221
|
+
pointerEvents="none"
|
|
222
|
+
>
|
|
223
|
+
{({ path }) => {
|
|
224
|
+
const pathData = path(data) || '';
|
|
225
|
+
return (
|
|
226
|
+
<motion.path
|
|
227
|
+
d={pathData}
|
|
228
|
+
stroke={lineConfig.color || theme.colors.primary}
|
|
229
|
+
strokeWidth={2.5}
|
|
230
|
+
strokeOpacity={0.9}
|
|
231
|
+
strokeLinecap="round"
|
|
232
|
+
strokeLinejoin="round"
|
|
233
|
+
fill="none"
|
|
234
|
+
pointerEvents="none"
|
|
235
|
+
initial={animate ? { pathLength: 0, opacity: 0 } : { pathLength: 1, opacity: 0.9 }}
|
|
236
|
+
animate={{ pathLength: 1, opacity: 0.9 }}
|
|
237
|
+
transition={
|
|
238
|
+
animate
|
|
239
|
+
? {
|
|
240
|
+
pathLength: { duration: 1, delay: index * 0.2, ease: 'easeOut' },
|
|
241
|
+
}
|
|
242
|
+
: { duration: 0 }
|
|
243
|
+
}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}}
|
|
247
|
+
</LinePath>
|
|
248
|
+
))}
|
|
164
249
|
</Group>
|
|
165
|
-
{tooltipData && (
|
|
250
|
+
{showTooltip && tooltipData && (
|
|
166
251
|
<PointHighlight margin={margin} tooltipTop={tooltipTop} tooltipLeft={tooltipLeft} yMax={innerHeight} />
|
|
167
252
|
)}
|
|
168
253
|
{showAxisY && (
|
|
169
254
|
<AxisLeft
|
|
170
255
|
top={margin.top}
|
|
171
|
-
left={margin.left
|
|
172
|
-
strokeWidth={1}
|
|
173
|
-
hideZero
|
|
256
|
+
left={margin.left}
|
|
257
|
+
strokeWidth={1.5}
|
|
174
258
|
stroke={theme.colors.backgroundAlt}
|
|
175
259
|
labelOffset={42}
|
|
176
260
|
tickStroke={theme.colors.backgroundAlt}
|
|
261
|
+
tickLength={4}
|
|
262
|
+
numTicks={numTicksY}
|
|
177
263
|
tickLabelProps={{
|
|
178
264
|
fill: theme.colors.text,
|
|
179
265
|
fontSize: theme.fontSize.small,
|
|
180
266
|
textAnchor: 'end',
|
|
267
|
+
fontWeight: 500,
|
|
268
|
+
}}
|
|
269
|
+
labelProps={{
|
|
270
|
+
fill: theme.colors.textAlt,
|
|
271
|
+
fontSize: theme.fontSize.small,
|
|
272
|
+
fontWeight: 600,
|
|
181
273
|
}}
|
|
182
274
|
scale={yScale}
|
|
183
275
|
label={axisYLabel}
|
|
@@ -187,31 +279,60 @@ const Chart = <T,>({
|
|
|
187
279
|
<AxisBottom
|
|
188
280
|
top={innerHeight + margin.top}
|
|
189
281
|
left={margin.left}
|
|
190
|
-
strokeWidth={1}
|
|
282
|
+
strokeWidth={1.5}
|
|
191
283
|
stroke={theme.colors.backgroundAlt}
|
|
192
284
|
hideZero
|
|
285
|
+
tickLength={4}
|
|
286
|
+
numTicks={numTicksX}
|
|
193
287
|
tickLabelProps={{
|
|
194
288
|
fill: theme.colors.text,
|
|
195
289
|
fontSize: theme.fontSize.small,
|
|
196
290
|
textAnchor: 'middle',
|
|
291
|
+
fontWeight: 500,
|
|
197
292
|
}}
|
|
198
293
|
labelProps={{
|
|
199
294
|
fill: theme.colors.textAlt,
|
|
295
|
+
fontSize: theme.fontSize.small,
|
|
296
|
+
fontWeight: 600,
|
|
200
297
|
}}
|
|
201
298
|
scale={xScale}
|
|
202
299
|
label={axisXLabel}
|
|
203
300
|
/>
|
|
204
301
|
)}
|
|
205
302
|
</svg>
|
|
206
|
-
{tooltipData && (
|
|
303
|
+
{showTooltip && tooltipData && (
|
|
207
304
|
<div>
|
|
208
305
|
<TooltipWithBounds
|
|
209
306
|
key={`${name}-tooltip`}
|
|
210
|
-
top={tooltipTop
|
|
211
|
-
left={tooltipLeft + margin.left}
|
|
307
|
+
top={tooltipTop}
|
|
308
|
+
left={tooltipLeft + margin.left + 10}
|
|
212
309
|
style={getDefaultTooltipStyles(theme)}
|
|
213
310
|
>
|
|
214
|
-
|
|
311
|
+
<div>
|
|
312
|
+
{lines.map((line) => {
|
|
313
|
+
const value = line.yAccessor(tooltipData.data);
|
|
314
|
+
const displayText = line.tooltipAccessor
|
|
315
|
+
? line.tooltipAccessor(tooltipData.data)
|
|
316
|
+
: line.label
|
|
317
|
+
? `${line.label}: ${value}`
|
|
318
|
+
: value;
|
|
319
|
+
return (
|
|
320
|
+
<div key={line.id} style={{ marginBottom: '4px' }}>
|
|
321
|
+
<span
|
|
322
|
+
style={{
|
|
323
|
+
display: 'inline-block',
|
|
324
|
+
width: '10px',
|
|
325
|
+
height: '10px',
|
|
326
|
+
backgroundColor: line.color || theme.colors.primary,
|
|
327
|
+
marginRight: '6px',
|
|
328
|
+
borderRadius: '2px',
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
{displayText}
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
335
|
+
</div>
|
|
215
336
|
</TooltipWithBounds>
|
|
216
337
|
<Tooltip
|
|
217
338
|
top={innerHeight + margin.top - 14}
|
|
@@ -223,7 +344,7 @@ const Chart = <T,>({
|
|
|
223
344
|
transform: 'translateX(-50%)',
|
|
224
345
|
}}
|
|
225
346
|
>
|
|
226
|
-
{formatDate(xAccessor(tooltipData))}
|
|
347
|
+
{formatDate(xAccessor(tooltipData.data))}
|
|
227
348
|
</Tooltip>
|
|
228
349
|
</div>
|
|
229
350
|
)}
|
|
@@ -1,43 +1,151 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { Meta, StoryFn } from '@storybook/react';
|
|
3
3
|
import { PieChart, PieChartProps } from '.';
|
|
4
4
|
import { styled } from '../../../styled';
|
|
5
5
|
import letterFrequency, { LetterFrequency } from '@visx/mock-data/lib/mocks/letterFrequency';
|
|
6
|
+
import { Card } from '../../visual/Card';
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
title: 'Charts/PieChart',
|
|
9
10
|
component: PieChart,
|
|
10
11
|
args: {
|
|
11
|
-
|
|
12
|
+
innerRadius: 0,
|
|
13
|
+
padAngle: 0.005,
|
|
14
|
+
cornerRadius: 0,
|
|
15
|
+
legendPosition: 'none',
|
|
16
|
+
labelPosition: 'inside',
|
|
17
|
+
animate: true,
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
cornerRadius: {
|
|
21
|
+
description:
|
|
22
|
+
'Note: Set to 0 when using small innerRadius (<0.3) with large padAngle (>0.02) to keep slices consistent',
|
|
23
|
+
},
|
|
12
24
|
},
|
|
13
25
|
} as Meta<PieChartProps<LetterFrequency>>;
|
|
14
26
|
|
|
15
27
|
const Wrapper = styled.div`
|
|
16
|
-
height:
|
|
28
|
+
height: 500px;
|
|
17
29
|
width: 100%;
|
|
18
30
|
`;
|
|
19
31
|
|
|
32
|
+
const getLetter = (d: LetterFrequency) => d.letter;
|
|
33
|
+
const getLetterFrequency = (d: LetterFrequency) => Number(d.frequency) * 100;
|
|
34
|
+
|
|
35
|
+
// Limit data to first 6 items for better visualization
|
|
36
|
+
const limitedData = letterFrequency.slice(0, 6);
|
|
37
|
+
|
|
20
38
|
export const Default: StoryFn<PieChartProps<LetterFrequency>> = (args) => {
|
|
21
|
-
const
|
|
22
|
-
const getLetterFrequency = (d: LetterFrequency) => Number(d.frequency) * 100;
|
|
39
|
+
const [selectedLetter, setSelectedLetter] = useState<string | null>(null);
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<div style={{ marginBottom: '16px', minHeight: '24px' }}>
|
|
44
|
+
{selectedLetter && (
|
|
45
|
+
<p>
|
|
46
|
+
Selected: <strong>{selectedLetter}</strong>
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
<Wrapper>
|
|
51
|
+
<PieChart<LetterFrequency>
|
|
52
|
+
{...args}
|
|
53
|
+
name="default-pie"
|
|
54
|
+
xAccessor={getLetter}
|
|
55
|
+
yAccessor={getLetterFrequency}
|
|
56
|
+
data={limitedData}
|
|
57
|
+
onSliceClick={(d) => setSelectedLetter(getLetter(d))}
|
|
58
|
+
/>
|
|
59
|
+
</Wrapper>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// User Distribution Card Story
|
|
65
|
+
interface UserType {
|
|
66
|
+
role: string;
|
|
67
|
+
count: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const userData: UserType[] = [
|
|
71
|
+
{ role: 'Free', count: 8200 },
|
|
72
|
+
{ role: 'Premium', count: 3500 },
|
|
73
|
+
{ role: 'Moderator', count: 1200 },
|
|
74
|
+
{ role: 'Admin', count: 150 },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const CardWrapper = styled.div`
|
|
78
|
+
width: 600px;
|
|
79
|
+
`;
|
|
27
80
|
|
|
28
|
-
|
|
29
|
-
|
|
81
|
+
const ChartContainer = styled.div`
|
|
82
|
+
height: 300px;
|
|
83
|
+
width: 600px;
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const CenterContent = styled.div`
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const TotalCount = styled.div`
|
|
94
|
+
font-size: 2.5rem;
|
|
95
|
+
font-weight: 700;
|
|
96
|
+
line-height: 1;
|
|
97
|
+
margin-bottom: 0.25rem;
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const TotalLabel = styled.div`
|
|
101
|
+
font-size: 0.875rem;
|
|
102
|
+
color: ${({ theme }) => theme.colors.textAlt};
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
letter-spacing: 0.05em;
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
export const UserDistributionCard: StoryFn = () => {
|
|
108
|
+
const userColors = ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b'];
|
|
109
|
+
|
|
110
|
+
const total = userData.reduce((sum, d) => sum + d.count, 0);
|
|
111
|
+
|
|
112
|
+
const formatNumber = (num: number) => {
|
|
113
|
+
return new Intl.NumberFormat('en-US').format(num);
|
|
114
|
+
};
|
|
30
115
|
|
|
31
116
|
return (
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
117
|
+
<CardWrapper>
|
|
118
|
+
<Card>
|
|
119
|
+
<Card.Title label="User Distribution" />
|
|
120
|
+
<Card.Body>
|
|
121
|
+
<ChartContainer>
|
|
122
|
+
<PieChart<UserType>
|
|
123
|
+
name="user-distribution-pie"
|
|
124
|
+
data={userData}
|
|
125
|
+
xAccessor={(d) => d.role}
|
|
126
|
+
yAccessor={(d) => d.count}
|
|
127
|
+
innerRadius={0.6}
|
|
128
|
+
labelPosition="inside"
|
|
129
|
+
legendPosition="left"
|
|
130
|
+
colors={userColors}
|
|
131
|
+
cornerRadius={3}
|
|
132
|
+
padAngle={0.02}
|
|
133
|
+
tooltip={{
|
|
134
|
+
accessor: (d) => {
|
|
135
|
+
const percentage = ((d.count / total) * 100).toFixed(1);
|
|
136
|
+
return `${d.role}: ${formatNumber(d.count)} users (${percentage}%)`;
|
|
137
|
+
},
|
|
138
|
+
}}
|
|
139
|
+
centerContent={(totalValue) => (
|
|
140
|
+
<CenterContent>
|
|
141
|
+
<TotalCount>{formatNumber(totalValue)}</TotalCount>
|
|
142
|
+
<TotalLabel>Total Users</TotalLabel>
|
|
143
|
+
</CenterContent>
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
</ChartContainer>
|
|
147
|
+
</Card.Body>
|
|
148
|
+
</Card>
|
|
149
|
+
</CardWrapper>
|
|
42
150
|
);
|
|
43
151
|
};
|