@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,3 +1,5 @@
|
|
|
1
|
+
import { MouseEvent, useCallback } from 'react';
|
|
2
|
+
|
|
1
3
|
import { ParentSize } from '@visx/responsive';
|
|
2
4
|
import { Group } from '@visx/group';
|
|
3
5
|
import { LineRadial } from '@visx/shape';
|
|
@@ -7,19 +9,22 @@ import { AxisLeft } from '@visx/axis';
|
|
|
7
9
|
import { motion } from 'framer-motion';
|
|
8
10
|
import { extent } from '@visx/vendor/d3-array';
|
|
9
11
|
import { curveBasisOpen } from '@visx/curve';
|
|
12
|
+
import { useTooltipInPortal, useTooltip } from '@visx/tooltip';
|
|
13
|
+
import { localPoint } from '@visx/event';
|
|
10
14
|
|
|
11
|
-
import { InnerChartProps,
|
|
15
|
+
import { InnerChartProps, getDefaultTooltipStyles, TooltipConfig, ChartProps } from '../util';
|
|
12
16
|
import { useTheme } from '../../../hooks';
|
|
13
17
|
import { useGradients } from '../useGradients';
|
|
18
|
+
import { EmptyChart } from '../EmptyChart';
|
|
14
19
|
|
|
15
20
|
const formatTicks = (val: NumberLike) => String(val);
|
|
16
21
|
|
|
17
|
-
export interface RadialLineChartProps<T> {
|
|
18
|
-
name: string;
|
|
22
|
+
export interface RadialLineChartProps<T> extends ChartProps {
|
|
19
23
|
data: T[];
|
|
20
|
-
margin?: Margin;
|
|
21
24
|
xAccessor: (d: T) => number;
|
|
22
25
|
yAccessor: (d: T) => number;
|
|
26
|
+
/** Tooltip configuration */
|
|
27
|
+
tooltip?: TooltipConfig<T>;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const defaultMargin = { top: 10, right: 0, bottom: 25, left: 40 };
|
|
@@ -29,21 +34,29 @@ export const RadialLineChart = <T,>({
|
|
|
29
34
|
xAccessor,
|
|
30
35
|
name,
|
|
31
36
|
margin = defaultMargin,
|
|
37
|
+
animate = true,
|
|
38
|
+
tooltip,
|
|
32
39
|
}: RadialLineChartProps<T>) => {
|
|
40
|
+
const hasData = data && data.length > 0;
|
|
41
|
+
|
|
33
42
|
return (
|
|
34
43
|
<>
|
|
35
44
|
<ParentSize>
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
{hasData
|
|
46
|
+
? (parent) => (
|
|
47
|
+
<Chart<T>
|
|
48
|
+
name={name}
|
|
49
|
+
data={data}
|
|
50
|
+
width={parent.width}
|
|
51
|
+
height={parent.height}
|
|
52
|
+
margin={margin}
|
|
53
|
+
yAccessor={yAccessor}
|
|
54
|
+
xAccessor={xAccessor}
|
|
55
|
+
tooltip={tooltip}
|
|
56
|
+
animate={animate}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
: () => <EmptyChart />}
|
|
47
60
|
</ParentSize>
|
|
48
61
|
</>
|
|
49
62
|
);
|
|
@@ -51,10 +64,26 @@ export const RadialLineChart = <T,>({
|
|
|
51
64
|
|
|
52
65
|
type InnerRadialLineChartProps<T> = InnerChartProps & RadialLineChartProps<T>;
|
|
53
66
|
|
|
54
|
-
const Chart = <T,>({
|
|
67
|
+
const Chart = <T,>({
|
|
68
|
+
width,
|
|
69
|
+
xAccessor,
|
|
70
|
+
yAccessor,
|
|
71
|
+
data,
|
|
72
|
+
name,
|
|
73
|
+
height,
|
|
74
|
+
tooltip,
|
|
75
|
+
animate = true,
|
|
76
|
+
}: InnerRadialLineChartProps<T>) => {
|
|
77
|
+
const tooltipAccessor = tooltip?.accessor;
|
|
55
78
|
const theme = useTheme();
|
|
56
79
|
const gradients = useGradients(name);
|
|
57
80
|
|
|
81
|
+
const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, hideTooltip, showTooltip } = useTooltip<T>();
|
|
82
|
+
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
|
83
|
+
detectBounds: true,
|
|
84
|
+
scroll: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
58
87
|
const xScale = scaleTime<number>({
|
|
59
88
|
range: [0, Math.PI * 2],
|
|
60
89
|
domain: extent(data, xAccessor) as [number, number],
|
|
@@ -64,19 +93,32 @@ const Chart = <T,>({ width, xAccessor, yAccessor, data, name, height }: InnerRad
|
|
|
64
93
|
domain: extent(data, yAccessor) as [number, number],
|
|
65
94
|
});
|
|
66
95
|
|
|
67
|
-
const firstPoint = yAccessor(data[0]);
|
|
68
|
-
const lastPoint = yAccessor(data[data.length - 1]);
|
|
69
|
-
|
|
70
96
|
const angle = (d: T) => xScale(xAccessor(d)) ?? 0;
|
|
71
97
|
const radius = (d: T) => yScale(yAccessor(d)) ?? 0;
|
|
72
98
|
const padding = 15;
|
|
73
99
|
|
|
100
|
+
const handleMouseOver = useCallback(
|
|
101
|
+
(event: MouseEvent, dataPoint: T) => {
|
|
102
|
+
if (!tooltipAccessor) return;
|
|
103
|
+
|
|
104
|
+
const target = event.target as SVGElement;
|
|
105
|
+
const coords = localPoint(target.ownerSVGElement!, event);
|
|
106
|
+
|
|
107
|
+
showTooltip({
|
|
108
|
+
tooltipLeft: coords?.x,
|
|
109
|
+
tooltipTop: coords?.y,
|
|
110
|
+
tooltipData: dataPoint,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
[tooltipAccessor, showTooltip],
|
|
114
|
+
);
|
|
115
|
+
|
|
74
116
|
// Update scale output to match component dimensions
|
|
75
117
|
yScale.range([0, height / 2 - padding]);
|
|
76
118
|
const reverseYScale = yScale.copy().range(yScale.range().reverse());
|
|
77
119
|
|
|
78
120
|
return width < 10 ? null : (
|
|
79
|
-
<svg width={width} height={height}>
|
|
121
|
+
<svg ref={containerRef} width={width} height={height}>
|
|
80
122
|
{gradients.chart.gradient}
|
|
81
123
|
<rect width={width} height={height} fill={theme.colors.background} rx={14} />
|
|
82
124
|
<Group top={height / 2} left={width / 2}>
|
|
@@ -121,21 +163,69 @@ const Chart = <T,>({ width, xAccessor, yAccessor, data, name, height }: InnerRad
|
|
|
121
163
|
return (
|
|
122
164
|
<motion.path
|
|
123
165
|
d={d}
|
|
124
|
-
strokeWidth={
|
|
166
|
+
strokeWidth={3}
|
|
125
167
|
strokeOpacity={0.8}
|
|
126
168
|
strokeLinecap="round"
|
|
127
169
|
fill="none"
|
|
128
170
|
stroke={theme.colors.primary}
|
|
171
|
+
initial={animate ? { pathLength: 0 } : { pathLength: 1 }}
|
|
172
|
+
animate={{ pathLength: 1 }}
|
|
173
|
+
transition={
|
|
174
|
+
animate
|
|
175
|
+
? {
|
|
176
|
+
duration: 1.5,
|
|
177
|
+
ease: 'easeInOut',
|
|
178
|
+
}
|
|
179
|
+
: { duration: 0 }
|
|
180
|
+
}
|
|
129
181
|
/>
|
|
130
182
|
);
|
|
131
183
|
}}
|
|
132
184
|
</LineRadial>
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
185
|
+
{tooltipAccessor &&
|
|
186
|
+
data.map((d, i) => {
|
|
187
|
+
const angleVal = angle(d);
|
|
188
|
+
const radiusVal = radius(d);
|
|
189
|
+
const x = Math.cos(angleVal - Math.PI / 2) * radiusVal;
|
|
190
|
+
const y = Math.sin(angleVal - Math.PI / 2) * radiusVal;
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<g key={`data-point-${i}`}>
|
|
194
|
+
{/* Invisible hit area for tooltip - no need for visible dots */}
|
|
195
|
+
<motion.circle
|
|
196
|
+
cx={x}
|
|
197
|
+
cy={y}
|
|
198
|
+
r={12}
|
|
199
|
+
fill="transparent"
|
|
200
|
+
style={{ cursor: 'pointer' }}
|
|
201
|
+
onMouseOut={hideTooltip}
|
|
202
|
+
onMouseOver={(e) => handleMouseOver(e, d)}
|
|
203
|
+
initial={animate ? { scale: 0 } : { scale: 1 }}
|
|
204
|
+
animate={{ scale: 1 }}
|
|
205
|
+
transition={
|
|
206
|
+
animate
|
|
207
|
+
? {
|
|
208
|
+
duration: 0.3,
|
|
209
|
+
delay: 1.5,
|
|
210
|
+
ease: 'easeOut',
|
|
211
|
+
}
|
|
212
|
+
: { duration: 0 }
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
</g>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
138
218
|
</Group>
|
|
219
|
+
{tooltipOpen && tooltipData && tooltipAccessor && (
|
|
220
|
+
<TooltipInPortal
|
|
221
|
+
key={`tooltip-${name}`}
|
|
222
|
+
top={tooltipTop}
|
|
223
|
+
left={tooltipLeft}
|
|
224
|
+
style={getDefaultTooltipStyles(theme)}
|
|
225
|
+
>
|
|
226
|
+
{tooltipAccessor(tooltipData)}
|
|
227
|
+
</TooltipInPortal>
|
|
228
|
+
)}
|
|
139
229
|
</svg>
|
|
140
230
|
);
|
|
141
231
|
};
|
|
@@ -24,29 +24,3 @@ export type { RadialBarChartProps } from './RadialBarChart';
|
|
|
24
24
|
|
|
25
25
|
export { GeoMercator } from './GeoMercator';
|
|
26
26
|
export type { GeoMercatorProps } from './GeoMercator';
|
|
27
|
-
|
|
28
|
-
// ECharts components
|
|
29
|
-
export {
|
|
30
|
-
EChartsBase,
|
|
31
|
-
ResponsiveECharts,
|
|
32
|
-
EChartsLine,
|
|
33
|
-
EChartsBar,
|
|
34
|
-
EChartsPie,
|
|
35
|
-
EChartsArea,
|
|
36
|
-
EChartsHeatmap,
|
|
37
|
-
EChartsRadialBar,
|
|
38
|
-
EChartsScatter,
|
|
39
|
-
EChartsFunnel,
|
|
40
|
-
} from './echarts';
|
|
41
|
-
|
|
42
|
-
export type {
|
|
43
|
-
EChartsBaseProps,
|
|
44
|
-
EChartsLineProps,
|
|
45
|
-
EChartsBarProps,
|
|
46
|
-
EChartsPieProps,
|
|
47
|
-
EChartsAreaProps,
|
|
48
|
-
EChartsHeatmapProps,
|
|
49
|
-
EChartsRadialBarProps,
|
|
50
|
-
EChartsScatterProps,
|
|
51
|
-
EChartsFunnelProps,
|
|
52
|
-
} from './echarts';
|
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
import { defaultStyles } from '@visx/tooltip';
|
|
2
2
|
import { ThemeType } from '../../styled';
|
|
3
3
|
|
|
4
|
+
export interface Margin {
|
|
5
|
+
top: number;
|
|
6
|
+
right: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
left: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AxisConfig {
|
|
12
|
+
showX?: boolean;
|
|
13
|
+
showY?: boolean;
|
|
14
|
+
labelX?: string;
|
|
15
|
+
labelY?: string;
|
|
16
|
+
numTicksX?: number;
|
|
17
|
+
numTicksY?: number;
|
|
18
|
+
/** Whether to include zero in the Y-axis domain. Default: false (auto-scale from min) */
|
|
19
|
+
includeZeroY?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TooltipConfig<T> {
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
accessor?: (d: T, ...additionalArgs: any[]) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BrushConfig {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
margin?: Margin;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type LegendPosition = 'top' | 'right' | 'bottom' | 'left' | 'none';
|
|
33
|
+
|
|
34
|
+
/** Grid display options for charts */
|
|
35
|
+
export type GridDisplay = 'none' | 'x' | 'y' | 'xy';
|
|
36
|
+
|
|
4
37
|
export interface ChartProps {
|
|
5
38
|
/// Unique identifier for the chart
|
|
6
39
|
name: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
40
|
+
/** Display grid lines. Options: 'none', 'x', 'y', 'xy'. Default: 'none' */
|
|
41
|
+
grid?: GridDisplay;
|
|
42
|
+
/** Axis configuration */
|
|
43
|
+
axis?: AxisConfig;
|
|
44
|
+
/** Enable or disable animations. Default: true */
|
|
45
|
+
animate?: boolean;
|
|
46
|
+
/** Chart margins */
|
|
47
|
+
margin?: Margin;
|
|
12
48
|
}
|
|
13
49
|
|
|
14
50
|
export interface InnerChartProps extends ChartProps {
|
|
@@ -16,13 +52,6 @@ export interface InnerChartProps extends ChartProps {
|
|
|
16
52
|
height: number;
|
|
17
53
|
}
|
|
18
54
|
|
|
19
|
-
export interface Margin {
|
|
20
|
-
top: number;
|
|
21
|
-
right: number;
|
|
22
|
-
bottom: number;
|
|
23
|
-
left: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
55
|
export const getDefaultTooltipStyles = (theme: ThemeType) => ({
|
|
27
56
|
...defaultStyles,
|
|
28
57
|
background: theme.colors.background,
|
|
@@ -30,6 +59,7 @@ export const getDefaultTooltipStyles = (theme: ThemeType) => ({
|
|
|
30
59
|
borderRadius: theme.borderRadius.small,
|
|
31
60
|
color: theme.colors.text,
|
|
32
61
|
fontSize: theme.fontSize.small,
|
|
62
|
+
whiteSpace: 'pre-wrap' as const,
|
|
33
63
|
});
|
|
34
64
|
|
|
35
65
|
export const getChartColors = (theme: ThemeType) => [
|
|
@@ -37,4 +67,12 @@ export const getChartColors = (theme: ThemeType) => [
|
|
|
37
67
|
'#FFD700', // Gold for a contrasting yet harmonious color
|
|
38
68
|
'#87CEEB', // Sky blue to complement the purple
|
|
39
69
|
'#FF69B4', // Hot pink for a playful, vibrant look
|
|
70
|
+
'#32CD32', // Lime green for nature-inspired freshness
|
|
71
|
+
'#FF8C00', // Dark orange for warmth
|
|
72
|
+
'#9370DB', // Medium purple for variety
|
|
73
|
+
'#20B2AA', // Light sea green for coolness
|
|
74
|
+
'#FF6347', // Tomato red for emphasis
|
|
75
|
+
'#4682B4', // Steel blue for professionalism
|
|
76
|
+
'#DAA520', // Goldenrod for richness
|
|
77
|
+
'#8B4789', // Dark orchid for depth
|
|
40
78
|
];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { styled } from '../../../styled';
|
|
2
|
+
import { Flag } from '../../visual/Flag';
|
|
3
|
+
import { alpha2ToAlpha3 } from '../../charts/GeoMercator/iso3166-alpha2-to-alpha3';
|
|
4
|
+
|
|
5
|
+
const alpha3ToAlpha2: Record<string, string> = Object.entries(alpha2ToAlpha3).reduce(
|
|
6
|
+
(acc, [alpha2, alpha3]) => {
|
|
7
|
+
acc[alpha3] = alpha2;
|
|
8
|
+
return acc;
|
|
9
|
+
},
|
|
10
|
+
{} as Record<string, string>,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const Container = styled.div`
|
|
14
|
+
width: 320px;
|
|
15
|
+
min-width: 320px;
|
|
16
|
+
flex-shrink: 0;
|
|
17
|
+
height: 100%;
|
|
18
|
+
overflow: auto;
|
|
19
|
+
background-color: ${({ theme }) => theme.colors.backgroundAlt};
|
|
20
|
+
border: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
21
|
+
border-radius: ${({ theme }) => theme.borderRadius.medium};
|
|
22
|
+
padding: ${({ theme }) => theme.spacing['3']};
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const Title = styled.h3`
|
|
26
|
+
margin: 0 0 ${({ theme }) => theme.spacing['2']} 0;
|
|
27
|
+
font-size: ${({ theme }) => theme.fontSize.tiny};
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
color: ${({ theme }) => theme.colors.textAlt};
|
|
30
|
+
text-transform: uppercase;
|
|
31
|
+
letter-spacing: 0.5px;
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const CountryGrid = styled.div`
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: 1fr 1fr;
|
|
37
|
+
gap: ${({ theme }) => theme.spacing['2']};
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const CountryTable = styled.div`
|
|
41
|
+
display: table;
|
|
42
|
+
width: 100%;
|
|
43
|
+
font-size: ${({ theme }) => theme.fontSize.small};
|
|
44
|
+
border-collapse: collapse;
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const CountryRow = styled.div`
|
|
48
|
+
display: table-row;
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
background-color: ${({ theme }) => theme.colors.backgroundAccent};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:last-child > div {
|
|
55
|
+
border-bottom: none;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const CountryCell = styled.div`
|
|
60
|
+
display: table-cell;
|
|
61
|
+
padding: 2px 0;
|
|
62
|
+
vertical-align: middle;
|
|
63
|
+
border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
|
|
64
|
+
color: ${({ theme }) => theme.colors.text};
|
|
65
|
+
line-height: 1.3;
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const FlagCell = styled(CountryCell)`
|
|
69
|
+
width: 20px;
|
|
70
|
+
text-align: center;
|
|
71
|
+
padding-right: 4px;
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const CountryCodeCell = styled(CountryCell)`
|
|
75
|
+
padding-right: 4px;
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
font-size: ${({ theme }) => theme.fontSize.tiny};
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const CountCell = styled(CountryCell)`
|
|
81
|
+
text-align: right;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
color: ${({ theme }) => theme.colors.primary};
|
|
84
|
+
width: 20px;
|
|
85
|
+
font-size: ${({ theme }) => theme.fontSize.tiny};
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
export interface CountryListProps<T> {
|
|
89
|
+
/** Array of data items containing country information */
|
|
90
|
+
data: T[];
|
|
91
|
+
/** Function to extract country code from data item (supports both alpha-2 and alpha-3 codes) */
|
|
92
|
+
xAccessor: (d: T) => string;
|
|
93
|
+
/** Function to extract numeric value from data item */
|
|
94
|
+
yAccessor: (d: T) => number;
|
|
95
|
+
/** Optional title for the list. Defaults to "Countries ({count})" */
|
|
96
|
+
title?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* CountryList displays a sorted list of countries with flags and numeric values in a two-column layout.
|
|
101
|
+
*
|
|
102
|
+
* Features:
|
|
103
|
+
* - Automatically sorts data by numeric value (descending)
|
|
104
|
+
* - Displays country flags when available
|
|
105
|
+
* - Supports both alpha-2 (US) and alpha-3 (USA) country codes
|
|
106
|
+
* - Two-column grid layout for compact display
|
|
107
|
+
* - Hover effects on rows
|
|
108
|
+
*/
|
|
109
|
+
export const CountryList = <T,>({ data, xAccessor, yAccessor, title }: CountryListProps<T>) => {
|
|
110
|
+
const sortedData = [...data].sort((a, b) => yAccessor(b) - yAccessor(a));
|
|
111
|
+
|
|
112
|
+
// Split data into two columns
|
|
113
|
+
const midPoint = Math.ceil(sortedData.length / 2);
|
|
114
|
+
const leftColumnData = sortedData.slice(0, midPoint);
|
|
115
|
+
const rightColumnData = sortedData.slice(midPoint);
|
|
116
|
+
|
|
117
|
+
const renderCountryTable = (columnData: T[]) => (
|
|
118
|
+
<CountryTable>
|
|
119
|
+
{columnData.map((item, index) => {
|
|
120
|
+
const countryCode = xAccessor(item);
|
|
121
|
+
const count = yAccessor(item);
|
|
122
|
+
const alpha2Code = countryCode.length === 3 ? alpha3ToAlpha2[countryCode] : countryCode;
|
|
123
|
+
// Prefer 2-letter country codes for display if available
|
|
124
|
+
const displayCode = alpha2Code || countryCode;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<CountryRow key={`${countryCode}-${index}`}>
|
|
128
|
+
<FlagCell>{alpha2Code && <Flag countryCode={alpha2Code} size={1} />}</FlagCell>
|
|
129
|
+
<CountryCodeCell>{displayCode}</CountryCodeCell>
|
|
130
|
+
<CountCell>{count}</CountCell>
|
|
131
|
+
</CountryRow>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</CountryTable>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Container>
|
|
139
|
+
<Title>{title || `Countries (${sortedData.length})`}</Title>
|
|
140
|
+
<CountryGrid>
|
|
141
|
+
{renderCountryTable(leftColumnData)}
|
|
142
|
+
{renderCountryTable(rightColumnData)}
|
|
143
|
+
</CountryGrid>
|
|
144
|
+
</Container>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SparklineProps {
|
|
4
|
+
data: number[];
|
|
5
|
+
color?: string;
|
|
6
|
+
width?: string | number;
|
|
7
|
+
height?: string | number;
|
|
8
|
+
strokeWidth?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sparkline component - renders a simple SVG line chart for trend visualization
|
|
13
|
+
* Designed to be used as Stats.Sparkline for showing mini trend charts
|
|
14
|
+
*/
|
|
15
|
+
export const Sparkline: FC<SparklineProps> = ({
|
|
16
|
+
data,
|
|
17
|
+
color = 'currentColor',
|
|
18
|
+
width = '100%',
|
|
19
|
+
height = '100%',
|
|
20
|
+
strokeWidth = 2,
|
|
21
|
+
}) => {
|
|
22
|
+
if (!data || data.length === 0) return null;
|
|
23
|
+
|
|
24
|
+
const max = Math.max(...data);
|
|
25
|
+
const min = Math.min(...data);
|
|
26
|
+
const range = max - min || 1;
|
|
27
|
+
|
|
28
|
+
const points = data
|
|
29
|
+
.map((value, index) => {
|
|
30
|
+
const divisor = Math.max(data.length - 1, 1); // in case of single data point
|
|
31
|
+
const x = (index / divisor) * 100;
|
|
32
|
+
const y = 100 - ((value - min) / range) * 100;
|
|
33
|
+
return `${x},${y}`;
|
|
34
|
+
})
|
|
35
|
+
.join(' ');
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<svg width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
39
|
+
<polyline
|
|
40
|
+
points={points}
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke={color}
|
|
43
|
+
strokeWidth={strokeWidth}
|
|
44
|
+
vectorEffect="non-scaling-stroke"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { FC, useContext, ReactNode, cloneElement, isValidElement } from 'react';
|
|
2
|
-
import { styled } from '../../../styled';
|
|
3
|
-
import { StatContext, Direction
|
|
2
|
+
import { styled, Size } from '../../../styled';
|
|
3
|
+
import { StatContext, Direction } from './context';
|
|
4
4
|
import { AiOutlineArrowUp, AiOutlineArrowDown } from 'react-icons/ai';
|
|
5
5
|
|
|
6
6
|
const Container = styled.div<{ isGrouped: boolean; direction: Direction; size: Size }>`
|
|
7
7
|
background-color: ${({ theme }) => theme.colors.backgroundAlt};
|
|
8
8
|
padding: ${({ theme, size }) => {
|
|
9
9
|
switch (size) {
|
|
10
|
+
case 'tiny':
|
|
11
|
+
return theme.spacing['0_5'];
|
|
10
12
|
case 'small':
|
|
11
13
|
return theme.spacing['1'];
|
|
12
14
|
case 'medium':
|
|
13
15
|
return theme.spacing['2'];
|
|
14
16
|
case 'large':
|
|
15
17
|
return theme.spacing['3'];
|
|
18
|
+
case 'huge':
|
|
19
|
+
return theme.spacing['4'];
|
|
16
20
|
}
|
|
17
21
|
}};
|
|
18
22
|
|
|
@@ -58,16 +62,19 @@ const Container = styled.div<{ isGrouped: boolean; direction: Direction; size: S
|
|
|
58
62
|
dt {
|
|
59
63
|
font-size: ${({ theme, size }) => {
|
|
60
64
|
switch (size) {
|
|
65
|
+
case 'tiny':
|
|
66
|
+
return theme.fontSize.tiny;
|
|
61
67
|
case 'small':
|
|
62
68
|
return theme.fontSize.small;
|
|
63
69
|
case 'medium':
|
|
64
70
|
return theme.fontSize.medium;
|
|
65
71
|
case 'large':
|
|
66
72
|
return theme.fontSize.mediumLarge;
|
|
73
|
+
case 'huge':
|
|
74
|
+
return theme.fontSize.large;
|
|
67
75
|
}
|
|
68
76
|
}};
|
|
69
|
-
color: ${({ theme }) => theme.colors.
|
|
70
|
-
font-size: ${({ theme }) => theme.fontSize.medium};
|
|
77
|
+
color: ${({ theme }) => theme.colors.textAlt};
|
|
71
78
|
margin-bottom: ${({ theme }) => theme.spacing['0_5']};
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -76,12 +83,16 @@ const Container = styled.div<{ isGrouped: boolean; direction: Direction; size: S
|
|
|
76
83
|
color: ${({ theme }) => theme.colors.white};
|
|
77
84
|
font-size: ${({ theme, size }) => {
|
|
78
85
|
switch (size) {
|
|
86
|
+
case 'tiny':
|
|
87
|
+
return theme.fontSize.small;
|
|
79
88
|
case 'small':
|
|
80
89
|
return theme.fontSize.medium;
|
|
81
90
|
case 'medium':
|
|
82
91
|
return theme.fontSize.mediumLarge;
|
|
83
92
|
case 'large':
|
|
84
93
|
return theme.fontSize.large;
|
|
94
|
+
case 'huge':
|
|
95
|
+
return theme.fontSize.huge;
|
|
85
96
|
}
|
|
86
97
|
}};
|
|
87
98
|
color: ${({ theme }) => theme.colors.text};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { FC, PropsWithChildren, Children, isValidElement } from 'react';
|
|
2
|
-
import { styled } from '../../../styled';
|
|
2
|
+
import { styled, Size } from '../../../styled';
|
|
3
3
|
import { Stat } from './Stat';
|
|
4
|
-
import {
|
|
4
|
+
import { Sparkline } from './Sparkline';
|
|
5
|
+
import { StatContext, Direction } from './context';
|
|
5
6
|
|
|
6
7
|
export const Container = styled.dl<{ direction: Direction; grouped: boolean; count: number }>`
|
|
7
8
|
display: grid;
|
|
@@ -26,6 +27,7 @@ export interface StatsProps {
|
|
|
26
27
|
|
|
27
28
|
interface SubComponents {
|
|
28
29
|
Stat: typeof Stat;
|
|
30
|
+
Sparkline: typeof Sparkline;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export const Stats: FC<PropsWithChildren<StatsProps>> & SubComponents = ({
|
|
@@ -52,6 +54,9 @@ export const Stats: FC<PropsWithChildren<StatsProps>> & SubComponents = ({
|
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
Stats.Stat = Stat;
|
|
57
|
+
Stats.Sparkline = Sparkline;
|
|
55
58
|
|
|
56
|
-
export type { Direction
|
|
59
|
+
export type { Direction } from './context';
|
|
60
|
+
export type { Size } from '../../../styled';
|
|
57
61
|
export type { TrendConfig } from './Stat';
|
|
62
|
+
export type { SparklineProps } from './Sparkline';
|
|
@@ -3,12 +3,11 @@ import { Tooltip, TooltipProps } from '../Tooltip';
|
|
|
3
3
|
import { Size, styled } from '../../../styled';
|
|
4
4
|
import { getIconSize } from '../../actions/IconButton/getIconSize';
|
|
5
5
|
import { ButtonColor } from '../../actions/Button/style';
|
|
6
|
+
import { useTheme } from '../../../hooks';
|
|
6
7
|
|
|
7
|
-
type TooltipIconColor =
|
|
8
|
+
type TooltipIconColor = ButtonColor;
|
|
8
9
|
|
|
9
|
-
const TriggerContainer = styled.div
|
|
10
|
-
border: 1px solid ${({ theme, color }) => theme.colors[color]};
|
|
11
|
-
border-radius: ${({ theme }) => theme.borderRadius.small};
|
|
10
|
+
const TriggerContainer = styled.div`
|
|
12
11
|
display: flex;
|
|
13
12
|
align-items: center;
|
|
14
13
|
justify-content: center;
|
|
@@ -33,12 +32,16 @@ export const IconTooltip: FC<IconTooltipProps> = ({
|
|
|
33
32
|
open,
|
|
34
33
|
icon,
|
|
35
34
|
size = 'medium',
|
|
36
|
-
color = '
|
|
35
|
+
color = 'white',
|
|
37
36
|
}) => {
|
|
37
|
+
const theme = useTheme();
|
|
38
|
+
|
|
38
39
|
return (
|
|
39
40
|
<Tooltip initialOpen={initialOpen} open={open}>
|
|
40
41
|
<Tooltip.Trigger asChild>
|
|
41
|
-
<TriggerContainer
|
|
42
|
+
<TriggerContainer>
|
|
43
|
+
{cloneElement(icon, { size: getIconSize(size), fill: theme.colors[color] })}
|
|
44
|
+
</TriggerContainer>
|
|
42
45
|
</Tooltip.Trigger>
|
|
43
46
|
<Tooltip.Content>
|
|
44
47
|
<ContentContainer>{children}</ContentContainer>
|