@takaro/lib-components 0.4.8 → 0.4.10

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.
Files changed (57) hide show
  1. package/package.json +1 -1
  2. package/src/components/actions/Button/__snapshots__/Button.test.tsx.snap +1 -1
  3. package/src/components/actions/IconButton/__snapshots__/IconButton.test.tsx.snap +1 -1
  4. package/src/components/charts/AreaChart/AreaChart.stories.tsx +11 -7
  5. package/src/components/charts/AreaChart/index.tsx +114 -63
  6. package/src/components/charts/BarChart/BarChart.stories.tsx +33 -10
  7. package/src/components/charts/BarChart/index.tsx +280 -147
  8. package/src/components/charts/EmptyChart.tsx +45 -0
  9. package/src/components/charts/GeoMercator/GeoMercator.stories.tsx +15 -9
  10. package/src/components/charts/GeoMercator/index.tsx +15 -172
  11. package/src/components/charts/Heatmap/Heatmap.stories.tsx +167 -33
  12. package/src/components/charts/Heatmap/index.tsx +427 -193
  13. package/src/components/charts/LineChart/LineChart.stories.tsx +77 -3
  14. package/src/components/charts/LineChart/index.tsx +200 -79
  15. package/src/components/charts/PieChart/PieChart.stories.tsx +128 -20
  16. package/src/components/charts/PieChart/index.tsx +353 -59
  17. package/src/components/charts/PointHighlight.tsx +2 -2
  18. package/src/components/charts/RadarChart/RadarChart.stories.tsx +14 -5
  19. package/src/components/charts/RadarChart/index.tsx +94 -45
  20. package/src/components/charts/RadialBarChart/RadialBarChart.stories.tsx +26 -1
  21. package/src/components/charts/RadialBarChart/index.tsx +100 -34
  22. package/src/components/charts/RadialLineChart/RadialLineChart.stories.tsx +19 -2
  23. package/src/components/charts/RadialLineChart/index.tsx +116 -26
  24. package/src/components/charts/index.tsx +0 -26
  25. package/src/components/charts/util.ts +50 -12
  26. package/src/components/data/CountryList/index.tsx +146 -0
  27. package/src/components/data/Stats/Sparkline.tsx +48 -0
  28. package/src/components/data/Stats/Stat.tsx +15 -4
  29. package/src/components/data/Stats/context.tsx +1 -1
  30. package/src/components/data/Stats/index.tsx +8 -3
  31. package/src/components/data/index.ts +3 -0
  32. package/src/components/feedback/IconTooltip/index.tsx +9 -6
  33. package/src/components/feedback/ProgressBar/ProgressBar.stories.tsx +13 -14
  34. package/src/components/feedback/ProgressBar/index.tsx +1 -1
  35. package/src/components/inputs/DurationField/__tests__/Generic.test.tsx +12 -0
  36. package/src/components/visual/Card/CardTitle.tsx +7 -1
  37. package/src/components/visual/Card/index.tsx +0 -4
  38. package/src/helpers/formatNumber.ts +6 -0
  39. package/src/helpers/index.ts +1 -0
  40. package/src/components/charts/echarts/EChartsArea.stories.tsx +0 -139
  41. package/src/components/charts/echarts/EChartsArea.tsx +0 -139
  42. package/src/components/charts/echarts/EChartsBar.stories.tsx +0 -141
  43. package/src/components/charts/echarts/EChartsBar.tsx +0 -133
  44. package/src/components/charts/echarts/EChartsBase.tsx +0 -264
  45. package/src/components/charts/echarts/EChartsFunnel.stories.tsx +0 -164
  46. package/src/components/charts/echarts/EChartsFunnel.tsx +0 -114
  47. package/src/components/charts/echarts/EChartsHeatmap.stories.tsx +0 -168
  48. package/src/components/charts/echarts/EChartsHeatmap.tsx +0 -141
  49. package/src/components/charts/echarts/EChartsLine.stories.tsx +0 -132
  50. package/src/components/charts/echarts/EChartsLine.tsx +0 -111
  51. package/src/components/charts/echarts/EChartsPie.stories.tsx +0 -131
  52. package/src/components/charts/echarts/EChartsPie.tsx +0 -124
  53. package/src/components/charts/echarts/EChartsRadialBar.stories.tsx +0 -124
  54. package/src/components/charts/echarts/EChartsRadialBar.tsx +0 -118
  55. package/src/components/charts/echarts/EChartsScatter.stories.tsx +0 -166
  56. package/src/components/charts/echarts/EChartsScatter.tsx +0 -135
  57. 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
- curveType={args.curveType}
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 { Margin, ChartProps, InnerChartProps, getDefaultTooltipStyles } from '../util';
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
- axisYLabel,
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
- if (!data || data.length === 0) return null;
56
+ const hasData = data && data.length > 0;
48
57
 
49
58
  return (
50
59
  <ParentSize>
51
- {(parent) => (
52
- <Chart<T>
53
- name={name}
54
- xAccessor={xAccessor}
55
- yAccessor={yAccessor}
56
- tooltipAccessor={tooltipAccessor}
57
- data={data}
58
- width={parent.width}
59
- height={parent.height}
60
- showGrid={showGrid}
61
- margin={margin}
62
- axisYLabel={axisYLabel}
63
- axisXLabel={axisXLabel}
64
- showAxisX={showAxisX}
65
- showAxisY={showAxisY}
66
- curveType={curveType}
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
- tooltipAccessor,
85
- showAxisX,
86
- showAxisY,
87
- axisYLabel,
88
- axisXLabel,
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 bottomAxisHeight = showAxisX ? 25 : 0;
93
- const leftAxisWidth = showAxisX ? 25 : 0;
94
- const innerWidth = width - margin.left - margin.right - leftAxisWidth;
95
- const innerHeight = height - margin.top - margin.bottom - bottomAxisHeight;
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
- const { hideTooltip, showTooltip, tooltipData, tooltipLeft = 0, tooltipTop = 0 } = useTooltip<T>();
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: [margin.left, innerWidth],
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, margin.bottom],
112
- domain: [0, max(data, yAccessor) as number],
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
- const x0 = xScale.invert(x - margin.left);
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
- showTooltip({
131
- tooltipData: d,
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, yAccessor, xAccessor, width, height],
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={margin.left}
146
- width={innerWidth - margin.left - margin.right}
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 + 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 - margin.top}
211
- left={tooltipLeft + margin.left}
307
+ top={tooltipTop}
308
+ left={tooltipLeft + margin.left + 10}
212
309
  style={getDefaultTooltipStyles(theme)}
213
310
  >
214
- {tooltipAccessor ? tooltipAccessor(tooltipData) : yAccessor(tooltipData)}
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
- variant: 'pie',
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: 50vh;
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 getLetter = (d: LetterFrequency) => d.letter;
22
- const getLetterFrequency = (d: LetterFrequency) => Number(d.frequency) * 100;
39
+ const [selectedLetter, setSelectedLetter] = useState<string | null>(null);
23
40
 
24
- const tooltipAccessor = (d: LetterFrequency) => {
25
- return `Tooltip content for '${getLetter(d)}' with frequency ${getLetterFrequency(d)}`;
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
- // only show the first 5 letters
29
- letterFrequency.length = 6;
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
- <Wrapper>
33
- <PieChart<LetterFrequency>
34
- name="letterFrequency"
35
- xAccessor={getLetter}
36
- yAccessor={getLetterFrequency}
37
- tooltipAccessor={tooltipAccessor}
38
- data={letterFrequency}
39
- variant={args.variant}
40
- />
41
- </Wrapper>
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
  };