@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MouseEvent, useCallback } from 'react';
|
|
1
|
+
import { MouseEvent, useCallback, useState, useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
import { ParentSize } from '@visx/responsive';
|
|
4
4
|
import { Group } from '@visx/group';
|
|
@@ -6,21 +6,99 @@ import { Pie } from '@visx/shape';
|
|
|
6
6
|
import { scaleOrdinal } from '@visx/scale';
|
|
7
7
|
import { useTooltipInPortal, useTooltip } from '@visx/tooltip';
|
|
8
8
|
import { localPoint } from '@visx/event';
|
|
9
|
-
import {
|
|
9
|
+
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend';
|
|
10
|
+
import { motion } from 'framer-motion';
|
|
10
11
|
|
|
11
12
|
import { useTheme } from '../../../hooks';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getChartColors,
|
|
15
|
+
getDefaultTooltipStyles,
|
|
16
|
+
InnerChartProps,
|
|
17
|
+
LegendPosition,
|
|
18
|
+
TooltipConfig,
|
|
19
|
+
ChartProps,
|
|
20
|
+
} from '../util';
|
|
21
|
+
import { styled } from '../../../styled';
|
|
22
|
+
import { EmptyChart } from '../EmptyChart';
|
|
13
23
|
|
|
14
|
-
type
|
|
24
|
+
type LabelPosition = 'inside' | 'outside';
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
const ChartWrapper = styled.div<{ $legendPosition: LegendPosition }>`
|
|
27
|
+
display: flex;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
|
|
31
|
+
${({ $legendPosition }) => {
|
|
32
|
+
switch ($legendPosition) {
|
|
33
|
+
case 'top':
|
|
34
|
+
case 'bottom':
|
|
35
|
+
return 'flex-direction: column;';
|
|
36
|
+
case 'left':
|
|
37
|
+
return 'flex-direction: row;';
|
|
38
|
+
case 'right':
|
|
39
|
+
return 'flex-direction: row-reverse;';
|
|
40
|
+
default:
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const LegendContainer = styled.div<{ $position: LegendPosition }>`
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: ${({ $position }) => ($position === 'top' || $position === 'bottom' ? 'row' : 'column')};
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
gap: ${({ theme }) => theme.spacing[1]};
|
|
51
|
+
font-size: ${({ theme }) => theme.fontSize.small};
|
|
52
|
+
|
|
53
|
+
${({ $position }) => {
|
|
54
|
+
switch ($position) {
|
|
55
|
+
case 'top':
|
|
56
|
+
return 'justify-content: center; margin-bottom: 16px;';
|
|
57
|
+
case 'bottom':
|
|
58
|
+
return 'justify-content: center; margin-top: 16px;';
|
|
59
|
+
case 'left':
|
|
60
|
+
return 'margin-right: 16px; justify-content: center;';
|
|
61
|
+
case 'right':
|
|
62
|
+
return 'margin-left: 16px; justify-content: center;';
|
|
63
|
+
default:
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const SvgContainer = styled.div`
|
|
70
|
+
flex: 1;
|
|
71
|
+
min-width: 0;
|
|
72
|
+
min-height: 0;
|
|
73
|
+
display: flex;
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
export interface PieChartProps<T> extends ChartProps {
|
|
18
77
|
data: T[];
|
|
19
|
-
variant: PieChartVariant;
|
|
20
|
-
margin?: Margin;
|
|
21
78
|
xAccessor: (d: T) => string;
|
|
22
79
|
yAccessor: (d: T) => number;
|
|
23
|
-
|
|
80
|
+
/** Tooltip configuration */
|
|
81
|
+
tooltip?: TooltipConfig<T>;
|
|
82
|
+
/** Label accessor - receives data item, percentage, and value. If not provided, shows name if space allows */
|
|
83
|
+
labelAccessor?: (d: T, percentage: number, value: number) => string;
|
|
84
|
+
/** Position labels inside or outside the slices. Default: 'inside' */
|
|
85
|
+
labelPosition?: LabelPosition;
|
|
86
|
+
/** Click handler for pie slices */
|
|
87
|
+
onSliceClick?: (d: T, index: number) => void;
|
|
88
|
+
/** Show legend and its position. Default: 'none' */
|
|
89
|
+
legendPosition?: LegendPosition;
|
|
90
|
+
/** Enable animation on mount and interactions. Default: true */
|
|
91
|
+
animate?: boolean;
|
|
92
|
+
/** Custom colors for slices. If not provided, uses theme colors */
|
|
93
|
+
colors?: string[];
|
|
94
|
+
/** Inner radius as percentage of outer radius (0-1). 0 = pie chart, >0 = donut chart. Example: 0.6 = 60% of outer radius. Default: 0 */
|
|
95
|
+
innerRadius?: number;
|
|
96
|
+
/** Gap between slices in radians. 0 = no gap, 0.02 = small gap, 0.05 = large gap. Default: 0.005 */
|
|
97
|
+
padAngle?: number;
|
|
98
|
+
/** Corner radius in pixels for rounded corners. 0 = sharp corners. Default: 3. Note: With small innerRadius + large padAngle, set to 0 for consistent slice lengths. */
|
|
99
|
+
cornerRadius?: number;
|
|
100
|
+
/** Content to render in the center when innerRadius > 0. Receives total value and data array */
|
|
101
|
+
centerContent?: (total: number, data: T[]) => React.ReactNode;
|
|
24
102
|
}
|
|
25
103
|
|
|
26
104
|
const defaultMargin = { top: 10, right: 0, bottom: 25, left: 40 };
|
|
@@ -28,33 +106,108 @@ export const PieChart = <T,>({
|
|
|
28
106
|
data,
|
|
29
107
|
yAccessor,
|
|
30
108
|
xAccessor,
|
|
31
|
-
|
|
109
|
+
tooltip,
|
|
110
|
+
labelAccessor,
|
|
111
|
+
labelPosition = 'inside',
|
|
32
112
|
name,
|
|
33
|
-
margin
|
|
34
|
-
|
|
113
|
+
margin,
|
|
114
|
+
colors,
|
|
115
|
+
innerRadius = 0,
|
|
116
|
+
padAngle = 0.005,
|
|
117
|
+
cornerRadius = 3,
|
|
118
|
+
onSliceClick,
|
|
119
|
+
legendPosition = 'none',
|
|
120
|
+
animate = true,
|
|
121
|
+
centerContent,
|
|
35
122
|
}: PieChartProps<T>) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
123
|
+
const theme = useTheme();
|
|
124
|
+
const chartColors = colors || getChartColors(theme);
|
|
125
|
+
|
|
126
|
+
const adjustedMargin =
|
|
127
|
+
margin || (labelPosition === 'outside' ? { top: 40, right: 40, bottom: 40, left: 40 } : defaultMargin);
|
|
128
|
+
const total = useMemo(() => data.reduce((sum, d) => sum + yAccessor(d), 0), [data, yAccessor]);
|
|
129
|
+
const legendGlyphSize = 15;
|
|
130
|
+
|
|
131
|
+
const ordinalColorScale = useMemo(
|
|
132
|
+
() =>
|
|
133
|
+
scaleOrdinal({
|
|
134
|
+
domain: data.map(xAccessor),
|
|
135
|
+
range: chartColors,
|
|
136
|
+
}),
|
|
137
|
+
[data, xAccessor, chartColors],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const renderLegend = () => (
|
|
141
|
+
<LegendContainer $position={legendPosition}>
|
|
142
|
+
<LegendOrdinal scale={ordinalColorScale}>
|
|
143
|
+
{(labels) => (
|
|
144
|
+
<div
|
|
145
|
+
style={{
|
|
146
|
+
display: 'flex',
|
|
147
|
+
flexDirection: legendPosition === 'top' || legendPosition === 'bottom' ? 'row' : 'column',
|
|
148
|
+
flexWrap: 'wrap',
|
|
149
|
+
gap: '8px',
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{labels.map((label, i) => (
|
|
153
|
+
<LegendItem key={`legend-item-${i}`} margin="0">
|
|
154
|
+
<svg width={legendGlyphSize} height={legendGlyphSize} style={{ marginRight: '4px' }}>
|
|
155
|
+
<rect fill={label.value} width={legendGlyphSize} height={legendGlyphSize} rx={2} />
|
|
156
|
+
</svg>
|
|
157
|
+
<LegendLabel align="left" margin="0">
|
|
158
|
+
{label.text}
|
|
159
|
+
</LegendLabel>
|
|
160
|
+
</LegendItem>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
51
163
|
)}
|
|
52
|
-
</
|
|
53
|
-
|
|
164
|
+
</LegendOrdinal>
|
|
165
|
+
</LegendContainer>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<ChartWrapper $legendPosition={legendPosition}>
|
|
170
|
+
{legendPosition !== 'none' && (legendPosition === 'top' || legendPosition === 'left') && renderLegend()}
|
|
171
|
+
|
|
172
|
+
<SvgContainer>
|
|
173
|
+
<ParentSize>
|
|
174
|
+
{total > 0
|
|
175
|
+
? (parent) => (
|
|
176
|
+
<Chart<T>
|
|
177
|
+
name={name}
|
|
178
|
+
data={data}
|
|
179
|
+
width={parent.width}
|
|
180
|
+
height={parent.height}
|
|
181
|
+
margin={adjustedMargin}
|
|
182
|
+
yAccessor={yAccessor}
|
|
183
|
+
xAccessor={xAccessor}
|
|
184
|
+
labelAccessor={labelAccessor}
|
|
185
|
+
labelPosition={labelPosition}
|
|
186
|
+
ordinalColorScale={ordinalColorScale}
|
|
187
|
+
total={total}
|
|
188
|
+
onSliceClick={onSliceClick}
|
|
189
|
+
animate={animate}
|
|
190
|
+
innerRadius={innerRadius}
|
|
191
|
+
padAngle={padAngle}
|
|
192
|
+
cornerRadius={cornerRadius}
|
|
193
|
+
centerContent={centerContent}
|
|
194
|
+
tooltip={tooltip}
|
|
195
|
+
/>
|
|
196
|
+
)
|
|
197
|
+
: () => <EmptyChart />}
|
|
198
|
+
</ParentSize>
|
|
199
|
+
</SvgContainer>
|
|
200
|
+
|
|
201
|
+
{legendPosition !== 'none' && (legendPosition === 'bottom' || legendPosition === 'right') && renderLegend()}
|
|
202
|
+
</ChartWrapper>
|
|
54
203
|
);
|
|
55
204
|
};
|
|
56
205
|
|
|
57
|
-
type InnerPieChartProps<T> = InnerChartProps &
|
|
206
|
+
type InnerPieChartProps<T> = InnerChartProps &
|
|
207
|
+
Omit<PieChartProps<T>, 'colors' | 'legendPosition'> & {
|
|
208
|
+
ordinalColorScale: ReturnType<typeof scaleOrdinal<string, string>>;
|
|
209
|
+
total: number;
|
|
210
|
+
};
|
|
58
211
|
|
|
59
212
|
const Chart = <T,>({
|
|
60
213
|
width,
|
|
@@ -63,9 +216,18 @@ const Chart = <T,>({
|
|
|
63
216
|
data,
|
|
64
217
|
name,
|
|
65
218
|
height,
|
|
66
|
-
|
|
67
|
-
|
|
219
|
+
labelAccessor,
|
|
220
|
+
labelPosition,
|
|
221
|
+
tooltip,
|
|
68
222
|
margin = defaultMargin,
|
|
223
|
+
ordinalColorScale,
|
|
224
|
+
total,
|
|
225
|
+
onSliceClick,
|
|
226
|
+
animate,
|
|
227
|
+
innerRadius = 0,
|
|
228
|
+
padAngle = 0.005,
|
|
229
|
+
cornerRadius = 3,
|
|
230
|
+
centerContent,
|
|
69
231
|
}: InnerPieChartProps<T>) => {
|
|
70
232
|
const theme = useTheme();
|
|
71
233
|
const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, hideTooltip, showTooltip } = useTooltip<string>();
|
|
@@ -74,31 +236,59 @@ const Chart = <T,>({
|
|
|
74
236
|
scroll: true,
|
|
75
237
|
});
|
|
76
238
|
|
|
239
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
240
|
+
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
241
|
+
|
|
77
242
|
const innerWidth = width - margin.left - margin.right;
|
|
78
243
|
const innerHeight = height - margin.top - margin.bottom;
|
|
79
244
|
const radius = Math.min(innerWidth, innerHeight) / 2;
|
|
80
245
|
const centerX = innerWidth / 2;
|
|
81
246
|
const centerY = innerHeight / 2;
|
|
82
|
-
const donutThickness = 50;
|
|
83
247
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
248
|
+
// Calculate inner radius as percentage of outer radius
|
|
249
|
+
const calculatedInnerRadius = radius * innerRadius;
|
|
250
|
+
|
|
251
|
+
// When innerRadius is 0, force cornerRadius to 0 to prevent center artifact
|
|
252
|
+
// (you can't geometrically round a point where all slices converge)
|
|
253
|
+
const actualCornerRadius = innerRadius === 0 ? 0 : cornerRadius;
|
|
254
|
+
|
|
255
|
+
// When innerRadius is 0, remove stroke to prevent white rectangle artifact at center
|
|
256
|
+
// (overlapping stroke borders create a visible square where slices meet)
|
|
257
|
+
const actualStrokeWidth = innerRadius === 0 ? 0 : 2;
|
|
258
|
+
|
|
88
259
|
const pieSortValues = (a: number, b: number) => b - a;
|
|
89
260
|
|
|
90
261
|
const handleMouseOver = useCallback(
|
|
91
|
-
(event: MouseEvent,
|
|
262
|
+
(event: MouseEvent, datum: T, index: number, percentage: number, value: number) => {
|
|
92
263
|
const target = event.target as SVGElement;
|
|
93
264
|
const coords = localPoint(target.ownerSVGElement!, event);
|
|
94
265
|
|
|
266
|
+
setHoveredIndex(index);
|
|
267
|
+
|
|
268
|
+
const defaultTooltip = `${xAccessor(datum)}: ${value.toFixed(2)} (${percentage.toFixed(1)}%)`;
|
|
269
|
+
|
|
95
270
|
showTooltip({
|
|
96
271
|
tooltipLeft: coords?.x,
|
|
97
272
|
tooltipTop: coords?.y,
|
|
98
|
-
tooltipData:
|
|
273
|
+
tooltipData: tooltip?.accessor ? tooltip.accessor(datum) : defaultTooltip,
|
|
99
274
|
});
|
|
100
275
|
},
|
|
101
|
-
[
|
|
276
|
+
[tooltip?.accessor, xAccessor, showTooltip],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const handleMouseLeave = useCallback(() => {
|
|
280
|
+
setHoveredIndex(null);
|
|
281
|
+
hideTooltip();
|
|
282
|
+
}, [hideTooltip]);
|
|
283
|
+
|
|
284
|
+
const handleClick = useCallback(
|
|
285
|
+
(datum: T, index: number) => {
|
|
286
|
+
setSelectedIndex(index === selectedIndex ? null : index);
|
|
287
|
+
if (onSliceClick) {
|
|
288
|
+
onSliceClick(datum, index);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
[onSliceClick, selectedIndex],
|
|
102
292
|
);
|
|
103
293
|
|
|
104
294
|
return width < 10 ? null : (
|
|
@@ -107,9 +297,9 @@ const Chart = <T,>({
|
|
|
107
297
|
<Pie
|
|
108
298
|
data={data}
|
|
109
299
|
outerRadius={radius}
|
|
110
|
-
innerRadius={
|
|
111
|
-
cornerRadius={
|
|
112
|
-
padAngle={
|
|
300
|
+
innerRadius={calculatedInnerRadius}
|
|
301
|
+
cornerRadius={actualCornerRadius}
|
|
302
|
+
padAngle={padAngle}
|
|
113
303
|
pieSortValues={pieSortValues}
|
|
114
304
|
pieValue={yAccessor}
|
|
115
305
|
>
|
|
@@ -118,35 +308,139 @@ const Chart = <T,>({
|
|
|
118
308
|
const [centroidX, centroidY] = pie.path.centroid(arc);
|
|
119
309
|
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1;
|
|
120
310
|
const arcPath = pie.path(arc) ?? '';
|
|
121
|
-
const arcFill =
|
|
311
|
+
const arcFill = ordinalColorScale(xAccessor(arc.data));
|
|
312
|
+
const value = yAccessor(arc.data);
|
|
313
|
+
const percentage = (value / total) * 100;
|
|
314
|
+
|
|
315
|
+
const isHovered = hoveredIndex === index;
|
|
316
|
+
const isSelected = selectedIndex === index;
|
|
317
|
+
|
|
318
|
+
// Calculate expanded position for hover/select effect (only if animate is enabled)
|
|
319
|
+
const angle = (arc.startAngle + arc.endAngle) / 2;
|
|
320
|
+
const expandDistance = animate && (isHovered || isSelected) ? 10 : 0;
|
|
321
|
+
const translateX = Math.cos(angle - Math.PI / 2) * expandDistance;
|
|
322
|
+
const translateY = Math.sin(angle - Math.PI / 2) * expandDistance;
|
|
323
|
+
|
|
324
|
+
// Calculate label position
|
|
325
|
+
let labelX = centroidX;
|
|
326
|
+
let labelY = centroidY;
|
|
327
|
+
|
|
328
|
+
if (labelPosition === 'outside') {
|
|
329
|
+
const labelRadius = radius + 20;
|
|
330
|
+
labelX = Math.cos(angle - Math.PI / 2) * labelRadius;
|
|
331
|
+
labelY = Math.sin(angle - Math.PI / 2) * labelRadius;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const labelText = labelAccessor
|
|
335
|
+
? labelAccessor(arc.data, percentage, value)
|
|
336
|
+
: hasSpaceForLabel
|
|
337
|
+
? xAccessor(arc.data)
|
|
338
|
+
: '';
|
|
339
|
+
|
|
122
340
|
return (
|
|
123
|
-
<g
|
|
124
|
-
|
|
341
|
+
<g
|
|
342
|
+
key={`arc-${xAccessor(arc.data)}-${index}`}
|
|
343
|
+
style={{
|
|
344
|
+
transform: `translate(${translateX}px, ${translateY}px)`,
|
|
345
|
+
transition: animate ? 'transform 0.2s ease-out' : 'none',
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<motion.path
|
|
125
349
|
d={arcPath}
|
|
126
|
-
fill={
|
|
127
|
-
stroke={
|
|
128
|
-
strokeWidth={
|
|
129
|
-
|
|
130
|
-
|
|
350
|
+
fill={arcFill}
|
|
351
|
+
stroke={theme.colors.background}
|
|
352
|
+
strokeWidth={actualStrokeWidth}
|
|
353
|
+
onMouseOver={(e) => handleMouseOver(e, arc.data, index, percentage, value)}
|
|
354
|
+
onMouseLeave={handleMouseLeave}
|
|
355
|
+
onClick={() => handleClick(arc.data, index)}
|
|
356
|
+
style={{
|
|
357
|
+
cursor: onSliceClick ? 'pointer' : 'default',
|
|
358
|
+
transition: animate ? 'opacity 0.2s ease-out' : 'none',
|
|
359
|
+
filter: animate && (isHovered || isSelected) ? 'brightness(1.1)' : 'none',
|
|
360
|
+
}}
|
|
361
|
+
initial={animate ? { opacity: 0 } : { opacity: 0.9 }}
|
|
362
|
+
animate={{
|
|
363
|
+
opacity: isHovered || isSelected ? 1 : hoveredIndex !== null ? 0.6 : 0.9,
|
|
364
|
+
}}
|
|
365
|
+
transition={
|
|
366
|
+
animate
|
|
367
|
+
? {
|
|
368
|
+
opacity: {
|
|
369
|
+
duration: 0.3,
|
|
370
|
+
delay: (arc.startAngle / (Math.PI * 2)) * 0.6,
|
|
371
|
+
ease: 'linear',
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
: { duration: 0 }
|
|
375
|
+
}
|
|
131
376
|
/>
|
|
132
|
-
{
|
|
133
|
-
<text
|
|
134
|
-
x={
|
|
135
|
-
y={
|
|
377
|
+
{labelText && (
|
|
378
|
+
<motion.text
|
|
379
|
+
x={labelX}
|
|
380
|
+
y={labelY}
|
|
136
381
|
dy=".33em"
|
|
137
|
-
fill=
|
|
138
|
-
fontSize={
|
|
382
|
+
fill={labelPosition === 'outside' ? theme.colors.text : 'white'}
|
|
383
|
+
fontSize={labelPosition === 'outside' ? 12 : 11}
|
|
384
|
+
fontWeight={animate && (isHovered || isSelected) ? 600 : 500}
|
|
139
385
|
textAnchor="middle"
|
|
140
386
|
pointerEvents="none"
|
|
387
|
+
style={{
|
|
388
|
+
transition: animate ? 'font-weight 0.2s ease-out' : 'none',
|
|
389
|
+
}}
|
|
390
|
+
initial={animate ? { opacity: 0 } : { opacity: 1 }}
|
|
391
|
+
animate={{ opacity: 1 }}
|
|
392
|
+
transition={
|
|
393
|
+
animate
|
|
394
|
+
? {
|
|
395
|
+
duration: 0.3,
|
|
396
|
+
delay: 1.0,
|
|
397
|
+
ease: 'easeOut',
|
|
398
|
+
}
|
|
399
|
+
: { duration: 0 }
|
|
400
|
+
}
|
|
141
401
|
>
|
|
142
|
-
{
|
|
143
|
-
</text>
|
|
402
|
+
{labelText}
|
|
403
|
+
</motion.text>
|
|
144
404
|
)}
|
|
145
405
|
</g>
|
|
146
406
|
);
|
|
147
407
|
});
|
|
148
408
|
}}
|
|
149
409
|
</Pie>
|
|
410
|
+
{innerRadius > 0 && centerContent && (
|
|
411
|
+
<foreignObject
|
|
412
|
+
x={-calculatedInnerRadius}
|
|
413
|
+
y={-calculatedInnerRadius}
|
|
414
|
+
width={calculatedInnerRadius * 2}
|
|
415
|
+
height={calculatedInnerRadius * 2}
|
|
416
|
+
style={{ pointerEvents: 'none' }}
|
|
417
|
+
>
|
|
418
|
+
<motion.div
|
|
419
|
+
style={{
|
|
420
|
+
width: '100%',
|
|
421
|
+
height: '100%',
|
|
422
|
+
display: 'flex',
|
|
423
|
+
alignItems: 'center',
|
|
424
|
+
justifyContent: 'center',
|
|
425
|
+
textAlign: 'center',
|
|
426
|
+
color: theme.colors.text,
|
|
427
|
+
}}
|
|
428
|
+
initial={animate ? { opacity: 0, scale: 0.8 } : { opacity: 1, scale: 1 }}
|
|
429
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
430
|
+
transition={
|
|
431
|
+
animate
|
|
432
|
+
? {
|
|
433
|
+
duration: 0.4,
|
|
434
|
+
delay: 1.1,
|
|
435
|
+
ease: 'easeOut',
|
|
436
|
+
}
|
|
437
|
+
: { duration: 0 }
|
|
438
|
+
}
|
|
439
|
+
>
|
|
440
|
+
{centerContent(total, data)}
|
|
441
|
+
</motion.div>
|
|
442
|
+
</foreignObject>
|
|
443
|
+
)}
|
|
150
444
|
</Group>
|
|
151
445
|
{tooltipOpen && tooltipData && (
|
|
152
446
|
<TooltipInPortal
|
|
@@ -17,8 +17,8 @@ export const PointHighlight: FC<PointHighlightProps> = ({ margin, yMax, tooltipL
|
|
|
17
17
|
return (
|
|
18
18
|
<Group left={margin.left} top={margin.top}>
|
|
19
19
|
<Line
|
|
20
|
-
from={{ x: tooltipLeft, y:
|
|
21
|
-
to={{ x: tooltipLeft, y: yMax
|
|
20
|
+
from={{ x: tooltipLeft, y: 0 }}
|
|
21
|
+
to={{ x: tooltipLeft, y: yMax }}
|
|
22
22
|
stroke={theme.colors.backgroundAccent}
|
|
23
23
|
strokeWidth={1}
|
|
24
24
|
pointerEvents="none"
|
|
@@ -7,14 +7,19 @@ import letterFrequency, { LetterFrequency } from '@visx/mock-data/lib/mocks/lett
|
|
|
7
7
|
export default {
|
|
8
8
|
title: 'Charts/RadarChart',
|
|
9
9
|
component: RadarChart,
|
|
10
|
+
args: {
|
|
11
|
+
levels: 5,
|
|
12
|
+
items: 6,
|
|
13
|
+
animate: true,
|
|
14
|
+
},
|
|
10
15
|
} as Meta<RadarChartProps<LetterFrequency>>;
|
|
11
16
|
|
|
12
17
|
const Wrapper = styled.div`
|
|
13
|
-
height:
|
|
14
|
-
width:
|
|
18
|
+
height: 400px;
|
|
19
|
+
width: 400px;
|
|
15
20
|
`;
|
|
16
21
|
|
|
17
|
-
export const Default: StoryFn<RadarChartProps<LetterFrequency>> = () => {
|
|
22
|
+
export const Default: StoryFn<RadarChartProps<LetterFrequency>> = (args) => {
|
|
18
23
|
const getLetter = (d: LetterFrequency) => d.letter;
|
|
19
24
|
const getLetterFrequency = (d: LetterFrequency) => Number(d.frequency);
|
|
20
25
|
const tooltipAccessor = (d: LetterFrequency) => {
|
|
@@ -22,7 +27,7 @@ export const Default: StoryFn<RadarChartProps<LetterFrequency>> = () => {
|
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
// only show the first few letters
|
|
25
|
-
const data = letterFrequency.slice(
|
|
30
|
+
const data = letterFrequency.slice(0, (args as any).items);
|
|
26
31
|
|
|
27
32
|
return (
|
|
28
33
|
<Wrapper>
|
|
@@ -30,8 +35,12 @@ export const Default: StoryFn<RadarChartProps<LetterFrequency>> = () => {
|
|
|
30
35
|
name="letterFrequency"
|
|
31
36
|
yAccessor={getLetterFrequency}
|
|
32
37
|
xAccessor={getLetter}
|
|
33
|
-
|
|
38
|
+
tooltip={{
|
|
39
|
+
accessor: tooltipAccessor,
|
|
40
|
+
}}
|
|
34
41
|
data={data}
|
|
42
|
+
levels={args.levels}
|
|
43
|
+
animate={args.animate}
|
|
35
44
|
/>
|
|
36
45
|
</Wrapper>
|
|
37
46
|
);
|