]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/heatmap.js
Merge remote-tracking branch 'upstream/pull/5814'
[rails.git] / app / assets / javascripts / heatmap.js
1 //= require d3/dist/d3
2 //= require cal-heatmap/dist/cal-heatmap
3 //= require popper
4 //= require cal-heatmap/dist/plugins/Tooltip
5
6 /* global CalHeatmap, Tooltip */
7 document.addEventListener("DOMContentLoaded", () => {
8   const heatmapElement = document.querySelector("#cal-heatmap");
9
10   if (!heatmapElement) {
11     return;
12   }
13
14   const heatmapData = heatmapElement.dataset.heatmap ? JSON.parse(heatmapElement.dataset.heatmap) : [];
15   const displayName = heatmapElement.dataset.displayName;
16   const colorScheme = document.documentElement.getAttribute("data-bs-theme") ?? "auto";
17   const rangeColors = ["#14432a", "#166b34", "#37a446", "#4dd05a"];
18   const startDate = new Date(Date.now() - (365 * 24 * 60 * 60 * 1000));
19   const monthNames = I18n.t("date.abbr_month_names");
20
21   const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
22
23   let cal = new CalHeatmap();
24   let currentTheme = getTheme();
25
26   function renderHeatmap() {
27     cal.destroy();
28     cal = new CalHeatmap();
29
30     cal.paint({
31       itemSelector: "#cal-heatmap",
32       theme: currentTheme,
33       domain: {
34         type: "month",
35         gutter: 4,
36         label: {
37           text: (timestamp) => monthNames[new Date(timestamp).getUTCMonth() + 1],
38           position: "top",
39           textAlign: "middle"
40         },
41         dynamicDimension: true
42       },
43       subDomain: {
44         type: "ghDay",
45         radius: 2,
46         width: 11,
47         height: 11,
48         gutter: 4
49       },
50       date: {
51         start: startDate
52       },
53       range: 13,
54       data: {
55         source: heatmapData,
56         type: "json",
57         x: "date",
58         y: "total_changes"
59       },
60       scale: {
61         color: {
62           type: "threshold",
63           range: currentTheme === "dark" ? rangeColors : Array.from(rangeColors).reverse(),
64           domain: [10, 20, 30, 40]
65         }
66       }
67     }, [
68       [Tooltip, {
69         text: (date, value) => getTooltipText(date, value)
70       }]
71     ]);
72
73     cal.on("mouseover", (event, timestamp, value) => {
74       if (!displayName || !value) return;
75       if (event.target.parentElement.nodeName === "a") return;
76
77       for (const { date, max_id } of heatmapData) {
78         if (!max_id) continue;
79         if (timestamp !== Date.parse(date)) continue;
80
81         const params = new URLSearchParams([["before", max_id + 1]]);
82         const a = document.createElementNS("http://www.w3.org/2000/svg", "a");
83         a.setAttribute("href", `/user/${encodeURIComponent(displayName)}/history?${params}`);
84         $(event.target).wrap(a);
85         break;
86       }
87     });
88   }
89
90   function getTooltipText(date, value) {
91     const localizedDate = I18n.l("date.formats.long", date);
92
93     if (value > 0) {
94       return I18n.t("javascripts.heatmap.tooltip.contributions", { count: value, date: localizedDate });
95     }
96
97     return I18n.t("javascripts.heatmap.tooltip.no_contributions", { date: localizedDate });
98   }
99
100   function getTheme() {
101     if (colorScheme === "auto") {
102       return mediaQuery.matches ? "dark" : "light";
103     }
104
105     return colorScheme;
106   }
107
108   if (colorScheme === "auto") {
109     mediaQuery.addEventListener("change", (e) => {
110       currentTheme = e.matches ? "dark" : "light";
111       renderHeatmap();
112     });
113   }
114
115   renderHeatmap();
116 });
117
118