Skip to content

Commit aa5ce63

Browse files
author
Paul Armstrong
committed
blog: checkpoint periodization
blog: checkpoint periodization refactor: use external css for contribution-graph refactor: use external css for contribution-graph
1 parent ac8ff0c commit aa5ce63

File tree

11 files changed

+1350
-12
lines changed

11 files changed

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

0 commit comments

Comments
 (0)