From a0abf9eb5c20f4166499147b4b0d37410aef2fc1 Mon Sep 17 00:00:00 2001 From: Emin Kocan Date: Mon, 16 Dec 2024 16:29:11 +0100 Subject: [PATCH] Add heatmap javascript logic, styles and localization --- app/assets/javascripts/heatmap.js | 135 +++++++++++++++++++++++++++++ app/assets/stylesheets/heatmap.css | 3 + config/locales/en.yml | 4 + 3 files changed, 142 insertions(+) create mode 100644 app/assets/javascripts/heatmap.js create mode 100644 app/assets/stylesheets/heatmap.css diff --git a/app/assets/javascripts/heatmap.js b/app/assets/javascripts/heatmap.js new file mode 100644 index 0000000000..15dc31331d --- /dev/null +++ b/app/assets/javascripts/heatmap.js @@ -0,0 +1,135 @@ +/* global CalHeatmap, CalendarLabel, Tooltip */ +document.addEventListener("DOMContentLoaded", () => { + const heatmapElement = document.querySelector("#cal-heatmap"); + + // Don't render heatmap if there is no heatmap element in the DOM + if (!heatmapElement) { + console.warn("Heatmap element not found in the DOM."); + return; + } + + const { heatmapData, locale, weekdays, rangeColors, startDate } = initializeHeatmapConfig(heatmapElement); + + let cal = new CalHeatmap(); + + // Initialize/repaint the heatmap + function renderHeatmap(theme) { + cal.destroy(); // Ensure no duplication when repainting + cal = new CalHeatmap(); + cal.paint({ + itemSelector: "#cal-heatmap", + theme: theme, + domain: { + type: "month", + gutter: 4, + label: { + text: (timestamp) => getMonthNameFromTranslations(locale, new Date(timestamp).getMonth()), + position: "top", + textAlign: "middle" + }, + dynamicDimension: true + }, + subDomain: { + type: "ghDay", + radius: 2, + width: 11, + height: 11, + gutter: 4 + }, + date: { + start: startDate + }, + range: 13, + data: { + source: heatmapData, + type: "json", + x: "date", + y: "total_changes" + }, + scale: { + color: { + type: "threshold", + range: theme === "dark" ? rangeColors : rangeColors.reverse(), + domain: [10, 20, 30, 40] + } + } + }, [ + [CalendarLabel, { + position: "left", + key: "left", + text: () => weekdays, + textAlign: "end", + width: 30, + padding: [23, 10, 0, 0] + }], + [Tooltip, { + text: (date, value) => getTooltipText(date, value, locale) + }] + ]); + } + + // Initialize the heatmap with the current theme + let currentTheme = getTheme(); + renderHeatmap(currentTheme); + + // Listen for theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + mediaQuery.addEventListener("change", (e) => { + const newTheme = e.matches ? "dark" : "light"; + if (newTheme !== currentTheme) { + currentTheme = newTheme; + renderHeatmap(currentTheme); + } + }); +}, { once: true }); + +function initializeHeatmapConfig(heatmapElement) { + const heatmapData = heatmapElement.dataset.heatmap ? JSON.parse(heatmapElement.dataset.heatmap) : []; + const applicationData = $("head").data(); + const locale = applicationData.locale; + const weekdays = getLocalizedWeekdays(locale); + const rangeColors = ["#14432a", "#166b34", "#37a446", "#4dd05a"]; + const startDate = new Date(Date.now() - (365 * 24 * 60 * 60 * 1000)); + + return { heatmapData, locale, weekdays, rangeColors, startDate }; +} + +function getTheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +function getLocalizedWeekdays(locale) { + const translations = I18n.translations[locale] || I18n.translations.en; + const date = translations && translations.date; + const abbrDayNames = date && date.abbr_day_names; + + return (abbrDayNames || []).map((day, index) => + index % 2 === 0 ? day : "" + ); +} + +function getMonthNameFromTranslations(locale, monthIndex) { + const translations = I18n.translations[locale] || I18n.translations.en; + const date = translations && translations.date; + const abbrMonthNames = date && date.abbr_month_names; + + const months = abbrMonthNames || []; + return months[monthIndex + 1] || ""; +} + +function getTooltipText(date, value, locale) { + const jsDate = new Date(date); + const translations = I18n.translations[locale] || I18n.translations.en; + const dateObj = translations && translations.date; + const monthNames = dateObj && dateObj.month_names; + + const months = monthNames || []; + const monthName = months[jsDate.getMonth() + 1] || ""; + const day = jsDate.getDate(); + const year = jsDate.getFullYear(); + + const localizedDate = `${monthName} ${day}. ${year}.`; + return value > 0 ? + I18n.t("javascripts.heatmap.tooltip.contributions", { count: value, date: localizedDate }) : + I18n.t("javascripts.heatmap.tooltip.no_contributions", { date: localizedDate }); +} diff --git a/app/assets/stylesheets/heatmap.css b/app/assets/stylesheets/heatmap.css new file mode 100644 index 0000000000..0352c79d99 --- /dev/null +++ b/app/assets/stylesheets/heatmap.css @@ -0,0 +1,3 @@ +/* + *= require cal-heatmap + */ diff --git a/config/locales/en.yml b/config/locales/en.yml index 94fc77247b..32eb5dc957 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3215,6 +3215,10 @@ en: show_address: Show address query_features: Query features centre_map: Centre map here + heatmap: + tooltip: + contributions: "%{count} contributions on %{date}" + no_contributions: "No contributions on %{date}" redactions: edit: heading: "Edit Redaction"