@pearpages/heatmap 0.1.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +311 -1
- package/dist/index.css +134 -135
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.js +209 -225
- package/dist/index.js.map +1 -1
- package/package.json +24 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pere Pages
|
|
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
CHANGED
|
@@ -5,12 +5,322 @@
|
|
|
5
5
|
|
|
6
6
|
# Heatmap
|
|
7
7
|
|
|
8
|
+
React component that "copies" the style of _Github_ for showing heatmaps.
|
|
9
|
+
|
|
10
|
+
[Demo](https://heatmap.pearpages.com)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @pearpages/heatmap
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`react` and `react-dom` (`^19.1.1`) are **peer dependencies** — the package does not
|
|
19
|
+
bundle them. The package is **ESM only**.
|
|
20
|
+
|
|
8
21
|
## Getting started
|
|
9
22
|
|
|
10
|
-
When using the library, make sure to import the CSS file
|
|
23
|
+
When using the library, make sure to import the CSS file:
|
|
11
24
|
|
|
12
25
|
```tsx
|
|
13
26
|
import '@pearpages/heatmap/styles.css';
|
|
14
27
|
```
|
|
15
28
|
|
|
16
29
|
This ensures that all necessary styles are applied to the heatmap components.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import {
|
|
35
|
+
ContributionHeatmap,
|
|
36
|
+
groupByWeeks,
|
|
37
|
+
type ContributionData,
|
|
38
|
+
type Period,
|
|
39
|
+
} from '@pearpages/heatmap';
|
|
40
|
+
import '@pearpages/heatmap/styles.css';
|
|
41
|
+
|
|
42
|
+
const period: Period = {
|
|
43
|
+
start: new Date('2024-01-01'),
|
|
44
|
+
end: new Date('2024-12-31'),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const contribution: ContributionData[] = [
|
|
48
|
+
{ date: '2024-01-01', count: 3, level: 2 },
|
|
49
|
+
{ date: '2024-01-02', count: 0, level: 0 },
|
|
50
|
+
// ...one entry per day
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function MyHeatmap() {
|
|
54
|
+
return (
|
|
55
|
+
<ContributionHeatmap
|
|
56
|
+
data={{ contribution, period, weeks: groupByWeeks(contribution) }}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### The `level` contract
|
|
63
|
+
|
|
64
|
+
`level` is **caller-supplied** and is what drives the cell colour — the component does
|
|
65
|
+
not derive it from `count`. You decide how your counts map onto the five buckets.
|
|
66
|
+
|
|
67
|
+
For reference, these are the thresholds the bundled mock generator uses:
|
|
68
|
+
|
|
69
|
+
| count | level |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `0` | `0` |
|
|
72
|
+
| `1–2` | `1` |
|
|
73
|
+
| `3–5` | `2` |
|
|
74
|
+
| `6–8` | `3` |
|
|
75
|
+
| `9+` | `4` |
|
|
76
|
+
|
|
77
|
+
### Data shape
|
|
78
|
+
|
|
79
|
+
`groupByWeeks` turns a flat, day-per-entry array into the `Week[]` the component renders.
|
|
80
|
+
It pads out to whole weeks and fills any missing day with `{ count: 0, level: 0 }`, so
|
|
81
|
+
gaps in your input are safe.
|
|
82
|
+
|
|
83
|
+
Weeks start on Sunday by default, the GitHub convention. Pass `weekStartsOn` for anywhere
|
|
84
|
+
that doesn't — most of Europe runs Monday to Sunday:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
groupByWeeks(contribution, { weekStartsOn: 1 }) // Monday
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The component takes no matching prop: it reads the first day off the weeks it is given, so
|
|
91
|
+
the two can never disagree.
|
|
92
|
+
|
|
93
|
+
Two period helpers are exported for the common cases:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { getLastYearPeriod, getLastMonthPeriod } from '@pearpages/heatmap';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To try the component out without wiring up real data:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { generateMockData, getLastYearPeriod } from '@pearpages/heatmap';
|
|
103
|
+
|
|
104
|
+
const period = getLastYearPeriod();
|
|
105
|
+
const contribution = generateMockData({ period, isRealistic: true });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`isRealistic: true` produces weekday-weighted data — quieter weekends, a summer lull in
|
|
109
|
+
June–August, and the occasional spike. `false` is uniform random.
|
|
110
|
+
|
|
111
|
+
## Props
|
|
112
|
+
|
|
113
|
+
`ContributionHeatmap`:
|
|
114
|
+
|
|
115
|
+
| Prop | Type | Default | Notes |
|
|
116
|
+
| --- | --- | --- | --- |
|
|
117
|
+
| `data.contribution` | `ContributionData[]` | required | Flat, one entry per day |
|
|
118
|
+
| `data.period` | `Period` | required | `{ start: Date; end: Date }`. Drives the month headers and whether a tooltip reads as in-range |
|
|
119
|
+
| `data.weeks` | `Week[]` | required | 7-item tuples — use `groupByWeeks` |
|
|
120
|
+
| `className` | `string` | `''` | Appended to the root element; this is how themes and the colour scheme are applied |
|
|
121
|
+
| `isReverse` | `boolean` | `false` | Vertical layout: one row per week, weekdays as columns |
|
|
122
|
+
| `locale` | `string` | `'en-US'` | Drives day names, month names and the tooltip date through `Intl` |
|
|
123
|
+
| `labels` | `HeatmapLabels` | English | The few strings `Intl` cannot derive |
|
|
124
|
+
|
|
125
|
+
There is no `weekStartsOn` prop: the component reads the first day off the weeks it is
|
|
126
|
+
given, so it cannot disagree with them. Set it on `groupByWeeks` instead.
|
|
127
|
+
|
|
128
|
+
Each cell is a `<td>` carrying `data-count`, `data-date`, a
|
|
129
|
+
`contribution-heatmap__day--level-N` class, and a `title` / `aria-label` tooltip.
|
|
130
|
+
|
|
131
|
+
The exception is the padding days that fall outside your period: they keep `data-count`
|
|
132
|
+
and `data-date`, but carry `contribution-heatmap__day--outside` and **no** tooltip, `role`
|
|
133
|
+
or `tabIndex`, so they stay out of the tab order and the accessibility tree.
|
|
134
|
+
|
|
135
|
+
## Themes
|
|
136
|
+
|
|
137
|
+
Four palettes ship with the library. Apply one through `className`:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<ContributionHeatmap className="contribution-heatmap--ocean" data={...} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Theme | `className` |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| GitHub (default) | _none_ |
|
|
146
|
+
| Ocean | `contribution-heatmap--ocean` |
|
|
147
|
+
| Sunset | `contribution-heatmap--sunset` |
|
|
148
|
+
| Purple | `contribution-heatmap--purple` |
|
|
149
|
+
|
|
150
|
+
Themes are nothing more than overrides of the `--color-level-0` … `--color-level-4`
|
|
151
|
+
custom properties, so you can define your own the same way:
|
|
152
|
+
|
|
153
|
+
```css
|
|
154
|
+
.contribution-heatmap--brand {
|
|
155
|
+
--color-level-0: #eee;
|
|
156
|
+
--color-level-1: #cfe8ff;
|
|
157
|
+
--color-level-2: #7cc0ff;
|
|
158
|
+
--color-level-3: #3b93f0;
|
|
159
|
+
--color-level-4: #0b5cad;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Light and dark
|
|
164
|
+
|
|
165
|
+
By default the component follows `prefers-color-scheme`. Two more classes force it:
|
|
166
|
+
|
|
167
|
+
| | `className` |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| Follow the system | _none_ |
|
|
170
|
+
| Always light | `contribution-heatmap--light` |
|
|
171
|
+
| Always dark | `contribution-heatmap--dark` |
|
|
172
|
+
|
|
173
|
+
They combine with a colour theme, and they also set `color-scheme`, so the grid's
|
|
174
|
+
scrollbar matches:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<ContributionHeatmap
|
|
178
|
+
className="contribution-heatmap--light contribution-heatmap--ocean"
|
|
179
|
+
data={...}
|
|
180
|
+
/>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Typography and sizing
|
|
184
|
+
|
|
185
|
+
The same mechanism covers the font and the cell size, so there is no prop for these
|
|
186
|
+
either:
|
|
187
|
+
|
|
188
|
+
| Custom property | Default | What it does |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `--heatmap-font-family` | a system UI stack | the font for every label |
|
|
191
|
+
| `--heatmap-label-size` | `11px` | day names, month names, legend text |
|
|
192
|
+
| `--heatmap-day-header-font-family` | a monospace stack | the day names above the reversed layout's columns |
|
|
193
|
+
| `--day-size` | `12px`; `20px` minimum when reversed | the side of each square in the default layout |
|
|
194
|
+
| `--day-gap` | `2px` (`1px` ≤768px) | the space between squares |
|
|
195
|
+
|
|
196
|
+
```css
|
|
197
|
+
.contribution-heatmap {
|
|
198
|
+
--heatmap-font-family: "Inter", sans-serif;
|
|
199
|
+
--day-size: 16px;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The component declares its own font rather than inheriting the host page's, so it looks
|
|
204
|
+
the same wherever it is dropped. Override the property to blend it back in.
|
|
205
|
+
|
|
206
|
+
Squares are always square: the grid is sized by its content and scrolls horizontally when
|
|
207
|
+
it doesn't fit, rather than stretching to the container. The day-name column stays pinned
|
|
208
|
+
while the weeks scroll under it.
|
|
209
|
+
|
|
210
|
+
The card is an ordinary block, so it fills its container; the legend is aligned to the
|
|
211
|
+
start so it sits under the squares rather than floating in the middle of a wide card.
|
|
212
|
+
Wrap it in a narrower element of your own if you want it tighter. Below 480px the
|
|
213
|
+
reversed layout scales its squares up to fill the width, since at that size it is the
|
|
214
|
+
only thing in the card.
|
|
215
|
+
|
|
216
|
+
The squares keep their size on small screens rather than shrinking. A year is 53 weeks, so
|
|
217
|
+
the grid overflows a phone whatever size they take — it scrolls either way, and shrinking
|
|
218
|
+
would only cost legibility and tap area. Set `--day-size` yourself if you want a denser
|
|
219
|
+
grid.
|
|
220
|
+
|
|
221
|
+
With `isReverse`, the root also carries `contribution-heatmap--reverse`. That layout puts
|
|
222
|
+
the day names above their columns rather than beside the rows, so no column can be
|
|
223
|
+
narrower than its header and the squares are sized to match rather than floating in
|
|
224
|
+
oversized cells.
|
|
225
|
+
|
|
226
|
+
Those headers are set in a monospace face. Every day name is exactly three characters, so
|
|
227
|
+
a monospace one renders all seven at an identical width — in a proportional face `Fri` is
|
|
228
|
+
10px narrower than `Wed`, which leaves visibly more air around it.
|
|
229
|
+
|
|
230
|
+
Because the column can be no narrower than its header, and that width depends on the
|
|
231
|
+
locale — `Wed` is 20px, French `mer.` is 26.5px — the reversed square **fills its cell**
|
|
232
|
+
rather than taking a fixed size. `--day-size` is only a floor there. That is what keeps the
|
|
233
|
+
squares flush in any language. Override `--heatmap-day-header-font-family` under that
|
|
234
|
+
class to change the face.
|
|
235
|
+
|
|
236
|
+
The grid always renders whole weeks, so the first and last week can hold days outside your
|
|
237
|
+
period. Those slots are left blank — no square, no tooltip, not focusable — rather than
|
|
238
|
+
being drawn as zero-contribution days.
|
|
239
|
+
|
|
240
|
+
## Languages
|
|
241
|
+
|
|
242
|
+
The library ships no translations. Day and month names come from `Intl` for whatever
|
|
243
|
+
`locale` you pass, and the handful of strings `Intl` cannot derive are props:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
<ContributionHeatmap
|
|
247
|
+
locale="ca"
|
|
248
|
+
labels={{
|
|
249
|
+
less: 'Menys',
|
|
250
|
+
more: 'Més',
|
|
251
|
+
level: (level) => `Nivell ${level}`,
|
|
252
|
+
noContributions: 'Cap contribució',
|
|
253
|
+
contributions: (n) => (n === 1 ? '1 contribució' : `${n} contribucions`),
|
|
254
|
+
}}
|
|
255
|
+
data={{ contribution, period, weeks: groupByWeeks(contribution, { weekStartsOn: 1 }) }}
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`weekStartsOn` matters as much as the strings: Catalan — like most of Europe — runs
|
|
260
|
+
weeks Monday to Sunday, so a translated heatmap that still starts on Sunday reads as
|
|
261
|
+
wrong. It defaults to `0` (Sunday, the GitHub convention), and the component picks the
|
|
262
|
+
order of its day labels up from the data.
|
|
263
|
+
|
|
264
|
+
## Local development
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npm install # workspace root; installs demo/ too
|
|
268
|
+
npm run demo # the sandbox, resolving the library from src/ (HMR)
|
|
269
|
+
npm run build # build dist/
|
|
270
|
+
npm test -- --run # run the test suite once
|
|
271
|
+
npm run lint
|
|
272
|
+
npm run check:package # publint + arethetypeswrong
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`demo/` is a single Vite app that serves as both the development sandbox and the site
|
|
276
|
+
deployed to [heatmap.pearpages.com](https://heatmap.pearpages.com). It imports the library
|
|
277
|
+
by its public specifier (`@pearpages/heatmap`) and resolves it two ways:
|
|
278
|
+
|
|
279
|
+
| Command | Resolves to | Use for |
|
|
280
|
+
| --- | --- | --- |
|
|
281
|
+
| `npm run demo` | `../src` — HMR, no build step | working on the component |
|
|
282
|
+
| `npm run demo:dist` | `../dist` through the package's `exports` map | checking what a consumer installs |
|
|
283
|
+
|
|
284
|
+
`npm run demo:dist` rebuilds the library first. Because one `App.tsx` serves both modes,
|
|
285
|
+
they can't drift: if they render differently, the packaging is wrong.
|
|
286
|
+
|
|
287
|
+
`npm run check:package` is the automated version of that check — `publint` and
|
|
288
|
+
`arethetypeswrong` validate the `exports` map, the `files` field and the `.d.ts`
|
|
289
|
+
resolution. It runs in `publish.yml` and in `prepublishOnly`.
|
|
290
|
+
|
|
291
|
+
## Releasing
|
|
292
|
+
|
|
293
|
+
There are two **independent** pipelines. Knowing which one a push triggers is the whole
|
|
294
|
+
game:
|
|
295
|
+
|
|
296
|
+
| Trigger | Workflow | Effect |
|
|
297
|
+
| --- | --- | --- |
|
|
298
|
+
| Push to `main` | `.github/workflows/deploy.yml` | Runs the tests, builds the library + `demo/`, deploys to GitHub Pages. **Never touches npm.** |
|
|
299
|
+
| Push a `v*` tag | `.github/workflows/publish.yml` | Builds and publishes to npm with provenance |
|
|
300
|
+
|
|
301
|
+
So you can push to `main` freely — nothing reaches npm until a `v*` tag is pushed.
|
|
302
|
+
|
|
303
|
+
### The happy path
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm version minor # bumps package.json, commits, and tags vX.Y.Z
|
|
307
|
+
git push --follow-tags # pushes main (deploys the demo) and the tag (publishes to npm)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Four things that are easy to get wrong
|
|
311
|
+
|
|
312
|
+
1. **The tag name does not set the published version.** `publish.yml` just runs
|
|
313
|
+
`npm publish`, which reads the version out of `package.json`. Tagging `v0.3.0` while
|
|
314
|
+
`package.json` still says `0.2.0` fails with a 403
|
|
315
|
+
(`cannot publish over previously published version`). Use `npm version`, which keeps
|
|
316
|
+
the two in sync for you.
|
|
317
|
+
2. **A plain `git push` does not push tags.** Use `git push --follow-tags`, or push the
|
|
318
|
+
tag explicitly with `git push origin vX.Y.Z`.
|
|
319
|
+
3. **The main-branch guard skips rather than fails.** Every step in `publish.yml` is gated
|
|
320
|
+
on `git merge-base --is-ancestor $GITHUB_SHA origin/main`. If the tag is not on `main`,
|
|
321
|
+
all steps skip and the job still reports **green** — it now emits a workflow warning
|
|
322
|
+
saying why, but it is still a green run with nothing published.
|
|
323
|
+
4. **The demo advertises the new version before npm has it.** `demo/src/App.tsx`
|
|
324
|
+
renders the version read from the root `package.json`, and the Pages deploy runs on
|
|
325
|
+
push to `main`. The site therefore shows the bumped version as soon as the bump commit
|
|
326
|
+
lands — before the tag exists, and before npm has anything.
|