Skip to content

Commit 6476204

Browse files
Paul Armstrongpaularmstrong
authored andcommitted
blog: checkpoint periodization
blog: checkpoint periodization refactor: use external css for contribution-graph refactor: use external css for contribution-graph
1 parent f98c55c commit 6476204

File tree

11 files changed

+1356
-12
lines changed

11 files changed

+1356
-12
lines changed

.astro/types.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,13 @@ declare module 'astro:content' {
415415
collection: "blog";
416416
data: InferEntrySchema<"blog">
417417
} & { render(): Render[".mdx"] };
418+
"2023-09-19-software-development-periodization.mdx": {
419+
id: "2023-09-19-software-development-periodization.mdx";
420+
slug: "2023-09-19-software-development-periodization";
421+
body: string;
422+
collection: "blog";
423+
data: InferEntrySchema<"blog">
424+
} & { render(): Render[".mdx"] };
418425
};
419426
"labs": {
420427
"move-fast-with-confidence.mdx": {

astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ import tailwind from '@astrojs/tailwind';
99
import { remarkReadingTime } from './plugins/remark-reading-time.mjs';
1010
import react from '@astrojs/react';
1111
import rehypePrettyCode from 'rehype-pretty-code';
12+
import rehypeMermaid from 'rehype-mermaidjs';
1213

1314
const remarkPlugins = [remarkReadingTime];
1415
const rehypePlugins = [
16+
[
17+
rehypeMermaid,
18+
{
19+
strategy: 'inline-svg',
20+
},
21+
],
1522
[
1623
rehypePrettyCode,
1724
{

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"react-component-benchmark": "^2.0.0",
2323
"react-dom": "^18.2.0",
2424
"reading-time": "^1.5.0",
25+
"rehype-mermaidjs": "^1.0.1",
2526
"rehype-pretty-code": "^0.10.1",
2627
"sharp": "^0.32.5",
2728
"solid-js": "^1.7.11",
@@ -45,6 +46,7 @@
4546
"husky": "^8.0.3",
4647
"netlify-cli": "^16.2.0",
4748
"onerepo": "0.13.0",
49+
"playwright": "^1.36.1",
4850
"prettier": "^3.0.3",
4951
"prettier-plugin-astro": "^0.12.0",
5052
"typescript": "^5.2.2"
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import clsx from 'clsx';
2+
import './contribution-graph.css';
3+
4+
interface Props {
5+
caption?: string;
6+
contributions: Array<Array<[Date, number, boolean]>>;
7+
typeRange?: [number, number];
8+
}
9+
10+
const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeZone: 'UTC' }).format;
11+
const dayFullFormat = new Intl.DateTimeFormat('en-US', { weekday: 'long', timeZone: 'UTC' }).format;
12+
const dayShortFormat = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' }).format;
13+
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'short', timeZone: 'UTC' }).format;
14+
const monthFormatFull = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format;
15+
16+
export function ContributionGraph(props: Props) {
17+
const totals =
18+
props.contributions[0]?.reduce(
19+
(memo, [, , inRange], i) => {
20+
memo.push([
21+
props.contributions.reduce((memo, weekday) => {
22+
if (!weekday[i]) {
23+
return memo;
24+
}
25+
return memo + weekday[i]![1];
26+
}, 0),
27+
inRange,
28+
]);
29+
return memo;
30+
},
31+
[] as Array<[number, boolean]>,
32+
) ?? [];
33+
const max = Math.max(...totals.map(([t]) => t), 6);
34+
35+
const months = props.contributions[0]!.reduce(
36+
(memo, [day]) => {
37+
if (day.getMonth() === memo[memo.length - 1]?.date.getMonth()) {
38+
memo[memo.length - 1]!.colSpan += 1;
39+
} else {
40+
memo.push({ date: day, colSpan: 1 });
41+
}
42+
return memo;
43+
},
44+
[] as Array<{ colSpan: number; date: Date }>,
45+
);
46+
47+
return (
48+
<div class="contribution-graph not-prose overflow-scroll text-[10px] leading-none">
49+
<table>
50+
{props.caption ? <caption>{props.caption}</caption> : null}
51+
<thead>
52+
<tr>
53+
<th></th>
54+
{months.map(({ colSpan, date }) =>
55+
colSpan < 3 ? (
56+
<th colSpan={colSpan} />
57+
) : (
58+
<th role="columnheader" colSpan={colSpan}>
59+
<span aria-hidden>{monthFormat(date)}</span>
60+
<span class="sr-only">{monthFormatFull(date)}</span>
61+
</th>
62+
),
63+
)}
64+
</tr>
65+
</thead>
66+
<tbody>
67+
{props.contributions.map((dayOfWeek, w) => {
68+
return (
69+
<tr>
70+
<th role="rowheader">
71+
{!(w % 2) || w === 7 ? null : (
72+
<>
73+
<span class="sr-only">{dayFullFormat(dayOfWeek[0]![0])}</span>
74+
<span aria-hidden>{dayShortFormat(dayOfWeek[0]![0] ?? 0)}</span>
75+
</>
76+
)}
77+
</th>
78+
{dayOfWeek.map(([day, contributions, inRange]) => {
79+
const title = `${contributions} contributions on ${formatter(day)}`;
80+
return (
81+
<td class={clsx('day', opacity[contributions], getColor(inRange))} title={title}>
82+
<div class="h-2 w-2" />
83+
<span class="sr-only">{title}</span>
84+
</td>
85+
);
86+
})}
87+
</tr>
88+
);
89+
})}
90+
</tbody>
91+
</table>
92+
<table>
93+
<caption>Number of contributions by week</caption>
94+
<tbody>
95+
<tr role="presentation">
96+
<th role="rowheader" />
97+
{totals.map(([value, inRange]) => {
98+
return (
99+
<td class="total-bar-cell">
100+
<div class="total-bar-container">
101+
<div
102+
style={{ 'flex-basis': `${Math.ceil((value / max) * 100)}%` }}
103+
class={clsx('total-bar', opacity[Math.floor((value / max) * 10)], getColor(inRange))}
104+
/>
105+
</div>
106+
</td>
107+
);
108+
})}
109+
</tr>
110+
<tr>
111+
<th role="rowheader">
112+
<span aria-hidden>Tot.</span>
113+
<span class="sr-only">Total</span>
114+
</th>
115+
{totals.map(([value]) => (
116+
<td class="total-bar-cell">
117+
<span class="total">{value}</span>
118+
</td>
119+
))}
120+
</tr>
121+
</tbody>
122+
</table>
123+
</div>
124+
);
125+
}
126+
127+
function weekInRange(week: number, range: [number, number] = [-1, 1000]) {
128+
return !range || (range[0] <= week && range[1] >= week);
129+
}
130+
131+
function getColor(inRange: boolean) {
132+
return inRange ? 'in-range' : '';
133+
}
134+
function randomInt(min: number, max: number) {
135+
return Math.floor(Math.random() * (max - min) + min);
136+
}
137+
138+
function randomIdeal(week: number, low = false) {
139+
if (week === 15 || week === 35 || week === 36 || week === 54 || Math.random() < 0.08) {
140+
return 0;
141+
}
142+
return randomInt(...(low ? idealLowRanges : idealRanges)[week % 4]!);
143+
}
144+
145+
const idealRanges: Array<[number, number]> = [
146+
[0, 3],
147+
[2, 6],
148+
[2, 8],
149+
[4, 9],
150+
];
151+
152+
const idealLowRanges: Array<[number, number]> = [
153+
[0, 1],
154+
[0, 2],
155+
[0, 3],
156+
[0, 3],
157+
];
158+
159+
function randomBase(week: number) {
160+
return randomInt(...baseRanges[week % baseRanges.length]!);
161+
}
162+
163+
const baseRanges: Array<[number, number]> = [
164+
[0, 1],
165+
[0, 1],
166+
[0, 2],
167+
[0, 3],
168+
[0, 4],
169+
[1, 4],
170+
[1, 3],
171+
[0, 2],
172+
[1, 5],
173+
[1, 8],
174+
[3, 7],
175+
[3, 5],
176+
[3, 8],
177+
[3, 7],
178+
];
179+
180+
function randomSpecial() {
181+
return randomInt(1, 3);
182+
}
183+
184+
const opacity = [
185+
'opacity-10',
186+
'opacity-20',
187+
'opacity-30',
188+
'opacity-40',
189+
'opacity-50',
190+
'opacity-60',
191+
'opacity-70',
192+
'opacity-75',
193+
'opacity-80',
194+
'opacity-90',
195+
'opacity-100',
196+
];
197+
198+
const myContributions = [
199+
0, 0, 1, 3, 0, 3, 0, 0, 1, 4, 4, 2, 1, 0, 0, 1, 5, 11, 10, 0, 0, 0, 1, 0, 5, 3, 27, 0, 3, 3, 3, 1, 3, 6, 4, 5, 4, 14,
200+
4, 17, 13, 1, 0, 23, 23, 27, 36, 28, 13, 9, 14, 19, 36, 19, 28, 10, 10, 34, 8, 29, 37, 7, 12, 3, 50, 43, 30, 15, 29,
201+
17, 20, 28, 14, 10, 40, 26, 0, 18, 35, 45, 47, 8, 8, 0, 13, 30, 22, 9, 19, 34, 0, 5, 40, 26, 20, 17, 7, 0, 1, 32, 5,
202+
12, 12, 24, 0, 0, 31, 4, 9, 6, 0, 0, 0, 16, 1, 17, 6, 5, 0, 0, 18, 6, 7, 8, 23, 13, 0, 10, 1, 2, 2, 1, 0, 0, 8, 1, 3,
203+
2, 5, 0, 0, 32, 9, 10, 13, 3, 0, 0, 0, 9, 7, 1, 5, 0, 1, 20, 20, 9, 5, 7, 0, 0, 5, 6, 1, 0, 0, 0, 0, 0, 0, 6, 7, 6, 0,
204+
3, 3, 6, 3, 5, 7, 0, 18, 3, 3, 6, 7, 8, 1, 0, 3, 15, 19, 7, 9, 1, 4, 10, 4, 4, 3, 3, 3, 0, 6, 1, 2, 6, 10, 0, 0, 6, 1,
205+
6, 3, 8, 0, 0, 0, 0, 0, 0, 0, 0,
206+
];
207+
208+
type Contributor =
209+
| 'full'
210+
| 'underperformer'
211+
| 'weekdays'
212+
| 'ideal'
213+
| 'ideal-low'
214+
| 'base'
215+
| 'build'
216+
| 'specialization'
217+
| 'mine';
218+
219+
export function getContributions({
220+
type,
221+
start = new Date(Date.UTC(2022, 6, 24)),
222+
end = new Date(Date.UTC(2023, 6, 30)),
223+
visibleRange,
224+
}: {
225+
type: Contributor;
226+
start?: Date;
227+
end?: Date;
228+
visibleRange: [number, number];
229+
}) {
230+
let day = new Date(start);
231+
const days: Array<Array<[Date, number, boolean]>> = [[], [], [], [], [], [], []];
232+
let weekNum = 0;
233+
let i = 0;
234+
while (day < end) {
235+
const dayOfWeek = day.getDay();
236+
let contributions = 0;
237+
if (type === 'full') {
238+
contributions = randomInt(3, 10);
239+
} else if (type === 'mine') {
240+
contributions = myContributions[i]!;
241+
} else if (dayOfWeek <= 4) {
242+
switch (type) {
243+
case 'weekdays':
244+
contributions = randomInt(1, 9);
245+
break;
246+
case 'underperformer':
247+
contributions = Math.round(Math.random() * 0.55);
248+
break;
249+
case 'ideal':
250+
contributions = randomIdeal(weekNum);
251+
break;
252+
case 'ideal-low':
253+
contributions = randomIdeal(weekNum, true);
254+
break;
255+
case 'base':
256+
contributions = weekInRange(weekNum, visibleRange) ? randomBase(weekNum) : randomIdeal(weekNum);
257+
break;
258+
case 'build':
259+
contributions = weekInRange(weekNum, visibleRange) ? randomIdeal(weekNum) : randomBase(weekNum);
260+
break;
261+
case 'specialization':
262+
contributions = weekInRange(weekNum, visibleRange) ? randomSpecial() : randomIdeal(weekNum);
263+
break;
264+
}
265+
}
266+
267+
days[dayOfWeek]!.push([day, contributions, weekInRange(weekNum, visibleRange)]);
268+
day = new Date(day.setDate(day.getDate() + 1));
269+
weekNum += dayOfWeek === 6 ? 1 : 0;
270+
i += 1;
271+
}
272+
const sunday = days.pop();
273+
days.unshift(sunday!);
274+
return days;
275+
}

0 commit comments

Comments
 (0)