@thilakbhat/heatmap-ui 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/dist/calendar.d.ts +40 -0
  4. package/dist/calendar.d.ts.map +1 -0
  5. package/dist/calendar.js +136 -0
  6. package/dist/calendar.js.map +1 -0
  7. package/dist/colors.d.ts +112 -0
  8. package/dist/colors.d.ts.map +1 -0
  9. package/dist/colors.js +186 -0
  10. package/dist/colors.js.map +1 -0
  11. package/dist/grid.d.ts +26 -0
  12. package/dist/grid.d.ts.map +1 -0
  13. package/dist/grid.js +102 -0
  14. package/dist/grid.js.map +1 -0
  15. package/dist/heatmap.d.ts +4 -0
  16. package/dist/heatmap.d.ts.map +1 -0
  17. package/dist/heatmap.js +134 -0
  18. package/dist/heatmap.js.map +1 -0
  19. package/dist/heatmap3d-patterns.d.ts +19 -0
  20. package/dist/heatmap3d-patterns.d.ts.map +1 -0
  21. package/dist/heatmap3d-patterns.js +57 -0
  22. package/dist/heatmap3d-patterns.js.map +1 -0
  23. package/dist/heatmap3d-scene.d.ts +49 -0
  24. package/dist/heatmap3d-scene.d.ts.map +1 -0
  25. package/dist/heatmap3d-scene.js +104 -0
  26. package/dist/heatmap3d-scene.js.map +1 -0
  27. package/dist/heatmap3d-visuals.d.ts +30 -0
  28. package/dist/heatmap3d-visuals.d.ts.map +1 -0
  29. package/dist/heatmap3d-visuals.js +50 -0
  30. package/dist/heatmap3d-visuals.js.map +1 -0
  31. package/dist/heatmap3d.d.ts +6 -0
  32. package/dist/heatmap3d.d.ts.map +1 -0
  33. package/dist/heatmap3d.js +268 -0
  34. package/dist/heatmap3d.js.map +1 -0
  35. package/dist/index.d.ts +11 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +11 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/projection3d.d.ts +29 -0
  40. package/dist/projection3d.d.ts.map +1 -0
  41. package/dist/projection3d.js +55 -0
  42. package/dist/projection3d.js.map +1 -0
  43. package/dist/scale.d.ts +21 -0
  44. package/dist/scale.d.ts.map +1 -0
  45. package/dist/scale.js +61 -0
  46. package/dist/scale.js.map +1 -0
  47. package/dist/shapes.d.ts +13 -0
  48. package/dist/shapes.d.ts.map +1 -0
  49. package/dist/shapes.js +63 -0
  50. package/dist/shapes.js.map +1 -0
  51. package/dist/static-svg.d.ts +45 -0
  52. package/dist/static-svg.d.ts.map +1 -0
  53. package/dist/static-svg.js +177 -0
  54. package/dist/static-svg.js.map +1 -0
  55. package/dist/types.d.ts +151 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +2 -0
  58. package/dist/types.js.map +1 -0
  59. package/docs/cover.png +0 -0
  60. package/docs/playground-2d.png +0 -0
  61. package/docs/playground-3d.png +0 -0
  62. package/package.json +61 -0
  63. package/src/styles.css +385 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thilak Bhat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,471 @@
