@matthitachi/react-heatmap 1.0.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/Heatmap.tsx +378 -0
- package/README.md +342 -0
- package/index.ts +41 -0
- package/package.json +52 -0
- package/styles.module.scss +215 -0
- package/types.ts +153 -0
package/Heatmap.tsx
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
HeatmapProps,
|
|
4
|
+
HeatmapValue,
|
|
5
|
+
ProcessedWeekData,
|
|
6
|
+
ColorScale,
|
|
7
|
+
} from './types';
|
|
8
|
+
import styles from './styles.module.scss';
|
|
9
|
+
|
|
10
|
+
// Default color scale (GitHub-style greens)
|
|
11
|
+
const DEFAULT_COLOR_SCALE: ColorScale = {
|
|
12
|
+
0: '#ebedf0',
|
|
13
|
+
2: '#9be9a8',
|
|
14
|
+
5: '#40c463',
|
|
15
|
+
10: '#30a14e',
|
|
16
|
+
20: '#216e39',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
20
|
+
const DEFAULT_WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Heatmap - A flexible, customizable calendar heatmap component
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <Heatmap
|
|
28
|
+
* values={[{ date: '2024-01-01', count: 5 }, { date: '2024-01-02', count: 12 }]}
|
|
29
|
+
* startDate="2024-01-01"
|
|
30
|
+
* endDate="2024-12-31"
|
|
31
|
+
* showMonthLabels
|
|
32
|
+
* showWeekdayLabels
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const Heatmap: React.FC<HeatmapProps> = ({
|
|
37
|
+
values,
|
|
38
|
+
startDate,
|
|
39
|
+
endDate,
|
|
40
|
+
showMonthLabels = true,
|
|
41
|
+
showWeekdayLabels = true,
|
|
42
|
+
monthPlacement = 'top',
|
|
43
|
+
weekStart = 0,
|
|
44
|
+
showLegend = true,
|
|
45
|
+
emptyMessage = 'No data available for the selected period.',
|
|
46
|
+
cellSize = 12,
|
|
47
|
+
cellGap = 3,
|
|
48
|
+
cellRadius = 2,
|
|
49
|
+
colorScale = DEFAULT_COLOR_SCALE,
|
|
50
|
+
surface = 'light',
|
|
51
|
+
className,
|
|
52
|
+
classNames = {},
|
|
53
|
+
style,
|
|
54
|
+
styles: customStyles = {},
|
|
55
|
+
tooltipFormatter,
|
|
56
|
+
onClick,
|
|
57
|
+
onMouseOver,
|
|
58
|
+
onMouseLeave,
|
|
59
|
+
monthLabels = DEFAULT_MONTH_LABELS,
|
|
60
|
+
weekdayLabels = DEFAULT_WEEKDAY_LABELS,
|
|
61
|
+
}) => {
|
|
62
|
+
const [hoveredCell, setHoveredCell] = useState<{
|
|
63
|
+
value: HeatmapValue;
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
} | null>(null);
|
|
67
|
+
|
|
68
|
+
// Get color based on count and color scale
|
|
69
|
+
const getColor = (count: number, maxCount: number): string => {
|
|
70
|
+
if (count === 0) return colorScale[0] || DEFAULT_COLOR_SCALE[0];
|
|
71
|
+
|
|
72
|
+
const thresholds = Object.keys(colorScale)
|
|
73
|
+
.map(Number)
|
|
74
|
+
.sort((a, b) => b - a);
|
|
75
|
+
|
|
76
|
+
for (const threshold of thresholds) {
|
|
77
|
+
if (count >= threshold) {
|
|
78
|
+
return colorScale[threshold];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return colorScale[0] || DEFAULT_COLOR_SCALE[0];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Process data into weeks
|
|
86
|
+
const { weeks, maxCount } = useMemo(() => {
|
|
87
|
+
if (!values || values.length === 0) {
|
|
88
|
+
return { weeks: [], maxCount: 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Find max count for color scaling
|
|
92
|
+
const max = Math.max(...values.map((v) => v.count));
|
|
93
|
+
|
|
94
|
+
// Create a map of dates to counts
|
|
95
|
+
const dateMap = new Map(values.map((v) => [v.date, v.count]));
|
|
96
|
+
|
|
97
|
+
// Parse start and end dates
|
|
98
|
+
const minDate = new Date(startDate);
|
|
99
|
+
const maxDate = new Date(endDate);
|
|
100
|
+
|
|
101
|
+
// Adjust to week boundaries based on weekStart
|
|
102
|
+
const startWeekDate = new Date(minDate);
|
|
103
|
+
const dayOffset = (startWeekDate.getDay() - weekStart + 7) % 7;
|
|
104
|
+
startWeekDate.setDate(startWeekDate.getDate() - dayOffset);
|
|
105
|
+
|
|
106
|
+
const endWeekDate = new Date(maxDate);
|
|
107
|
+
const endDayOffset = (6 - (endWeekDate.getDay() - weekStart + 7) % 7);
|
|
108
|
+
endWeekDate.setDate(endWeekDate.getDate() + endDayOffset);
|
|
109
|
+
|
|
110
|
+
// Generate all weeks
|
|
111
|
+
const weeksData: ProcessedWeekData[][] = [];
|
|
112
|
+
let currentWeek: ProcessedWeekData[] = [];
|
|
113
|
+
const currentDate = new Date(startWeekDate);
|
|
114
|
+
|
|
115
|
+
while (currentDate <= endWeekDate) {
|
|
116
|
+
const dateStr = currentDate.toISOString().split('T')[0];
|
|
117
|
+
const mapValue = dateMap.get(dateStr);
|
|
118
|
+
const count: number = typeof mapValue === 'number' ? mapValue : 0;
|
|
119
|
+
|
|
120
|
+
currentWeek.push({
|
|
121
|
+
date: new Date(currentDate),
|
|
122
|
+
count,
|
|
123
|
+
dateStr,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (currentWeek.length === 7) {
|
|
127
|
+
weeksData.push(currentWeek);
|
|
128
|
+
currentWeek = [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (currentWeek.length > 0) {
|
|
135
|
+
weeksData.push(currentWeek);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { weeks: weeksData, maxCount: max };
|
|
139
|
+
}, [values, startDate, endDate, weekStart]);
|
|
140
|
+
|
|
141
|
+
// Calculate month positions for labels
|
|
142
|
+
const monthPositions = useMemo(() => {
|
|
143
|
+
const positions: { month: string; left: number }[] = [];
|
|
144
|
+
let lastMonth = -1;
|
|
145
|
+
|
|
146
|
+
weeks.forEach((week, weekIndex) => {
|
|
147
|
+
const firstDay = week[0];
|
|
148
|
+
const month = firstDay.date.getMonth();
|
|
149
|
+
|
|
150
|
+
if (month !== lastMonth) {
|
|
151
|
+
// Skip the first month label if it's a partial week at the beginning
|
|
152
|
+
if (weekIndex === 0) {
|
|
153
|
+
const daysInCurrentMonth = week.filter((day) => day.date.getMonth() === month).length;
|
|
154
|
+
if (daysInCurrentMonth < 4) {
|
|
155
|
+
lastMonth = month;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
positions.push({
|
|
161
|
+
month: monthLabels[month],
|
|
162
|
+
left: weekIndex * (cellSize + cellGap),
|
|
163
|
+
});
|
|
164
|
+
lastMonth = month;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return positions;
|
|
169
|
+
}, [weeks, monthLabels, cellSize, cellGap]);
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// Default tooltip formatter
|
|
173
|
+
const defaultTooltipFormatter = (value: HeatmapValue): string => {
|
|
174
|
+
const count = value.count;
|
|
175
|
+
const label = count === 1 ? 'activity' : 'activities';
|
|
176
|
+
return `${count} ${label} on ${value.date}`;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const formatTooltip = tooltipFormatter || defaultTooltipFormatter;
|
|
180
|
+
|
|
181
|
+
// Handle cell interactions
|
|
182
|
+
const handleCellMouseEnter = (day: ProcessedWeekData, event: React.MouseEvent<HTMLDivElement>) => {
|
|
183
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
184
|
+
const value: HeatmapValue = { date: day.dateStr, count: day.count };
|
|
185
|
+
|
|
186
|
+
setHoveredCell({
|
|
187
|
+
value,
|
|
188
|
+
x: rect.left + rect.width / 2,
|
|
189
|
+
y: rect.top,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (onMouseOver) {
|
|
193
|
+
onMouseOver(value, event);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleCellMouseLeave = (day: ProcessedWeekData, event: React.MouseEvent<HTMLDivElement>) => {
|
|
198
|
+
setHoveredCell(null);
|
|
199
|
+
|
|
200
|
+
if (onMouseLeave) {
|
|
201
|
+
const value: HeatmapValue = { date: day.dateStr, count: day.count };
|
|
202
|
+
onMouseLeave(null, event);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleCellClick = (day: ProcessedWeekData, event: React.MouseEvent<HTMLDivElement>) => {
|
|
207
|
+
if (onClick) {
|
|
208
|
+
const value: HeatmapValue = { date: day.dateStr, count: day.count };
|
|
209
|
+
onClick(value, event);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Empty state
|
|
214
|
+
if (!values || values.length === 0) {
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
className={`${styles.heatmapEmptyState} ${styles[surface]} ${classNames.emptyState || ''} ${className || ''}`}
|
|
218
|
+
style={{ ...customStyles.emptyState, ...style }}
|
|
219
|
+
>
|
|
220
|
+
<p>{emptyMessage}</p>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get color scale values for legend
|
|
226
|
+
const colorScaleValues = Object.keys(colorScale)
|
|
227
|
+
.map(Number)
|
|
228
|
+
.sort((a, b) => a - b);
|
|
229
|
+
|
|
230
|
+
const legendColors = colorScaleValues.map((threshold) => colorScale[threshold]);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
className={`${styles.heatmapContainer} ${styles[surface]} ${classNames.container || ''} ${className || ''}`}
|
|
235
|
+
style={{ ...customStyles.container, ...style }}
|
|
236
|
+
>
|
|
237
|
+
<div
|
|
238
|
+
className={`${styles.heatmapWrapper} ${classNames.wrapper || ''}`}
|
|
239
|
+
style={customStyles.wrapper}
|
|
240
|
+
>
|
|
241
|
+
<div
|
|
242
|
+
className={`${styles.heatmap} ${classNames.heatmap || ''}`}
|
|
243
|
+
style={{
|
|
244
|
+
paddingTop: showMonthLabels && monthPlacement === 'top' ? 20 : 0,
|
|
245
|
+
paddingBottom: showMonthLabels && monthPlacement === 'bottom' ? 20 : 0,
|
|
246
|
+
...customStyles.heatmap,
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
{/* Month labels */}
|
|
250
|
+
{showMonthLabels && (
|
|
251
|
+
<div
|
|
252
|
+
className={`${styles.monthLabels} ${styles[`monthLabels-${monthPlacement}`]} ${classNames.monthLabels || ''}`}
|
|
253
|
+
style={{
|
|
254
|
+
[monthPlacement]: 0,
|
|
255
|
+
left: showWeekdayLabels ? 50 : 0,
|
|
256
|
+
...customStyles.monthLabels,
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{monthPositions.map((pos, index) => (
|
|
260
|
+
<div
|
|
261
|
+
key={index}
|
|
262
|
+
className={`${styles.monthLabel} ${classNames.monthLabel || ''}`}
|
|
263
|
+
style={{ left: pos.left, ...customStyles.monthLabel }}
|
|
264
|
+
>
|
|
265
|
+
{pos.month}
|
|
266
|
+
</div>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{/* Weekday labels */}
|
|
272
|
+
{showWeekdayLabels && (
|
|
273
|
+
<div
|
|
274
|
+
className={`${styles.weekdayLabels} ${classNames.weekdayLabels || ''}`}
|
|
275
|
+
style={customStyles.weekdayLabels}
|
|
276
|
+
>
|
|
277
|
+
{weekdayLabels.map((day, i) => {
|
|
278
|
+
// Adjust index based on weekStart
|
|
279
|
+
const adjustedIndex = (i + weekStart) % 7;
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
key={day}
|
|
283
|
+
className={`${styles.weekdayLabel} ${classNames.weekdayLabel || ''}`}
|
|
284
|
+
style={{
|
|
285
|
+
height: cellSize,
|
|
286
|
+
lineHeight: `${cellSize}px`,
|
|
287
|
+
...customStyles.weekdayLabel,
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
{i % 2 === 1 ? weekdayLabels[adjustedIndex] : ''}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
})}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* Heatmap grid */}
|
|
298
|
+
<div
|
|
299
|
+
className={`${styles.grid} ${classNames.grid || ''}`}
|
|
300
|
+
style={{ gap: cellGap, ...customStyles.grid }}
|
|
301
|
+
>
|
|
302
|
+
{weeks.map((week, weekIndex) => (
|
|
303
|
+
<div
|
|
304
|
+
key={weekIndex}
|
|
305
|
+
className={`${styles.week} ${classNames.week || ''}`}
|
|
306
|
+
style={{ gap: cellGap, ...customStyles.week }}
|
|
307
|
+
>
|
|
308
|
+
{week.map((day, dayIndex) => {
|
|
309
|
+
const color = getColor(day.count, maxCount);
|
|
310
|
+
const isActive = day.count > 0;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div
|
|
314
|
+
key={`${weekIndex}-${dayIndex}`}
|
|
315
|
+
className={`${styles.cell} ${isActive ? styles.cellActive : styles.cellEmpty} ${
|
|
316
|
+
classNames.cell || ''
|
|
317
|
+
} ${isActive ? classNames.cellActive || '' : classNames.cellEmpty || ''}`}
|
|
318
|
+
style={{
|
|
319
|
+
backgroundColor: color,
|
|
320
|
+
width: cellSize,
|
|
321
|
+
height: cellSize,
|
|
322
|
+
borderRadius: cellRadius,
|
|
323
|
+
marginBottom: cellGap,
|
|
324
|
+
cursor: onClick ? 'pointer' : 'default',
|
|
325
|
+
...customStyles.cell,
|
|
326
|
+
}}
|
|
327
|
+
onMouseEnter={(e) => handleCellMouseEnter(day, e)}
|
|
328
|
+
onMouseLeave={(e) => handleCellMouseLeave(day, e)}
|
|
329
|
+
onClick={(e) => handleCellClick(day, e)}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
})}
|
|
333
|
+
</div>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Tooltip */}
|
|
340
|
+
{hoveredCell && (
|
|
341
|
+
<div
|
|
342
|
+
className={`${styles.tooltip} ${classNames.tooltip || ''}`}
|
|
343
|
+
style={{
|
|
344
|
+
left: hoveredCell.x,
|
|
345
|
+
top: hoveredCell.y - 10,
|
|
346
|
+
...customStyles.tooltip,
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
{formatTooltip(hoveredCell.value)}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{/* Legend */}
|
|
354
|
+
{showLegend && (
|
|
355
|
+
<div
|
|
356
|
+
className={`${styles.legend} ${classNames.legend || ''}`}
|
|
357
|
+
style={customStyles.legend}
|
|
358
|
+
>
|
|
359
|
+
<span>Less</span>
|
|
360
|
+
<div className={`${styles.legendColors} ${classNames.legendColors || ''}`}>
|
|
361
|
+
{legendColors.map((color, index) => (
|
|
362
|
+
<span
|
|
363
|
+
key={index}
|
|
364
|
+
className={`${styles.legendBox} ${classNames.legendBox || ''}`}
|
|
365
|
+
style={{ backgroundColor: color }}
|
|
366
|
+
/>
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
<span>More</span>
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export default Heatmap;
|
|
377
|
+
|
|
378
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Heatmap Component
|
|
2
|
+
|
|
3
|
+
A flexible, customizable calendar heatmap component for React, inspired by GitHub's contribution graph.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📅 **Flexible Date Ranges** - Display any date range with automatic week alignment
|
|
8
|
+
- 🎨 **Customizable Colors** - Define your own color scales and thresholds
|
|
9
|
+
- 🌓 **Light/Dark Mode** - Built-in theme support
|
|
10
|
+
- 📱 **Responsive** - Horizontal scrolling for large date ranges
|
|
11
|
+
- 🎯 **Interactive** - Click and hover handlers for cells
|
|
12
|
+
- 🔧 **Highly Customizable** - Override styles and classes for any element
|
|
13
|
+
- 📊 **Smart Labels** - Automatic month label positioning with overlap prevention
|
|
14
|
+
- 🌍 **Week Start Options** - Start weeks on any day (Sunday, Monday, etc.)
|
|
15
|
+
- 💡 **TypeScript** - Full TypeScript support with exported types
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# If publishing as npm package
|
|
21
|
+
npm install @your-org/react-heatmap
|
|
22
|
+
|
|
23
|
+
# Or copy the component files to your project
|
|
24
|
+
cp -r components/common/Heatmap /your-project/components/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Heatmap } from '@/components/common/Heatmap';
|
|
31
|
+
|
|
32
|
+
function MyComponent() {
|
|
33
|
+
const data = [
|
|
34
|
+
{ date: '2024-01-01', count: 5 },
|
|
35
|
+
{ date: '2024-01-02', count: 12 },
|
|
36
|
+
{ date: '2024-01-03', count: 0 },
|
|
37
|
+
// ... more data
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Heatmap
|
|
42
|
+
values={data}
|
|
43
|
+
startDate="2024-01-01"
|
|
44
|
+
endDate="2024-12-31"
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Props API
|
|
51
|
+
|
|
52
|
+
### Data Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Required | Default | Description |
|
|
55
|
+
|------|------|----------|---------|-------------|
|
|
56
|
+
| `values` | `HeatmapValue[]` | ✅ | - | Array of `{ date: string, count: number }` objects |
|
|
57
|
+
| `startDate` | `string` | ✅ | - | Start date in ISO format (YYYY-MM-DD) |
|
|
58
|
+
| `endDate` | `string` | ✅ | - | End date in ISO format (YYYY-MM-DD) |
|
|
59
|
+
|
|
60
|
+
### Display Options
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Required | Default | Description |
|
|
63
|
+
|------|------|----------|---------|-------------|
|
|
64
|
+
| `showMonthLabels` | `boolean` | ❌ | `true` | Show month labels |
|
|
65
|
+
| `showWeekdayLabels` | `boolean` | ❌ | `true` | Show weekday labels |
|
|
66
|
+
| `monthPlacement` | `'top' \| 'bottom'` | ❌ | `'top'` | Position of month labels |
|
|
67
|
+
| `weekStart` | `0-6` | ❌ | `0` | Starting day of week (0=Sunday, 1=Monday, etc.) |
|
|
68
|
+
| `showLegend` | `boolean` | ❌ | `true` | Show color legend |
|
|
69
|
+
| `emptyMessage` | `string` | ❌ | `'No data available...'` | Message shown when no data |
|
|
70
|
+
|
|
71
|
+
### Sizing Props
|
|
72
|
+
|
|
73
|
+
| Prop | Type | Required | Default | Description |
|
|
74
|
+
|------|------|----------|---------|-------------|
|
|
75
|
+
| `cellSize` | `number` | ❌ | `12` | Size of each cell in pixels |
|
|
76
|
+
| `cellGap` | `number` | ❌ | `3` | Gap between cells in pixels |
|
|
77
|
+
| `cellRadius` | `number` | ❌ | `2` | Border radius for cells in pixels |
|
|
78
|
+
|
|
79
|
+
### Color Props
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Required | Default | Description |
|
|
82
|
+
|------|------|----------|---------|-------------|
|
|
83
|
+
| `colorScale` | `ColorScale` | ❌ | GitHub greens | Color thresholds object |
|
|
84
|
+
| `surface` | `'light' \| 'dark'` | ❌ | `'light'` | Theme mode |
|
|
85
|
+
|
|
86
|
+
### Styling Props
|
|
87
|
+
|
|
88
|
+
| Prop | Type | Required | Default | Description |
|
|
89
|
+
|------|------|----------|---------|-------------|
|
|
90
|
+
| `className` | `string` | ❌ | - | Custom class for container |
|
|
91
|
+
| `classNames` | `HeatmapClassNames` | ❌ | - | Override classes for elements |
|
|
92
|
+
| `style` | `CSSProperties` | ❌ | - | Inline styles for container |
|
|
93
|
+
| `styles` | `HeatmapStyles` | ❌ | - | Override styles for elements |
|
|
94
|
+
|
|
95
|
+
### Interaction Props
|
|
96
|
+
|
|
97
|
+
| Prop | Type | Required | Default | Description |
|
|
98
|
+
|------|------|----------|---------|-------------|
|
|
99
|
+
| `tooltipFormatter` | `(value) => string` | ❌ | Default formatter | Custom tooltip content |
|
|
100
|
+
| `onClick` | `(value, event) => void` | ❌ | - | Cell click handler |
|
|
101
|
+
| `onMouseOver` | `(value, event) => void` | ❌ | - | Cell hover handler |
|
|
102
|
+
| `onMouseLeave` | `(value, event) => void` | ❌ | - | Cell leave handler |
|
|
103
|
+
|
|
104
|
+
### Label Props
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Required | Default | Description |
|
|
107
|
+
|------|------|----------|---------|-------------|
|
|
108
|
+
| `monthLabels` | `string[]` | ❌ | `['Jan', 'Feb', ...]` | Custom month labels |
|
|
109
|
+
| `weekdayLabels` | `string[]` | ❌ | `['Sun', 'Mon', ...]` | Custom weekday labels |
|
|
110
|
+
|
|
111
|
+
## Advanced Examples
|
|
112
|
+
|
|
113
|
+
### Custom Color Scale
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<Heatmap
|
|
117
|
+
values={data}
|
|
118
|
+
startDate="2024-01-01"
|
|
119
|
+
endDate="2024-12-31"
|
|
120
|
+
colorScale={{
|
|
121
|
+
0: '#f0f0f0',
|
|
122
|
+
5: '#c6e48b',
|
|
123
|
+
10: '#7bc96f',
|
|
124
|
+
15: '#239a3b',
|
|
125
|
+
20: '#196127',
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Week Starting on Monday
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<Heatmap
|
|
134
|
+
values={data}
|
|
135
|
+
startDate="2024-01-01"
|
|
136
|
+
endDate="2024-12-31"
|
|
137
|
+
weekStart={1}
|
|
138
|
+
weekdayLabels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}
|
|
139
|
+
/>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Custom Tooltip
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
<Heatmap
|
|
146
|
+
values={data}
|
|
147
|
+
startDate="2024-01-01"
|
|
148
|
+
endDate="2024-12-31"
|
|
149
|
+
tooltipFormatter={(value) =>
|
|
150
|
+
`${value.count} contributions on ${new Date(value.date).toLocaleDateString()}`
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Dark Mode
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<Heatmap
|
|
159
|
+
values={data}
|
|
160
|
+
startDate="2024-01-01"
|
|
161
|
+
endDate="2024-12-31"
|
|
162
|
+
surface="dark"
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Month Labels at Bottom
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<Heatmap
|
|
170
|
+
values={data}
|
|
171
|
+
startDate="2024-01-01"
|
|
172
|
+
endDate="2024-12-31"
|
|
173
|
+
monthPlacement="bottom"
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Minimal Heatmap (No Labels or Legend)
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
<Heatmap
|
|
181
|
+
values={data}
|
|
182
|
+
startDate="2024-01-01"
|
|
183
|
+
endDate="2024-12-31"
|
|
184
|
+
showMonthLabels={false}
|
|
185
|
+
showWeekdayLabels={false}
|
|
186
|
+
showLegend={false}
|
|
187
|
+
/>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## TypeScript Types
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
interface HeatmapValue {
|
|
194
|
+
date: string; // ISO format: YYYY-MM-DD
|
|
195
|
+
count: number;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface ColorScale {
|
|
199
|
+
[threshold: number]: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type TooltipFormatter = (value: HeatmapValue) => string;
|
|
203
|
+
type CellClickHandler = (value: HeatmapValue, event: MouseEvent<HTMLDivElement>) => void;
|
|
204
|
+
type CellHoverHandler = (value: HeatmapValue | null, event: MouseEvent<HTMLDivElement>) => void;
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Customization Guide
|
|
208
|
+
|
|
209
|
+
### Override Individual Element Classes
|
|
210
|
+
|
|
211
|
+
The `classNames` prop allows you to override classes for specific elements:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
<Heatmap
|
|
215
|
+
values={data}
|
|
216
|
+
startDate="2024-01-01"
|
|
217
|
+
endDate="2024-12-31"
|
|
218
|
+
classNames={{
|
|
219
|
+
container: 'my-container',
|
|
220
|
+
cell: 'my-cell',
|
|
221
|
+
cellActive: 'my-active-cell',
|
|
222
|
+
cellEmpty: 'my-empty-cell',
|
|
223
|
+
tooltip: 'my-tooltip',
|
|
224
|
+
legend: 'my-legend',
|
|
225
|
+
monthLabel: 'my-month-label',
|
|
226
|
+
weekdayLabel: 'my-weekday-label',
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Override Individual Element Styles
|
|
232
|
+
|
|
233
|
+
The `styles` prop allows you to override inline styles:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
<Heatmap
|
|
237
|
+
values={data}
|
|
238
|
+
startDate="2024-01-01"
|
|
239
|
+
endDate="2024-12-31"
|
|
240
|
+
styles={{
|
|
241
|
+
container: { backgroundColor: '#f5f5f5', padding: '20px' },
|
|
242
|
+
cell: { borderRadius: '4px' },
|
|
243
|
+
tooltip: { fontSize: '14px', padding: '8px 12px' },
|
|
244
|
+
legend: { justifyContent: 'center' },
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Default Color Scale
|
|
250
|
+
|
|
251
|
+
The default color scale uses GitHub-style greens:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
{
|
|
255
|
+
0: '#ebedf0', // Empty/no activity
|
|
256
|
+
2: '#9be9a8', // Low activity
|
|
257
|
+
5: '#40c463', // Medium activity
|
|
258
|
+
10: '#30a14e', // High activity
|
|
259
|
+
20: '#216e39', // Very high activity
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Browser Support
|
|
264
|
+
|
|
265
|
+
- Chrome (latest)
|
|
266
|
+
- Firefox (latest)
|
|
267
|
+
- Safari (latest)
|
|
268
|
+
- Edge (latest)
|
|
269
|
+
|
|
270
|
+
## Performance Considerations
|
|
271
|
+
|
|
272
|
+
- The component efficiently handles large date ranges (multiple years)
|
|
273
|
+
- Uses `useMemo` for expensive calculations
|
|
274
|
+
- Horizontal scrolling for wide heatmaps
|
|
275
|
+
- Optimized re-renders with proper dependency arrays
|
|
276
|
+
|
|
277
|
+
## Accessibility
|
|
278
|
+
|
|
279
|
+
- Semantic HTML structure
|
|
280
|
+
- Keyboard navigation support (when click handlers are provided)
|
|
281
|
+
- ARIA labels can be added via `classNames` and custom attributes
|
|
282
|
+
- High contrast mode compatible
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|
|
287
|
+
|
|
288
|
+
## Credits
|
|
289
|
+
|
|
290
|
+
Inspired by:
|
|
291
|
+
- [GitHub's contribution graph](https://github.com)
|
|
292
|
+
- [react-calendar-heatmap](https://github.com/kevinsqi/react-calendar-heatmap)
|
|
293
|
+
- [@uiw/react-heat-map](https://github.com/uiwjs/react-heat-map)
|
|
294
|
+
|
|
295
|
+
## Contributing
|
|
296
|
+
|
|
297
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
298
|
+
|
|
299
|
+
## Changelog
|
|
300
|
+
|
|
301
|
+
### v1.0.0
|
|
302
|
+
- Initial release
|
|
303
|
+
- Full TypeScript support
|
|
304
|
+
- Customizable color scales
|
|
305
|
+
- Light/Dark mode
|
|
306
|
+
- Interactive tooltips
|
|
307
|
+
- Click and hover handlers
|
|
308
|
+
- Smart month label positioning
|
|
309
|
+
- Week start customization
|
|
310
|
+
|
|
311
|
+
### Click Handler
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<Heatmap
|
|
315
|
+
values={data}
|
|
316
|
+
startDate="2024-01-01"
|
|
317
|
+
endDate="2024-12-31"
|
|
318
|
+
onClick={(value, event) => {
|
|
319
|
+
console.log(`Clicked on ${value.date} with ${value.count} activities`);
|
|
320
|
+
}}
|
|
321
|
+
/>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Custom Styling
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
<Heatmap
|
|
328
|
+
values={data}
|
|
329
|
+
startDate="2024-01-01"
|
|
330
|
+
endDate="2024-12-31"
|
|
331
|
+
className="my-custom-heatmap"
|
|
332
|
+
classNames={{
|
|
333
|
+
cell: 'custom-cell',
|
|
334
|
+
tooltip: 'custom-tooltip',
|
|
335
|
+
}}
|
|
336
|
+
styles={{
|
|
337
|
+
container: { padding: '24px' },
|
|
338
|
+
cell: { borderRadius: '50%' },
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
```
|
|
342
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heatmap Component Package
|
|
3
|
+
*
|
|
4
|
+
* A flexible, customizable calendar heatmap component for React
|
|
5
|
+
* Inspired by GitHub's contribution graph
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { Heatmap } from '@/components/common/Heatmap';
|
|
10
|
+
*
|
|
11
|
+
* function MyComponent() {
|
|
12
|
+
* return (
|
|
13
|
+
* <Heatmap
|
|
14
|
+
* values={[
|
|
15
|
+
* { date: '2024-01-01', count: 5 },
|
|
16
|
+
* { date: '2024-01-02', count: 12 }
|
|
17
|
+
* ]}
|
|
18
|
+
* startDate="2024-01-01"
|
|
19
|
+
* endDate="2024-12-31"
|
|
20
|
+
* showMonthLabels
|
|
21
|
+
* showWeekdayLabels
|
|
22
|
+
* showLegend
|
|
23
|
+
* />
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export { Heatmap, default } from './Heatmap';
|
|
30
|
+
export type {
|
|
31
|
+
HeatmapValue,
|
|
32
|
+
HeatmapProps,
|
|
33
|
+
HeatmapClassNames,
|
|
34
|
+
HeatmapStyles,
|
|
35
|
+
ColorScale,
|
|
36
|
+
TooltipFormatter,
|
|
37
|
+
CellClickHandler,
|
|
38
|
+
CellHoverHandler,
|
|
39
|
+
ProcessedWeekData,
|
|
40
|
+
} from './types';
|
|
41
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matthitachi/react-heatmap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A flexible, customizable calendar heatmap component for React, inspired by GitHub's contribution graph",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"types": "types.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"format": "prettier --write ."
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react",
|
|
15
|
+
"heatmap",
|
|
16
|
+
"calendar",
|
|
17
|
+
"contribution-graph",
|
|
18
|
+
"github",
|
|
19
|
+
"visualization",
|
|
20
|
+
"chart",
|
|
21
|
+
"typescript",
|
|
22
|
+
"component"
|
|
23
|
+
],
|
|
24
|
+
"author": "Matthre Adiorho",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
28
|
+
"react-dom": "^17.0.0 || ^18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^18.0.0",
|
|
32
|
+
"@types/react-dom": "^18.0.0",
|
|
33
|
+
"typescript": "^5.0.0",
|
|
34
|
+
"sass": "^1.60.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/matthitachi/react-heatmap.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/matthitachi/react-heatmap/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/matthitachi/react-heatmap#readme",
|
|
44
|
+
"files": [
|
|
45
|
+
"Heatmap.tsx",
|
|
46
|
+
"types.ts",
|
|
47
|
+
"styles.module.scss",
|
|
48
|
+
"index.ts",
|
|
49
|
+
"README.md"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Heatmap Component Styles
|
|
2
|
+
// Framework-agnostic, reusable styles
|
|
3
|
+
|
|
4
|
+
.heatmapContainer {
|
|
5
|
+
width: 100%;
|
|
6
|
+
padding: 16px;
|
|
7
|
+
border-radius: 8px;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 16px;
|
|
11
|
+
position: relative;
|
|
12
|
+
|
|
13
|
+
// Light mode (default)
|
|
14
|
+
&.light {
|
|
15
|
+
background-color: #ffffff;
|
|
16
|
+
|
|
17
|
+
.weekdayLabel,
|
|
18
|
+
.monthLabel {
|
|
19
|
+
color: #666;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.cell {
|
|
23
|
+
border: 1px solid rgba(27, 31, 35, 0.06);
|
|
24
|
+
|
|
25
|
+
&:hover {
|
|
26
|
+
border-color: rgba(27, 31, 35, 0.15);
|
|
27
|
+
transform: scale(1.1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.legend {
|
|
32
|
+
color: #666;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.legendBox {
|
|
36
|
+
border: 1px solid rgba(27, 31, 35, 0.06);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Dark mode
|
|
41
|
+
&.dark {
|
|
42
|
+
background-color: rgba(225, 223, 246, 0.15);
|
|
43
|
+
|
|
44
|
+
.weekdayLabel,
|
|
45
|
+
.monthLabel {
|
|
46
|
+
color: rgba(255, 255, 255, 0.5);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.cell {
|
|
50
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
51
|
+
|
|
52
|
+
&:hover {
|
|
53
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
54
|
+
transform: scale(1.1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.legend {
|
|
59
|
+
color: rgba(255, 255, 255, 0.7);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.legendBox {
|
|
63
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.tooltip {
|
|
67
|
+
background-color: rgba(0, 0, 0, 0.95);
|
|
68
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.heatmapWrapper {
|
|
74
|
+
width: 100%;
|
|
75
|
+
overflow-x: auto;
|
|
76
|
+
overflow-y: hidden;
|
|
77
|
+
padding: 10px 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.heatmap {
|
|
81
|
+
display: flex;
|
|
82
|
+
gap: 10px;
|
|
83
|
+
min-width: fit-content;
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.monthLabels {
|
|
88
|
+
position: absolute;
|
|
89
|
+
left: 50px;
|
|
90
|
+
height: 18px;
|
|
91
|
+
display: flex;
|
|
92
|
+
font-size: 11px;
|
|
93
|
+
|
|
94
|
+
&.monthLabels-top {
|
|
95
|
+
top: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&.monthLabels-bottom {
|
|
99
|
+
bottom: 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.monthLabel {
|
|
104
|
+
position: absolute;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.weekdayLabels {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
gap: 3px;
|
|
112
|
+
padding-right: 5px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.weekdayLabel {
|
|
116
|
+
font-size: 10px;
|
|
117
|
+
text-align: right;
|
|
118
|
+
padding-right: 5px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.grid {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: 3px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.week {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cell {
|
|
133
|
+
border-radius: 2px;
|
|
134
|
+
transition: all 0.2s ease;
|
|
135
|
+
|
|
136
|
+
&.cellActive {
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&.cellEmpty {
|
|
141
|
+
opacity: 0.5;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.tooltip {
|
|
146
|
+
position: fixed;
|
|
147
|
+
transform: translate(-50%, -100%);
|
|
148
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
149
|
+
color: white;
|
|
150
|
+
padding: 6px 10px;
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
pointer-events: none;
|
|
155
|
+
z-index: 1000;
|
|
156
|
+
|
|
157
|
+
&::after {
|
|
158
|
+
content: '';
|
|
159
|
+
position: absolute;
|
|
160
|
+
top: 100%;
|
|
161
|
+
left: 50%;
|
|
162
|
+
transform: translateX(-50%);
|
|
163
|
+
border: 5px solid transparent;
|
|
164
|
+
border-top-color: rgba(0, 0, 0, 0.9);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.legend {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: flex-end;
|
|
172
|
+
gap: 4px;
|
|
173
|
+
font-size: 12px;
|
|
174
|
+
margin-top: 8px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.legendColors {
|
|
178
|
+
display: flex;
|
|
179
|
+
gap: 2px;
|
|
180
|
+
margin: 0 4px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.legendBox {
|
|
184
|
+
width: 12px;
|
|
185
|
+
height: 12px;
|
|
186
|
+
border-radius: 2px;
|
|
187
|
+
display: inline-block;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.heatmapEmptyState {
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
min-height: 200px;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
padding: 20px;
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
|
|
199
|
+
p {
|
|
200
|
+
margin: 0;
|
|
201
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
|
202
|
+
'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
&.light {
|
|
206
|
+
background-color: #ffffff;
|
|
207
|
+
color: #666;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
&.dark {
|
|
211
|
+
background-color: rgba(225, 223, 246, 0.15);
|
|
212
|
+
color: rgba(255, 255, 255, 0.6);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
package/types.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { CSSProperties, MouseEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Data point for a single day in the heatmap
|
|
5
|
+
*/
|
|
6
|
+
export interface HeatmapValue {
|
|
7
|
+
/** Date in ISO format (YYYY-MM-DD) */
|
|
8
|
+
date: string;
|
|
9
|
+
/** Activity count for this date */
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Color scale configuration
|
|
15
|
+
* Maps threshold values to colors
|
|
16
|
+
*/
|
|
17
|
+
export interface ColorScale {
|
|
18
|
+
[threshold: number]: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Custom class names for heatmap elements
|
|
23
|
+
*/
|
|
24
|
+
export interface HeatmapClassNames {
|
|
25
|
+
container?: string;
|
|
26
|
+
wrapper?: string;
|
|
27
|
+
heatmap?: string;
|
|
28
|
+
monthLabels?: string;
|
|
29
|
+
monthLabel?: string;
|
|
30
|
+
weekdayLabels?: string;
|
|
31
|
+
weekdayLabel?: string;
|
|
32
|
+
grid?: string;
|
|
33
|
+
week?: string;
|
|
34
|
+
cell?: string;
|
|
35
|
+
cellEmpty?: string;
|
|
36
|
+
cellActive?: string;
|
|
37
|
+
tooltip?: string;
|
|
38
|
+
legend?: string;
|
|
39
|
+
legendColors?: string;
|
|
40
|
+
legendBox?: string;
|
|
41
|
+
emptyState?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Custom inline styles for heatmap elements
|
|
46
|
+
*/
|
|
47
|
+
export interface HeatmapStyles {
|
|
48
|
+
container?: CSSProperties;
|
|
49
|
+
wrapper?: CSSProperties;
|
|
50
|
+
heatmap?: CSSProperties;
|
|
51
|
+
monthLabels?: CSSProperties;
|
|
52
|
+
monthLabel?: CSSProperties;
|
|
53
|
+
weekdayLabels?: CSSProperties;
|
|
54
|
+
weekdayLabel?: CSSProperties;
|
|
55
|
+
grid?: CSSProperties;
|
|
56
|
+
week?: CSSProperties;
|
|
57
|
+
cell?: CSSProperties;
|
|
58
|
+
tooltip?: CSSProperties;
|
|
59
|
+
legend?: CSSProperties;
|
|
60
|
+
emptyState?: CSSProperties;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tooltip formatter function
|
|
65
|
+
*/
|
|
66
|
+
export type TooltipFormatter = (value: HeatmapValue) => string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cell click handler
|
|
70
|
+
*/
|
|
71
|
+
export type CellClickHandler = (value: HeatmapValue, event: MouseEvent<HTMLDivElement>) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cell hover handlers
|
|
75
|
+
*/
|
|
76
|
+
export type CellHoverHandler = (value: HeatmapValue | null, event: MouseEvent<HTMLDivElement>) => void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Main props for the Heatmap component
|
|
80
|
+
*/
|
|
81
|
+
export interface HeatmapProps {
|
|
82
|
+
// Data props
|
|
83
|
+
/** Array of data points with date and count */
|
|
84
|
+
values: HeatmapValue[];
|
|
85
|
+
/** Start date for the heatmap (ISO format YYYY-MM-DD) */
|
|
86
|
+
startDate: string;
|
|
87
|
+
/** End date for the heatmap (ISO format YYYY-MM-DD) */
|
|
88
|
+
endDate: string;
|
|
89
|
+
|
|
90
|
+
// Display options
|
|
91
|
+
/** Show month labels at top or bottom (default: true) */
|
|
92
|
+
showMonthLabels?: boolean;
|
|
93
|
+
/** Show weekday labels on the left (default: true) */
|
|
94
|
+
showWeekdayLabels?: boolean;
|
|
95
|
+
/** Position of month labels (default: 'top') */
|
|
96
|
+
monthPlacement?: 'top' | 'bottom';
|
|
97
|
+
/** Starting day of the week: 0 (Sunday) to 6 (Saturday) (default: 0) */
|
|
98
|
+
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
99
|
+
/** Show legend (default: true) */
|
|
100
|
+
showLegend?: boolean;
|
|
101
|
+
/** Custom empty state message */
|
|
102
|
+
emptyMessage?: string;
|
|
103
|
+
|
|
104
|
+
// Sizing props
|
|
105
|
+
/** Size of each cell in pixels (default: 12) */
|
|
106
|
+
cellSize?: number;
|
|
107
|
+
/** Gap between cells in pixels (default: 3) */
|
|
108
|
+
cellGap?: number;
|
|
109
|
+
/** Border radius for cells in pixels (default: 2) */
|
|
110
|
+
cellRadius?: number;
|
|
111
|
+
|
|
112
|
+
// Color props
|
|
113
|
+
/** Color scale with thresholds (default: GitHub-style greens) */
|
|
114
|
+
colorScale?: ColorScale;
|
|
115
|
+
/** Theme mode (default: 'light') */
|
|
116
|
+
surface?: 'light' | 'dark';
|
|
117
|
+
|
|
118
|
+
// Styling props
|
|
119
|
+
/** Custom class name for the container */
|
|
120
|
+
className?: string;
|
|
121
|
+
/** Custom class names for individual elements */
|
|
122
|
+
classNames?: HeatmapClassNames;
|
|
123
|
+
/** Inline styles for the container */
|
|
124
|
+
style?: CSSProperties;
|
|
125
|
+
/** Inline styles for individual elements */
|
|
126
|
+
styles?: HeatmapStyles;
|
|
127
|
+
|
|
128
|
+
// Interaction props
|
|
129
|
+
/** Custom tooltip formatter function */
|
|
130
|
+
tooltipFormatter?: TooltipFormatter;
|
|
131
|
+
/** Click handler for cells */
|
|
132
|
+
onClick?: CellClickHandler;
|
|
133
|
+
/** Mouse enter handler for cells */
|
|
134
|
+
onMouseOver?: CellHoverHandler;
|
|
135
|
+
/** Mouse leave handler for cells */
|
|
136
|
+
onMouseLeave?: CellHoverHandler;
|
|
137
|
+
|
|
138
|
+
// Labels
|
|
139
|
+
/** Custom month labels (default: ['Jan', 'Feb', ...]) */
|
|
140
|
+
monthLabels?: string[];
|
|
141
|
+
/** Custom weekday labels (default: ['Sun', 'Mon', ...]) */
|
|
142
|
+
weekdayLabels?: string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Internal processed data structure
|
|
147
|
+
*/
|
|
148
|
+
export interface ProcessedWeekData {
|
|
149
|
+
date: Date;
|
|
150
|
+
count: number;
|
|
151
|
+
dateStr: string;
|
|
152
|
+
}
|
|
153
|
+
|