@danielwh2/contribution-graph 1.0.14
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 +327 -0
- package/dist/contributionGraph.d.ts +150 -0
- package/dist/contributionGraph.js +328 -0
- package/dist/github.d.ts +19 -0
- package/dist/github.js +109 -0
- package/dist/htmlGraph.d.ts +13 -0
- package/dist/htmlGraph.js +199 -0
- package/dist/hudGraph.d.ts +25 -0
- package/dist/hudGraph.js +194 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +109 -0
- package/dist/profileGraph.d.ts +7 -0
- package/dist/profileGraph.js +81 -0
- package/dist/react.d.ts +7 -0
- package/dist/react.js +22 -0
- package/dist/solid.d.ts +14 -0
- package/dist/solid.js +13 -0
- package/dist/svelte.d.ts +9 -0
- package/dist/svelte.js +12 -0
- package/dist/tooltip.d.ts +2 -0
- package/dist/tooltip.js +84 -0
- package/dist/vue.d.ts +25 -0
- package/dist/vue.js +33 -0
- package/package.json +101 -0
- package/style.css +610 -0
- package/style.css.d.ts +2 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HUD renderer for contribution-graph.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the SVG graph in a cinematic "GIT COMMITS" heads-up display: a
|
|
5
|
+
* corner-bracketed glass panel over an animated teal→ember glitch backdrop,
|
|
6
|
+
* with auto-computed week stats and commit streak rolling in via slot-text.
|
|
7
|
+
*
|
|
8
|
+
* Everything is scoped inside the host element (no fixed/full-viewport chrome)
|
|
9
|
+
* so the HUD stays a self-contained, embeddable component.
|
|
10
|
+
*/
|
|
11
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./contributionGraph.js";
|
|
12
|
+
export interface HudGraphHandle {
|
|
13
|
+
update(data: ContributionDay[], options: ContributionGraphOptions): void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
16
|
+
export interface HudStats {
|
|
17
|
+
thisWeek: number;
|
|
18
|
+
lastWeek: number;
|
|
19
|
+
deltaPct: number;
|
|
20
|
+
streak: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function buildHudGraph(element: HTMLElement, data: ContributionDay[], options?: ContributionGraphOptions): HudGraphHandle;
|
|
23
|
+
export declare function clearHudGraph(element: HTMLElement): void;
|
|
24
|
+
/** Week totals, delta, and current streak derived from the data, ending at `until`. */
|
|
25
|
+
export declare function computeStats(data: ContributionDay[], options: ContributionGraphOptions): HudStats;
|
package/dist/hudGraph.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HUD renderer for contribution-graph.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the SVG graph in a cinematic "GIT COMMITS" heads-up display: a
|
|
5
|
+
* corner-bracketed glass panel over an animated teal→ember glitch backdrop,
|
|
6
|
+
* with auto-computed week stats and commit streak rolling in via slot-text.
|
|
7
|
+
*
|
|
8
|
+
* Everything is scoped inside the host element (no fixed/full-viewport chrome)
|
|
9
|
+
* so the HUD stays a self-contained, embeddable component.
|
|
10
|
+
*/
|
|
11
|
+
import { slotText } from "slot-text";
|
|
12
|
+
import { buildGraph, clearGraph, formatDate, resolveOptions, } from "./contributionGraph.js";
|
|
13
|
+
import { attachTooltip } from "./tooltip.js";
|
|
14
|
+
const HUD_CLASS = "cg-hud";
|
|
15
|
+
/** Monochrome dark→white ramp used by the HUD heatmap. */
|
|
16
|
+
const HUD_COLORS = ["#1d2422", "#3a4441", "#6c7873", "#aab4ae", "#f1f4f1"];
|
|
17
|
+
const HUD_WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"];
|
|
18
|
+
const ASCII_CHARS = "01xX#@$+=·:.SBO80";
|
|
19
|
+
const FLAME_PATH = "M12 2.5c.6 3.2-1.2 4.7-2.6 6C7.8 9.9 6 11.4 6 14.5A6 6 0 0 0 18 15c0-2.4-1-4-2.2-5.4-.3 1-.9 1.7-1.7 2-.2-2.6-1-4.6-2.1-9.1z";
|
|
20
|
+
const HUD_TEMPLATE = `
|
|
21
|
+
<div class="cg-hud-bg" aria-hidden="true"></div>
|
|
22
|
+
<pre class="cg-hud-ascii cg-hud-ascii--top" aria-hidden="true"></pre>
|
|
23
|
+
<pre class="cg-hud-ascii cg-hud-ascii--bot" aria-hidden="true"></pre>
|
|
24
|
+
<div class="cg-hud-scan" aria-hidden="true"></div>
|
|
25
|
+
<section class="cg-hud-panel" aria-label="Git commits">
|
|
26
|
+
<span class="cg-hud-corner cg-hud-corner--tl" aria-hidden="true"></span>
|
|
27
|
+
<span class="cg-hud-corner cg-hud-corner--tr" aria-hidden="true"></span>
|
|
28
|
+
<span class="cg-hud-corner cg-hud-corner--bl" aria-hidden="true"></span>
|
|
29
|
+
<span class="cg-hud-corner cg-hud-corner--br" aria-hidden="true"></span>
|
|
30
|
+
<header class="cg-hud-head"><h2 class="cg-hud-title">GIT COMMITS</h2></header>
|
|
31
|
+
<div class="cg-hud-rule"></div>
|
|
32
|
+
<div class="cg-hud-week">
|
|
33
|
+
<span class="cg-hud-stat">
|
|
34
|
+
<span class="cg-hud-k">THIS WEEK:</span>
|
|
35
|
+
<span class="cg-hud-big" data-hud="this">0</span>
|
|
36
|
+
<span class="cg-hud-delta" data-hud="delta">+0%</span>
|
|
37
|
+
</span>
|
|
38
|
+
<span class="cg-hud-stat">
|
|
39
|
+
<span class="cg-hud-k">LAST WEEK:</span>
|
|
40
|
+
<span class="cg-hud-big" data-hud="last">0</span>
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="cg-hud-graph-wrap"><div class="cg-hud-graph"></div></div>
|
|
44
|
+
<div class="cg-hud-rule"></div>
|
|
45
|
+
<div class="cg-hud-foot">
|
|
46
|
+
<div class="cg-hud-streak" title="Current commit streak">
|
|
47
|
+
<svg class="cg-hud-flame" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="${FLAME_PATH}"/></svg>
|
|
48
|
+
<span data-hud="streak">0</span> DAY STREAK
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
`;
|
|
53
|
+
export function buildHudGraph(element, data, options = {}) {
|
|
54
|
+
clearHudGraph(element);
|
|
55
|
+
element.classList.add(HUD_CLASS);
|
|
56
|
+
element.innerHTML = HUD_TEMPLATE;
|
|
57
|
+
const graphEl = element.querySelector(".cg-hud-graph");
|
|
58
|
+
const deltaEl = element.querySelector('[data-hud="delta"]');
|
|
59
|
+
const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
60
|
+
const rollers = {
|
|
61
|
+
this: slotText(element.querySelector('[data-hud="this"]'), "0"),
|
|
62
|
+
delta: slotText(deltaEl, "0%"),
|
|
63
|
+
last: slotText(element.querySelector('[data-hud="last"]'), "0"),
|
|
64
|
+
streak: slotText(element.querySelector('[data-hud="streak"]'), "0"),
|
|
65
|
+
};
|
|
66
|
+
fillAscii(element);
|
|
67
|
+
let graphOptions = mergeGraphOptions(options);
|
|
68
|
+
buildGraph(graphEl, data, graphOptions);
|
|
69
|
+
const cleanupTooltip = attachTooltip(graphEl, () => resolveOptions(graphOptions));
|
|
70
|
+
const instant = { duration: 0, stagger: 0 };
|
|
71
|
+
const applyStats = (stats, animate) => {
|
|
72
|
+
deltaEl.classList.toggle("is-neg", stats.deltaPct < 0);
|
|
73
|
+
const delta = `${stats.deltaPct >= 0 ? "+" : ""}${stats.deltaPct}%`;
|
|
74
|
+
const roll = (ctrl, text, duration, stagger) => ctrl.set(text, animate ? { direction: "up", duration, stagger } : instant);
|
|
75
|
+
roll(rollers.this, String(stats.thisWeek), 700, 50);
|
|
76
|
+
roll(rollers.delta, delta, 750, 45);
|
|
77
|
+
roll(rollers.last, String(stats.lastWeek), 700, 50);
|
|
78
|
+
roll(rollers.streak, String(stats.streak), 650, 60);
|
|
79
|
+
};
|
|
80
|
+
let stats = computeStats(data, options);
|
|
81
|
+
let bootTimer;
|
|
82
|
+
let pulseTimer;
|
|
83
|
+
if (reduce) {
|
|
84
|
+
applyStats(stats, false);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
applyStats(stats, false);
|
|
88
|
+
bootTimer = setTimeout(() => applyStats(stats, true), 420);
|
|
89
|
+
pulseTimer = setInterval(() => {
|
|
90
|
+
const enter = { direction: "up", duration: 600, stagger: 40 };
|
|
91
|
+
rollers.this.flash(String(stats.thisWeek), { revertAfter: 1, enter });
|
|
92
|
+
rollers.streak.flash(String(stats.streak), { revertAfter: 1, enter });
|
|
93
|
+
}, 6500);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
update(nextData, nextOptions) {
|
|
97
|
+
graphOptions = mergeGraphOptions(nextOptions);
|
|
98
|
+
buildGraph(graphEl, nextData, graphOptions);
|
|
99
|
+
stats = computeStats(nextData, nextOptions);
|
|
100
|
+
applyStats(stats, !reduce);
|
|
101
|
+
},
|
|
102
|
+
destroy() {
|
|
103
|
+
if (bootTimer)
|
|
104
|
+
clearTimeout(bootTimer);
|
|
105
|
+
if (pulseTimer)
|
|
106
|
+
clearInterval(pulseTimer);
|
|
107
|
+
cleanupTooltip();
|
|
108
|
+
rollers.this.destroy();
|
|
109
|
+
rollers.delta.destroy();
|
|
110
|
+
rollers.last.destroy();
|
|
111
|
+
rollers.streak.destroy();
|
|
112
|
+
clearGraph(graphEl);
|
|
113
|
+
clearHudGraph(element);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function clearHudGraph(element) {
|
|
118
|
+
element.classList.remove(HUD_CLASS);
|
|
119
|
+
element.replaceChildren();
|
|
120
|
+
}
|
|
121
|
+
/** HUD heatmap defaults; any explicit caller option still wins. */
|
|
122
|
+
function mergeGraphOptions(options) {
|
|
123
|
+
const { hud, showCounts, ...rest } = options;
|
|
124
|
+
return {
|
|
125
|
+
weekStart: 0,
|
|
126
|
+
cellSize: 15,
|
|
127
|
+
cellGap: 3,
|
|
128
|
+
cellRadius: 1,
|
|
129
|
+
colors: HUD_COLORS,
|
|
130
|
+
showMonthLabels: false,
|
|
131
|
+
labelColor: "#8a988f",
|
|
132
|
+
labelFontSize: 12,
|
|
133
|
+
weekdayLabels: HUD_WEEKDAYS,
|
|
134
|
+
tooltip: defaultTooltip,
|
|
135
|
+
...rest,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function defaultTooltip(day) {
|
|
139
|
+
return day.count === 0
|
|
140
|
+
? `no commits · ${day.dateLabel}`
|
|
141
|
+
: `${day.count} commits · ${day.dateLabel}`;
|
|
142
|
+
}
|
|
143
|
+
/** Week totals, delta, and current streak derived from the data, ending at `until`. */
|
|
144
|
+
export function computeStats(data, options) {
|
|
145
|
+
const until = resolveOptions(options).until;
|
|
146
|
+
const counts = new Map();
|
|
147
|
+
for (const d of data)
|
|
148
|
+
counts.set(d.date, d.count);
|
|
149
|
+
const countAt = (offset) => {
|
|
150
|
+
const day = new Date(Date.UTC(until.getUTCFullYear(), until.getUTCMonth(), until.getUTCDate() - offset));
|
|
151
|
+
return counts.get(formatDate(day)) ?? 0;
|
|
152
|
+
};
|
|
153
|
+
let thisWeek = 0;
|
|
154
|
+
let lastWeek = 0;
|
|
155
|
+
for (let i = 0; i < 7; i++)
|
|
156
|
+
thisWeek += countAt(i);
|
|
157
|
+
for (let i = 7; i < 14; i++)
|
|
158
|
+
lastWeek += countAt(i);
|
|
159
|
+
const deltaPct = lastWeek === 0
|
|
160
|
+
? thisWeek > 0
|
|
161
|
+
? 100
|
|
162
|
+
: 0
|
|
163
|
+
: Math.round(((thisWeek - lastWeek) / lastWeek) * 100);
|
|
164
|
+
let streak = 0;
|
|
165
|
+
const cap = Math.max(data.length, 1) + 1;
|
|
166
|
+
for (let i = 0; i < cap; i++) {
|
|
167
|
+
if (countAt(i) > 0)
|
|
168
|
+
streak++;
|
|
169
|
+
else
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
return { thisWeek, lastWeek, deltaPct, streak };
|
|
173
|
+
}
|
|
174
|
+
/** Paint the decorative glitch ASCII bands once. */
|
|
175
|
+
function fillAscii(element) {
|
|
176
|
+
const width = element.clientWidth || 1080;
|
|
177
|
+
const cols = Math.ceil(width / 8) + 4;
|
|
178
|
+
const paint = (selector, rows) => {
|
|
179
|
+
const el = element.querySelector(selector);
|
|
180
|
+
if (!el)
|
|
181
|
+
return;
|
|
182
|
+
let html = "";
|
|
183
|
+
for (let r = 0; r < rows; r++) {
|
|
184
|
+
for (let c = 0; c < cols; c++) {
|
|
185
|
+
const ch = ASCII_CHARS[(Math.random() * ASCII_CHARS.length) | 0];
|
|
186
|
+
html += Math.random() < 0.06 ? `<span>${ch}</span>` : ch;
|
|
187
|
+
}
|
|
188
|
+
html += "\n";
|
|
189
|
+
}
|
|
190
|
+
el.innerHTML = html;
|
|
191
|
+
};
|
|
192
|
+
paint(".cg-hud-ascii--top", 11);
|
|
193
|
+
paint(".cg-hud-ascii--bot", 13);
|
|
194
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contribution-graph — GitHub-style contribution heatmap for any framework.
|
|
3
|
+
*
|
|
4
|
+
* Import `contribution-graph/style.css` once in your app, then:
|
|
5
|
+
*
|
|
6
|
+
* import { contributionGraph } from "contribution-graph";
|
|
7
|
+
*
|
|
8
|
+
* const graph = contributionGraph(element, data, {
|
|
9
|
+
* until: "2026-06-22",
|
|
10
|
+
* onCellClick: (day) => console.log(day.date, day.count),
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* graph.set(newData);
|
|
14
|
+
* graph.destroy();
|
|
15
|
+
*
|
|
16
|
+
* For HTML cells and optional animated count labels, set `showCounts: true`
|
|
17
|
+
* and import `slot-text/style.css` with the graph stylesheet:
|
|
18
|
+
*
|
|
19
|
+
* import "contribution-graph/style.css";
|
|
20
|
+
* import "slot-text/style.css";
|
|
21
|
+
*
|
|
22
|
+
* contributionGraph(element, data, { showCounts: true, countFontSize: 9 });
|
|
23
|
+
*/
|
|
24
|
+
export { buildGraph, clearGraph, computeGrid, formatDate, formatHumanDate, levelForCount, parseDate, resolveOptions, type ContributionDay, type ContributionDayContext, type ContributionGraphOptions, type Grid, type GridCell, type ResolvedOptions, } from "./contributionGraph.js";
|
|
25
|
+
export { buildHtmlGraph, clearHtmlGraph, type HtmlGraphHandle, } from "./htmlGraph.js";
|
|
26
|
+
export { buildHudGraph, clearHudGraph, type HudGraphHandle, } from "./hudGraph.js";
|
|
27
|
+
export { fetchGitHubContributions, type FetchGitHubContributionsOptions, type GitHubContributionsResult, } from "./github.js";
|
|
28
|
+
export { buildProfileGraph, clearProfileGraph, type ProfileGraphHandle, } from "./profileGraph.js";
|
|
29
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./contributionGraph.js";
|
|
30
|
+
export interface ContributionGraphController {
|
|
31
|
+
readonly element: HTMLElement;
|
|
32
|
+
/** Re-render with new data and/or options. */
|
|
33
|
+
set(data: ContributionDay[], options?: ContributionGraphOptions): void;
|
|
34
|
+
/** Remove the graph and clean up listeners. */
|
|
35
|
+
destroy(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a contribution graph controller for one element.
|
|
39
|
+
*/
|
|
40
|
+
export declare function contributionGraph(element: HTMLElement, data: ContributionDay[], options?: ContributionGraphOptions): ContributionGraphController;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contribution-graph — GitHub-style contribution heatmap for any framework.
|
|
3
|
+
*
|
|
4
|
+
* Import `contribution-graph/style.css` once in your app, then:
|
|
5
|
+
*
|
|
6
|
+
* import { contributionGraph } from "contribution-graph";
|
|
7
|
+
*
|
|
8
|
+
* const graph = contributionGraph(element, data, {
|
|
9
|
+
* until: "2026-06-22",
|
|
10
|
+
* onCellClick: (day) => console.log(day.date, day.count),
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* graph.set(newData);
|
|
14
|
+
* graph.destroy();
|
|
15
|
+
*
|
|
16
|
+
* For HTML cells and optional animated count labels, set `showCounts: true`
|
|
17
|
+
* and import `slot-text/style.css` with the graph stylesheet:
|
|
18
|
+
*
|
|
19
|
+
* import "contribution-graph/style.css";
|
|
20
|
+
* import "slot-text/style.css";
|
|
21
|
+
*
|
|
22
|
+
* contributionGraph(element, data, { showCounts: true, countFontSize: 9 });
|
|
23
|
+
*/
|
|
24
|
+
export { buildGraph, clearGraph, computeGrid, formatDate, formatHumanDate, levelForCount, parseDate, resolveOptions, } from "./contributionGraph.js";
|
|
25
|
+
export { buildHtmlGraph, clearHtmlGraph, } from "./htmlGraph.js";
|
|
26
|
+
export { buildHudGraph, clearHudGraph, } from "./hudGraph.js";
|
|
27
|
+
export { fetchGitHubContributions, } from "./github.js";
|
|
28
|
+
export { buildProfileGraph, clearProfileGraph, } from "./profileGraph.js";
|
|
29
|
+
import { buildGraph, clearGraph, resolveOptions, } from "./contributionGraph.js";
|
|
30
|
+
import { buildHtmlGraph, } from "./htmlGraph.js";
|
|
31
|
+
import { buildHudGraph, clearHudGraph, } from "./hudGraph.js";
|
|
32
|
+
import { buildProfileGraph, clearProfileGraph, } from "./profileGraph.js";
|
|
33
|
+
import { attachTooltip } from "./tooltip.js";
|
|
34
|
+
function modeFor(options) {
|
|
35
|
+
if (options.hud)
|
|
36
|
+
return "hud";
|
|
37
|
+
if (options.profile)
|
|
38
|
+
return "profile";
|
|
39
|
+
return options.showCounts ? "html" : "svg";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a contribution graph controller for one element.
|
|
43
|
+
*/
|
|
44
|
+
export function contributionGraph(element, data, options = {}) {
|
|
45
|
+
let currentOptions = options;
|
|
46
|
+
let mode = null;
|
|
47
|
+
// HTML count mode and HUD mode both expose an update/destroy handle.
|
|
48
|
+
let handle = null;
|
|
49
|
+
let cleanupTooltip = null;
|
|
50
|
+
const teardown = () => {
|
|
51
|
+
cleanupTooltip?.();
|
|
52
|
+
cleanupTooltip = null;
|
|
53
|
+
handle?.destroy();
|
|
54
|
+
handle = null;
|
|
55
|
+
if (mode === "svg")
|
|
56
|
+
clearGraph(element);
|
|
57
|
+
if (mode === "hud")
|
|
58
|
+
clearHudGraph(element);
|
|
59
|
+
if (mode === "profile")
|
|
60
|
+
clearProfileGraph(element);
|
|
61
|
+
};
|
|
62
|
+
const render = (nextData, nextOptions) => {
|
|
63
|
+
const nextMode = modeFor(nextOptions);
|
|
64
|
+
currentOptions = nextOptions;
|
|
65
|
+
if (nextMode !== mode) {
|
|
66
|
+
teardown();
|
|
67
|
+
mode = nextMode;
|
|
68
|
+
if (nextMode === "hud") {
|
|
69
|
+
// The HUD owns its own graph + tooltip; no outer tooltip needed.
|
|
70
|
+
handle = buildHudGraph(element, nextData, nextOptions);
|
|
71
|
+
}
|
|
72
|
+
else if (nextMode === "profile") {
|
|
73
|
+
handle = buildProfileGraph(element, nextData, nextOptions);
|
|
74
|
+
}
|
|
75
|
+
else if (nextMode === "html") {
|
|
76
|
+
handle = buildHtmlGraph(element, nextData, nextOptions);
|
|
77
|
+
cleanupTooltip = attachTooltip(element, () => resolveOptions(currentOptions));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
buildGraph(element, nextData, nextOptions);
|
|
81
|
+
cleanupTooltip = attachTooltip(element, () => resolveOptions(currentOptions));
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (nextMode === "hud") {
|
|
86
|
+
handle.update(nextData, nextOptions);
|
|
87
|
+
}
|
|
88
|
+
else if (nextMode === "profile") {
|
|
89
|
+
handle.update(nextData, nextOptions);
|
|
90
|
+
}
|
|
91
|
+
else if (nextMode === "html") {
|
|
92
|
+
handle.update(nextData, resolveOptions(nextOptions));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
buildGraph(element, nextData, nextOptions);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
render(data, currentOptions);
|
|
99
|
+
return {
|
|
100
|
+
element,
|
|
101
|
+
set(data, options) {
|
|
102
|
+
render(data, options ?? currentOptions);
|
|
103
|
+
},
|
|
104
|
+
destroy() {
|
|
105
|
+
teardown();
|
|
106
|
+
mode = null;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./contributionGraph.js";
|
|
2
|
+
export interface ProfileGraphHandle {
|
|
3
|
+
update(data: ContributionDay[], options: ContributionGraphOptions): void;
|
|
4
|
+
destroy(): void;
|
|
5
|
+
}
|
|
6
|
+
export declare function buildProfileGraph(element: HTMLElement, data: ContributionDay[], options?: ContributionGraphOptions): ProfileGraphHandle;
|
|
7
|
+
export declare function clearProfileGraph(element: HTMLElement): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { buildGraph, clearGraph, resolveOptions, } from "./contributionGraph.js";
|
|
2
|
+
import { attachTooltip } from "./tooltip.js";
|
|
3
|
+
const PROFILE_CLASS = "cg-profile";
|
|
4
|
+
const GITHUB_SVG = `<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.866-.013-1.699-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.379.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z"></path></svg>`;
|
|
5
|
+
export function buildProfileGraph(element, data, options = {}) {
|
|
6
|
+
clearProfileGraph(element);
|
|
7
|
+
element.classList.add(PROFILE_CLASS);
|
|
8
|
+
const username = typeof options.profile === "object" && options.profile.username
|
|
9
|
+
? options.profile.username
|
|
10
|
+
: "";
|
|
11
|
+
let total = 0;
|
|
12
|
+
for (const day of data) {
|
|
13
|
+
total += day.count;
|
|
14
|
+
}
|
|
15
|
+
const formattedTotal = new Intl.NumberFormat("en-US").format(total);
|
|
16
|
+
element.innerHTML = `
|
|
17
|
+
<div class="cg-profile-header">
|
|
18
|
+
<span class="cg-profile-logo">${GITHUB_SVG}</span>
|
|
19
|
+
<span class="cg-profile-brand">GitHub</span>
|
|
20
|
+
${username ? `<span class="cg-profile-sep" aria-hidden="true"></span><span class="cg-profile-user">@${username}</span>` : ''}
|
|
21
|
+
</div>
|
|
22
|
+
<div class="cg-profile-inner">
|
|
23
|
+
<div class="cg-profile-graph"></div>
|
|
24
|
+
<div class="cg-profile-footer">
|
|
25
|
+
<div class="cg-profile-legend">
|
|
26
|
+
<span>Less</span>
|
|
27
|
+
<div class="cg-profile-legend-colors">
|
|
28
|
+
<i></i><i></i><i></i><i></i><i></i>
|
|
29
|
+
</div>
|
|
30
|
+
<span>More</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="cg-profile-total">
|
|
33
|
+
<strong>${formattedTotal}</strong> contributions this year
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
`;
|
|
38
|
+
const graphEl = element.querySelector(".cg-profile-graph");
|
|
39
|
+
// Luxury graph options
|
|
40
|
+
let graphOptions = mergeGraphOptions(options);
|
|
41
|
+
buildGraph(graphEl, data, graphOptions);
|
|
42
|
+
const cleanupTooltip = attachTooltip(graphEl, () => resolveOptions(graphOptions));
|
|
43
|
+
return {
|
|
44
|
+
update(nextData, nextOptions) {
|
|
45
|
+
graphOptions = mergeGraphOptions(nextOptions);
|
|
46
|
+
buildGraph(graphEl, nextData, graphOptions);
|
|
47
|
+
let nextTotal = 0;
|
|
48
|
+
for (const day of nextData)
|
|
49
|
+
nextTotal += day.count;
|
|
50
|
+
const fTotal = new Intl.NumberFormat("en-US").format(nextTotal);
|
|
51
|
+
const totalEl = element.querySelector(".cg-profile-total strong");
|
|
52
|
+
if (totalEl)
|
|
53
|
+
totalEl.textContent = fTotal;
|
|
54
|
+
},
|
|
55
|
+
destroy() {
|
|
56
|
+
cleanupTooltip();
|
|
57
|
+
clearGraph(graphEl);
|
|
58
|
+
clearProfileGraph(element);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function clearProfileGraph(element) {
|
|
63
|
+
element.classList.remove(PROFILE_CLASS);
|
|
64
|
+
element.replaceChildren();
|
|
65
|
+
}
|
|
66
|
+
/** Profile mode heatmap defaults */
|
|
67
|
+
function mergeGraphOptions(options) {
|
|
68
|
+
const { profile, ...rest } = options;
|
|
69
|
+
return {
|
|
70
|
+
// GitHub light-theme green ramp
|
|
71
|
+
colors: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
|
72
|
+
cellSize: 15,
|
|
73
|
+
cellGap: 3,
|
|
74
|
+
cellRadius: 4,
|
|
75
|
+
labelColor: "#71717a",
|
|
76
|
+
labelFontSize: 13,
|
|
77
|
+
weekStart: 0,
|
|
78
|
+
weekdayLabels: ["", "Mon", "", "Wed", "", "Fri", ""], // usually only Mon/Wed/Fri are shown
|
|
79
|
+
...rest,
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./index.js";
|
|
3
|
+
export interface ContributionGraphProps extends Omit<HTMLAttributes<HTMLDivElement>, "data"> {
|
|
4
|
+
data: ContributionDay[];
|
|
5
|
+
options?: ContributionGraphOptions;
|
|
6
|
+
}
|
|
7
|
+
export declare const ContributionGraph: import("react").ForwardRefExoticComponent<ContributionGraphProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createElement, forwardRef, useEffect, useImperativeHandle, useRef, } from "react";
|
|
2
|
+
import { contributionGraph, } from "./index.js";
|
|
3
|
+
export const ContributionGraph = forwardRef(({ data, options, ...props }, forwardedRef) => {
|
|
4
|
+
const elementRef = useRef(null);
|
|
5
|
+
const controllerRef = useRef(null);
|
|
6
|
+
useImperativeHandle(forwardedRef, () => elementRef.current, []);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const element = elementRef.current;
|
|
9
|
+
if (!element)
|
|
10
|
+
return;
|
|
11
|
+
controllerRef.current = contributionGraph(element, data, options);
|
|
12
|
+
return () => {
|
|
13
|
+
controllerRef.current?.destroy();
|
|
14
|
+
controllerRef.current = null;
|
|
15
|
+
};
|
|
16
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
controllerRef.current?.set(data, options);
|
|
19
|
+
}, [data, options]);
|
|
20
|
+
return createElement("div", { ...props, ref: elementRef });
|
|
21
|
+
});
|
|
22
|
+
ContributionGraph.displayName = "ContributionGraph";
|
package/dist/solid.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./index.js";
|
|
3
|
+
export interface ContributionGraphParams {
|
|
4
|
+
data: ContributionDay[];
|
|
5
|
+
options?: ContributionGraphOptions;
|
|
6
|
+
}
|
|
7
|
+
export declare function contributionGraph(element: HTMLElement, accessor: Accessor<ContributionGraphParams>): void;
|
|
8
|
+
declare module "solid-js" {
|
|
9
|
+
namespace JSX {
|
|
10
|
+
interface Directives {
|
|
11
|
+
contributionGraph: ContributionGraphParams;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/solid.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createRenderEffect, onCleanup } from "solid-js";
|
|
2
|
+
import { contributionGraph as createContributionGraph, } from "./index.js";
|
|
3
|
+
export function contributionGraph(element, accessor) {
|
|
4
|
+
const initial = accessor();
|
|
5
|
+
const controller = createContributionGraph(element, initial.data, initial.options);
|
|
6
|
+
createRenderEffect(() => {
|
|
7
|
+
const next = accessor();
|
|
8
|
+
controller.set(next.data, next.options);
|
|
9
|
+
});
|
|
10
|
+
onCleanup(() => {
|
|
11
|
+
controller.destroy();
|
|
12
|
+
});
|
|
13
|
+
}
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./index.js";
|
|
2
|
+
export interface ContributionGraphParams {
|
|
3
|
+
data: ContributionDay[];
|
|
4
|
+
options?: ContributionGraphOptions;
|
|
5
|
+
}
|
|
6
|
+
export declare function contributionGraph(element: HTMLElement, params: ContributionGraphParams): {
|
|
7
|
+
update(params: ContributionGraphParams): void;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
};
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { contributionGraph as createContributionGraph, } from "./index.js";
|
|
2
|
+
export function contributionGraph(element, params) {
|
|
3
|
+
const controller = createContributionGraph(element, params.data, params.options);
|
|
4
|
+
return {
|
|
5
|
+
update(params) {
|
|
6
|
+
controller.set(params.data, params.options);
|
|
7
|
+
},
|
|
8
|
+
destroy() {
|
|
9
|
+
controller.destroy();
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
package/dist/tooltip.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { slotText } from "slot-text";
|
|
2
|
+
import { formatHumanDate } from "./contributionGraph.js";
|
|
3
|
+
let tooltipEl = null;
|
|
4
|
+
let textController = null;
|
|
5
|
+
function getTooltip() {
|
|
6
|
+
if (!tooltipEl || !textController) {
|
|
7
|
+
tooltipEl = document.createElement("div");
|
|
8
|
+
tooltipEl.className = "cg-tooltip";
|
|
9
|
+
const textSpan = document.createElement("span");
|
|
10
|
+
textSpan.className = "cg-tooltip-text";
|
|
11
|
+
tooltipEl.appendChild(textSpan);
|
|
12
|
+
document.body.appendChild(tooltipEl);
|
|
13
|
+
textController = slotText(textSpan, "", {
|
|
14
|
+
direction: "up",
|
|
15
|
+
duration: 250,
|
|
16
|
+
stagger: 20,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return { el: tooltipEl, controller: textController };
|
|
20
|
+
}
|
|
21
|
+
export function attachTooltip(container, getOptions) {
|
|
22
|
+
let activeDate = null;
|
|
23
|
+
const show = (event) => {
|
|
24
|
+
const target = event.target;
|
|
25
|
+
const cell = target?.closest("rect, .cg-cell");
|
|
26
|
+
if (!cell) {
|
|
27
|
+
hide();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const date = cell.getAttribute("data-date");
|
|
31
|
+
if (!date)
|
|
32
|
+
return;
|
|
33
|
+
if (activeDate === date && tooltipEl?.classList.contains("is-visible")) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const count = Number(cell.getAttribute("data-count") ?? 0);
|
|
37
|
+
const level = Number(cell.getAttribute("data-level") ?? 0);
|
|
38
|
+
const opts = getOptions();
|
|
39
|
+
if (!opts.tooltip)
|
|
40
|
+
return;
|
|
41
|
+
const day = { date, count, level, dateLabel: formatHumanDate(date) };
|
|
42
|
+
const tip = opts.tooltip(day);
|
|
43
|
+
if (!tip) {
|
|
44
|
+
hide();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
activeDate = date;
|
|
48
|
+
const { el, controller } = getTooltip();
|
|
49
|
+
controller.set(tip, { skipUnchanged: true, direction: "up" });
|
|
50
|
+
const rect = cell.getBoundingClientRect();
|
|
51
|
+
const left = rect.left + window.scrollX + rect.width / 2;
|
|
52
|
+
const top = rect.top + window.scrollY - 6;
|
|
53
|
+
el.style.left = `${left}px`;
|
|
54
|
+
el.style.top = `${top}px`;
|
|
55
|
+
el.classList.add("is-visible");
|
|
56
|
+
};
|
|
57
|
+
const hide = (event) => {
|
|
58
|
+
if (event && event.type === "mouseout") {
|
|
59
|
+
const e = event;
|
|
60
|
+
const target = e.target;
|
|
61
|
+
const cell = target?.closest("rect, .cg-cell");
|
|
62
|
+
const related = e.relatedTarget;
|
|
63
|
+
if (cell && related && cell.contains(related)) {
|
|
64
|
+
// Mouse moved between children of the same cell; ignore.
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
activeDate = null;
|
|
69
|
+
if (tooltipEl) {
|
|
70
|
+
tooltipEl.classList.remove("is-visible");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
container.addEventListener("mouseover", show);
|
|
74
|
+
container.addEventListener("mouseout", hide);
|
|
75
|
+
container.addEventListener("focusin", show);
|
|
76
|
+
container.addEventListener("focusout", hide);
|
|
77
|
+
return () => {
|
|
78
|
+
container.removeEventListener("mouseover", show);
|
|
79
|
+
container.removeEventListener("mouseout", hide);
|
|
80
|
+
container.removeEventListener("focusin", show);
|
|
81
|
+
container.removeEventListener("focusout", hide);
|
|
82
|
+
hide();
|
|
83
|
+
};
|
|
84
|
+
}
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PropType } from "vue";
|
|
2
|
+
import { type ContributionDay, type ContributionGraphOptions } from "./index.js";
|
|
3
|
+
export declare const ContributionGraph: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
4
|
+
data: {
|
|
5
|
+
type: PropType<ContributionDay[]>;
|
|
6
|
+
required: true;
|
|
7
|
+
};
|
|
8
|
+
options: {
|
|
9
|
+
type: PropType<ContributionGraphOptions>;
|
|
10
|
+
default: undefined;
|
|
11
|
+
};
|
|
12
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
15
|
+
data: {
|
|
16
|
+
type: PropType<ContributionDay[]>;
|
|
17
|
+
required: true;
|
|
18
|
+
};
|
|
19
|
+
options: {
|
|
20
|
+
type: PropType<ContributionGraphOptions>;
|
|
21
|
+
default: undefined;
|
|
22
|
+
};
|
|
23
|
+
}>> & Readonly<{}>, {
|
|
24
|
+
options: ContributionGraphOptions;
|
|
25
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|