1
+ # heatmap-ui
2
+
3
+ ![heatmap-ui playground preview](docs/cover.png)
4
+
5
+ [![CI](https://github.com/tb962/heatmap-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/tb962/heatmap-ui/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/@thilakbhat/heatmap-ui.svg)](https://www.npmjs.com/package/@thilakbhat/heatmap-ui)
7
+ [![license](https://img.shields.io/npm/l/@thilakbhat/heatmap-ui.svg)](LICENSE)
8
+
9
+ A headless heatmap toolkit for React. Eight cell shapes, an interactive 3D mode
10
+ that runs on plain SVG, and control over every colour, size and label on the
11
+ grid.
12
+
13
+ **[Try it in the playground →](https://tb962.github.io/heatmap-ui/)**
14
+
15
+ ```bash
16
+ npm install @thilakbhat/heatmap-ui
17
+ ```
18
+
19
+ ```tsx
20
+ import { CalendarHeatmap } from "@thilakbhat/heatmap-ui";
21
+ import "@thilakbhat/heatmap-ui/styles.css";
22
+
23
+ <CalendarHeatmap values={days} weeks={20} showLegend />;
24
+ ```
25
+
26
+ ## What you get
27
+
28
+ Five entry points: `Heatmap` for any grid, `CalendarHeatmap` for dates, their
29
+ `Heatmap3D` and `CalendarHeatmap3D` counterparts, and `renderHeatmap3DSvg`,
30
+ which returns an SVG string without React or a DOM.
31
+
32
+ Everything below is a prop. None of it needs a wrapper, a fork, or a stylesheet
33
+ override.
34
+
35
+ | | |
36
+ | --- | --- |
37
+ | Cell shapes | `rounded`, `square`, `circle`, `diamond`, `hexagon`, `plus`, `bar`, `ring` |
38
+ | 3D forms | `rectangle`, `circle` (cylinder), `bar` (slender column) |
39
+ | 3D block styles | `solid`, `lego` (studs), `building` (windowed facades) |
40
+ | 3D themes | `color` (your ramp), `night`, `seasonal`, `rainbow` |
41
+ | Surfaces | flat fills, or SVG bitmap patterns you define per colour level |
42
+ | Intensity | colour, cell size, or both |
43
+ | Shading | `linear`, `quantile`, `log`, or a function you supply |
44
+ | Colour | any ramp, palest first, with separate colours for zero and for no data |
45
+ | Labels | row and column labels, tooltips, and arbitrary content inside cells |
46
+ | Layout | cell size, gap, corner radius, and a predicate for hiding slots |
47
+
48
+ ### The 3D mode
49
+
50
+ `Heatmap3D` draws an isometric scene in SVG. No WebGL, no canvas, no 3D library,
51
+ and no second bundle: it ships in the same package as the flat grid and takes
52
+ the same data, labels, tooltips, click handlers and legend props.
53
+
54
+ Height comes from the value itself rather than from its colour band, so a 40
55
+ is twice as tall as a 20. Blocks can be plain solids, LEGO bricks with
56
+ studs sized to fit, or buildings with lit windows. You can drive the camera
57
+ yourself or let the reader drag it. If you would rather ship a picture,
58
+ `renderHeatmap3DSvg` renders the identical scene to a string for an email or a
59
+ README.
60
+
61
+ ### Customisation
62
+
63
+ Colour is a prop, not a theme file. Pass any ramp and the bands follow it. Pass
64
+ `faceColor` and you control the top, left and right fills of every 3D block
65
+ independently. Pass `patterns` and each colour level gets its own SVG bitmap,
66
+ written as binary or hexadecimal rows.
67
+
68
+ The stylesheet only handles structure and four CSS variables, so you are not
69
+ fighting a design system to make the chart look like yours.
70
+
71
+ ### Data that stays honest
72
+
73
+ A slot with no observation is not a slot with a zero. Pass `known: false` and it
74
+ renders at reduced opacity and announces "No data" to a screen reader, instead
75
+ of quietly reading as a day off. Ragged grids are supported through
76
+ `isSlotHidden`, so a cohort table missing its future quarters does not have to
77
+ invent them.
78
+
79
+ Colour alone is invisible to roughly one reader in twelve, so `encode="size"`
80
+ and `encode="both"` carry intensity redundantly.
81
+
82
+ ## The core
83
+
84
+ ```tsx
85
+ import { Heatmap } from "@thilakbhat/heatmap-ui";
86
+
87
+ <Heatmap rows={24} columns={7} values={matrix} />;
88
+ ```
89
+
90
+ You can pass `values` in either form:
91
+
92
+ ```ts
93
+ // Dense. null means no data; 0 means measured and empty.
94
+ values={[[3, 0, null], [1, 5, 2]]}
95
+
96
+ // Sparse. Anything not listed has no data.
97
+ values={[{ row: 0, column: 0, value: 3, known: true, meta: anything }]}
98
+ ```
99
+
100
+ | Prop | Default | |
101
+ | --- | --- | --- |
102
+ | `rows`, `columns` | required | Grid size. |
103
+ | `values` | `[]` | Matrix or sparse cells. |
104
+ | `scale` | `"linear"` | `"linear"`, `"quantile"`, `"log"`, or `(value, all) => 0..1`. |
105
+ | `levels` | `colors.length` | Number of shade bands. |
106
+ | `thresholds` | none | Explicit band ceilings; skips `scale`. |
107
+ | `shape` | `"rounded"` | See below. |
108
+ | `encode` | `"color"` | `"color"`, `"size"`, or `"both"`. |
109
+ | `cellSize` | `13` | Pixels. |
110
+ | `gap` | `3` | Pixels. |
111
+ | `radius` | none | Overrides the shape's corner rounding. |
112
+ | `colors` | GitHub green | The ramp, palest first. |
113
+ | `emptyColor` | `#ebedf0` | A known value of zero. |
114
+ | `unknownOpacity` | `0.5` | Applied to slots with no data. |
115
+ | `isSlotHidden` | none | `(row, column) => boolean` for ragged grids. |
116
+ | `rowLabels`, `columnLabels` | none | Positioned against the grid. |
117
+ | `tooltip` | none | `(cell) => ReactNode`; opens on hover and focus. |
118
+ | `cellContent` | none | Optional visual React node centred inside each cell; values are hidden unless provided. |
119
+ | `cellLabel` | none | `(cell) => string`; the accessible name for a cell. |
120
+ | `onCellClick` | none | Makes cells buttons. |
121
+ | `showLegend` | `false` | less/more key, plus "no data" when relevant. |
122
+
123
+ ### Shading
124
+
125
+ `scale="linear"` is the default. Bands are cut at even fractions of the largest
126
+ value, which is what GitHub and most other heatmaps do, so a reader who knows
127
+ one chart can read yours without relearning it.
128
+
129
+ Switch to `quantile` when the data has a long tail. Ranking the active values
130
+ keeps every band populated whatever the unit. On one real dataset, linear
131
+ banding put 58% of active days in the first band while quantile banding put 25%
132
+ in each:
133
+
134
+ ```
135
+ linear ████████████████████████████ ▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒ ░░░░░
136
+ quantile ████████████ ▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒ ░░░░░░░░░░░░
137
+ ```
138
+
139
+ `log` suits values spanning orders of magnitude. A function gets the value and
140
+ the full set and returns 0..1, and `thresholds` skips the whole question by
141
+ naming the band ceilings yourself.
142
+
143
+ ## 3D heatmaps
144
+
145
+ Use `Heatmap3D` for any grid, or `CalendarHeatmap3D` for dates. Both render a
146
+ shaded, interactive SVG scene without WebGL.
147
+
148
+ ```tsx
149
+ import { Heatmap3D, CalendarHeatmap3D } from "@thilakbhat/heatmap-ui";
150
+ import "@thilakbhat/heatmap-ui/styles.css";
151
+
152
+ <Heatmap3D
153
+ rows={2}
154
+ columns={3}
155
+ values={[[20, 40, 80], [0, null, 60]]}
156
+ shape="rectangle"
157
+ blockStyle="lego"
158
+ maxHeight={75}
159
+ showLegend
160
+ tooltip={cell => cell.known ? `${cell.value} events` : "No data"}
161
+ />;
162
+
163
+ <CalendarHeatmap3D values={days} weeks={20} blockStyle="building" />;
164
+ ```
165
+
166
+ ### Playground previews
167
+
168
+ **2D calendar view**
169
+
170
+ ![2D calendar heatmap preview in the playground.](docs/playground-2d.png)
171
+
172
+ **3D calendar view**
173
+
174
+ ![3D calendar heatmap preview in the playground, with height-based values and orbit controls.](docs/playground-3d.png)
175
+
176
+ Height is proportional to the actual value: with the default domain, 40 is
177
+ twice as tall as 20. The `scale`, `levels`, and `thresholds` props only affect
178
+ colour bands. LEGO studs fit inside that height. Measured zeroes are flat
179
+ tiles. Missing slots get an outline, and hidden slots are omitted.
180
+
181
+ | Prop | Default | Behaviour |
182
+ | --- | --- | --- |
183
+ | `shape` | `"rectangle"` | `"rectangle"`, `"circle"` (cylinder), or `"bar"` (slender column). |
184
+ | `blockStyle` | `"solid"` | `"solid"`, `"lego"` (studs), or `"building"` (windowed facades). |
185
+ | `theme` | `"color"` | `"color"` paints with `colors` and sets only the face-aware neutrals around it. `"night"`, `"seasonal"`, and `"rainbow"` bring their own ramp, so leave `colors` unset with those. |
186
+ | `material` | `"solid"` | `"solid"` or `"pattern"`; pattern material uses SVG bitmap marks over each face. |
187
+ | `patterns` | built-in | Optional `{ top, side }` arrays of `{ width, bitmap, background, foreground }`, indexed by colour level. |
188
+ | `faceColor` | preset shading | `(args) => string` resolver for different top, left, and right fills. |
189
+ | `animation` | `"none"` | `"grow"` adds a staggered entrance; reduced motion uses a fade instead. |
190
+ | `maxHeight` | `100` | Maximum elevation in the same grid units as cell size and gap. |
191
+ | `heightDomain` | `[0, largest positive value]` | Fixed numeric domain for comparisons across charts. Out-of-range values clamp. |
192
+ | `yaw`, `pitch`, `zoom` | `-35`, `38`, `1` | Camera orbit, elevation, and magnification. |
193
+ | `onCameraChange` | none | Receives `{ yaw, pitch, zoom }` after camera interactions. |
194
+ | `interactive` | `true` | Enables pointer dragging and keyboard camera control. |
195
+ | `showControls` | `true` | Shows zoom and camera-reset buttons. |
196
+
197
+ 3D accepts the same data, labels, tooltips, click callbacks, and legend props.
198
+ `encode`, `radius`, and `minScale` belong to the 2D component. Negative values
199
+ keep their labels but render at the zero plane. The 3D component is intended
200
+ for nonnegative activity and magnitude data.
201
+
202
+ Drag to orbit the scene. Focus the chart to use the arrow keys, `+` / `-`, or
203
+ `Home` to rotate, zoom, or reset it. Cells expose their values to assistive
204
+ technology. Click handlers also respond to Enter and Space.
205
+
206
+ ### Patterns, themes, and static SVG
207
+
208
+ The built-in patterns are small and repeatable. A row can be a number, a
209
+ hexadecimal string, or a binary string. The most-significant bit is drawn on
210
+ the left:
211
+
212
+ ```tsx
213
+ <Heatmap3D
214
+ rows={1}
215
+ columns={3}
216
+ values={[[2, 8, 20]]}
217
+ theme="night"
218
+ material="pattern"
219
+ animation="grow"
220
+ patterns={{
221
+ top: [{ width: 4, bitmap: ["1000", "0100", "0010", "0001"] }],
222
+ side: [{ width: 2, bitmap: ["10", "01"] }],
223
+ }}
224
+ />
225
+ ```
226
+
227
+ For an email, README image, or scheduled asset, use the same scene without React
228
+ or a DOM:
229
+
230
+ ```ts
231
+ import { renderHeatmap3DSvg } from "@thilakbhat/heatmap-ui";
232
+
233
+ const svg = renderHeatmap3DSvg({
234
+ rows: 1,
235
+ columns: 3,
236
+ values: [[2, 8, 20]],
237
+ theme: "seasonal",
238
+ material: "pattern",
239
+ showLegend: true,
240
+ });
241
+ ```
242
+
243
+ `renderHeatmap3DSvg` uses the same height domain, geometry, face visibility,
244
+ paint order, themes, and pattern definitions as `Heatmap3D`. It returns an
245
+ accessible, non-interactive SVG snapshot.
246
+
247
+ ## Shapes
248
+
249
+ | | |
250
+ | --- | --- |
251
+ | `rounded` `square` `circle` | The basic shapes. |
252
+ | `diamond` | A 45° square. Reads denser over long ranges. |
253
+ | `hexagon` | Offset rows, honeycomb. The standard form for hex-binned data. |
254
+ | `plus` | Stays legible at sizes where circles turn to mush. |
255
+ | `bar` | Height tracks the value; colour stays flat. |
256
+ | `ring` | Stroke thickness tracks the value. Works on any ground. |
257
+
258
+ ## Encoding without colour
259
+
260
+ Colour alone is not enough for roughly one reader in twelve.
261
+
262
+ ```tsx
263
+ <Heatmap encode="both" ... />
264
+ ```
265
+
266
+ With `"size"`, each cell scales within its slot, so intensity still reads in
267
+ greyscale. `"both"` uses colour and size. `minScale` sets the smallest size for
268
+ the weakest cell.
269
+
270
+ ### Values inside cells
271
+
272
+ Use `cellContent` for short values, counts, or other compact content. It is
273
+ separate from `cellLabel`, so a dense visual can still have a complete
274
+ accessible description:
275
+
276
+ ```tsx
277
+ <Heatmap
278
+ rows={7}
279
+ columns={20}
280
+ values={activity}
281
+ cellContent={(cell) => cell.known && cell.value > 0 ? cell.value : null}
282
+ cellLabel={(cell) => cell.known ? `${cell.value} events` : "No data"}
283
+ />
284
+ ```
285
+
286
+ The content is decorative to assistive technology; use `cellLabel` for its
287
+ meaning. This option belongs to the flat grid. The 3D component keeps values in
288
+ labels and tooltips, so it stays an SVG renderer without an HTML overlay.
289
+
290
+ ## Calendar
291
+
292
+ ```tsx
293
+ <CalendarHeatmap
294
+ values={[{ date: "2026-09-07", value: 12 }]}
295
+ to="2026-09-07"
296
+ weeks={20}
297
+ weekStart={1}
298
+ showMonthLabels
299
+ showWeekdayLabels
300
+ unitLabel="commits"
301
+ tooltip={(day) => `${day.date}: ${day.value}`}
302
+ />
303
+ ```
304
+
305
+ `weeks` defaults to 53 when omitted.
306
+
307
+ The calendar adapter handles date mapping, month labels, weekday labels, and a
308
+ default accessible name for each day. It hides days after `to` instead of
309
+ drawing them as missing, so a final partial column stays partial. Every
310
+ `Heatmap` prop passes through.
311
+
312
+ ## Recipes
313
+
314
+ Most heatmap variants are just different grid dimensions. Each example below
315
+ uses `Heatmap`; the playground can generate the same code from its current
316
+ settings.
317
+
318
+ ### Punchcard: hours down, weekdays across
319
+
320
+ ```tsx
321
+ const hourLabels = Array.from({ length: 24 }, (_, i) => (i % 6 === 0 ? `${i}:00` : ""));
322
+ const dayLabels = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
323
+ .map((text, column) => ({ column, text }));
324
+
325
+ <Heatmap
326
+ rows={24}
327
+ columns={7}
328
+ values={commits} // number[24][7]
329
+ rowLabels={hourLabels}
330
+ columnLabels={dayLabels}
331
+ ariaLabel="Commits by hour and weekday"
332
+ tooltip={(cell) => `${cell.row}:00 — ${cell.value}`}
333
+ />
334
+ ```
335
+
336
+ ### Cohort retention: a ragged grid
337
+
338
+ A cohort that signed up last week has no week-10 number until ten weeks pass.
339
+ That slot does not exist, so `isSlotHidden` removes it from the grid. This
340
+ differs from `known: false`, which represents an existing slot that was not
341
+ measured.
342
+
343
+ ```tsx
344
+ <Heatmap
345
+ rows={cohorts.length}
346
+ columns={12}
347
+ values={retention} // { row, column, value }[]
348
+ isSlotHidden={(row, column) => column >= cohorts.length - row}
349
+ rowLabels={cohorts} // ["Jul 6", "Jul 13", …]
350
+ columnLabels={Array.from({ length: 12 }, (_, column) => ({ column, text: `W${column}` }))}
351
+ ariaLabel="Retention by signup cohort"
352
+ tooltip={(cell) => `Week ${cell.column} — ${cell.value}% retained`}
353
+ />
354
+ ```
355
+
356
+ ### Uptime: wide and short, with days that were never collected
357
+
358
+ This is the other kind of gap: the slots exist, but nobody was watching them.
359
+
360
+ ```tsx
361
+ <Heatmap
362
+ rows={services.length}
363
+ columns={30}
364
+ values={uptime} // { row, column, value, known }[]
365
+ rowLabels={services} // ["api", "web", …]
366
+ columnLabels={dayLabels}
367
+ ariaLabel="Service uptime by day"
368
+ tooltip={(cell) =>
369
+ cell.known ? `${cell.value}% up` : "not monitored"}
370
+ />
371
+ ```
372
+
373
+ ### Co-occurrence: symmetric, diagonal removed
374
+
375
+ The diagonal compares each label with itself, so it adds noise at full strength.
376
+ Hide it instead of letting it dominate the scale.
377
+
378
+ ```tsx
379
+ <Heatmap
380
+ rows={topics.length}
381
+ columns={topics.length}
382
+ values={overlap} // symmetric: value(i, j) === value(j, i)
383
+ isSlotHidden={(row, column) => row === column}
384
+ rowLabels={topics}
385
+ columnLabels={topics.map((text, column) => ({ column, text }))}
386
+ ariaLabel="Label co-occurrence"
387
+ />
388
+ ```
389
+
390
+ ### A note on label density
391
+
392
+ `Heatmap` positions the labels you provide, but it cannot measure their width.
393
+ A label such as `"Wednesday"` can run into a neighbouring 16px column. Decide
394
+ how many labels fit in the calling code. Use shorter labels or thin them:
395
+
396
+ ```tsx
397
+ // Keep only as many labels as the column stride can hold.
398
+ function thinLabels(texts, stride) {
399
+ const widest = Math.max(...texts.map((t) => t.length)) * 6 + 8; // ~6px/char at 10px
400
+ const step = Math.max(1, Math.ceil(widest / stride));
401
+ return texts
402
+ .map((text, column) => ({ column, text }))
403
+ .filter((_, i) => i % step === 0);
404
+ }
405
+
406
+ columnLabels={thinLabels(dates, cellSize + gap)}
407
+ ```
408
+
409
+ `CalendarHeatmap` already thins month labels when one would land too close to
410
+ the previous label.
411
+
412
+ ## Accessibility
413
+
414
+ - The grid uses a labelled `group`, not `role="img"`, so the cells inside it can
415
+ still receive focus.
416
+ - Cells receive focus only when they carry information, such as a `cellLabel`,
417
+ `tooltip`, or `onCellClick`.
418
+ - Tooltips open on focus as well as hover, after a 300ms delay, and are wired
419
+ with `aria-describedby`.
420
+ - `encode` lets you carry intensity without relying on colour alone.
421
+ - Hover and focus scaling is dropped under `prefers-reduced-motion`.
422
+
423
+ ## Styling
424
+
425
+ The stylesheet handles structure. Props provide the colour, and the stylesheet
426
+ defines four variables you can override:
427
+
428
+ ```css
429
+ .heatmap {
430
+ --heatmap-tooltip-bg: …;
431
+ --heatmap-tooltip-text: …;
432
+ --heatmap-label: …;
433
+ --heatmap-cell-content-color: …;
434
+ }
435
+ ```
436
+
437
+ Tooltip colours follow `prefers-color-scheme`. Set
438
+ `data-heatmap-theme="light" | "dark"` to pin them.
439
+
440
+ ## Playground
441
+
442
+ **<https://tb962.github.io/heatmap-ui/>**. No install required.
443
+
444
+ To run it from your working copy:
445
+
446
+ ```bash
447
+ npm run build
448
+ npx serve .
449
+ ```
450
+
451
+ Open `examples/playground.html`. You can switch between 2D and 3D, choose Solid,
452
+ LEGO, or Skyline cells, and orbit the scene. The graph choices include calendar,
453
+ punchcard, cohort retention, co-occurrence, and uptime. Adjust a prop and the
454
+ panel below the chart shows the exact code for the current view, including its
455
+ imports.
456
+
457
+ ## Development
458
+
459
+ ```bash
460
+ npm install
461
+ npm run typecheck
462
+ npm test
463
+ npm run build
464
+ ```
465
+
466
+ For contribution details, see [CONTRIBUTING.md](CONTRIBUTING.md). To report a
467
+ vulnerability, see [SECURITY.md](SECURITY.md).
468
+
469
+ ## License
470
+
471
+ MIT.
@@ -0,0 +1,40 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Heatmap3DProps, HeatmapProps, ResolvedCell } from "./types.js";
3
+ export type CalendarDay = {
4
+ /** ISO date, YYYY-MM-DD. */
5
+ date: string;
6
+ value: number;
7
+ /** false when the day is outside what the source could observe. */
8
+ known?: boolean;
9
+ meta?: unknown;
10
+ };
11
+ type CalendarOptions = {
12
+ values?: ReadonlyArray<CalendarDay>;
13
+ /** Last day shown. Defaults to today. */
14
+ to?: string | Date;
15
+ /** Columns to render. Defaults to 53. */
16
+ weeks?: number;
17
+ /** 0 = Sunday (default), 1 = Monday. */
18
+ weekStart?: 0 | 1;
19
+ showMonthLabels?: boolean;
20
+ showWeekdayLabels?: boolean;
21
+ /** Which weekday rows to label. Defaults to every other one. */
22
+ weekdayLabelRows?: readonly number[];
23
+ unitLabel?: string;
24
+ tooltip?: (day: CalendarCell) => ReactNode;
25
+ cellLabel?: (day: CalendarCell) => string;
26
+ };
27
+ type CalendarManagedProps = "rows" | "columns" | "values" | "rowLabels" | "columnLabels" | "tooltip" | "cellLabel";
28
+ export type CalendarHeatmapProps = Omit<HeatmapProps, CalendarManagedProps> & CalendarOptions;
29
+ /** A calendar whose values rise into 3D columns, bricks or buildings. */
30
+ export type CalendarHeatmap3DProps = Omit<Heatmap3DProps, CalendarManagedProps> & CalendarOptions;
31
+ /** A resolved cell with its calendar date attached. */
32
+ export type CalendarCell = ResolvedCell & {
33
+ date: string;
34
+ };
35
+ /** Lays days onto a flat grid, with one column per week. */
36
+ export declare function CalendarHeatmap(props: CalendarHeatmapProps): import("react").JSX.Element;
37
+ /** Lays the same calendar onto a rotatable 3D grid, with height encoding value. */
38
+ export declare function CalendarHeatmap3D(props: CalendarHeatmap3DProps): import("react").JSX.Element;
39
+ export default CalendarHeatmap;
40
+ //# sourceMappingURL=calendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../src/calendar.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIhD,OAAO,KAAK,EAAE,cAAc,EAAe,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS1F,MAAM,MAAM,WAAW,GAAG;IACxB,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,MAAM,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IACpC,yCAAyC;IACzC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,SAAS,CAAC;IAC3C,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,CAAC;CAC3C,CAAC;AAEF,KAAK,oBAAoB,GACrB,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,YAAY,EAAE,oBAAoB,CAAC,GAAG,eAAe,CAAC;AAE9F,yEAAyE;AACzE,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,cAAc,EAAE,oBAAoB,CAAC,GAAG,eAAe,CAAC;AAElG,uDAAuD;AACvD,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AA8I3D,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAG1D;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,+BAG9D;AAED,eAAe,eAAe,CAAC"}
@@ -0,0 +1,136 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { Heatmap } from "./heatmap.js";
5
+ import { Heatmap3D } from "./heatmap3d.js";
6
+ const DAY_MS = 24 * 60 * 60 * 1000;
7
+ const MONTHS = [
8
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
9
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
10
+ ];
11
+ const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
12
+ function toUtcMidnight(value) {
13
+ const date = typeof value === "string" ? parseIsoDate(value) : value;
14
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
15
+ }
16
+ function parseIsoDate(value) {
17
+ const [year, month, day] = value.split("-").map(Number);
18
+ return new Date(Date.UTC(year, (month ?? 1) - 1, day ?? 1));
19
+ }
20
+ function isoKey(date) {
21
+ return date.toISOString().slice(0, 10);
22
+ }
23
+ /**
24
+ * Lays days onto the grid: one column per week, one row per weekday. This is
25
+ * the only place in the package that knows what a date is.
26
+ */
27
+ function useCalendarProps({ values = [], to, weeks = 53, weekStart = 0, showMonthLabels = true, showWeekdayLabels = false, weekdayLabelRows = [1, 3, 5], unitLabel = "", tooltip, cellLabel, ...heatmapProps }) {
28
+ const end = useMemo(() => toUtcMidnight(to ?? new Date()), [to]);
29
+ const { cells, columnLabels, dates, hidden } = useMemo(() => {
30
+ const byDate = new Map(values.map((day) => [day.date, day]));
31
+ // The grid ends on the week containing `end`, so the last column is
32
+ // partial whenever today is not the last day of the week.
33
+ const endWeekday = (end.getUTCDay() - weekStart + 7) % 7;
34
+ const lastColumnStart = new Date(end.getTime() - endWeekday * DAY_MS);
35
+ const firstColumnStart = new Date(lastColumnStart.getTime() - (weeks - 1) * 7 * DAY_MS);
36
+ const resolved = [];
37
+ const dateByKey = new Map();
38
+ const hiddenSlots = new Set();
39
+ const labels = [];
40
+ let previousMonth = -1;
41
+ for (let column = 0; column < weeks; column += 1) {
42
+ // Which month a column belongs to is a property of the calendar, not of
43
+ // the data, so the label is derived from the date before any value is
44
+ // looked up. Deriving it inside the cell loop lost the label whenever
45
+ // the caller had supplied nothing for that column's first day.
46
+ const columnMonth = new Date(firstColumnStart.getTime() + column * 7 * DAY_MS).getUTCMonth();
47
+ if (columnMonth !== previousMonth) {
48
+ labels.push({ column, text: MONTHS[columnMonth] });
49
+ previousMonth = columnMonth;
50
+ }
51
+ for (let row = 0; row < 7; row += 1) {
52
+ const date = new Date(firstColumnStart.getTime() + (column * 7 + row) * DAY_MS);
53
+ // Days after `end` are not "no data", they simply do not exist yet.
54
+ if (date.getTime() > end.getTime()) {
55
+ hiddenSlots.add(row + ":" + column);
56
+ continue;
57
+ }
58
+ const key = isoKey(date);
59
+ dateByKey.set(row + ":" + column, key);
60
+ const day = byDate.get(key);
61
+ if (!day)
62
+ continue;
63
+ resolved.push({
64
+ row,
65
+ column,
66
+ value: day.value,
67
+ known: day.known !== false,
68
+ meta: day.meta,
69
+ });
70
+ }
71
+ }
72
+ return { cells: resolved, columnLabels: labels, dates: dateByKey, hidden: hiddenSlots };
73
+ }, [values, end, weeks, weekStart]);
74
+ // A three-letter month is about 24px at the label's 10px font. When a month
75
+ // starts too few columns after the previous label there is no room for both,
76
+ // so the later one is dropped rather than drawn on top of its neighbour.
77
+ const monthLabels = useMemo(() => {
78
+ const stride = (heatmapProps.cellSize ?? 13) + (heatmapProps.gap ?? 3);
79
+ const minColumns = Math.ceil(30 / stride);
80
+ const kept = [];
81
+ let lastColumn = Number.NEGATIVE_INFINITY;
82
+ for (const label of columnLabels) {
83
+ if (label.column - lastColumn < minColumns)
84
+ continue;
85
+ kept.push(label);
86
+ lastColumn = label.column;
87
+ }
88
+ return kept;
89
+ }, [columnLabels, heatmapProps.cellSize, heatmapProps.gap]);
90
+ const withDate = (cell) => ({
91
+ ...cell,
92
+ date: dates.get(cell.row + ":" + cell.column) ?? "",
93
+ });
94
+ const rowLabels = showWeekdayLabels
95
+ ? Array.from({ length: 7 }, (_, row) => weekdayLabelRows.includes(row) ? WEEKDAYS[(row + weekStart) % 7] : "")
96
+ : undefined;
97
+ return {
98
+ rows: 7,
99
+ columns: weeks,
100
+ values: cells,
101
+ isSlotHidden: (row, column) => hidden.has(row + ":" + column),
102
+ rowLabels,
103
+ columnLabels: showMonthLabels ? monthLabels : undefined,
104
+ tooltip: tooltip ? (cell) => tooltip(withDate(cell)) : undefined,
105
+ cellLabel: (cell) => {
106
+ const resolved = withDate(cell);
107
+ if (cellLabel)
108
+ return cellLabel(resolved);
109
+ if (!resolved.date)
110
+ return "";
111
+ const readable = parseIsoDate(resolved.date).toLocaleDateString("en-US", {
112
+ weekday: "long",
113
+ day: "numeric",
114
+ month: "long",
115
+ year: "numeric",
116
+ timeZone: "UTC",
117
+ });
118
+ if (!resolved.known)
119
+ return readable + ". No data.";
120
+ return readable + ". " + resolved.value + (unitLabel ? " " + unitLabel : "");
121
+ },
122
+ ...heatmapProps,
123
+ };
124
+ }
125
+ /** Lays days onto a flat grid, with one column per week. */
126
+ export function CalendarHeatmap(props) {
127
+ const heatmapProps = useCalendarProps(props);
128
+ return _jsx(Heatmap, { ...heatmapProps });
129
+ }
130
+ /** Lays the same calendar onto a rotatable 3D grid, with height encoding value. */
131
+ export function CalendarHeatmap3D(props) {
132
+ const heatmapProps = useCalendarProps(props);
133
+ return _jsx(Heatmap3D, { ...heatmapProps });
134
+ }
135
+ export default CalendarHeatmap;
136
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../src/calendar.tsx"],"names":[],"mappings":"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"}