Skip to content

Commit 622d48e

Browse files
feat: Add support for excluding days of the week from the streak (#490)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 9fe466c commit 622d48e

File tree

14 files changed

+282
-24
lines changed

14 files changed

+282
-24
lines changed

README.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,28 @@ The `user` field is the only required option. All other fields are optional.
4343

4444
If the `theme` parameter is specified, any color customizations specified will be applied on top of the theme, overriding the theme's values.
4545

46-
| Parameter | Details | Example |
47-
| :------------------: | :---------------------------------------------: | :------------------------------------------------------------------------------------------------: |
48-
| `user` | GitHub username to show stats for | `DenverCoder1` |
49-
| `theme` | The theme to apply (Default: `default`) | `dark`, `radical`, etc. [🎨➜](./docs/themes.md) |
50-
| `hide_border` | Make the border transparent (Default: `false`) | `true` or `false` |
51-
| `border_radius` | Set the roundness of the edges (Default: `4.5`) | Number `0` (sharp corners) to `248` (ellipse) |
52-
| `background` | Background color (eg. `f2f2f2`, `35,d22,00f`) | **hex code** without `#`, **css color**, or gradient in the form `angle,start_color,...,end_color` |
53-
| `border` | Border color | **hex code** without `#` or **css color** |
54-
| `stroke` | Stroke line color between sections | **hex code** without `#` or **css color** |
55-
| `ring` | Color of the ring around the current streak | **hex code** without `#` or **css color** |
56-
| `fire` | Color of the fire in the ring | **hex code** without `#` or **css color** |
57-
| `currStreakNum` | Current streak number | **hex code** without `#` or **css color** |
58-
| `sideNums` | Total and longest streak numbers | **hex code** without `#` or **css color** |
59-
| `currStreakLabel` | Current streak label | **hex code** without `#` or **css color** |
60-
| `sideLabels` | Total and longest streak labels | **hex code** without `#` or **css color** |
61-
| `dates` | Date range text color | **hex code** without `#` or **css color** |
62-
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
63-
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
64-
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
65-
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |
66-
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |
46+
| Parameter | Details | Example |
47+
| :------------------: | :----------------------------------------------: | :------------------------------------------------------------------------------------------------: |
48+
| `user` | GitHub username to show stats for | `DenverCoder1` |
49+
| `theme` | The theme to apply (Default: `default`) | `dark`, `radical`, etc. [🎨➜](./docs/themes.md) |
50+
| `hide_border` | Make the border transparent (Default: `false`) | `true` or `false` |
51+
| `border_radius` | Set the roundness of the edges (Default: `4.5`) | Number `0` (sharp corners) to `248` (ellipse) |
52+
| `background` | Background color (eg. `f2f2f2`, `35,d22,00f`) | **hex code** without `#`, **css color**, or gradient in the form `angle,start_color,...,end_color` |
53+
| `border` | Border color | **hex code** without `#` or **css color** |
54+
| `stroke` | Stroke line color between sections | **hex code** without `#` or **css color** |
55+
| `ring` | Color of the ring around the current streak | **hex code** without `#` or **css color** |
56+
| `fire` | Color of the fire in the ring | **hex code** without `#` or **css color** |
57+
| `currStreakNum` | Current streak number | **hex code** without `#` or **css color** |
58+
| `sideNums` | Total and longest streak numbers | **hex code** without `#` or **css color** |
59+
| `currStreakLabel` | Current streak label | **hex code** without `#` or **css color** |
60+
| `sideLabels` | Total and longest streak labels | **hex code** without `#` or **css color** |
61+
| `dates` | Date range text color | **hex code** without `#` or **css color** |
62+
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
63+
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
64+
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
65+
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |
66+
| `exclude_days` | List of days of the week to exclude from streaks | Comma-separated list of day abbreviations (Sun,Mon,Tue,Wed,Thu,Fri,Sat) e.g. `Sun,Sat` |
67+
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |
6768

6869
### 🖌 Themes
6970

scripts/translation-progress.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function getProgress(array $translations): array
1717
"Week Streak",
1818
"Longest Week Streak",
1919
"Present",
20+
"Excluding",
2021
];
2122

2223
$translations_file = file(__DIR__ . "/../src/translations.php");

src/card.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,37 @@ function formatDate(string $dateString, string|null $format, string $locale): st
5353
return htmlspecialchars($formatted);
5454
}
5555

56+
/**
57+
* Translate days of the week
58+
*
59+
* Takes a list of days (eg. ["Sun", "Mon", "Sat"]) and returns the short abbreviation of the days of the week in another locale
60+
* e.g. ["Sun", "Mon", "Sat"] -> ["dim", "lun", "sam"]
61+
*
62+
* @param array<string> $days List of days to translate
63+
* @param string $locale Locale code
64+
*
65+
* @return array<string> Translated days
66+
*/
67+
function translateDays(array $days, string $locale): array
68+
{
69+
if ($locale === "en") {
70+
return $days;
71+
}
72+
$patternGenerator = new IntlDatePatternGenerator($locale);
73+
$pattern = $patternGenerator->getBestPattern("EEE");
74+
$dateFormatter = new IntlDateFormatter(
75+
$locale,
76+
IntlDateFormatter::NONE,
77+
IntlDateFormatter::NONE,
78+
pattern: $pattern
79+
);
80+
$translatedDays = [];
81+
foreach ($days as $day) {
82+
$translatedDays[] = $dateFormatter->format(new DateTime($day));
83+
}
84+
return $translatedDays;
85+
}
86+
5687
/**
5788
* Normalize a theme name
5889
*
@@ -335,6 +366,21 @@ function generateCard(array $stats, array $params = null): string
335366
$currentStreakRange = splitLines($currentStreakRange, 28, 0);
336367
$longestStreakRange = splitLines($longestStreakRange, 28, 0);
337368

369+
// if days are excluded, add a note to the corner
370+
$excludedDays = "";
371+
if (!empty($stats["excludedDays"])) {
372+
$daysCommaSeparated = implode(", ", translateDays($stats["excludedDays"], $localeCode));
373+
$offset = $direction === "rtl" ? 495 - 5 : 5;
374+
$excludedDays = "<g style='isolation: isolate'>
375+
<!-- Excluded Days -->
376+
<g transform='translate({$offset},187)'>
377+
<text stroke-width='0' text-anchor='right' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='10px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
378+
* {$localeTranslations["Excluding"]} {$daysCommaSeparated}
379+
</text>
380+
</g>
381+
</g>";
382+
}
383+
338384
return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'
339385
style='isolation: isolate' viewBox='0 0 495 195' width='495px' height='195px' direction='{$direction}'>
340386
<style>
@@ -443,6 +489,7 @@ function generateCard(array $stats, array $params = null): string
443489
</text>
444490
</g>
445491
</g>
492+
{$excludedDays}
446493
</g>
447494
</svg>
448495
";

src/demo/css/style.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,35 @@ h2 {
185185
text-transform: capitalize;
186186
}
187187

188+
.weekdays input {
189+
display: none !important;
190+
}
191+
192+
.weekdays input[type="checkbox"] + label {
193+
font-size: 90%;
194+
display: inline-block;
195+
border-radius: 6px;
196+
height: 30px;
197+
width: 30px;
198+
margin-right: 3px;
199+
line-height: 28px;
200+
text-align: center;
201+
cursor: pointer;
202+
background: var(--card-background);
203+
color: var(--text);
204+
border: 1px solid var(--border);
205+
}
206+
207+
.weekdays input[type="checkbox"]:checked + label {
208+
background: var(--text);
209+
color: var(--background);
210+
}
211+
212+
.weekdays input[type="checkbox"]:disabled + label {
213+
background: var(--card-background);
214+
color: var(--stroke);
215+
}
216+
188217
span[title="required"] {
189218
color: var(--red);
190219
}

src/demo/index.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,25 @@ function gtag() {
137137
<option value="weekly">Weekly</option>
138138
</select>
139139

140+
<label for="exclude_days">Exclude Days</label>
141+
<div class="weekdays">
142+
<input type="checkbox" value="Sun" id="weekday-sun" />
143+
<label for="weekday-sun" data-tooltip="Exclude Sunday" title="Exclude Sunday">S</label>
144+
<input type="checkbox" value="Mon" id="weekday-mon" />
145+
<label for="weekday-mon" data-tooltip="Exclude Monday" title="Exclude Monday">M</label>
146+
<input type="checkbox" value="Tue" id="weekday-tue" />
147+
<label for="weekday-tue" data-tooltip="Exclude Tuesday" title="Exclude Tuesday">T</label>
148+
<input type="checkbox" value="Wed" id="weekday-wed" />
149+
<label for="weekday-wed" data-tooltip="Exclude Wednesday" title="Exclude Wednesday">W</label>
150+
<input type="checkbox" value="Thu" id="weekday-thu" />
151+
<label for="weekday-thu" data-tooltip="Exclude Thursday" title="Exclude Thursday">T</label>
152+
<input type="checkbox" value="Fri" id="weekday-fri" />
153+
<label for="weekday-fri" data-tooltip="Exclude Friday" title="Exclude Friday">F</label>
154+
<input type="checkbox" value="Sat" id="weekday-sat" />
155+
<label for="weekday-sat" data-tooltip="Exclude Saturday" title="Exclude Saturday">S</label>
156+
<input type="text" id="exclude-days" name="exclude_days" class="param" />
157+
</div>
158+
140159
<label for="type">Output Type</label>
141160
<select class="param" id="type" name="type">
142161
<option value="svg">SVG</option>

src/demo/js/script.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const preview = {
1313
border_radius: "4.5",
1414
mode: "daily",
1515
type: "svg",
16+
exclude_days: "",
1617
},
1718

1819
/**
@@ -374,6 +375,38 @@ window.addEventListener(
374375
preview.checkColor(backgroundParams[1], "background-color1");
375376
preview.checkColor(backgroundParams[2], "background-color2");
376377
}
378+
// set weekday checkboxes
379+
const excludeDays = searchParams.get("exclude_days");
380+
if (excludeDays) {
381+
excludeDays.split(",").forEach((day) => {
382+
const checkbox = document.querySelector(`.weekdays input[type="checkbox"][value="${day}"]`);
383+
if (checkbox) {
384+
checkbox.checked = true;
385+
}
386+
});
387+
}
388+
// when weekdays are toggled, update the input field
389+
document.querySelectorAll('.weekdays input[type="checkbox"]').forEach((el) => {
390+
el.addEventListener("click", () => {
391+
const checked = document.querySelectorAll('.weekdays input[type="checkbox"]:checked');
392+
document.querySelector("#exclude-days").value = [...checked].map((node) => node.value).join(",");
393+
preview.update();
394+
});
395+
});
396+
// when mode is set to "weekly", disable checkboxes, otherwise enable them
397+
document.querySelector("#mode").addEventListener("change", () => {
398+
const mode = document.querySelector("#mode").value;
399+
document.querySelectorAll(".weekdays input[type='checkbox']").forEach((el) => {
400+
const labelEl = el.nextElementSibling;
401+
if (mode === "weekly") {
402+
el.disabled = true;
403+
labelEl.title = "Disabled in weekly mode";
404+
} else {
405+
el.disabled = false;
406+
labelEl.title = labelEl.dataset.tooltip;
407+
}
408+
});
409+
});
377410
// update previews
378411
preview.update();
379412
},

src/demo/preview.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"end" => date("Y-m-d"),
2323
"length" => 16,
2424
],
25+
"excludedDays" => normalizeDays(explode(",", $_GET["exclude_days"] ?? "")),
2526
];
2627

2728
if ($mode == "weekly") {
@@ -36,6 +37,7 @@
3637
"end" => getPreviousSunday(date("Y-m-d")),
3738
"length" => 3,
3839
];
40+
unset($demoStats["excludedDays"]);
3941
}
4042

4143
// set content type to SVG image

src/index.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
if (isset($_GET["mode"]) && $_GET["mode"] === "weekly") {
3939
$stats = getWeeklyContributionStats($contributions);
4040
} else {
41-
$stats = getContributionStats($contributions);
41+
// split and normalize excluded days
42+
$excludeDays = normalizeDays(explode(",", $_GET["exclude_days"] ?? ""));
43+
$stats = getContributionStats($contributions, $excludeDays);
4244
}
4345
renderOutput($stats);
4446
} catch (InvalidArgumentException | AssertionError $error) {

src/stats.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,48 @@ function getContributionDates(array $contributionGraphs): array
267267
return $contributions;
268268
}
269269

270+
/**
271+
* Normalize names of days of the week (eg. ["Sunday", " mon", "TUE"] -> ["Sun", "Mon", "Tue"])
272+
*
273+
* @param array<string> $days List of days of the week
274+
* @return array<string> List of normalized days of the week
275+
*/
276+
function normalizeDays(array $days): array
277+
{
278+
return array_filter(
279+
array_map(function ($dayOfWeek) {
280+
// trim whitespace, capitalize first letter only, return first 3 characters
281+
$dayOfWeek = substr(ucfirst(strtolower(trim($dayOfWeek))), 0, 3);
282+
// return day if valid, otherwise return null
283+
return in_array($dayOfWeek, ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]) ? $dayOfWeek : null;
284+
}, $days)
285+
);
286+
}
287+
288+
/**
289+
* Check if a day is an excluded day of the week
290+
*
291+
* @param string $date Date to check (Y-m-d)
292+
* @param array<string> $excludedDays List of days of the week to exclude
293+
* @return bool True if the day is excluded, false otherwise
294+
*/
295+
function isExcludedDay(string $date, array $excludedDays): bool
296+
{
297+
if (empty($excludedDays)) {
298+
return false;
299+
}
300+
$day = date("D", strtotime($date)); // "D" = Mon, Tue, Wed, etc.
301+
return in_array($day, $excludedDays);
302+
}
303+
270304
/**
271305
* Get a stats array with the contribution count, daily streak, and dates
272306
*
273307
* @param array<string,int> $contributions Y-M-D contribution dates with contribution counts
308+
* @param array<string> $excludedDays List of days of the week to exclude
274309
* @return array<string,mixed> Streak stats
275310
*/
276-
function getContributionStats(array $contributions): array
311+
function getContributionStats(array $contributions, array $excludedDays = []): array
277312
{
278313
// if no contributions, display error
279314
if (empty($contributions)) {
@@ -295,14 +330,15 @@ function getContributionStats(array $contributions): array
295330
"end" => $first,
296331
"length" => 0,
297332
],
333+
"excludedDays" => $excludedDays,
298334
];
299335

300336
// calculate the stats from the contributions array
301337
foreach ($contributions as $date => $count) {
302338
// add contribution count to total
303339
$stats["totalContributions"] += $count;
304340
// check if still in streak
305-
if ($count > 0) {
341+
if ($count > 0 || ($stats["currentStreak"]["length"] > 0 && isExcludedDay($date, $excludedDays))) {
306342
// increment streak
307343
++$stats["currentStreak"]["length"];
308344
$stats["currentStreak"]["end"] = $date;

src/translations.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"Week Streak" => "Week Streak",
3838
"Longest Week Streak" => "Longest Week Streak",
3939
"Present" => "Present",
40+
"Excluding" => "Excluding",
4041
],
4142
// Locales below are sorted alphabetically
4243
"ar" => [
@@ -121,6 +122,7 @@
121122
"Week Streak" => "רצף שבועי",
122123
"Longest Week Streak" => "רצף שבועי הכי ארוך",
123124
"Present" => "היום",
125+
"Excluding" => "לא כולל",
124126
],
125127
"hi" => [
126128
"Total Contributions" => "कुल योगदान",

0 commit comments

Comments
 (0)