Skip to content

Commit 7a78f0b

Browse files
authored
Merge pull request #64 from JacobLinCool/feat-heatmap
feat: add heatmap
2 parents 4c5cbd8 + 09c2140 commit 7a78f0b

File tree

7 files changed

+195
-15
lines changed

7 files changed

+195
-15
lines changed

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ LeetCode and LeetCode CN are both supported.
1818
- ⚡️ Fast and global edge network - [Cloudflare Workers](https://workers.cloudflare.com/)
1919
- 🚫 No tracking, controlable cache - [Cache](#cache-default-60)
2020
- 🍀 Open source - [MIT License](./LICENSE)
21+
- ⚙️ Extended-cards: `activity`, `contest`, `heatmap`
2122

2223
It also has a [NPM package](https://www.npmjs.com/package/leetcode-card) and a [highly extensible system](./src/core/index.ts), so you can easily customize it to your needs.
2324

@@ -133,11 +134,9 @@ Hide elements on the card, it is a comma-separated list of element ids.
133134

134135
Extension, it is a comma-separated list of extension names.
135136

136-
Now there is only two notable extension: `activity` and `contest`.
137+
NOTICE: You can only use one of extended-card extensions (`activity`, `contest`, `heatmap`) at a time now, maybe they can be used together in the future.
137138

138-
NOTICE: You can only use one of `activity` and `contest` at a time now, maybe they can be used together in the future.
139-
140-
> But actually animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default.
139+
> Animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default.
141140
142141
Want to contribute a `nyan-cat` extension? PR is welcome!
143142

@@ -153,6 +152,12 @@ Want to contribute a `nyan-cat` extension? PR is welcome!
153152

154153
[![](https://leetcard.jacoblin.cool/lapor?ext=contest)](https://leetcard.jacoblin.cool/lapor?ext=contest)
155154

155+
```md
156+
![](https://leetcard.jacoblin.cool/lapor?ext=heatmap)
157+
```
158+
159+
[![](https://leetcard.jacoblin.cool/lapor?ext=heatmap)](https://leetcard.jacoblin.cool/lapor?ext=heatmap)
160+
156161
#### `cache` (default: `60`)
157162

158163
Cache time in seconds.
@@ -273,11 +278,9 @@ Some examples:
273278

274279
Extension, it is a comma-separated list of extension names.
275280

276-
Now there is only two notable extension: `activity` and `contest`.
277-
278-
NOTICE: You can only use one of `activity` and `contest` at a time now, maybe they can be used together in the future.
281+
NOTICE: You can only use one of extended-card extensions (`activity`, `contest`, `heatmap`) at a time now, maybe they can be used together in the future.
279282

280-
> But actually animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default.
283+
> Animation, font, theme, and external stylesheet are all implemented by extensions and enabled by default.
281284
282285
Want to contribute a `nyan-cat` extension? PR is welcome!
283286

@@ -300,3 +303,13 @@ Show your contest rating history.
300303
```
301304

302305
[![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=contest)](https://leetcard.jacoblin.cool/lapor?ext=contest)
306+
307+
#### `heatmap`
308+
309+
Show heatmap in the past 52 weeks.
310+
311+
```md
312+
![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=heatmap)
313+
```
314+
315+
[![Leetcode Stats](https://leetcard.jacoblin.cool/lapor?ext=heatmap)](https://leetcard.jacoblin.cool/lapor?ext=heatmap)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
},
4242
"dependencies": {
4343
"itty-router": "2.6.1",
44-
"leetcode-query": "0.2.3",
44+
"leetcode-query": "0.2.5",
4545
"nano-font": "0.3.1"
4646
},
4747
"devDependencies": {

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cloudflare-worker/demo/demo.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ <h1>LeetCode Stats Card</h1>
2727
<option value="" selected>No Extension</option>
2828
<option value="activity">Activity</option>
2929
<option value="contest">Contest</option>
30+
<option value="heatmap">Heatmap</option>
31+
</select>
32+
<select id="site">
33+
<option value="us" selected>Source: LeetCode</option>
34+
<option value="cn">Source: LeetCode CN</option>
3035
</select>
3136
<div>
3237
<button onclick="preview()">Preview</button>
@@ -96,7 +101,8 @@ <h1>LeetCode Stats Card</h1>
96101
encodeURIComponent(value("theme")) +
97102
"&font=" +
98103
encodeURIComponent(value("font")) +
99-
(value("extension") ? "&ext=" + encodeURIComponent(value("extension")) : "")
104+
(value("extension") ? "&ext=" + encodeURIComponent(value("extension")) : "") +
105+
(value("site") === "cn" ? "&site=cn" : "")
100106
);
101107
}
102108
function preview() {

src/cloudflare-worker/sanitize.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Config,
55
ContestExtension,
66
FontExtension,
7+
HeatmapExtension,
78
RemoteStyleExtension,
89
ThemeExtension,
910
} from "../core";
@@ -72,6 +73,8 @@ export function sanitize(config: Record<string, string>): Config {
7273
sanitized.extensions.push(ActivityExtension);
7374
} else if (config.ext === "contest" || config.extension === "contest") {
7475
sanitized.extensions.push(ContestExtension);
76+
} else if (config.ext === "heatmap" || config.extension === "heatmap") {
77+
sanitized.extensions.push(HeatmapExtension);
7578
}
7679

7780
if (config.border) {

src/core/exts/heatmap.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { LeetCode, LeetCodeCN } from "leetcode-query";
2+
import { Generator } from "../card";
3+
import { Item } from "../item";
4+
import { Extension } from "../types";
5+
6+
export async function HeatmapExtension(generator: Generator): Promise<Extension> {
7+
const pre_counts = new Promise<Record<string, number>>((resolve) => {
8+
if (generator.config.site === "us") {
9+
const lc = new LeetCode();
10+
lc.once("receive-graphql", async (res) => {
11+
try {
12+
const { data } = (await res.json()) as {
13+
data: { user: { calendar: { calendar: string } } };
14+
};
15+
resolve(JSON.parse(data.user.calendar.calendar));
16+
} catch (e) {
17+
resolve({});
18+
}
19+
});
20+
lc.graphql({
21+
operationName: "calendar",
22+
query: `query calendar($username: String!, $year: Int) { user: matchedUser(username: $username) { calendar: userCalendar(year: $year) { calendar: submissionCalendar } } }`,
23+
variables: { username: generator.config.username },
24+
});
25+
} else {
26+
const lc = new LeetCodeCN();
27+
lc.once("receive-graphql", async (res) => {
28+
try {
29+
const { data } = (await res.json()) as {
30+
data: { calendar: { calendar: string } };
31+
};
32+
resolve(JSON.parse(data.calendar.calendar));
33+
} catch (e) {
34+
resolve({});
35+
}
36+
});
37+
lc.graphql(
38+
{
39+
operationName: "calendar",
40+
query: `query calendar($username: String!, $year: Int) { calendar: userCalendar(userSlug: $username, year: $year) { calendar: submissionCalendar } }`,
41+
variables: { username: generator.config.username },
42+
},
43+
"/graphql/noj-go/",
44+
);
45+
}
46+
});
47+
48+
return async function Heatmap(generator, data, body, styles) {
49+
if (generator.config.height < 320) {
50+
generator.config.height = 320;
51+
}
52+
53+
const counts = await pre_counts;
54+
const today = Math.floor(Date.now() / 86400_000) * 86400;
55+
const width = generator.config.width - 40;
56+
const wrap = +(width / 52).toFixed(2);
57+
const block = wrap * 0.9;
58+
59+
const extension = new Item("g", {
60+
id: "ext-heatmap",
61+
style: { transform: `translate(0px, 200px)` },
62+
children: [
63+
new Item("line", {
64+
attr: { x1: 10, y1: 0, x2: generator.config.width - 10, y2: 0 },
65+
style: { stroke: "var(--bg-1)", "stroke-width": 1 },
66+
}),
67+
new Item("text", {
68+
content: "Heatmap (Last 52 Weeks)",
69+
id: "ext-heatmap-title",
70+
style: {
71+
transform: `translate(20px, 20px)`,
72+
fill: "var(--text-0)",
73+
opacity: generator.config.animation !== false ? 0 : 1,
74+
animation:
75+
generator.config.animation !== false
76+
? "fade_in 1 0.3s 1.7s forwards"
77+
: "",
78+
},
79+
}),
80+
],
81+
});
82+
83+
const blocks = new Item("g", {
84+
id: "ext-heatmap-blocks",
85+
children: [],
86+
style: {
87+
transform: `translate(20px, 35px)`,
88+
opacity: generator.config.animation !== false ? 0 : 1,
89+
animation:
90+
generator.config.animation !== false ? "fade_in 1 0.3s 1.9s forwards" : "",
91+
},
92+
});
93+
extension.children?.push(blocks);
94+
95+
for (let i = 0; i < 364; i++) {
96+
const count = counts[today - i * 86400] || 0;
97+
const opacity = calc_opacity(count);
98+
99+
blocks.children?.push(
100+
new Item("rect", {
101+
attr: {
102+
class: `ext-heatmap-${count}`,
103+
},
104+
style: {
105+
transform: `translate(${width - wrap * (Math.floor(i / 7) + 1)}px, ${
106+
wrap * (6 - (i % 7))
107+
}px)`,
108+
fill: `var(--color-1)`,
109+
opacity: opacity,
110+
width: `${block}px`,
111+
height: `${block}px`,
112+
stroke: "var(--color-0)",
113+
"stroke-width": +(i === 0),
114+
rx: 2,
115+
},
116+
}),
117+
);
118+
}
119+
120+
const from = new Date((today - 86400 * 364) * 1000);
121+
const to = new Date(today * 1000);
122+
extension.children?.push(
123+
new Item("text", {
124+
content: `${from.getFullYear()}.${from.getMonth() + 1}.${from.getDate()}`,
125+
id: "ext-heatmap-from",
126+
style: {
127+
transform: `translate(20px, 110px)`,
128+
fill: "var(--text-0)",
129+
opacity: generator.config.animation !== false ? 0 : 1,
130+
animation:
131+
generator.config.animation !== false ? "fade_in 1 0.3s 2.1s forwards" : "",
132+
"font-size": "10px",
133+
},
134+
}),
135+
new Item("text", {
136+
content: `${to.getFullYear()}.${to.getMonth() + 1}.${to.getDate()}`,
137+
id: "ext-heatmap-to",
138+
style: {
139+
transform: `translate(${generator.config.width - 20}px, 110px)`,
140+
fill: "var(--text-0)",
141+
opacity: generator.config.animation !== false ? 0 : 1,
142+
animation:
143+
generator.config.animation !== false ? "fade_in 1 0.3s 2.1s forwards" : "",
144+
"font-size": "10px",
145+
"text-anchor": "end",
146+
},
147+
}),
148+
);
149+
150+
body["ext-heatmap"] = () => extension;
151+
};
152+
}
153+
154+
function calc_opacity(count: number, max = 8): number {
155+
return Math.sin(Math.min(1, (count + 0.5) / max) * Math.PI * 0.5);
156+
}

src/core/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ActivityExtension } from "./exts/activity";
44
import { AnimationExtension } from "./exts/animation";
55
import { ContestExtension } from "./exts/contest";
66
import { FontExtension } from "./exts/font";
7+
import { HeatmapExtension } from "./exts/heatmap";
78
import { RemoteStyleExtension } from "./exts/remote-style";
89
import { ThemeExtension } from "./exts/theme";
910
import { Config } from "./types";
@@ -43,6 +44,7 @@ export {
4344
AnimationExtension,
4445
ContestExtension,
4546
FontExtension,
46-
ThemeExtension,
47+
HeatmapExtension,
4748
RemoteStyleExtension,
49+
ThemeExtension,
4850
};

0 commit comments

Comments
 (0)