@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.
Files changed (58) hide show
  1. package/package.json +13 -6
  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/vite.config.mts +4 -0
  41. package/src/components/charts/echarts/EChartsArea.stories.tsx +0 -139
  42. package/src/components/charts/echarts/EChartsArea.tsx +0 -139
  43. package/src/components/charts/echarts/EChartsBar.stories.tsx +0 -141
  44. package/src/components/charts/echarts/EChartsBar.tsx +0 -133
  45. package/src/components/charts/echarts/EChartsBase.tsx +0 -264
  46. package/src/components/charts/echarts/EChartsFunnel.stories.tsx +0 -164
  47. package/src/components/charts/echarts/EChartsFunnel.tsx +0 -114
  48. package/src/components/charts/echarts/EChartsHeatmap.stories.tsx +0 -168
  49. package/src/components/charts/echarts/EChartsHeatmap.tsx +0 -141
  50. package/src/components/charts/echarts/EChartsLine.stories.tsx +0 -132
  51. package/src/components/charts/echarts/EChartsLine.tsx +0 -111
  52. package/src/components/charts/echarts/EChartsPie.stories.tsx +0 -131
  53. package/src/components/charts/echarts/EChartsPie.tsx +0 -124
  54. package/src/components/charts/echarts/EChartsRadialBar.stories.tsx +0 -124
  55. package/src/components/charts/echarts/EChartsRadialBar.tsx +0 -118
  56. package/src/components/charts/echarts/EChartsScatter.stories.tsx +0 -166
  57. package/src/components/charts/echarts/EChartsScatter.tsx +0 -135
  58. package/src/components/charts/echarts/index.ts +0 -26
@@ -13,156 +13,12 @@ import { useCallback } from 'react';
13
13
  import { localPoint } from '@visx/event';
14
14
  import { ZoomControls } from '../ZoomControls';
15
15
  import { alpha2ToAlpha3 } from './iso3166-alpha2-to-alpha3';
16
- import { Flag } from '../../visual/Flag';
17
-
18
- const alpha3ToAlpha2: Record<string, string> = Object.entries(alpha2ToAlpha3).reduce(
19
- (acc, [alpha2, alpha3]) => {
20
- acc[alpha3] = alpha2;
21
- return acc;
22
- },
23
- {} as Record<string, string>,
24
- );
25
-
26
- const SidebarContainer = styled.div`
27
- width: 320px;
28
- min-width: 320px;
29
- flex-shrink: 0;
30
- height: 100%;
31
- overflow: auto;
32
- background-color: ${({ theme }) => theme.colors.backgroundAlt};
33
- border: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
34
- border-radius: ${({ theme }) => theme.borderRadius.medium};
35
- padding: ${({ theme }) => theme.spacing['3']};
36
- margin-left: ${({ theme }) => theme.spacing['4']};
37
- `;
38
-
39
- const SidebarTitle = styled.h3`
40
- margin: 0 0 ${({ theme }) => theme.spacing['2']} 0;
41
- font-size: ${({ theme }) => theme.fontSize.tiny};
42
- font-weight: 500;
43
- color: ${({ theme }) => theme.colors.textAlt};
44
- text-transform: uppercase;
45
- letter-spacing: 0.5px;
46
- `;
47
-
48
- const CountryGrid = styled.div`
49
- display: grid;
50
- grid-template-columns: 1fr 1fr;
51
- gap: ${({ theme }) => theme.spacing['2']};
52
- `;
53
-
54
- const CountryTable = styled.div`
55
- display: table;
56
- width: 100%;
57
- font-size: ${({ theme }) => theme.fontSize.small};
58
- border-collapse: collapse;
59
- `;
60
-
61
- const CountryRow = styled.div`
62
- display: table-row;
63
-
64
- &:hover {
65
- background-color: ${({ theme }) => theme.colors.backgroundAccent};
66
- }
67
-
68
- &:last-child > div {
69
- border-bottom: none;
70
- }
71
- `;
72
-
73
- const CountryCell = styled.div`
74
- display: table-cell;
75
- padding: 2px 0;
76
- vertical-align: middle;
77
- border-bottom: 1px solid ${({ theme }) => theme.colors.backgroundAccent};
78
- color: ${({ theme }) => theme.colors.text};
79
- line-height: 1.3;
80
- `;
81
-
82
- const FlagCell = styled(CountryCell)`
83
- width: 20px;
84
- text-align: center;
85
- padding-right: 4px;
86
- `;
87
-
88
- const CountryCell2 = styled(CountryCell)`
89
- padding-right: 4px;
90
- font-weight: 500;
91
- font-size: ${({ theme }) => theme.fontSize.tiny};
92
- `;
93
-
94
- const CountCell = styled(CountryCell)`
95
- text-align: right;
96
- font-weight: 600;
97
- color: ${({ theme }) => theme.colors.primary};
98
- width: 20px;
99
- font-size: ${({ theme }) => theme.fontSize.tiny};
100
- `;
101
-
102
- const FlexContainer = styled.div`
103
- display: flex;
104
- flex-direction: row;
105
- align-items: stretch;
106
- position: relative;
107
- height: 100%;
108
- width: 100%;
109
- `;
110
-
111
- const MapContainer = styled.div`
112
- flex: 1;
113
- position: relative;
114
- `;
115
16
 
