contrib-graphs 0.0.0-stage → 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,1101 @@
1
+ 'use client';
2
+ import { _ as weekdayLabels, a as fromGitHub, b as themeVars, c as generateSampleData, d as computeStats, f as cropCalendar, g as pluralize, h as makeLevelFn, i as fromDates, l as sampleProfiles, m as formatNumber, n as fetchGitHubContributions, o as fromGitLab, p as formatDate, r as fetchGitLabContributions, s as mergeSources, t as GITHUB_CONTRIBUTIONS_QUERY, u as buildCalendar, v as resolveTheme, x as themes, y as themeNames } from "./data-oHgoFFM5.js";
3
+ import { Fragment, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ // src/core.tsx
6
+ /**
7
+ * useLayoutEffect in the browser (measure before paint) and useEffect on the
8
+ * server, where effects don't run and React would warn once per component.
9
+ */
10
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
11
+ /** `:focus-visible` doesn't exist in old browsers (Safari < 15.4) or in some test environments. */
12
+ function isFocusVisible(el) {
13
+ try {
14
+ return el.matches(":focus-visible");
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ const indexOf = (target) => {
20
+ const el = target?.closest?.("[data-cg-i]");
21
+ return el ? Number(el.getAttribute("data-cg-i")) : null;
22
+ };
23
+ /**
24
+ * Hover and keyboard navigation through event delegation: cells only carry
25
+ * `data-cg-i`, so hundreds of handlers are never created. Left/right arrows move
26
+ * `stepX` items and up/down move `stepY`.
27
+ */
28
+ function useGraphInteraction(opts) {
29
+ const { rootRef, count, stepX, stepY, onActivate } = opts;
30
+ const [state, setState] = useState(null);
31
+ const [pos, setPos] = useState(null);
32
+ const index = state && state.index < count ? state.index : null;
33
+ useIsomorphicLayoutEffect(() => {
34
+ const root = rootRef.current;
35
+ if (!root) return;
36
+ const el = index == null ? null : root.querySelector(`[data-cg-i="${index}"]`);
37
+ root.querySelectorAll("[data-active]").forEach((n) => n !== el && n.removeAttribute("data-active"));
38
+ el?.setAttribute("data-active", "");
39
+ });
40
+ useIsomorphicLayoutEffect(() => {
41
+ const root = rootRef.current;
42
+ const el = index == null || !root ? null : root.querySelector(`[data-cg-i="${index}"]`);
43
+ if (!root || !el) {
44
+ setPos(null);
45
+ return;
46
+ }
47
+ const r = root.getBoundingClientRect();
48
+ const e = el.getBoundingClientRect();
49
+ setPos({
50
+ x: e.left + e.width / 2 - r.left,
51
+ y: e.top - r.top
52
+ });
53
+ }, [
54
+ rootRef,
55
+ index,
56
+ count
57
+ ]);
58
+ const onKeyDown = useCallback((e) => {
59
+ const cur = index ?? count - 1;
60
+ let next = null;
61
+ switch (e.key) {
62
+ case "ArrowRight":
63
+ next = cur + stepX;
64
+ break;
65
+ case "ArrowLeft":
66
+ next = cur - stepX;
67
+ break;
68
+ case "ArrowDown":
69
+ next = stepY ? cur + stepY : null;
70
+ break;
71
+ case "ArrowUp":
72
+ next = stepY ? cur - stepY : null;
73
+ break;
74
+ case "Home":
75
+ next = 0;
76
+ break;
77
+ case "End":
78
+ next = count - 1;
79
+ break;
80
+ case "Escape":
81
+ setState(null);
82
+ return;
83
+ case "Enter":
84
+ case " ":
85
+ if (index != null) {
86
+ e.preventDefault();
87
+ onActivate?.(index);
88
+ }
89
+ return;
90
+ default: return;
91
+ }
92
+ if (next == null || count === 0) return;
93
+ e.preventDefault();
94
+ setState({
95
+ index: Math.max(0, Math.min(count - 1, next)),
96
+ via: "keyboard"
97
+ });
98
+ }, [
99
+ index,
100
+ count,
101
+ stepX,
102
+ stepY,
103
+ onActivate
104
+ ]);
105
+ return {
106
+ rootRef,
107
+ active: index,
108
+ via: state?.via ?? null,
109
+ pos,
110
+ handlers: {
111
+ tabIndex: 0,
112
+ onKeyDown,
113
+ onPointerOver: (e) => {
114
+ const i = indexOf(e.target);
115
+ if (i != null && i !== index) setState({
116
+ index: i,
117
+ via: "pointer"
118
+ });
119
+ },
120
+ onPointerLeave: () => setState((s) => s?.via === "pointer" ? null : s),
121
+ onClick: (e) => {
122
+ const i = indexOf(e.target);
123
+ if (i != null) onActivate?.(i);
124
+ },
125
+ onFocus: (e) => {
126
+ if (e.target === e.currentTarget && isFocusVisible(e.currentTarget)) setState((s) => s ?? {
127
+ index: count - 1,
128
+ via: "keyboard"
129
+ });
130
+ },
131
+ onBlur: () => setState((s) => s?.via === "keyboard" ? null : s)
132
+ }
133
+ };
134
+ }
135
+ /** Reduces the visible weeks to fit the container width (instead of scrolling). */
136
+ function useFitWeeks(ref, requested, pitch, reserved, enabled) {
137
+ const [fit, setFit] = useState(requested);
138
+ useIsomorphicLayoutEffect(() => {
139
+ const el = ref.current;
140
+ if (!enabled || !el || typeof ResizeObserver === "undefined") {
141
+ setFit(requested);
142
+ return;
143
+ }
144
+ const update = () => {
145
+ const w = el.clientWidth;
146
+ if (w > 0) setFit(Math.max(4, Math.min(requested, Math.floor((w - reserved) / pitch))));
147
+ };
148
+ update();
149
+ const ro = new ResizeObserver(update);
150
+ ro.observe(el);
151
+ return () => ro.disconnect();
152
+ }, [
153
+ ref,
154
+ requested,
155
+ pitch,
156
+ reserved,
157
+ enabled
158
+ ]);
159
+ return enabled ? fit : requested;
160
+ }
161
+ /** Builds the calendar and the interaction. The base of every per-day variant. */
162
+ function useCalendarGraph(props, opts = {}) {
163
+ const rootRef = useRef(null);
164
+ const requested = props.weeks ?? 53;
165
+ const weeks = useFitWeeks(rootRef, requested, opts.fit?.pitch ?? 1, opts.fit?.reserved ?? 0, opts.fit?.enabled ?? false);
166
+ const endKey = props.endDate instanceof Date ? props.endDate.getTime() : props.endDate;
167
+ const scaleKey = typeof props.scale === "string" ? props.scale : props.scale?.join(",");
168
+ const full = useMemo(() => buildCalendar(props.data, {
169
+ weeks: requested,
170
+ endDate: props.endDate,
171
+ weekStart: props.weekStart,
172
+ scale: props.scale,
173
+ locale: props.locale
174
+ }), [
175
+ props.data,
176
+ requested,
177
+ endKey,
178
+ props.weekStart,
179
+ scaleKey,
180
+ props.locale
181
+ ]);
182
+ const calendar = useMemo(() => cropCalendar(full, weeks, props.locale), [
183
+ full,
184
+ weeks,
185
+ props.locale
186
+ ]);
187
+ const { onDayClick } = props;
188
+ const onActivate = useCallback((i) => onDayClick?.(calendar.days[i]), [onDayClick, calendar]);
189
+ const interaction = useGraphInteraction({
190
+ rootRef,
191
+ count: calendar.days.length,
192
+ stepX: opts.stepX ?? 7,
193
+ stepY: opts.stepY ?? 1,
194
+ onActivate
195
+ });
196
+ /** `periodTotal` is the total of the requested period, even when fewer weeks are shown. */
197
+ return {
198
+ calendar,
199
+ periodTotal: full.total,
200
+ interaction,
201
+ theme: resolveTheme(props.theme)
202
+ };
203
+ }
204
+ function describeDay(day, props) {
205
+ const n = day.count;
206
+ return {
207
+ title: `${formatNumber(n, props.locale)} ${pluralize(n, props.unit)}`,
208
+ subtitle: formatDate(day.date, props.locale)
209
+ };
210
+ }
211
+ /** Period text from the requested weeks (not the ones cropped to fit). */
212
+ function periodText(props) {
213
+ const weeks = props.weeks ?? 53;
214
+ const p = props.periodLabel;
215
+ if (typeof p === "function") return p(weeks);
216
+ if (typeof p === "string") return p;
217
+ return weeks >= 52 ? "in the last year" : `in the last ${weeks} weeks`;
218
+ }
219
+ function defaultTitle(total, props) {
220
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
221
+ /* @__PURE__ */ jsx("strong", { children: formatNumber(total, props.locale) }),
222
+ " ",
223
+ pluralize(total, props.unit),
224
+ " ",
225
+ periodText(props)
226
+ ] });
227
+ }
228
+ const cellDelay = (w) => ({ "--cg-w": w });
229
+ /** Stable id that is valid inside SVG `url(#...)`. */
230
+ function useSvgId(prefix) {
231
+ return `${prefix}-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
232
+ }
233
+ function Legend({ labels = ["Less", "More"], swatch }) {
234
+ return /* @__PURE__ */ jsxs("span", {
235
+ className: "cg-legend",
236
+ "aria-hidden": "true",
237
+ children: [
238
+ /* @__PURE__ */ jsx("span", { children: labels[0] }),
239
+ [
240
+ 0,
241
+ 1,
242
+ 2,
243
+ 3,
244
+ 4
245
+ ].map((l) => swatch ? /* @__PURE__ */ jsx("span", { children: swatch(l) }, l) : /* @__PURE__ */ jsx("span", {
246
+ className: "cg-swatch",
247
+ style: { background: `var(--cg-l${l})` }
248
+ }, l)),
249
+ /* @__PURE__ */ jsx("span", { children: labels[1] })
250
+ ]
251
+ });
252
+ }
253
+ /**
254
+ * Shared container: theme, light/dark mode, header, legend, tooltip and an
255
+ * aria-live region that announces the active day during keyboard navigation.
256
+ */
257
+ function GraphShell({ props, interaction, variant, describe, summary, title, headerEnd, footerStart, legendSwatch, glow, surface, children }) {
258
+ const theme = resolveTheme(props.theme);
259
+ const { active, pos, via } = interaction;
260
+ const desc = active != null ? describe(active) : null;
261
+ const tipRef = useRef(null);
262
+ const [shift, setShift] = useState(0);
263
+ useIsomorphicLayoutEffect(() => {
264
+ const tip = tipRef.current;
265
+ const root = interaction.rootRef.current;
266
+ if (!tip || !root || !pos) return;
267
+ const half = tip.offsetWidth / 2;
268
+ const w = root.clientWidth;
269
+ setShift(pos.x - half < 0 ? half - pos.x : pos.x + half > w ? w - half - pos.x : 0);
270
+ }, [
271
+ pos,
272
+ desc?.title,
273
+ desc?.subtitle,
274
+ interaction.rootRef
275
+ ]);
276
+ const showHeader = props.showTotal !== false || headerEnd;
277
+ return /* @__PURE__ */ jsxs("div", {
278
+ ref: interaction.rootRef,
279
+ className: [
280
+ "cg-root",
281
+ `cg-${variant}`,
282
+ props.className
283
+ ].filter(Boolean).join(" "),
284
+ style: {
285
+ ...themeVars(theme),
286
+ ...props.style
287
+ },
288
+ "data-mode": props.colorMode ?? "auto",
289
+ "data-animate": props.animate ? "" : void 0,
290
+ "data-glow": glow ? "" : void 0,
291
+ "data-surface": surface ? "" : void 0,
292
+ "data-has-active": active != null ? "" : void 0,
293
+ role: "group",
294
+ "aria-label": props["aria-label"] ?? `${summary}. Use arrow keys to explore.`,
295
+ ...interaction.handlers,
296
+ children: [
297
+ showHeader && /* @__PURE__ */ jsxs("div", {
298
+ className: "cg-header",
299
+ children: [props.showTotal !== false ? /* @__PURE__ */ jsx("span", {
300
+ className: "cg-title",
301
+ children: props.title ?? title
302
+ }) : /* @__PURE__ */ jsx("span", {}), headerEnd]
303
+ }),
304
+ /* @__PURE__ */ jsx("div", {
305
+ className: "cg-body",
306
+ children
307
+ }),
308
+ (props.showLegend !== false || footerStart) && /* @__PURE__ */ jsxs("div", {
309
+ className: "cg-footer",
310
+ children: [/* @__PURE__ */ jsx("span", { children: footerStart }), props.showLegend !== false && /* @__PURE__ */ jsx(Legend, {
311
+ labels: props.legendLabels,
312
+ swatch: legendSwatch
313
+ })]
314
+ }),
315
+ desc && pos && /* @__PURE__ */ jsxs("div", {
316
+ ref: tipRef,
317
+ className: "cg-tooltip",
318
+ style: {
319
+ left: pos.x + shift,
320
+ top: pos.y,
321
+ "--cg-arrow": `${-shift}px`
322
+ },
323
+ "aria-hidden": "true",
324
+ children: [/* @__PURE__ */ jsx("strong", { children: desc.title }), desc.subtitle && /* @__PURE__ */ jsxs("span", { children: [" · ", desc.subtitle] })]
325
+ }),
326
+ /* @__PURE__ */ jsx("span", {
327
+ className: "cg-sr-only",
328
+ "aria-live": "polite",
329
+ children: via === "keyboard" && desc ? `${desc.title}, ${desc.subtitle ?? ""}` : ""
330
+ })
331
+ ]
332
+ });
333
+ }
334
+ function summaryOf(calendar, props) {
335
+ return `${formatNumber(calendar.total, props.locale)} ${pluralize(calendar.total, props.unit)} from ${formatDate(calendar.start, props.locale)} to ${formatDate(calendar.end, props.locale)}`;
336
+ }
337
+ // src/variants/GridGraph.tsx
338
+ function useGrid(props, shape) {
339
+ const { cellSize = 11, gap = 3, radius = 2, showWeekdays = true, showMonths = true, fit = "crop" } = props;
340
+ const pitch = cellSize + gap;
341
+ const left = showWeekdays ? 30 : 0;
342
+ const top = showMonths ? 15 : 0;
343
+ const { calendar, periodTotal, interaction } = useCalendarGraph(props, { fit: {
344
+ pitch,
345
+ reserved: left - gap,
346
+ enabled: fit === "crop"
347
+ } });
348
+ const glow = props.glow ?? false;
349
+ const filterId = useSvgId("cg-glow");
350
+ const weekdays = useMemo(() => weekdayLabels(props.locale, props.weekStart ?? 0), [props.locale, props.weekStart]);
351
+ const width = left + calendar.weeks.length * pitch - gap;
352
+ const height = top + 7 * pitch - gap;
353
+ const svg = useMemo(() => /* @__PURE__ */ jsxs("svg", {
354
+ width: fit === "scale" ? "100%" : width,
355
+ height: fit === "scale" ? void 0 : height,
356
+ viewBox: `0 0 ${width} ${height}`,
357
+ "aria-hidden": "true",
358
+ children: [
359
+ glow && /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("filter", {
360
+ id: filterId,
361
+ x: "-50%",
362
+ y: "-50%",
363
+ width: "200%",
364
+ height: "200%",
365
+ children: [/* @__PURE__ */ jsx("feGaussianBlur", {
366
+ stdDeviation: cellSize / 4,
367
+ result: "b"
368
+ }), /* @__PURE__ */ jsxs("feMerge", { children: [/* @__PURE__ */ jsx("feMergeNode", { in: "b" }), /* @__PURE__ */ jsx("feMergeNode", { in: "SourceGraphic" })] })]
369
+ }) }),
370
+ showMonths && calendar.months.map((m) => /* @__PURE__ */ jsx("text", {
371
+ className: "cg-label",
372
+ x: left + m.weekIndex * pitch,
373
+ y: 9,
374
+ children: m.label
375
+ }, `${m.year}-${m.month}`)),
376
+ showWeekdays && [
377
+ 1,
378
+ 3,
379
+ 5
380
+ ].map((r) => /* @__PURE__ */ jsx("text", {
381
+ className: "cg-label",
382
+ x: 0,
383
+ y: top + r * pitch + cellSize - 2,
384
+ children: weekdays[r]
385
+ }, r)),
386
+ /* @__PURE__ */ jsx("g", { children: calendar.days.map((d) => {
387
+ const { kind, attrs } = shape.cell({
388
+ x: left + d.weekIndex * pitch,
389
+ y: top + d.dayIndex * pitch,
390
+ size: cellSize,
391
+ level: d.level,
392
+ radius
393
+ });
394
+ return /* @__PURE__ */ jsx(kind, {
395
+ ...attrs,
396
+ className: `cg-cell${glow && d.level >= 3 ? " cg-glow" : ""}`,
397
+ "data-cg-i": d.index,
398
+ "data-level": d.level,
399
+ fill: `var(--cg-l${d.level})`,
400
+ style: cellDelay(d.weekIndex)
401
+ }, d.date);
402
+ }) })
403
+ ]
404
+ }), [
405
+ calendar,
406
+ width,
407
+ height,
408
+ fit,
409
+ glow,
410
+ filterId,
411
+ cellSize,
412
+ showMonths,
413
+ showWeekdays,
414
+ left,
415
+ top,
416
+ pitch,
417
+ radius,
418
+ weekdays,
419
+ shape
420
+ ]);
421
+ return /* @__PURE__ */ jsx(GraphShell, {
422
+ props,
423
+ interaction,
424
+ variant: shape.variant,
425
+ describe: (i) => describeDay(calendar.days[i], props),
426
+ summary: summaryOf(calendar, props),
427
+ title: defaultTitle(periodTotal, props),
428
+ glow,
429
+ legendSwatch: shape.variant === "dots" ? (l) => /* @__PURE__ */ jsx("span", {
430
+ className: "cg-swatch",
431
+ style: {
432
+ background: `var(--cg-l${l})`,
433
+ borderRadius: "50%",
434
+ transform: `scale(${.55 + l * .11})`
435
+ }
436
+ }) : void 0,
437
+ children: /* @__PURE__ */ jsx("div", {
438
+ style: glow ? { "--cg-glow-filter": `url(#${filterId})` } : void 0,
439
+ children: svg
440
+ })
441
+ });
442
+ }
443
+ const classicShape = {
444
+ variant: "classic",
445
+ cell: ({ x, y, size, radius }) => ({
446
+ kind: "rect",
447
+ attrs: {
448
+ x,
449
+ y,
450
+ width: size,
451
+ height: size,
452
+ rx: radius,
453
+ ry: radius
454
+ }
455
+ })
456
+ };
457
+ const dotsShape = {
458
+ variant: "dots",
459
+ cell: ({ x, y, size, level }) => ({
460
+ kind: "circle",
461
+ attrs: {
462
+ cx: x + size / 2,
463
+ cy: y + size / 2,
464
+ r: size / 2 * (level === 0 ? .42 : .55 + level * .115)
465
+ }
466
+ })
467
+ };
468
+ /** The classic weeks × days grid. */
469
+ function ClassicGraph(props) {
470
+ return useGrid(props, classicShape);
471
+ }
472
+ /** Circles whose size also encodes intensity, so it still reads in monochrome. */
473
+ function DotsGraph(props) {
474
+ return useGrid(props, dotsShape);
475
+ }
476
+ // src/isometric.ts
477
+ const r = (v) => Math.round(v * 100) / 100;
478
+ /**
479
+ * Geometry of the isometric view, shared by `IsometricGraph` and
480
+ * `renderIsometricSvg` so the component and the static SVG look the same.
481
+ * Tiles are sorted back to front (paint order).
482
+ */
483
+ function isometricLayout(calendar, { tileSize = 12, maxHeight = 48, heightBy = "count" } = {}) {
484
+ const tw = tileSize;
485
+ const th = tileSize / 2;
486
+ const W = calendar.weeks.length;
487
+ const width = (W + 7) * (tw / 2);
488
+ const height = (W + 7) * (th / 2) + maxHeight + 2;
489
+ const ox = 7 * (tw / 2);
490
+ const max = Math.max(1, calendar.max);
491
+ return {
492
+ width,
493
+ height,
494
+ tiles: [...calendar.days].sort((a, b) => a.weekIndex + a.dayIndex - (b.weekIndex + b.dayIndex) || a.weekIndex - b.weekIndex).map((day) => {
495
+ const X = ox + (day.weekIndex - day.dayIndex) * (tw / 2);
496
+ const Y = maxHeight + (day.weekIndex + day.dayIndex) * (th / 2);
497
+ const h = day.count === 0 ? 0 : heightBy === "level" ? day.level / 4 * maxHeight : Math.max(3, day.count / max * maxHeight);
498
+ const p = (x, y) => `${r(x)},${r(y)}`;
499
+ return {
500
+ day,
501
+ height: h,
502
+ top: [
503
+ p(X, Y - h),
504
+ p(X + tw / 2, Y + th / 2 - h),
505
+ p(X, Y + th - h),
506
+ p(X - tw / 2, Y + th / 2 - h)
507
+ ].join(" "),
508
+ left: [
509
+ p(X - tw / 2, Y + th / 2 - h),
510
+ p(X, Y + th - h),
511
+ p(X, Y + th),
512
+ p(X - tw / 2, Y + th / 2)
513
+ ].join(" "),
514
+ right: [
515
+ p(X, Y + th - h),
516
+ p(X + tw / 2, Y + th / 2 - h),
517
+ p(X + tw / 2, Y + th / 2),
518
+ p(X, Y + th)
519
+ ].join(" ")
520
+ };
521
+ })
522
+ };
523
+ }
524
+ /** Color percentage of the left and right faces (the rest is mixed with black). */
525
+ const ISO_SHADES = {
526
+ left: 78,
527
+ right: 60
528
+ };
529
+ // src/variants/IsometricGraph.tsx
530
+ const shade = (level, pct) => `color-mix(in oklab, var(--cg-l${level}) ${pct}%, var(--cg-mix))`;
531
+ /** Isometric columns whose height shows each day's activity. */
532
+ function IsometricGraph(props) {
533
+ const { tileSize = 12, maxHeight = 48, heightBy = "count", showStats = true, statLabels = [
534
+ "longest streak",
535
+ "current streak",
536
+ "best day"
537
+ ] } = props;
538
+ const { calendar, periodTotal, interaction } = useCalendarGraph(props);
539
+ const stats = useMemo(() => computeStats(calendar.days), [calendar]);
540
+ const svg = useMemo(() => {
541
+ const { width, height, tiles } = isometricLayout(calendar, {
542
+ tileSize,
543
+ maxHeight,
544
+ heightBy
545
+ });
546
+ return /* @__PURE__ */ jsx("svg", {
547
+ width,
548
+ height,
549
+ viewBox: `0 0 ${width} ${height}`,
550
+ "aria-hidden": "true",
551
+ children: tiles.map(({ day: d, height: h, top, left, right }) => /* @__PURE__ */ jsxs("g", {
552
+ className: "cg-cell cg-rise",
553
+ "data-cg-i": d.index,
554
+ style: cellDelay(d.weekIndex),
555
+ children: [
556
+ h > 0 && /* @__PURE__ */ jsx("polygon", {
557
+ points: left,
558
+ style: { fill: shade(d.level, ISO_SHADES.left) }
559
+ }),
560
+ h > 0 && /* @__PURE__ */ jsx("polygon", {
561
+ points: right,
562
+ style: { fill: shade(d.level, ISO_SHADES.right) }
563
+ }),
564
+ /* @__PURE__ */ jsx("polygon", {
565
+ points: top,
566
+ style: { fill: `var(--cg-l${d.level})` }
567
+ })
568
+ ]
569
+ }, d.date))
570
+ });
571
+ }, [
572
+ calendar,
573
+ tileSize,
574
+ maxHeight,
575
+ heightBy
576
+ ]);
577
+ const n = (v) => formatNumber(v, props.locale);
578
+ return /* @__PURE__ */ jsx(GraphShell, {
579
+ props,
580
+ interaction,
581
+ variant: "isometric",
582
+ describe: (i) => describeDay(calendar.days[i], props),
583
+ summary: summaryOf(calendar, props),
584
+ title: defaultTitle(periodTotal, props),
585
+ headerEnd: showStats && /* @__PURE__ */ jsxs("span", {
586
+ className: "cg-stats",
587
+ children: [
588
+ /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("b", { children: n(stats.longestStreak) }), statLabels[0]] }),
589
+ /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("b", { children: n(stats.currentStreak) }), statLabels[1]] }),
590
+ /* @__PURE__ */ jsxs("span", { children: [
591
+ /* @__PURE__ */ jsx("b", { children: n(stats.best?.count ?? 0) }),
592
+ statLabels[2],
593
+ stats.best && ` · ${formatDate(stats.best.date, props.locale, {
594
+ month: "short",
595
+ day: "numeric"
596
+ })}`
597
+ ] })
598
+ ]
599
+ }),
600
+ children: svg
601
+ });
602
+ }
603
+ // src/variants/RadialGraph.tsx
604
+ function sector(cx, cy, r0, r1, a0, a1) {
605
+ const p = (r, a) => `${(cx + r * Math.cos(a)).toFixed(2)} ${(cy + r * Math.sin(a)).toFixed(2)}`;
606
+ return `M${p(r1, a0)}A${r1} ${r1} 0 0 1 ${p(r1, a1)}L${p(r0, a1)}A${r0} ${r0} 0 0 0 ${p(r0, a0)}Z`;
607
+ }
608
+ /** The year as a ring: each week is a slice and each weekday a band. */
609
+ function RadialGraph(props) {
610
+ const { size = 320, innerRadius = .38, showMonths = true } = props;
611
+ const { calendar, interaction } = useCalendarGraph(props);
612
+ const pad = showMonths ? 22 : 4;
613
+ const R = size / 2;
614
+ const outer = R - pad;
615
+ const inner = outer * innerRadius;
616
+ const ring = (outer - inner) / 7;
617
+ const W = calendar.weeks.length;
618
+ const slice = Math.PI * 2 / W;
619
+ const start = -Math.PI / 2;
620
+ const gapA = Math.min(.012, slice * .12);
621
+ const svg = useMemo(() => /* @__PURE__ */ jsxs("svg", {
622
+ width: size,
623
+ height: size,
624
+ viewBox: `0 0 ${size} ${size}`,
625
+ "aria-hidden": "true",
626
+ children: [
627
+ calendar.days.map((d) => {
628
+ const a0 = start + d.weekIndex * slice + gapA;
629
+ const a1 = start + (d.weekIndex + 1) * slice - gapA;
630
+ const r0 = inner + d.dayIndex * ring + .6;
631
+ const r1 = inner + (d.dayIndex + 1) * ring - .6;
632
+ return /* @__PURE__ */ jsx("path", {
633
+ d: sector(R, R, r0, r1, a0, a1),
634
+ className: "cg-cell",
635
+ "data-cg-i": d.index,
636
+ fill: `var(--cg-l${d.level})`,
637
+ style: cellDelay(d.weekIndex)
638
+ }, d.date);
639
+ }),
640
+ showMonths && calendar.months.map((m) => {
641
+ const a = start + (m.weekIndex + .5) * slice;
642
+ return /* @__PURE__ */ jsx("text", {
643
+ className: "cg-label",
644
+ x: R + (outer + 11) * Math.cos(a),
645
+ y: R + (outer + 11) * Math.sin(a),
646
+ textAnchor: "middle",
647
+ dominantBaseline: "middle",
648
+ children: m.label
649
+ }, `${m.year}-${m.month}`);
650
+ }),
651
+ /* @__PURE__ */ jsx("text", {
652
+ x: R,
653
+ y: R - 4,
654
+ textAnchor: "middle",
655
+ style: {
656
+ fill: "var(--cg-text)",
657
+ fontSize: size / 11,
658
+ fontWeight: 650
659
+ },
660
+ children: formatNumber(calendar.total, props.locale)
661
+ }),
662
+ /* @__PURE__ */ jsx("text", {
663
+ x: R,
664
+ y: R + size / 18,
665
+ textAnchor: "middle",
666
+ className: "cg-label",
667
+ style: { fontSize: 11 },
668
+ children: pluralize(calendar.total, props.unit)
669
+ })
670
+ ]
671
+ }), [
672
+ calendar,
673
+ size,
674
+ R,
675
+ inner,
676
+ ring,
677
+ outer,
678
+ slice,
679
+ start,
680
+ gapA,
681
+ showMonths,
682
+ props.locale,
683
+ props.unit
684
+ ]);
685
+ return /* @__PURE__ */ jsx(GraphShell, {
686
+ props: {
687
+ ...props,
688
+ showTotal: props.showTotal ?? false
689
+ },
690
+ interaction,
691
+ variant: "radial",
692
+ describe: (i) => describeDay(calendar.days[i], props),
693
+ summary: summaryOf(calendar, props),
694
+ children: /* @__PURE__ */ jsx("div", {
695
+ style: {
696
+ display: "flex",
697
+ justifyContent: "center"
698
+ },
699
+ children: svg
700
+ })
701
+ });
702
+ }
703
+ // src/variants/SkylineGraph.tsx
704
+ /** One building per week: your year as a city skyline. */
705
+ function SkylineGraph(props) {
706
+ const { height = 110, barWidth = 9, gap = 3, windows = true, fit = "crop" } = props;
707
+ const pitch = barWidth + gap;
708
+ const rootRef = useRef(null);
709
+ const weeksN = useFitWeeks(rootRef, props.weeks ?? 53, pitch, -gap, fit === "crop");
710
+ const endKey = props.endDate instanceof Date ? props.endDate.getTime() : props.endDate;
711
+ const requested = props.weeks ?? 53;
712
+ const full = useMemo(() => buildCalendar(props.data, {
713
+ weeks: requested,
714
+ endDate: props.endDate,
715
+ weekStart: props.weekStart,
716
+ locale: props.locale
717
+ }), [
718
+ props.data,
719
+ requested,
720
+ endKey,
721
+ props.weekStart,
722
+ props.locale
723
+ ]);
724
+ const calendar = useMemo(() => cropCalendar(full, weeksN, props.locale), [
725
+ full,
726
+ weeksN,
727
+ props.locale
728
+ ]);
729
+ const allWeeks = useMemo(() => {
730
+ const raw = full.weeks.map((w, index) => {
731
+ const days = w.filter((d) => d !== null);
732
+ return {
733
+ index,
734
+ days,
735
+ total: days.reduce((s, d) => s + d.count, 0),
736
+ activeDays: days.filter((d) => d.count > 0).length,
737
+ level: 0
738
+ };
739
+ });
740
+ const levelOf = makeLevelFn(raw.map((w) => w.total), props.scale);
741
+ raw.forEach((w) => w.level = levelOf(w.total));
742
+ return raw;
743
+ }, [full, typeof props.scale === "string" ? props.scale : props.scale?.join(",")]);
744
+ const maxWeek = Math.max(1, ...allWeeks.map((w) => w.total));
745
+ const weeks = useMemo(() => allWeeks.slice(allWeeks.length - calendar.weeks.length).map((w, index) => ({
746
+ ...w,
747
+ index,
748
+ days: calendar.weeks[index].filter((d) => d !== null)
749
+ })), [allWeeks, calendar]);
750
+ const { onWeekClick } = props;
751
+ const onActivate = useCallback((i) => onWeekClick?.(weeks[i]), [onWeekClick, weeks]);
752
+ const interaction = useGraphInteraction({
753
+ rootRef,
754
+ count: weeks.length,
755
+ stepX: 1,
756
+ stepY: 0,
757
+ onActivate
758
+ });
759
+ const labelH = 14;
760
+ const minH = windows ? 30 : 3;
761
+ const width = weeks.length * pitch - gap;
762
+ const svgH = height + labelH;
763
+ const svg = useMemo(() => /* @__PURE__ */ jsxs("svg", {
764
+ width: fit === "scale" ? "100%" : width,
765
+ height: fit === "scale" ? void 0 : svgH,
766
+ viewBox: `0 0 ${width} ${svgH}`,
767
+ "aria-hidden": "true",
768
+ children: [
769
+ /* @__PURE__ */ jsx("line", {
770
+ x1: 0,
771
+ x2: width,
772
+ y1: height + .5,
773
+ y2: height + .5,
774
+ stroke: "var(--cg-border)"
775
+ }),
776
+ weeks.map((w) => {
777
+ const h = w.total === 0 ? minH * .8 : minH + w.total / maxWeek * (height - minH);
778
+ const x = w.index * pitch;
779
+ const y = height - h;
780
+ const ww = Math.max(2, barWidth - 4);
781
+ return /* @__PURE__ */ jsxs("g", {
782
+ className: "cg-cell cg-rise",
783
+ "data-cg-i": w.index,
784
+ style: cellDelay(w.index),
785
+ children: [/* @__PURE__ */ jsx("rect", {
786
+ x,
787
+ y,
788
+ width: barWidth,
789
+ height: h,
790
+ rx: 1.5,
791
+ fill: `var(--cg-l${w.level})`
792
+ }), windows && w.days.map((d) => /* @__PURE__ */ jsx("rect", {
793
+ x: x + (barWidth - ww) / 2,
794
+ y: y + 3 + d.dayIndex * 3.6,
795
+ width: ww,
796
+ height: 2.2,
797
+ rx: .5,
798
+ fill: d.count > 0 ? "#fff6c8" : "currentColor",
799
+ opacity: d.count > 0 ? .92 : .12,
800
+ style: { color: "var(--cg-mix)" }
801
+ }, d.date))]
802
+ }, w.index);
803
+ }),
804
+ calendar.months.map((m) => /* @__PURE__ */ jsx("text", {
805
+ className: "cg-label",
806
+ x: m.weekIndex * pitch,
807
+ y: svgH - 2,
808
+ children: m.label
809
+ }, `${m.year}-${m.month}`))
810
+ ]
811
+ }), [
812
+ weeks,
813
+ calendar.months,
814
+ width,
815
+ svgH,
816
+ height,
817
+ pitch,
818
+ barWidth,
819
+ maxWeek,
820
+ minH,
821
+ windows,
822
+ fit
823
+ ]);
824
+ const describe = (i) => {
825
+ const w = weeks[i];
826
+ return {
827
+ title: `${formatNumber(w.total, props.locale)} ${pluralize(w.total, props.unit)}`,
828
+ subtitle: w.days[0] ? `${formatDate(w.days[0].date, props.locale, {
829
+ month: "short",
830
+ day: "numeric",
831
+ year: "numeric"
832
+ })} · ${w.activeDays}/7` : void 0
833
+ };
834
+ };
835
+ return /* @__PURE__ */ jsx(GraphShell, {
836
+ props,
837
+ interaction,
838
+ variant: "skyline",
839
+ describe,
840
+ summary: summaryOf(calendar, props),
841
+ title: defaultTitle(full.total, props),
842
+ children: svg
843
+ });
844
+ }
845
+ // src/variants/TerminalGraph.tsx
846
+ const terminalCharsets = {
847
+ blocks: [
848
+ "··",
849
+ "░░",
850
+ "▒▒",
851
+ "▓▓",
852
+ "██"
853
+ ],
854
+ squares: [
855
+ "□ ",
856
+ "▪ ",
857
+ "■ ",
858
+ "■ ",
859
+ "■ "
860
+ ],
861
+ ascii: [
862
+ ". ",
863
+ ": ",
864
+ "+ ",
865
+ "# ",
866
+ "@ "
867
+ ],
868
+ dots: [
869
+ "· ",
870
+ "• ",
871
+ "• ",
872
+ "● ",
873
+ "● "
874
+ ]
875
+ };
876
+ const CHAR_W = 7.22;
877
+ const MIN_FONT = 6;
878
+ /** Real monospace text, for portfolios with a terminal look. */
879
+ function TerminalGraph(props) {
880
+ const { charset = "blocks", command = "git log --since='1 year ago' | heatmap", prompt = "~ $", colored = true, fit = "crop" } = props;
881
+ const chars = terminalCharsets[charset];
882
+ const { calendar, periodTotal, interaction } = useCalendarGraph(props, { fit: (() => {
883
+ const k = fit === "scale" ? MIN_FONT / 12 : 1;
884
+ return {
885
+ pitch: CHAR_W * 2 * k,
886
+ reserved: CHAR_W * 5 * k + 34,
887
+ enabled: true
888
+ };
889
+ })() });
890
+ const weekdays = useMemo(() => weekdayLabels(props.locale, props.weekStart ?? 0), [props.locale, props.weekStart]);
891
+ const [fontSize, setFontSize] = useState(12);
892
+ const { rootRef } = interaction;
893
+ const cols = 5 + calendar.weeks.length * 2;
894
+ useIsomorphicLayoutEffect(() => {
895
+ const el = rootRef.current;
896
+ if (fit !== "scale" || !el || typeof ResizeObserver === "undefined") {
897
+ setFontSize(12);
898
+ return;
899
+ }
900
+ const update = () => {
901
+ const avail = el.clientWidth - 34;
902
+ if (avail > 0) setFontSize(Math.max(MIN_FONT, Math.min(12, avail / cols * (12 / CHAR_W))));
903
+ };
904
+ update();
905
+ const ro = new ResizeObserver(update);
906
+ ro.observe(el);
907
+ return () => ro.disconnect();
908
+ }, [
909
+ fit,
910
+ cols,
911
+ rootRef
912
+ ]);
913
+ const color = (l) => colored ? `var(--cg-l${l === 0 ? 2 : l})` : "var(--cg-text)";
914
+ const op = (l) => l === 0 ? .35 : colored ? 1 : .4 + l * .15;
915
+ const body = useMemo(() => {
916
+ const cols = calendar.weeks.length * 2;
917
+ const header = Array.from({ length: cols }, () => " ");
918
+ for (const m of calendar.months) {
919
+ const lbl = m.label.replace(".", "").slice(0, 3);
920
+ for (let i = 0; i < lbl.length && m.weekIndex * 2 + i < cols; i++) header[m.weekIndex * 2 + i] = lbl[i];
921
+ }
922
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
923
+ " ",
924
+ header.join(""),
925
+ "\n",
926
+ Array.from({ length: 7 }, (_, r) => /* @__PURE__ */ jsxs(Fragment, { children: [
927
+ (r % 2 === 1 ? weekdays[r].replace(".", "").slice(0, 3) : "").padEnd(5),
928
+ calendar.weeks.map((w, wi) => {
929
+ const d = w[r];
930
+ if (!d) return /* @__PURE__ */ jsx(Fragment, { children: " " }, wi);
931
+ return /* @__PURE__ */ jsx("span", {
932
+ className: "cg-cell",
933
+ "data-cg-i": d.index,
934
+ style: {
935
+ ...cellDelay(wi),
936
+ color: color(d.level),
937
+ opacity: op(d.level)
938
+ },
939
+ children: chars[d.level]
940
+ }, d.date);
941
+ }),
942
+ "\n"
943
+ ] }, r))
944
+ ] });
945
+ }, [
946
+ calendar,
947
+ weekdays,
948
+ chars,
949
+ colored
950
+ ]);
951
+ const [less, more] = props.legendLabels ?? ["less", "more"];
952
+ return /* @__PURE__ */ jsx(GraphShell, {
953
+ props: {
954
+ ...props,
955
+ showTotal: false,
956
+ showLegend: false
957
+ },
958
+ interaction,
959
+ variant: "terminal",
960
+ surface: true,
961
+ describe: (i) => describeDay(calendar.days[i], props),
962
+ summary: summaryOf(calendar, props),
963
+ children: /* @__PURE__ */ jsxs("pre", {
964
+ className: "cg-term",
965
+ style: fontSize !== 12 ? { fontSize } : void 0,
966
+ children: [
967
+ /* @__PURE__ */ jsxs("span", {
968
+ className: "cg-prompt cg-wrap",
969
+ children: [
970
+ /* @__PURE__ */ jsx("b", { children: prompt }),
971
+ " ",
972
+ command
973
+ ]
974
+ }),
975
+ "\n\n",
976
+ /* @__PURE__ */ jsx("span", {
977
+ "aria-hidden": "true",
978
+ children: body
979
+ }),
980
+ "\n",
981
+ (props.showTotal !== false || props.showLegend !== false) && /* @__PURE__ */ jsxs("span", {
982
+ className: "cg-foot",
983
+ children: [props.showTotal !== false && /* @__PURE__ */ jsx("span", {
984
+ className: "cg-prompt",
985
+ children: props.title ?? `${formatNumber(periodTotal, props.locale)} ${pluralize(periodTotal, props.unit)} ${periodText(props)}`
986
+ }), props.showLegend !== false && /* @__PURE__ */ jsxs("span", { children: [
987
+ less,
988
+ " ",
989
+ [
990
+ 0,
991
+ 1,
992
+ 2,
993
+ 3,
994
+ 4
995
+ ].map((l) => /* @__PURE__ */ jsx("span", {
996
+ style: {
997
+ color: color(l),
998
+ opacity: op(l)
999
+ },
1000
+ children: chars[l]
1001
+ }, l)),
1002
+ more
1003
+ ] })]
1004
+ }),
1005
+ /* @__PURE__ */ jsxs("span", {
1006
+ className: "cg-prompt",
1007
+ children: [
1008
+ /* @__PURE__ */ jsx("b", { children: prompt }),
1009
+ " ",
1010
+ /* @__PURE__ */ jsx("span", { className: "cg-caret" })
1011
+ ]
1012
+ })
1013
+ ]
1014
+ })
1015
+ });
1016
+ }
1017
+ // src/variants/CompactGraph.tsx
1018
+ /** A single row with the last N days, for headers, cards and footers. */
1019
+ function CompactGraph(props) {
1020
+ const { days: n = 30, cellSize = 10, gap = 3, radius = 2, showStreak = true } = props;
1021
+ const rootRef = useRef(null);
1022
+ const endKey = props.endDate instanceof Date ? props.endDate.getTime() : props.endDate;
1023
+ const scaleKey = typeof props.scale === "string" ? props.scale : props.scale?.join(",");
1024
+ const days = useMemo(() => {
1025
+ const slice = buildCalendar(props.data, {
1026
+ weeks: Math.ceil(n / 7) + 1,
1027
+ endDate: props.endDate
1028
+ }).days.slice(-n);
1029
+ const levelOf = makeLevelFn(slice.map((d) => d.count), props.scale);
1030
+ return slice.map((d, i) => ({
1031
+ ...d,
1032
+ index: i,
1033
+ weekIndex: i,
1034
+ level: levelOf(d.count)
1035
+ }));
1036
+ }, [
1037
+ props.data,
1038
+ n,
1039
+ endKey,
1040
+ scaleKey
1041
+ ]);
1042
+ const stats = useMemo(() => computeStats(days), [days]);
1043
+ const { onDayClick } = props;
1044
+ const onActivate = useCallback((i) => onDayClick?.(days[i]), [onDayClick, days]);
1045
+ const interaction = useGraphInteraction({
1046
+ rootRef,
1047
+ count: days.length,
1048
+ stepX: 1,
1049
+ stepY: 0,
1050
+ onActivate
1051
+ });
1052
+ const pitch = cellSize + gap;
1053
+ const width = days.length * pitch - gap;
1054
+ const svg = useMemo(() => /* @__PURE__ */ jsx("svg", {
1055
+ width,
1056
+ height: cellSize,
1057
+ viewBox: `0 0 ${width} ${cellSize}`,
1058
+ "aria-hidden": "true",
1059
+ children: days.map((d) => /* @__PURE__ */ jsx("rect", {
1060
+ className: "cg-cell",
1061
+ "data-cg-i": d.index,
1062
+ x: d.index * pitch,
1063
+ y: 0,
1064
+ width: cellSize,
1065
+ height: cellSize,
1066
+ rx: radius,
1067
+ fill: `var(--cg-l${d.level})`,
1068
+ style: cellDelay(d.index / 3)
1069
+ }, d.date))
1070
+ }), [
1071
+ days,
1072
+ width,
1073
+ cellSize,
1074
+ pitch,
1075
+ radius
1076
+ ]);
1077
+ const streak = props.streakLabel ?? ((s) => `${s} day streak`);
1078
+ return /* @__PURE__ */ jsx(GraphShell, {
1079
+ props: {
1080
+ ...props,
1081
+ showLegend: props.showLegend ?? false
1082
+ },
1083
+ interaction,
1084
+ variant: "compact",
1085
+ describe: (i) => describeDay(days[i], props),
1086
+ summary: `${stats.total} ${pluralize(stats.total, props.unit)} in the last ${n} days`,
1087
+ title: /* @__PURE__ */ jsxs(Fragment$1, { children: [
1088
+ /* @__PURE__ */ jsx("strong", { children: formatNumber(stats.total, props.locale) }),
1089
+ " ",
1090
+ pluralize(stats.total, props.unit),
1091
+ " ",
1092
+ (props.rangeLabel ?? ((d) => `in the last ${d} days`))(n)
1093
+ ] }),
1094
+ headerEnd: showStreak && stats.currentStreak > 0 && /* @__PURE__ */ jsx("span", {
1095
+ style: { color: "var(--cg-muted)" },
1096
+ children: streak(stats.currentStreak)
1097
+ }),
1098
+ children: svg
1099
+ });
1100
+ }
1101
+ export { ClassicGraph, CompactGraph, DotsGraph, GITHUB_CONTRIBUTIONS_QUERY, IsometricGraph, RadialGraph, SkylineGraph, TerminalGraph, buildCalendar, computeStats, cropCalendar, fetchGitHubContributions, fetchGitLabContributions, formatDate, fromDates, fromGitHub, fromGitLab, generateSampleData, makeLevelFn, mergeSources, resolveTheme, sampleProfiles, terminalCharsets, themeNames, themeVars, themes };