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