-
Notifications
You must be signed in to change notification settings - Fork 929
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
Add user profile heatmap visualization for contributions #5402
base: master
Are you sure you want to change the base?
Changes from all commits
ada1658
467ef94
f46796f
9d87115
197af84
904951e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,134 @@ | ||||||||||||||||||||||||||
//= require d3 | ||||||||||||||||||||||||||
//= require cal-heatmap | ||||||||||||||||||||||||||
//= require calendar-label | ||||||||||||||||||||||||||
//= require popper | ||||||||||||||||||||||||||
//= require tooltip | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/* global CalHeatmap, CalendarLabel, Tooltip */ | ||||||||||||||||||||||||||
document.addEventListener("DOMContentLoaded", () => { | ||||||||||||||||||||||||||
const heatmapElement = document.querySelector("#cal-heatmap"); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if (!heatmapElement) { | ||||||||||||||||||||||||||
console.warn("Heatmap element not found in the DOM."); | ||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const heatmapData = heatmapElement.dataset.heatmap ? JSON.parse(heatmapElement.dataset.heatmap) : []; | ||||||||||||||||||||||||||
const colorScheme = heatmapElement.dataset.siteColorScheme || "auto"; | ||||||||||||||||||||||||||
const locale = $("head").data().locale; | ||||||||||||||||||||||||||
const weekdays = getLocalizedWeekdays(locale); | ||||||||||||||||||||||||||
const rangeColors = ["#14432a", "#166b34", "#37a446", "#4dd05a"]; | ||||||||||||||||||||||||||
const startDate = new Date(Date.now() - (365 * 24 * 60 * 60 * 1000)); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const theme = getThemeFromColorScheme(colorScheme); | ||||||||||||||||||||||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let cal = new CalHeatmap(); | ||||||||||||||||||||||||||
renderHeatmap(theme); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if (colorScheme === "auto") { | ||||||||||||||||||||||||||
mediaQuery.addEventListener("change", (e) => { | ||||||||||||||||||||||||||
const newTheme = e.matches ? "dark" : "light"; | ||||||||||||||||||||||||||
renderHeatmap(newTheme); | ||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
kcne marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
function renderHeatmap(currentTheme) { | ||||||||||||||||||||||||||
cal.destroy(); | ||||||||||||||||||||||||||
cal = new CalHeatmap(); | ||||||||||||||||||||||||||
cal.paint({ | ||||||||||||||||||||||||||
itemSelector: "#cal-heatmap", | ||||||||||||||||||||||||||
theme: currentTheme, | ||||||||||||||||||||||||||
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: currentTheme === "dark" ? rangeColors : rangeColors.reverse(), | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
would solve that. |
||||||||||||||||||||||||||
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) | ||||||||||||||||||||||||||
}] | ||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||
kcne marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
function getThemeFromColorScheme(colorScheme) { | ||||||||||||||||||||||||||
if (colorScheme === "auto") { | ||||||||||||||||||||||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
return colorScheme; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
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 dateTranslations = translations && translations.date; | ||||||||||||||||||||||||||
const monthNames = dateTranslations && dateTranslations.month_names; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const months = monthNames || []; | ||||||||||||||||||||||||||
const monthName = months[jsDate.getMonth() + 1] || `${jsDate.getMonth + 1}.`; | ||||||||||||||||||||||||||
const day = jsDate.getDate(); | ||||||||||||||||||||||||||
const year = jsDate.getFullYear(); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const localizedDate = `${day}. ${monthName} ${year}.`; | ||||||||||||||||||||||||||
Comment on lines
+120
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
return value > 0 ? | ||||||||||||||||||||||||||
I18n.t("javascripts.heatmap.tooltip.contributions", { count: value, date: localizedDate }) : | ||||||||||||||||||||||||||
I18n.t("javascripts.heatmap.tooltip.no_contributions", { date: localizedDate }); | ||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/* | ||
*= require cal-heatmap | ||
*/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,23 @@ def show | |
if @user && | ||
(@user.visible? || current_user&.administrator?) | ||
@title = @user.display_name | ||
|
||
one_year_ago = 1.year.ago | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this done to freeze the starting point because we're not sure when the block below runs? Then why is the same not done with the ending point? And shouldn't it be truncated to a day? |
||
@heatmap_data = Rails.cache.fetch("heatmap_data_user_#{@user.id}", :expires_in => 1.day) do | ||
Changeset | ||
.where(:user_id => @user.id) | ||
.where(:created_at => one_year_ago..) | ||
.where(:num_changes => 1..) | ||
.group("date_trunc('day', created_at)") | ||
.select("date_trunc('day', created_at) AS date, SUM(num_changes) AS total_changes") | ||
.order("date") | ||
.map do |changeset| | ||
{ | ||
:date => changeset.date.to_date.to_s, | ||
:total_changes => changeset.total_changes.to_i | ||
} | ||
end | ||
end | ||
else | ||
render_unknown_user params[:display_name] | ||
end | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
application.js
is initialized, the locale is available inI18n.locale
. But looks like it's not necessarily initialized...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the entire heatmap can be delayed by putting it in turbo frame or maybe some collapsible section (maybe for performance reasons). You need
I18n
initialized before anyway, I don't know how you're going to translate dates correctly otherwise. Although popup dates seem to work even ifI18n
is not initialized before the heatmap because they are constructed later.