116
17
  const StyledTooltip = styled(Tooltip)`
117
18
  text-align: center;
118
19
  transform: translate(-50%);
119
20
  `;
120
21
 
121
- interface CountrySidebarProps<T> {
122
- data: T[];
123
- xAccessor: (d: T) => string;
124
- yAccessor: (d: T) => number;
125
- }
126
-
127
- const CountrySidebar = <T,>({ data, xAccessor, yAccessor }: CountrySidebarProps<T>) => {
128
- const sortedData = [...data].sort((a, b) => yAccessor(b) - yAccessor(a));
129
-
130
- // Split data into two columns
131
- const midPoint = Math.ceil(sortedData.length / 2);
132
- const leftColumnData = sortedData.slice(0, midPoint);
133
- const rightColumnData = sortedData.slice(midPoint);
134
-
135
- const renderCountryTable = (columnData: T[]) => (
136
- <CountryTable>
137
- {columnData.map((item, index) => {
138
- const countryCode = xAccessor(item);
139
- const playerCount = yAccessor(item);
140
- const alpha2Code = countryCode.length === 3 ? alpha3ToAlpha2[countryCode] : countryCode;
141
- // Prefer 2-letter country codes for display if available
142
- const displayCode = alpha2Code || countryCode;
143
-
144
- return (
145
- <CountryRow key={`${countryCode}-${index}`}>
146
- <FlagCell>{alpha2Code && <Flag countryCode={alpha2Code} size={1} />}</FlagCell>
147
- <CountryCell2>{displayCode}</CountryCell2>
148
- <CountCell>{playerCount}</CountCell>
149
- </CountryRow>
150
- );
151
- })}
152
- </CountryTable>
153
- );
154
-
155
- return (
156
- <SidebarContainer>
157
- <SidebarTitle>Countries ({sortedData.length})</SidebarTitle>
158
- <CountryGrid>
159
- {renderCountryTable(leftColumnData)}
160
- {renderCountryTable(rightColumnData)}
161
- </CountryGrid>
162
- </SidebarContainer>
163
- );
164
- };
165
-
166
22
  interface FeatureShape {
167
23
  type: 'Feature';
168
24
  id: string;
@@ -186,7 +42,6 @@ export interface GeoMercatorProps<T> {
186
42
  tooltipAccessor?: (d: T) => string;
187
43
  showZoomControls?: boolean;
188
44
  allowZoomAndDrag?: boolean;
189
- showCountrySidebar?: boolean;
190
45
  }
191
46
 
192
47
  type InnerGeoMercator<T> = InnerChartProps & GeoMercatorProps<T>;
@@ -200,7 +55,6 @@ export const GeoMercator = <T,>({
200
55
  tooltipAccessor,
201
56
  showZoomControls = false,
202
57
  allowZoomAndDrag = false,
203
- showCountrySidebar = false,
204
58
  }: GeoMercatorProps<T>) => {
205
59
  return (
206
60
  <ParentSize>
@@ -216,7 +70,6 @@ export const GeoMercator = <T,>({
216
70
  allowZoomAndDrag={allowZoomAndDrag}
217
71
  showZoomControls={showZoomControls}
218
72
  tooltipAccessor={tooltipAccessor}
219
- showCountrySidebar={showCountrySidebar}
220
73
  />
221
74
  )}
222
75
  </ParentSize>
@@ -233,20 +86,13 @@ const Chart = <T,>({
233
86
  name,
234
87
  allowZoomAndDrag,
235
88
  showZoomControls,
236
- showCountrySidebar,
237
89
  }: InnerGeoMercator<T>) => {
238
90
  const theme = useTheme();
239
91
  const { hideTooltip, showTooltip, tooltipData, tooltipLeft = 0, tooltipTop = 0 } = useTooltip<T>();
240
92
 
241
- // Calculate sidebar width including margin
242
- const sidebarTotalWidth = showCountrySidebar ? 320 + parseInt(theme.spacing['4']) : 0;
243
-
244
- // Adjust map width to account for sidebar
245
- const mapWidth = width - sidebarTotalWidth;
246
-
247
- const centerX = mapWidth / 2;
93
+ const centerX = width / 2;
248
94
  const centerY = height / 2;
249
- const scale = Math.min(mapWidth, height) * 0.25;
95
+ const scale = Math.min(width, height) * 0.25;
250
96
 
251
97
  const colorScale = scaleLinear({
252
98
  domain: [0, Math.max(...data.map((d) => yAccessor(d)))],
@@ -277,7 +123,7 @@ const Chart = <T,>({
277
123
  <svg
278
124
  id={name}
279
125
  name={name}
280
- width={mapWidth}
126
+ width={width}
281
127
  height={height}
282
128
  ref={zoom?.containerRef}
283
129
  style={{
@@ -285,7 +131,7 @@ const Chart = <T,>({
285
131
  cursor: allowZoomAndDrag && zoom ? (zoom.isDragging ? 'grabbing' : 'grab') : 'default',
286
132
  }}
287
133
  >
288
- <rect x={0} y={0} width={mapWidth} height={height} fill={theme.colors.background} rx={10} />
134
+ <rect x={0} y={0} width={width} height={height} fill={theme.colors.background} rx={10} />
289
135
  <Mercator<FeatureShape>
290
136
  data={world.features}
291
137
  scale={zoom?.transformMatrix.scaleX || scale}
@@ -327,7 +173,7 @@ const Chart = <T,>({
327
173
  <rect
328
174
  x={0}
329
175
  y={0}
330
- width={mapWidth}
176
+ width={width}
331
177
  height={height}
332
178
  rx={14}
333
179
  fill="transparent"
@@ -346,23 +192,20 @@ const Chart = <T,>({
346
192
  );
347
193
 
348
194
  const renderContent = (zoomProps?: any) => (
349
- <FlexContainer>
350
- <MapContainer>
351
- {renderMap(zoomProps)}
352
- {showZoomControls && zoomProps && <ZoomControls zoom={zoomProps} />}
353
- {tooltipData && (
354
- <StyledTooltip top={tooltipTop} left={tooltipLeft} style={getDefaultTooltipStyles(theme)}>
355
- {tooltipAccessor ? tooltipAccessor(tooltipData) : `${xAccessor(tooltipData)}: ${yAccessor(tooltipData)}`}
356
- </StyledTooltip>
357
- )}
358
- </MapContainer>
359
- {showCountrySidebar && <CountrySidebar data={data} xAccessor={xAccessor} yAccessor={yAccessor} />}
360
- </FlexContainer>
195
+ <>
196
+ {renderMap(zoomProps)}
197
+ {showZoomControls && zoomProps && <ZoomControls zoom={zoomProps} />}
198
+ {tooltipData && (
199
+ <StyledTooltip top={tooltipTop} left={tooltipLeft} style={getDefaultTooltipStyles(theme)}>
200
+ {tooltipAccessor ? tooltipAccessor(tooltipData) : `${xAccessor(tooltipData)}: ${yAccessor(tooltipData)}`}
201
+ </StyledTooltip>
202
+ )}
203
+ </>
361
204
  );
362
205
 
363
206
  return allowZoomAndDrag ? (
364
207
  <Zoom<SVGSVGElement>
365
- width={mapWidth}
208
+ width={width}
366
209
  height={height}
367
210
  scaleXMin={100}
368
211
  scaleXMax={1000}
@@ -2,53 +2,187 @@ import React from 'react';
2
2
  import { HeatMap, HeatmapProps } from '.';
3
3
  import { Meta, StoryFn } from '@storybook/react';
4
4
  import { styled } from '../../../styled';
5
- import { DateTime } from 'luxon';
6
5
 
7
6
  export default {
8
7
  title: 'Charts/Heatmap',
9
8
  component: HeatMap,
10
- } as Meta<HeatmapProps<number>>;
9
+ args: {
10
+ showMonthLabels: true,
11
+ showDayLabels: true,
12
+ },
13
+ argTypes: {
14
+ showMonthLabels: { control: 'boolean' },
15
+ showDayLabels: { control: 'boolean' },
16
+ },
17
+ } as Meta<HeatmapProps<ContributionData>>;
11
18
 
12
19
  const Wrapper = styled.div`
13
- height: 250px;
14
- width: 250px;
20
+ height: 200px;
21
+ width: 100%;
15
22
  `;
16
23
 
17
- interface Data {
18
- timestamp: string;
19
- value: boolean;
24
+ interface ContributionData {
25
+ date: Date;
26
+ count: number;
20
27
  }
21
28
 
22
- // this represents a month of a player's online status
23
- const data: Data[] = Array.from({ length: 30 }, (_, i) => ({
24
- timestamp: DateTime.local().minus({ days: i }).toISODate()!,
25
- value: Math.random() > 0.5,
26
- }));
27
-
28
- export const Default: StoryFn<HeatmapProps<number>> = () => {
29
- // Extract the day of the week from the timestamp (0 for Sunday, 6 for Saturday)
30
- const yAccessor = (d: Data) => DateTime.fromISO(d.timestamp).weekday;
31
- // Extract the week of the month from the timestamp
32
- const xAccessor = (d: Data): number => Math.floor(DateTime.fromISO(d.timestamp).day / 7);
33
- // Convert boolean value to 0 or 1 because the heatmap expects a number
34
- const zAccessor = (d: Data) => (d.value ? 1 : 0);
35
-
36
- const tooltipAccessor = (d: Data) => {
37
- const date = DateTime.fromISO(d.timestamp);
38
- // format: "Sun, 10 jan: online
39
- return `${date.weekdayShort}, ${date.day} ${date.monthShort}: ${d.value ? 'online' : 'offline'}`;
40
- };
29
+ // Generate realistic contribution data (GitHub-style)
30
+ const generateContributionData = (): ContributionData[] => {
31
+ const data: ContributionData[] = [];
32
+ const endDate = new Date();
33
+ const startDate = new Date();
34
+ startDate.setDate(startDate.getDate() - 364); // 1 year ago
35
+
36
+ // Generate contributions with realistic patterns
37
+ for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
38
+ const dayOfWeek = d.getDay();
39
+ const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
40
+
41
+ // Simulate realistic patterns:
42
+ // - Less activity on weekends
43
+ // - Random spikes in activity
44
+ // - Some periods of no activity
45
+ let count = 0;
46
+
47
+ if (Math.random() > 0.3) {
48
+ if (isWeekend) {
49
+ count = Math.floor(Math.random() * 5);
50
+ } else {
51
+ count = Math.floor(Math.random() * 15);
52
+ }
53
+ if (Math.random() > 0.9) {
54
+ count += Math.floor(Math.random() * 20);
55
+ }
56
+ }
57
+
58
+ data.push({
59
+ date: new Date(d),
60
+ count,
61
+ });
62
+ }
63
+
64
+ return data;
65
+ };
66
+
67
+ export const Default: StoryFn<HeatmapProps<ContributionData>> = (args) => {
68
+ const data = React.useMemo(() => generateContributionData(), []);
41
69
 
42
70
  return (
43
71
  <Wrapper>
44
- <HeatMap<Data>
72
+ <HeatMap<ContributionData>
73
+ name="contributions"
74
+ data={data}
75
+ dateAccessor={(d) => d.date}
76
+ valueAccessor={(d) => d.count}
77
+ showMonthLabels={args.showMonthLabels}
78
+ showDayLabels={args.showDayLabels}
79
+ />
80
+ </Wrapper>
81
+ );
82
+ };
83
+
84
+ export const LastThreeMonths: StoryFn<HeatmapProps<ContributionData>> = (args) => {
85
+ const data = React.useMemo(() => {
86
+ const result: ContributionData[] = [];
87
+ const endDate = new Date();
88
+ const startDate = new Date();
89
+ startDate.setMonth(startDate.getMonth() - 3);
90
+
91
+ for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
92
+ result.push({
93
+ date: new Date(d),
94
+ count: Math.floor(Math.random() * 10),
95
+ });
96
+ }
97
+
98
+ return result;
99
+ }, []);
100
+
101
+ const startDate = new Date();
102
+ startDate.setMonth(startDate.getMonth() - 3);
103
+
104
+ return (
105
+ <Wrapper>
106
+ <HeatMap<ContributionData>
107
+ name="contributions-3months"
108
+ data={data}
109
+ dateAccessor={(d) => d.date}
110
+ valueAccessor={(d) => d.count}
111
+ startDate={startDate}
112
+ endDate={new Date()}
113
+ showMonthLabels={args.showMonthLabels}
114
+ showDayLabels={args.showDayLabels}
115
+ />
116
+ </Wrapper>
117
+ );
118
+ };
119
+
120
+ // Data interface for categorical mode (24h x 7-day)
121
+ interface ActivityData {
122
+ hour: number; // 0-23
123
+ day: number; // 0-6 (Mon-Sun)
124
+ value: number;
125
+ }
126
+
127
+ // Generate realistic 24h x 7-day activity data
128
+ const generate24x7Data = (): ActivityData[] => {
129
+ const data: ActivityData[] = [];
130
+
131
+ for (let day = 0; day < 7; day++) {
132
+ for (let hour = 0; hour < 24; hour++) {
133
+ // Base activity level
134
+ let value = 5;
135
+
136
+ // Peak hours (lunch and evening)
137
+ if (hour >= 11 && hour <= 13) value = 40; // Lunch peak
138
+ if (hour >= 18 && hour <= 21) value = 60; // Evening peak
139
+
140
+ // Night hours (low activity)
141
+ if (hour >= 0 && hour <= 6) value = 2;
142
+
143
+ // Weekend boost
144
+ if (day === 5 || day === 6) value *= 1.5;
145
+
146
+ // Friday evening extra boost
147
+ if (day === 4 && hour >= 18) value *= 1.3;
148
+
149
+ // Add some randomness
150
+ value = Math.floor(value + Math.random() * 20 - 10);
151
+ value = Math.max(0, value); // Ensure non-negative
152
+
153
+ data.push({ hour, day, value });
154
+ }
155
+ }
156
+
157
+ return data;
158
+ };
159
+
160
+ export const WeeklyActivity: StoryFn<HeatmapProps<ActivityData>> = (args) => {
161
+ const data = React.useMemo(() => generate24x7Data(), []);
162
+
163
+ const hourLabels = Array.from({ length: 24 }, (_, i) =>
164
+ i === 0 ? '12am' : i < 12 ? `${i}am` : i === 12 ? '12pm' : `${i - 12}pm`,
165
+ );
166
+
167
+ const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
168
+
169
+ return (
170
+ <Wrapper style={{ height: '250px' }}>
171
+ <HeatMap<ActivityData>
172
+ name="weekly-activity"
45
173
  data={data}
46
- xAccessor={xAccessor}
47
- yAccessor={yAccessor}
48
- zAccessor={zAccessor}
49
- tooltipAccessor={tooltipAccessor}
50
- variant="month"
51
- name="online"
174
+ xAccessor={(d) => d.hour}
175
+ yAccessor={(d) => d.day}
176
+ valueAccessor={(d) => d.value}
177
+ xCategories={hourLabels}
178
+ yCategories={dayLabels}
179
+ showDayLabels={args.showDayLabels}
180
+ tooltip={{
181
+ accessor: (d) => {
182
+ const activityData = d as ActivityData;
183
+ return `${dayLabels[activityData.day]}, ${hourLabels[activityData.hour]}\n${activityData.value} activities`;
184
+ },
185
+ }}
52
186
  />
53
187
  </Wrapper>
54
188
  );