@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 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
+