@sybilion/uilib 1.2.24 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/ui/Chart/Chart.js +1 -0
- package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +7 -32
- package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.js +21 -0
- package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.js +7 -0
- package/dist/esm/components/ui/Chart/tools/chartPlotGeometry.js +65 -0
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +37 -1
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +5 -2
- package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.js +205 -0
- package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.js +7 -0
- package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.js +37 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.js +1 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.js +7 -60
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.js +2 -2
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.js +1 -0
- package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +2 -4
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +1 -1
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.styl.js +1 -1
- package/dist/esm/components/ui/DropdownMenu/DropdownMenu.js +4 -4
- package/dist/esm/components/ui/TimeRangeControls/TimeRangeControls.js +7 -2
- package/dist/esm/components/ui/WorldMap/WorldMap.js +11 -0
- package/dist/esm/components/ui/WorldMap/WorldMap.styl.js +7 -0
- package/dist/esm/components/ui/WorldMap/map.svg.js +3 -0
- package/dist/esm/components/widgets/DriverCard/DriverCard.js +89 -0
- package/dist/esm/components/widgets/DriverCard/DriverCard.styl.js +7 -0
- package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +79 -0
- package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.styl.js +7 -0
- package/dist/esm/components/widgets/DriverCard/driverPerformanceChartData.js +50 -0
- package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.constants.json.js +6 -0
- package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.js +21 -0
- package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.styl.js +7 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.helpers.js +107 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.js +129 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.styl.js +7 -0
- package/dist/esm/components/widgets/DriverMap/driverCategoryIcon.js +194 -0
- package/dist/esm/components/widgets/DriverMap/driverMapGeography.js +345 -0
- package/dist/esm/components/widgets/DriverMap/driverMapSelection.js +17 -0
- package/dist/esm/hooks/index.js +1 -0
- package/dist/esm/hooks/useEvent.js +0 -2
- package/dist/esm/index.js +7 -0
- package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -1
- package/dist/esm/types/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.d.ts +14 -0
- package/dist/esm/types/src/components/ui/Chart/tools/chartPlotGeometry.d.ts +30 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +11 -2
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -2
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.d.ts +15 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.d.ts +14 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.d.ts +1 -1
- package/dist/esm/types/src/components/ui/DropdownMenu/DropdownMenu.d.ts +2 -2
- package/dist/esm/types/src/components/ui/Page/PageColumns/PageColumns.d.ts +1 -1
- package/dist/esm/types/src/components/ui/TimeRangeControls/TimeRangeControls.d.ts +5 -7
- package/dist/esm/types/src/components/ui/WorldMap/WorldMap.d.ts +4 -0
- package/dist/esm/types/src/components/ui/WorldMap/index.d.ts +2 -0
- package/dist/esm/types/src/components/widgets/DriverCard/DriverCard.d.ts +9 -0
- package/dist/esm/types/src/components/widgets/DriverCard/DriverPerformanceChart.d.ts +5 -0
- package/dist/esm/types/src/components/widgets/DriverCard/driverPerformanceChartData.d.ts +7 -0
- package/dist/esm/types/src/components/widgets/DriverCard/index.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriverMap/DriverIcon/DriverIcon.d.ts +17 -0
- package/dist/esm/types/src/components/widgets/DriverMap/DriverMap.d.ts +8 -0
- package/dist/esm/types/src/components/widgets/DriverMap/DriverMap.helpers.d.ts +21 -0
- package/dist/esm/types/src/components/widgets/DriverMap/driverCategoryIcon.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriverMap/driverMapGeography.d.ts +80 -0
- package/dist/esm/types/src/components/widgets/DriverMap/driverMapSelection.d.ts +3 -0
- package/dist/esm/types/src/components/widgets/DriverMap/index.d.ts +6 -0
- package/dist/esm/types/src/docs/pages/DriverMapPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/PageColumnsPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/WorldMapPage.d.ts +1 -0
- package/dist/esm/types/src/docs/registry.d.ts +1 -1
- package/dist/esm/types/src/hooks/index.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/components/ui/Chart/Chart.tsx +5 -0
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +8 -41
- package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl +60 -0
- package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.d.ts +15 -0
- package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.tsx +66 -0
- package/src/components/ui/Chart/tools/chartPlotGeometry.ts +89 -0
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +44 -2
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +14 -1
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -3
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl +21 -0
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.d.ts +9 -0
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.tsx +285 -0
- package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.ts +55 -0
- package/src/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.ts +1 -0
- package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl +2 -7
- package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.d.ts +0 -1
- package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.tsx +7 -71
- package/src/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.ts +1 -0
- package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +2 -3
- package/src/components/ui/Chat/ChatSheet/ChatSelector.styl +3 -1
- package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +1 -1
- package/src/components/ui/DropdownMenu/DropdownMenu.tsx +4 -0
- package/src/components/ui/Page/PageColumns/PageColumns.tsx +1 -1
- package/src/components/ui/TimeRangeControls/TimeRangeControls.tsx +16 -17
- package/src/components/ui/WorldMap/WorldMap.styl +11 -0
- package/src/components/ui/WorldMap/WorldMap.styl.d.ts +7 -0
- package/src/components/ui/WorldMap/WorldMap.tsx +22 -0
- package/src/components/ui/WorldMap/index.ts +2 -0
- package/src/components/ui/WorldMap/map.svg +4337 -0
- package/src/components/ui/WorldMap/mapAspect.mixin.styl +3 -0
- package/src/components/ui/WorldMap/mapAspect.mixin.styl.d.ts +2 -0
- package/src/components/widgets/DriverCard/DriverCard.styl +169 -0
- package/src/components/widgets/DriverCard/DriverCard.styl.d.ts +40 -0
- package/src/components/widgets/DriverCard/DriverCard.tsx +219 -0
- package/src/components/widgets/DriverCard/DriverPerformanceChart.styl +43 -0
- package/src/components/widgets/DriverCard/DriverPerformanceChart.styl.d.ts +13 -0
- package/src/components/widgets/DriverCard/DriverPerformanceChart.tsx +150 -0
- package/src/components/widgets/DriverCard/driverPerformanceChartData.ts +64 -0
- package/src/components/widgets/DriverCard/index.ts +1 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.constants.json +3 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.styl +125 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.styl.d.ts +22 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.tsx +79 -0
- package/src/components/widgets/DriverMap/DriverMap.helpers.ts +164 -0
- package/src/components/widgets/DriverMap/DriverMap.styl +50 -0
- package/src/components/widgets/DriverMap/DriverMap.styl.d.ts +12 -0
- package/src/components/widgets/DriverMap/DriverMap.tsx +212 -0
- package/src/components/widgets/DriverMap/driverCategoryIcon.tsx +277 -0
- package/src/components/widgets/DriverMap/driverMapGeography.ts +478 -0
- package/src/components/widgets/DriverMap/driverMapSelection.ts +23 -0
- package/src/components/widgets/DriverMap/index.ts +16 -0
- package/src/docs/config/webpack.config.js +25 -9
- package/src/docs/pages/ChartAreaInteractivePage.tsx +2 -3
- package/src/docs/pages/DriverMapPage.tsx +268 -0
- package/src/docs/pages/PageColumnsPage.tsx +92 -0
- package/src/docs/pages/TimeRangeControlsPage.tsx +2 -3
- package/src/docs/pages/TooltipPage.tsx +14 -10
- package/src/docs/pages/WorldMapPage.styl +14 -0
- package/src/docs/pages/WorldMapPage.styl.d.ts +8 -0
- package/src/docs/pages/WorldMapPage.tsx +26 -0
- package/src/docs/registry.ts +18 -5
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useEvent.ts +0 -2
- package/src/index.ts +3 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
@import '../../../../lib/theme.styl';
|
|
2
|
+
json('./DriverIcon.constants.json')
|
|
3
|
+
|
|
4
|
+
$dur = unit(FADE_DURATION, 'ms')
|
|
5
|
+
$msOffset = 30ms
|
|
6
|
+
|
|
7
|
+
.root
|
|
8
|
+
position absolute
|
|
9
|
+
transform translateX(-50%) translateY(-50%)
|
|
10
|
+
cursor pointer
|
|
11
|
+
outline none
|
|
12
|
+
|
|
13
|
+
&:hover
|
|
14
|
+
transition transform 300ms ease-out
|
|
15
|
+
|
|
16
|
+
&:focus
|
|
17
|
+
outline none
|
|
18
|
+
|
|
19
|
+
.loadingHidden
|
|
20
|
+
opacity 0
|
|
21
|
+
pointer-events none
|
|
22
|
+
|
|
23
|
+
.badge
|
|
24
|
+
padding 0.25rem
|
|
25
|
+
transition all $dur ease-out
|
|
26
|
+
pointer-events none
|
|
27
|
+
|
|
28
|
+
opacity 0
|
|
29
|
+
transform translateY(10px)
|
|
30
|
+
|
|
31
|
+
svg
|
|
32
|
+
height 20px
|
|
33
|
+
width 20px
|
|
34
|
+
|
|
35
|
+
.size-s
|
|
36
|
+
.size-s .badge
|
|
37
|
+
width 2rem
|
|
38
|
+
height 2rem
|
|
39
|
+
border-radius 10px
|
|
40
|
+
.size-m
|
|
41
|
+
.size-m .badge
|
|
42
|
+
width 2.5rem
|
|
43
|
+
height 2.5rem
|
|
44
|
+
border-radius 12px
|
|
45
|
+
.size-l
|
|
46
|
+
.size-l .badge
|
|
47
|
+
width 3rem
|
|
48
|
+
height 3rem
|
|
49
|
+
border-radius 14px
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// show/hide animations
|
|
53
|
+
.visible .badge
|
|
54
|
+
transition $dur ease-out
|
|
55
|
+
transition-property opacity, transform
|
|
56
|
+
opacity 1
|
|
57
|
+
transform translateY(0)
|
|
58
|
+
|
|
59
|
+
span
|
|
60
|
+
transition box-shadow 0s .1s ease-out
|
|
61
|
+
|
|
62
|
+
for $i in (1..6)
|
|
63
|
+
.root:nth-child({$i}) .badge
|
|
64
|
+
transition-delay ($i * $msOffset)
|
|
65
|
+
.visible:nth-child({$i}) .badge
|
|
66
|
+
transition-delay ($i * ($msOffset * 2))
|
|
67
|
+
.hidden:nth-child({$i}) .badge
|
|
68
|
+
transition-duration 'calc(%s - %s)' % ($dur ($i * $msOffset))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Hover
|
|
72
|
+
.root:hover
|
|
73
|
+
z-index 10
|
|
74
|
+
|
|
75
|
+
.badge
|
|
76
|
+
box-shadow 0 0 0 1px var(--background)
|
|
77
|
+
transform scale(1.1)
|
|
78
|
+
transition-delay 0ms
|
|
79
|
+
transition-duration .2s
|
|
80
|
+
|
|
81
|
+
.selected .badge
|
|
82
|
+
.selected:hover .badge
|
|
83
|
+
transform scale(1.1)
|
|
84
|
+
|
|
85
|
+
// span
|
|
86
|
+
box-shadow inset 0 0 0 3px var(--sb-cyan-400)
|
|
87
|
+
|
|
88
|
+
:global(.dark) &
|
|
89
|
+
box-shadow inset 0 0 0 2px var(--sb-cyan-500)
|
|
90
|
+
|
|
91
|
+
.root:not(.selected):hover .icon
|
|
92
|
+
transform scale(1.05)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
.tooltipContent
|
|
96
|
+
max-width 20rem
|
|
97
|
+
border 1px solid var(--border)
|
|
98
|
+
background-color var(--popover)
|
|
99
|
+
color var(--popover-foreground)
|
|
100
|
+
font-size 0.75rem
|
|
101
|
+
font-family var(--font-family-body)
|
|
102
|
+
box-shadow 0 10px 15px -3px rgba(0, 0, 0, 0.1)
|
|
103
|
+
|
|
104
|
+
.tooltipCategory
|
|
105
|
+
color var(--muted-foreground)
|
|
106
|
+
display flex
|
|
107
|
+
align-items center
|
|
108
|
+
gap 0.25rem
|
|
109
|
+
|
|
110
|
+
.tooltipCategoryIcon
|
|
111
|
+
height 0.75rem
|
|
112
|
+
width 0.75rem
|
|
113
|
+
color var(--muted-foreground)
|
|
114
|
+
|
|
115
|
+
.tooltipCategoryText
|
|
116
|
+
line-clamp(1)
|
|
117
|
+
overflow hidden
|
|
118
|
+
|
|
119
|
+
.tooltipName
|
|
120
|
+
font-weight normal
|
|
121
|
+
|
|
122
|
+
.tooltipSpacing
|
|
123
|
+
display flex
|
|
124
|
+
flex-direction column
|
|
125
|
+
gap 0.25rem
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// This file is automatically generated.
|
|
2
|
+
// Please do not change this file!
|
|
3
|
+
interface CssExports {
|
|
4
|
+
'badge': string;
|
|
5
|
+
'hidden': string;
|
|
6
|
+
'icon': string;
|
|
7
|
+
'loadingHidden': string;
|
|
8
|
+
'root': string;
|
|
9
|
+
'selected': string;
|
|
10
|
+
'size-l': string;
|
|
11
|
+
'size-m': string;
|
|
12
|
+
'size-s': string;
|
|
13
|
+
'tooltipCategory': string;
|
|
14
|
+
'tooltipCategoryIcon': string;
|
|
15
|
+
'tooltipCategoryText': string;
|
|
16
|
+
'tooltipContent': string;
|
|
17
|
+
'tooltipName': string;
|
|
18
|
+
'tooltipSpacing': string;
|
|
19
|
+
'visible': string;
|
|
20
|
+
}
|
|
21
|
+
export const cssExports: CssExports;
|
|
22
|
+
export default cssExports;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '#uilib/components/ui/Badge';
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from '#uilib/components/ui/Tooltip';
|
|
9
|
+
|
|
10
|
+
import { getCategoryIcon } from '../driverCategoryIcon';
|
|
11
|
+
import type { DriverData } from '../driverMapGeography';
|
|
12
|
+
import S from './DriverIcon.styl';
|
|
13
|
+
|
|
14
|
+
export const BADGE_SIZES = {
|
|
15
|
+
s: 's',
|
|
16
|
+
m: 'm',
|
|
17
|
+
l: 'l',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type BadgeSize = (typeof BADGE_SIZES)[keyof typeof BADGE_SIZES];
|
|
21
|
+
|
|
22
|
+
interface DriverIconProps {
|
|
23
|
+
driver: DriverData;
|
|
24
|
+
size: BadgeSize;
|
|
25
|
+
isSelected: boolean;
|
|
26
|
+
onClick: () => void;
|
|
27
|
+
isVisible: boolean;
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DriverIcon({
|
|
32
|
+
driver,
|
|
33
|
+
size,
|
|
34
|
+
isSelected,
|
|
35
|
+
onClick,
|
|
36
|
+
isVisible,
|
|
37
|
+
isLoading = false,
|
|
38
|
+
}: DriverIconProps) {
|
|
39
|
+
return (
|
|
40
|
+
<Tooltip>
|
|
41
|
+
<TooltipTrigger
|
|
42
|
+
asChild
|
|
43
|
+
className={cn(
|
|
44
|
+
S.root,
|
|
45
|
+
S[`size-${size}`],
|
|
46
|
+
isSelected && S.selected,
|
|
47
|
+
isVisible ? S.visible : S.hidden,
|
|
48
|
+
isLoading && S.loadingHidden,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={onClick}
|
|
54
|
+
style={{
|
|
55
|
+
left: `${driver.coordinates.x}%`,
|
|
56
|
+
top: `${driver.coordinates.y}%`,
|
|
57
|
+
zIndex: isSelected ? 2 : '',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<Badge className={S.badge}>
|
|
61
|
+
<div className={S.icon}>{getCategoryIcon(driver.category)}</div>
|
|
62
|
+
</Badge>
|
|
63
|
+
</button>
|
|
64
|
+
</TooltipTrigger>
|
|
65
|
+
|
|
66
|
+
<TooltipContent side="top" className={S.tooltipContent}>
|
|
67
|
+
<div className={S.tooltipSpacing}>
|
|
68
|
+
<div className={S.tooltipCategory}>
|
|
69
|
+
<span className={S.tooltipCategoryIcon}>
|
|
70
|
+
{getCategoryIcon(driver.category)}
|
|
71
|
+
</span>
|
|
72
|
+
<span className={S.tooltipCategoryText}>{driver.category}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div className={S.tooltipName}>{driver.name}</div>
|
|
75
|
+
</div>
|
|
76
|
+
</TooltipContent>
|
|
77
|
+
</Tooltip>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { BADGE_SIZES, type BadgeSize } from './DriverIcon/DriverIcon';
|
|
2
|
+
import {
|
|
3
|
+
type DriverData,
|
|
4
|
+
geographicToSVG,
|
|
5
|
+
getResponsiveCoordinates,
|
|
6
|
+
svgToPercentage,
|
|
7
|
+
} from './driverMapGeography';
|
|
8
|
+
import { getDriverImportance } from './driverMapSelection';
|
|
9
|
+
|
|
10
|
+
// Calculate badge sizes for all drivers based on their importance
|
|
11
|
+
export const calculateBadgeSizes = (
|
|
12
|
+
drivers: DriverData[],
|
|
13
|
+
): Record<string, BadgeSize> => {
|
|
14
|
+
if (drivers.length === 0) return {};
|
|
15
|
+
|
|
16
|
+
const importanceValues = drivers
|
|
17
|
+
.map(getDriverImportance)
|
|
18
|
+
.filter(
|
|
19
|
+
(val): val is number =>
|
|
20
|
+
typeof val === 'number' && !isNaN(val) && val >= 0,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (importanceValues.length === 0) {
|
|
24
|
+
return drivers.reduce(
|
|
25
|
+
(acc, driver) => {
|
|
26
|
+
acc[driver.id] = BADGE_SIZES.s;
|
|
27
|
+
return acc;
|
|
28
|
+
},
|
|
29
|
+
{} as Record<string, BadgeSize>,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const min = Math.min(...importanceValues);
|
|
34
|
+
const max = Math.max(...importanceValues);
|
|
35
|
+
const range = max - min;
|
|
36
|
+
|
|
37
|
+
if (range === 0) {
|
|
38
|
+
return drivers.reduce(
|
|
39
|
+
(acc, driver) => {
|
|
40
|
+
acc[driver.id] = BADGE_SIZES.s;
|
|
41
|
+
return acc;
|
|
42
|
+
},
|
|
43
|
+
{} as Record<string, BadgeSize>,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return drivers.reduce(
|
|
48
|
+
(acc, driver) => {
|
|
49
|
+
const importance = getDriverImportance(driver);
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
typeof importance !== 'number' ||
|
|
53
|
+
isNaN(importance) ||
|
|
54
|
+
importance < 0
|
|
55
|
+
) {
|
|
56
|
+
acc[driver.id] = BADGE_SIZES.s;
|
|
57
|
+
return acc;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalized = (importance - min) / range;
|
|
61
|
+
acc[driver.id] =
|
|
62
|
+
normalized >= 0.66
|
|
63
|
+
? BADGE_SIZES.l
|
|
64
|
+
: normalized >= 0.33
|
|
65
|
+
? BADGE_SIZES.m
|
|
66
|
+
: BADGE_SIZES.s;
|
|
67
|
+
return acc;
|
|
68
|
+
},
|
|
69
|
+
{} as Record<string, BadgeSize>,
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const hasValidCoords = (
|
|
74
|
+
coords: { x: number; y: number } | null | undefined,
|
|
75
|
+
): boolean => {
|
|
76
|
+
return (
|
|
77
|
+
coords?.x != null &&
|
|
78
|
+
coords?.y != null &&
|
|
79
|
+
typeof coords.x === 'number' &&
|
|
80
|
+
typeof coords.y === 'number' &&
|
|
81
|
+
!isNaN(coords.x) &&
|
|
82
|
+
!isNaN(coords.y) &&
|
|
83
|
+
Math.abs(coords.x) > 0 &&
|
|
84
|
+
Math.abs(coords.y) > 0
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const findMostSpecificRegion = (
|
|
89
|
+
driver: DriverData,
|
|
90
|
+
): {
|
|
91
|
+
name: string | null;
|
|
92
|
+
coordinates: { x: number; y: number } | null;
|
|
93
|
+
} => {
|
|
94
|
+
if (driver.region?.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
name: driver.region[driver.region.length - 1],
|
|
97
|
+
coordinates: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const srcName = driver.src_region?.name;
|
|
102
|
+
const tgtName = driver.tgt_region?.name;
|
|
103
|
+
const srcCoords = driver.src_region?.coordinates;
|
|
104
|
+
const tgtCoords = driver.tgt_region?.coordinates;
|
|
105
|
+
const srcIsNonWorld =
|
|
106
|
+
srcName && srcName !== 'World' && hasValidCoords(srcCoords);
|
|
107
|
+
const tgtIsNonWorld =
|
|
108
|
+
tgtName && tgtName !== 'World' && hasValidCoords(tgtCoords);
|
|
109
|
+
|
|
110
|
+
if (tgtIsNonWorld) {
|
|
111
|
+
return {
|
|
112
|
+
name: tgtName,
|
|
113
|
+
coordinates: tgtCoords!,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (srcIsNonWorld) {
|
|
117
|
+
return {
|
|
118
|
+
name: srcName,
|
|
119
|
+
coordinates: srcCoords!,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { name: null, coordinates: null };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const getDriverCoordinates = (
|
|
127
|
+
driver: DriverData,
|
|
128
|
+
specificRegion: {
|
|
129
|
+
name: string | null;
|
|
130
|
+
coordinates: { x: number; y: number } | null;
|
|
131
|
+
},
|
|
132
|
+
existingDrivers: DriverData[] = [],
|
|
133
|
+
): DriverData['coordinates'] => {
|
|
134
|
+
if (!specificRegion.name || specificRegion.name === 'World') {
|
|
135
|
+
return driver.coordinates;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (specificRegion.coordinates) {
|
|
139
|
+
const isLatLng =
|
|
140
|
+
Math.abs(specificRegion.coordinates.x) <= 180 &&
|
|
141
|
+
Math.abs(specificRegion.coordinates.y) <= 90;
|
|
142
|
+
|
|
143
|
+
if (isLatLng) {
|
|
144
|
+
const svgCoords = geographicToSVG(
|
|
145
|
+
specificRegion.coordinates.y,
|
|
146
|
+
specificRegion.coordinates.x,
|
|
147
|
+
);
|
|
148
|
+
const percentageCoords = svgToPercentage(svgCoords.x, svgCoords.y);
|
|
149
|
+
return {
|
|
150
|
+
x: Math.min(95, Math.max(5, percentageCoords.x)),
|
|
151
|
+
y: Math.min(95, Math.max(5, percentageCoords.y)),
|
|
152
|
+
continent: driver.coordinates?.continent || 'Global',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
x: specificRegion.coordinates.x,
|
|
158
|
+
y: specificRegion.coordinates.y,
|
|
159
|
+
continent: driver.coordinates?.continent || 'Global',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return getResponsiveCoordinates(specificRegion.name, existingDrivers);
|
|
164
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@import '../../../lib/theme.styl'
|
|
2
|
+
@import '../../ui/WorldMap/mapAspect.mixin.styl'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
.root
|
|
6
|
+
position relative
|
|
7
|
+
width 100%
|
|
8
|
+
mapAspect()
|
|
9
|
+
padding 0
|
|
10
|
+
overflow hidden
|
|
11
|
+
|
|
12
|
+
@media (min-width: 768px)
|
|
13
|
+
height 35rem
|
|
14
|
+
width fit-content
|
|
15
|
+
flex 1
|
|
16
|
+
|
|
17
|
+
.mapInner
|
|
18
|
+
position relative
|
|
19
|
+
max-height 100%
|
|
20
|
+
mapAspect()
|
|
21
|
+
margin 0 auto
|
|
22
|
+
border-radius var(--p-4)
|
|
23
|
+
overflow hidden
|
|
24
|
+
|
|
25
|
+
.mapWorld
|
|
26
|
+
position absolute
|
|
27
|
+
inset 0
|
|
28
|
+
box-sizing border-box
|
|
29
|
+
|
|
30
|
+
.shimmerOverlay
|
|
31
|
+
z-index 20
|
|
32
|
+
|
|
33
|
+
.inTransition
|
|
34
|
+
pointer-events none
|
|
35
|
+
|
|
36
|
+
.worldDrivers
|
|
37
|
+
position absolute
|
|
38
|
+
bottom 0
|
|
39
|
+
display flex
|
|
40
|
+
align-items center
|
|
41
|
+
gap 0.5rem
|
|
42
|
+
padding 0.5rem
|
|
43
|
+
|
|
44
|
+
button
|
|
45
|
+
position static !important
|
|
46
|
+
transform none !important
|
|
47
|
+
|
|
48
|
+
& > span
|
|
49
|
+
transform none !important
|
|
50
|
+
opacity 1 !important
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// This file is automatically generated.
|
|
2
|
+
// Please do not change this file!
|
|
3
|
+
interface CssExports {
|
|
4
|
+
'inTransition': string;
|
|
5
|
+
'mapInner': string;
|
|
6
|
+
'mapWorld': string;
|
|
7
|
+
'root': string;
|
|
8
|
+
'shimmerOverlay': string;
|
|
9
|
+
'worldDrivers': string;
|
|
10
|
+
}
|
|
11
|
+
export const cssExports: CssExports;
|
|
12
|
+
export default cssExports;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { WorldMap } from '#uilib/components/ui/WorldMap';
|
|
5
|
+
import useEvent from '#uilib/hooks/useEvent';
|
|
6
|
+
import { Shimmer } from '@homecode/ui';
|
|
7
|
+
|
|
8
|
+
import { BadgeSize, DriverIcon } from './DriverIcon/DriverIcon';
|
|
9
|
+
import constants from './DriverIcon/DriverIcon.constants.json';
|
|
10
|
+
import {
|
|
11
|
+
calculateBadgeSizes,
|
|
12
|
+
findMostSpecificRegion,
|
|
13
|
+
getDriverCoordinates,
|
|
14
|
+
hasValidCoords,
|
|
15
|
+
} from './DriverMap.helpers';
|
|
16
|
+
import S from './DriverMap.styl';
|
|
17
|
+
import type { DriverData } from './driverMapGeography';
|
|
18
|
+
import { getHighestImportanceDriver } from './driverMapSelection';
|
|
19
|
+
|
|
20
|
+
export interface DriverMapProps {
|
|
21
|
+
drivers: DriverData[];
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
setSelectedDriver: (driver: DriverData) => void;
|
|
24
|
+
selectedDriver: DriverData | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const delay = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
|
|
28
|
+
|
|
29
|
+
const transitionDelay = () => delay(constants.FADE_DURATION);
|
|
30
|
+
|
|
31
|
+
export function DriverMap({
|
|
32
|
+
drivers,
|
|
33
|
+
isLoading,
|
|
34
|
+
setSelectedDriver,
|
|
35
|
+
selectedDriver,
|
|
36
|
+
}: DriverMapProps) {
|
|
37
|
+
const visibleDriversRef = useRef<DriverData[]>([]);
|
|
38
|
+
|
|
39
|
+
const [showingDrivers, setShowingDrivers] = useState<DriverData[]>(drivers);
|
|
40
|
+
const [badgeSizes, setBadgeSizes] = useState<Record<string, BadgeSize>>({});
|
|
41
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
42
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
43
|
+
|
|
44
|
+
const makeTransition = async (nextDrivers: DriverData[]) => {
|
|
45
|
+
const hadDrivers = showingDrivers.length > 0;
|
|
46
|
+
|
|
47
|
+
setIsTransitioning(true);
|
|
48
|
+
|
|
49
|
+
if (hadDrivers) {
|
|
50
|
+
setIsVisible(false);
|
|
51
|
+
await transitionDelay();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (nextDrivers.length > 0) {
|
|
55
|
+
setShowingDrivers(nextDrivers);
|
|
56
|
+
setIsVisible(false);
|
|
57
|
+
|
|
58
|
+
const sizes = calculateBadgeSizes(nextDrivers);
|
|
59
|
+
setBadgeSizes(sizes);
|
|
60
|
+
|
|
61
|
+
await delay(30);
|
|
62
|
+
setIsVisible(true);
|
|
63
|
+
await transitionDelay();
|
|
64
|
+
} else {
|
|
65
|
+
setShowingDrivers([]);
|
|
66
|
+
setBadgeSizes({});
|
|
67
|
+
setIsVisible(false);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setIsTransitioning(false);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const [worldDrivers, otherDrivers] = useMemo(() => {
|
|
74
|
+
const visibleDrivers: DriverData[] = [];
|
|
75
|
+
const worldDriversList: DriverData[] = [];
|
|
76
|
+
|
|
77
|
+
drivers.forEach(driver => {
|
|
78
|
+
let importance = driver.importance || 0;
|
|
79
|
+
if (importance === 0) {
|
|
80
|
+
const mean = driver?.rawImportance?.overall?.mean;
|
|
81
|
+
if (typeof mean === 'number') {
|
|
82
|
+
importance = Math.abs(mean) * 100;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const specificRegion = findMostSpecificRegion(driver);
|
|
87
|
+
const isEmptyRegion = driver.region?.length === 0;
|
|
88
|
+
const isWorldRegion =
|
|
89
|
+
specificRegion.name === 'World' ||
|
|
90
|
+
driver.region?.slice(-1)[0] === 'World';
|
|
91
|
+
|
|
92
|
+
const coordinates = getDriverCoordinates(
|
|
93
|
+
driver,
|
|
94
|
+
specificRegion,
|
|
95
|
+
visibleDrivers,
|
|
96
|
+
);
|
|
97
|
+
const driverToAdd = {
|
|
98
|
+
...driver,
|
|
99
|
+
coordinates,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const shouldGoToWorldDrivers =
|
|
103
|
+
isWorldRegion ||
|
|
104
|
+
!hasValidCoords(driverToAdd.coordinates) ||
|
|
105
|
+
(isEmptyRegion && !specificRegion.name);
|
|
106
|
+
|
|
107
|
+
if (shouldGoToWorldDrivers && importance >= 0) {
|
|
108
|
+
worldDriversList.push(driver);
|
|
109
|
+
} else if (hasValidCoords(driverToAdd.coordinates) && importance >= 0) {
|
|
110
|
+
visibleDrivers.push(driverToAdd);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
worldDriversList.sort((a, b) => b.importance - a.importance),
|
|
116
|
+
visibleDrivers,
|
|
117
|
+
];
|
|
118
|
+
}, [drivers]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
void makeTransition(drivers);
|
|
122
|
+
|
|
123
|
+
if (drivers.length > 0) {
|
|
124
|
+
const idx = getHighestImportanceDriver(drivers);
|
|
125
|
+
if (idx !== null && idx >= 0) {
|
|
126
|
+
setSelectedDriver(drivers[idx]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, [drivers]);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
visibleDriversRef.current = otherDrivers;
|
|
133
|
+
}, [otherDrivers]);
|
|
134
|
+
|
|
135
|
+
const handleKeyDown = useCallback(
|
|
136
|
+
(e: KeyboardEvent) => {
|
|
137
|
+
const items = visibleDriversRef.current;
|
|
138
|
+
|
|
139
|
+
if (!selectedDriver || items.length === 0 || e.metaKey || e.ctrlKey)
|
|
140
|
+
return;
|
|
141
|
+
|
|
142
|
+
const stop = () => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const currIndex = items.findIndex(d => d.id === selectedDriver.id);
|
|
148
|
+
|
|
149
|
+
if (e.key === 'ArrowLeft') {
|
|
150
|
+
const prevIndex = currIndex > 0 ? currIndex - 1 : items.length - 1;
|
|
151
|
+
|
|
152
|
+
setSelectedDriver(items[prevIndex]);
|
|
153
|
+
stop();
|
|
154
|
+
} else if (e.key === 'ArrowRight') {
|
|
155
|
+
const nextIndex = currIndex < items.length - 1 ? currIndex + 1 : 0;
|
|
156
|
+
|
|
157
|
+
setSelectedDriver(items[nextIndex]);
|
|
158
|
+
stop();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
[selectedDriver, setSelectedDriver],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
useEvent({
|
|
165
|
+
event: 'keydown',
|
|
166
|
+
callback: handleKeyDown,
|
|
167
|
+
isCapture: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className={cn(S.root, isTransitioning && S.inTransition)}>
|
|
172
|
+
<div className={S.mapInner}>
|
|
173
|
+
<WorldMap className={S.mapWorld} />
|
|
174
|
+
{isLoading && <Shimmer className={S.shimmerOverlay} size="l" />}
|
|
175
|
+
{otherDrivers.map(driver => (
|
|
176
|
+
<DriverIcon
|
|
177
|
+
key={driver.id}
|
|
178
|
+
isLoading={isLoading}
|
|
179
|
+
isVisible={isVisible}
|
|
180
|
+
driver={driver}
|
|
181
|
+
size={badgeSizes[driver.id] || 's'}
|
|
182
|
+
isSelected={selectedDriver?.id === driver.id}
|
|
183
|
+
onClick={() => setSelectedDriver(driver)}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
<div className={S.worldDrivers}>
|
|
188
|
+
{worldDrivers.map(driver => {
|
|
189
|
+
const driverWithCoords = {
|
|
190
|
+
...driver,
|
|
191
|
+
coordinates: driver.coordinates || {
|
|
192
|
+
x: 0,
|
|
193
|
+
y: 0,
|
|
194
|
+
continent: 'Global',
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
return (
|
|
198
|
+
<DriverIcon
|
|
199
|
+
key={driver.id}
|
|
200
|
+
isLoading={isLoading}
|
|
201
|
+
isVisible={isVisible}
|
|
202
|
+
driver={driverWithCoords}
|
|
203
|
+
size={badgeSizes[driver.id] || 's'}
|
|
204
|
+
isSelected={selectedDriver?.id === driver.id}
|
|
205
|
+
onClick={() => setSelectedDriver(driver)}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|