Commit 9451a82f authored by Jose's avatar Jose

Refactored activity calendar

parent fb46dfb2
...@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) { ...@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date(); const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset(); const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60; const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes); date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date; return date;
} }
...@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) { ...@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`; return `${contribText}<br />${dateDayName} ${dateText}`;
} }
const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); const initColorKey = () =>
d3
.scaleLinear()
.range(['#acd5f2', '#254e77'])
.domain([0, 3]);
export default class ActivityCalendar { export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) { constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath; this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this); this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = ''; this.currentSelectedDate = '';
this.daySpace = 1; this.daySpace = 1;
this.daySize = 15; this.daySize = 15;
this.daySizeWithSpace = this.daySize + (this.daySpace * 2); this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
this.months = []; this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects // Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are // The group of objects will be grouped based on the day of the week they are
...@@ -70,7 +88,7 @@ export default class ActivityCalendar { ...@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week // Create a new group array if this is the first day of the week
// or if is first object // or if is first object
if ((day === 0 && i !== 0) || i === 0) { if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]); this.timestampsTmp.push([]);
group += 1; group += 1;
} }
...@@ -109,21 +127,30 @@ export default class ActivityCalendar { ...@@ -109,21 +127,30 @@ export default class ActivityCalendar {
} }
renderSvg(container, group) { renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group); const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3.select(container) return d3
.select(container)
.append('svg') .append('svg')
.attr('width', width) .attr('width', width)
.attr('height', 167) .attr('height', 167)
.attr('class', 'contrib-calendar'); .attr('class', 'contrib-calendar');
}
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
} }
renderDays() { renderDays() {
this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g') this.svg
.selectAll('g')
.data(this.timestampsTmp)
.enter()
.append('g')
.attr('transform', (group, i) => { .attr('transform', (group, i) => {
_.each(group, (stamp, a) => { _.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) { if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth(); const month = stamp.date.getMonth();
const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace; const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months); const lastMonth = _.last(this.months);
if ( if (
lastMonth == null || lastMonth == null ||
...@@ -133,86 +160,113 @@ export default class ActivityCalendar { ...@@ -133,86 +160,113 @@ export default class ActivityCalendar {
} }
} }
}); });
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`; return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
}) })
.selectAll('rect') .selectAll('rect')
.data(stamp => stamp) .data(stamp => stamp)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', '0') .attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day) .attr('y', stamp => this.dayYPos(stamp.day))
.attr('width', this.daySize) .attr('width', this.daySize)
.attr('height', this.daySize) .attr('height', this.daySize)
.attr('fill', stamp => ( .attr(
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed' 'fill',
)) stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
.attr('title', stamp => formatTooltipText(stamp)) )
.attr('class', 'user-contrib-cell js-tooltip') .attr('title', stamp => formatTooltipText(stamp))
.attr('data-container', 'body') .attr('class', 'user-contrib-cell js-tooltip')
.on('click', this.clickDay); .attr('data-container', 'body')
.on('click', this.clickDay);
} }
renderDayTitles() { renderDayTitles() {
const days = [ const days = [
{ {
text: 'M', text: 'M',
y: 29 + (this.daySizeWithSpace * 1), y: 29 + this.dayYPos(1),
}, { },
{
text: 'W', text: 'W',
y: 29 + (this.daySizeWithSpace * 3), y: 29 + this.dayYPos(2),
}, { },
{
text: 'F', text: 'F',
y: 29 + (this.daySizeWithSpace * 5), y: 29 + this.dayYPos(3),
}, },
]; ];
this.svg.append('g') this.svg
.append('g')
.selectAll('text') .selectAll('text')
.data(days) .data(days)
.enter() .enter()
.append('text') .append('text')
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('x', 8) .attr('x', 8)
.attr('y', day => day.y) .attr('y', day => day.y)
.text(day => day.text) .text(day => day.text)
.attr('class', 'user-contrib-text'); .attr('class', 'user-contrib-text');
} }
renderMonths() { renderMonths() {
this.svg.append('g') this.svg
.append('g')
.attr('direction', 'ltr') .attr('direction', 'ltr')
.selectAll('text') .selectAll('text')
.data(this.months) .data(this.months)
.enter() .enter()
.append('text') .append('text')
.attr('x', date => date.x) .attr('x', date => date.x)
.attr('y', 10) .attr('y', 10)
.attr('class', 'user-contrib-text') .attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]); .text(date => this.monthNames[date.month]);
} }
renderKey() { renderKey() {
const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions']; const keyValues = [
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; 'no contributions',
'1-9 contributions',
'10-19 contributions',
'20-29 contributions',
'30+ contributions',
];
const keyColors = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
this.svg.append('g') this.svg
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`) .append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect') .selectAll('rect')
.data(keyColors) .data(keyColors)
.enter() .enter()
.append('rect') .append('rect')
.attr('width', this.daySize) .attr('width', this.daySize)
.attr('height', this.daySize) .attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i) .attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0) .attr('y', 0)
.attr('fill', color => color) .attr('fill', color => color)
.attr('class', 'js-tooltip') .attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i]) .attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body'); .attr('data-container', 'body');
} }
initColor() { initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; const colorRange = [
return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange); '#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
return d3
.scaleThreshold()
.domain([0, 10, 20, 30])
.range(colorRange);
} }
clickDay(stamp) { clickDay(stamp) {
...@@ -227,14 +281,15 @@ export default class ActivityCalendar { ...@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML); $('.user-calendar-activities').html(LOADING_HTML);
axios.get(this.calendarActivitiesPath, { axios
params: { .get(this.calendarActivitiesPath, {
date, params: {
}, date,
responseType: 'text', },
}) responseType: 'text',
.then(({ data }) => $('.user-calendar-activities').html(data)) })
.catch(() => flash(__('An error occurred while retrieving calendar activity'))); .then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else { } else {
this.currentSelectedDate = ''; this.currentSelectedDate = '';
$('.user-calendar-activities').html(''); $('.user-calendar-activities').html('');
......
---
title: Refactored activity calendar
merge_request: 18469
author: Enrico Scholz
type: changed
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment