Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for excluding days of the week from the streak #490

Merged
merged 16 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,28 @@ The `user` field is the only required option. All other fields are optional.

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

| Parameter | Details | Example |
| :------------------: | :---------------------------------------------: | :------------------------------------------------------------------------------------------------: |
| `user` | GitHub username to show stats for | `DenverCoder1` |
| `theme` | The theme to apply (Default: `default`) | `dark`, `radical`, etc. [🎨➜](./docs/themes.md) |
| `hide_border` | Make the border transparent (Default: `false`) | `true` or `false` |
| `border_radius` | Set the roundness of the edges (Default: `4.5`) | Number `0` (sharp corners) to `248` (ellipse) |
| `background` | Background color (eg. `f2f2f2`, `35,d22,00f`) | **hex code** without `#`, **css color**, or gradient in the form `angle,start_color,...,end_color` |
| `border` | Border color | **hex code** without `#` or **css color** |
| `stroke` | Stroke line color between sections | **hex code** without `#` or **css color** |
| `ring` | Color of the ring around the current streak | **hex code** without `#` or **css color** |
| `fire` | Color of the fire in the ring | **hex code** without `#` or **css color** |
| `currStreakNum` | Current streak number | **hex code** without `#` or **css color** |
| `sideNums` | Total and longest streak numbers | **hex code** without `#` or **css color** |
| `currStreakLabel` | Current streak label | **hex code** without `#` or **css color** |
| `sideLabels` | Total and longest streak labels | **hex code** without `#` or **css color** |
| `dates` | Date range text color | **hex code** without `#` or **css color** |
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |
| Parameter | Details | Example |
| :------------------: | :----------------------------------------------: | :------------------------------------------------------------------------------------------------: |
| `user` | GitHub username to show stats for | `DenverCoder1` |
| `theme` | The theme to apply (Default: `default`) | `dark`, `radical`, etc. [🎨➜](./docs/themes.md) |
| `hide_border` | Make the border transparent (Default: `false`) | `true` or `false` |
| `border_radius` | Set the roundness of the edges (Default: `4.5`) | Number `0` (sharp corners) to `248` (ellipse) |
| `background` | Background color (eg. `f2f2f2`, `35,d22,00f`) | **hex code** without `#`, **css color**, or gradient in the form `angle,start_color,...,end_color` |
| `border` | Border color | **hex code** without `#` or **css color** |
| `stroke` | Stroke line color between sections | **hex code** without `#` or **css color** |
| `ring` | Color of the ring around the current streak | **hex code** without `#` or **css color** |
| `fire` | Color of the fire in the ring | **hex code** without `#` or **css color** |
| `currStreakNum` | Current streak number | **hex code** without `#` or **css color** |
| `sideNums` | Total and longest streak numbers | **hex code** without `#` or **css color** |
| `currStreakLabel` | Current streak label | **hex code** without `#` or **css color** |
| `sideLabels` | Total and longest streak labels | **hex code** without `#` or **css color** |
| `dates` | Date range text color | **hex code** without `#` or **css color** |
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |
| `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` |
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |

### 🖌 Themes

Expand Down
1 change: 1 addition & 0 deletions scripts/translation-progress.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function getProgress(array $translations): array
"Week Streak",
"Longest Week Streak",
"Present",
"Excluding",
];

$translations_file = file(__DIR__ . "/../src/translations.php");
Expand Down
47 changes: 47 additions & 0 deletions src/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,37 @@ function formatDate(string $dateString, string|null $format, string $locale): st
return htmlspecialchars($formatted);
}

/**
* Translate days of the week
*
* Takes a list of days (eg. ["Sun", "Mon", "Sat"]) and returns the short abbreviation of the days of the week in another locale
* e.g. ["Sun", "Mon", "Sat"] -> ["dim", "lun", "sam"]
*
* @param array<string> $days List of days to translate
* @param string $locale Locale code
*
* @return array<string> Translated days
*/
function translateDays(array $days, string $locale): array
{
if ($locale === "en") {
return $days;
}
$patternGenerator = new IntlDatePatternGenerator($locale);
$pattern = $patternGenerator->getBestPattern("EEE");
$dateFormatter = new IntlDateFormatter(
$locale,
IntlDateFormatter::NONE,
IntlDateFormatter::NONE,
pattern: $pattern
);
$translatedDays = [];
foreach ($days as $day) {
$translatedDays[] = $dateFormatter->format(new DateTime($day));
}
return $translatedDays;
}

/**
* Normalize a theme name
*
Expand Down Expand Up @@ -335,6 +366,21 @@ function generateCard(array $stats, array $params = null): string
$currentStreakRange = splitLines($currentStreakRange, 28, 0);
$longestStreakRange = splitLines($longestStreakRange, 28, 0);

// if days are excluded, add a note to the corner
$excludedDays = "";
if (!empty($stats["excludedDays"])) {
$daysCommaSeparated = implode(", ", translateDays($stats["excludedDays"], $localeCode));
$offset = $direction === "rtl" ? 495 - 5 : 5;
$excludedDays = "<g style='isolation: isolate'>
<!-- Excluded Days -->
<g transform='translate({$offset},187)'>
<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'>
* {$localeTranslations["Excluding"]} {$daysCommaSeparated}
</text>
</g>
</g>";
}

return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'
style='isolation: isolate' viewBox='0 0 495 195' width='495px' height='195px' direction='{$direction}'>
<style>
Expand Down Expand Up @@ -443,6 +489,7 @@ function generateCard(array $stats, array $params = null): string
</text>
</g>
</g>
{$excludedDays}
</g>
</svg>
";
Expand Down
29 changes: 29 additions & 0 deletions src/demo/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,35 @@ h2 {
text-transform: capitalize;
}

.weekdays input {
display: none !important;
}

.weekdays input[type="checkbox"] + label {
font-size: 90%;
display: inline-block;
border-radius: 6px;
height: 30px;
width: 30px;
margin-right: 3px;
line-height: 28px;
text-align: center;
cursor: pointer;
background: var(--card-background);
color: var(--text);
border: 1px solid var(--border);
}

.weekdays input[type="checkbox"]:checked + label {
background: var(--text);
color: var(--background);
}

.weekdays input[type="checkbox"]:disabled + label {
background: var(--card-background);
color: var(--stroke);
}

span[title="required"] {
color: var(--red);
}
Expand Down
19 changes: 19 additions & 0 deletions src/demo/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,25 @@ function gtag() {
<option value="weekly">Weekly</option>
</select>

<label for="exclude_days">Exclude Days</label>
<div class="weekdays">
<input type="checkbox" value="Sun" id="weekday-sun" />
<label for="weekday-sun" data-tooltip="Exclude Sunday" title="Exclude Sunday">S</label>
<input type="checkbox" value="Mon" id="weekday-mon" />
<label for="weekday-mon" data-tooltip="Exclude Monday" title="Exclude Monday">M</label>
<input type="checkbox" value="Tue" id="weekday-tue" />
<label for="weekday-tue" data-tooltip="Exclude Tuesday" title="Exclude Tuesday">T</label>
<input type="checkbox" value="Wed" id="weekday-wed" />
<label for="weekday-wed" data-tooltip="Exclude Wednesday" title="Exclude Wednesday">W</label>
<input type="checkbox" value="Thu" id="weekday-thu" />
<label for="weekday-thu" data-tooltip="Exclude Thursday" title="Exclude Thursday">T</label>
<input type="checkbox" value="Fri" id="weekday-fri" />
<label for="weekday-fri" data-tooltip="Exclude Friday" title="Exclude Friday">F</label>
<input type="checkbox" value="Sat" id="weekday-sat" />
<label for="weekday-sat" data-tooltip="Exclude Saturday" title="Exclude Saturday">S</label>
<input type="text" id="exclude-days" name="exclude_days" class="param" />
</div>

<label for="type">Output Type</label>
<select class="param" id="type" name="type">
<option value="svg">SVG</option>
Expand Down
33 changes: 33 additions & 0 deletions src/demo/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const preview = {
border_radius: "4.5",
mode: "daily",
type: "svg",
exclude_days: "",
},

/**
Expand Down Expand Up @@ -374,6 +375,38 @@ window.addEventListener(
preview.checkColor(backgroundParams[1], "background-color1");
preview.checkColor(backgroundParams[2], "background-color2");
}
// set weekday checkboxes
const excludeDays = searchParams.get("exclude_days");
if (excludeDays) {
excludeDays.split(",").forEach((day) => {
const checkbox = document.querySelector(`.weekdays input[type="checkbox"][value="${day}"]`);
if (checkbox) {
checkbox.checked = true;
}
});
}
// when weekdays are toggled, update the input field
document.querySelectorAll('.weekdays input[type="checkbox"]').forEach((el) => {
el.addEventListener("click", () => {
const checked = document.querySelectorAll('.weekdays input[type="checkbox"]:checked');
document.querySelector("#exclude-days").value = [...checked].map((node) => node.value).join(",");
preview.update();
});
});
// when mode is set to "weekly", disable checkboxes, otherwise enable them
document.querySelector("#mode").addEventListener("change", () => {
const mode = document.querySelector("#mode").value;
document.querySelectorAll(".weekdays input[type='checkbox']").forEach((el) => {
const labelEl = el.nextElementSibling;
if (mode === "weekly") {
el.disabled = true;
labelEl.title = "Disabled in weekly mode";
} else {
el.disabled = false;
labelEl.title = labelEl.dataset.tooltip;
}
});
});
// update previews
preview.update();
},
Expand Down
2 changes: 2 additions & 0 deletions src/demo/preview.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"end" => date("Y-m-d"),
"length" => 16,
],
"excludedDays" => normalizeDays(explode(",", $_GET["exclude_days"] ?? "")),
];

if ($mode == "weekly") {
Expand All @@ -36,6 +37,7 @@
"end" => getPreviousSunday(date("Y-m-d")),
"length" => 3,
];
unset($demoStats["excludedDays"]);
}

// set content type to SVG image
Expand Down
4 changes: 3 additions & 1 deletion src/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
if (isset($_GET["mode"]) && $_GET["mode"] === "weekly") {
$stats = getWeeklyContributionStats($contributions);
} else {
$stats = getContributionStats($contributions);
// split and normalize excluded days
$excludeDays = normalizeDays(explode(",", $_GET["exclude_days"] ?? ""));
$stats = getContributionStats($contributions, $excludeDays);
}
renderOutput($stats);
} catch (InvalidArgumentException | AssertionError $error) {
Expand Down
40 changes: 38 additions & 2 deletions src/stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,13 +267,48 @@ function getContributionDates(array $contributionGraphs): array
return $contributions;
}

/**
* Normalize names of days of the week (eg. ["Sunday", " mon", "TUE"] -> ["Sun", "Mon", "Tue"])
*
* @param array<string> $days List of days of the week
* @return array<string> List of normalized days of the week
*/
function normalizeDays(array $days): array
{
return array_filter(
array_map(function ($dayOfWeek) {
// trim whitespace, capitalize first letter only, return first 3 characters
$dayOfWeek = substr(ucfirst(strtolower(trim($dayOfWeek))), 0, 3);
// return day if valid, otherwise return null
return in_array($dayOfWeek, ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]) ? $dayOfWeek : null;
}, $days)
);
}

/**
* Check if a day is an excluded day of the week
*
* @param string $date Date to check (Y-m-d)
* @param array<string> $excludedDays List of days of the week to exclude
* @return bool True if the day is excluded, false otherwise
*/
function isExcludedDay(string $date, array $excludedDays): bool
{
if (empty($excludedDays)) {
return false;
}
$day = date("D", strtotime($date)); // "D" = Mon, Tue, Wed, etc.
return in_array($day, $excludedDays);
}

/**
* Get a stats array with the contribution count, daily streak, and dates
*
* @param array<string,int> $contributions Y-M-D contribution dates with contribution counts
* @param array<string> $excludedDays List of days of the week to exclude
* @return array<string,mixed> Streak stats
*/
function getContributionStats(array $contributions): array
function getContributionStats(array $contributions, array $excludedDays = []): array
{
// if no contributions, display error
if (empty($contributions)) {
Expand All @@ -295,14 +330,15 @@ function getContributionStats(array $contributions): array
"end" => $first,
"length" => 0,
],
"excludedDays" => $excludedDays,
];

// calculate the stats from the contributions array
foreach ($contributions as $date => $count) {
// add contribution count to total
$stats["totalContributions"] += $count;
// check if still in streak
if ($count > 0) {
if ($count > 0 || ($stats["currentStreak"]["length"] > 0 && isExcludedDay($date, $excludedDays))) {
// increment streak
++$stats["currentStreak"]["length"];
$stats["currentStreak"]["end"] = $date;
Expand Down
2 changes: 2 additions & 0 deletions src/translations.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"Week Streak" => "Week Streak",
"Longest Week Streak" => "Longest Week Streak",
"Present" => "Present",
"Excluding" => "Excluding",
],
// Locales below are sorted alphabetically
"ar" => [
Expand Down Expand Up @@ -121,6 +122,7 @@
"Week Streak" => "רצף שבועי",
"Longest Week Streak" => "רצף שבועי הכי ארוך",
"Present" => "היום",
"Excluding" => "לא כולל",
],
"hi" => [
"Total Contributions" => "कुल योगदान",
Expand Down
11 changes: 11 additions & 0 deletions tests/RenderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class RenderTest extends TestCase
"end" => "2019-04-12",
"length" => 16,
],
"excludedDays" => [],
];

/**
Expand Down Expand Up @@ -204,4 +205,14 @@ public function testGradientBackgroundWithMoreThan2Colors(): void
$render
);
}

/**
* Test excluding days
*/
public function testExcludeDays(): void
{
$this->testStats["excludedDays"] = ["Sun", "Sat"];
$render = generateOutput($this->testStats, $this->testParams)["body"];
$this->assertStringContainsString("* Excluding Sun, Sat", $render);
}
}
Loading