mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 00:20:25 +08:00 
			
		
		
		
	* Added basic heatmap data * Added extra case for sqlite * Built basic heatmap into user profile * Get contribution data from api & styling * Fixed lint & added extra group by statements for all database types * generated swagger spec * generated swagger spec * generated swagger spec * fixed swagger spec * fmt * Added tests * Added setting to enable/disable user heatmap * Added locale for loading text * Removed UseTiDB * Updated librejs & moment.js * Fixed import order * Fixed heatmap in postgresql * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: kolaente <konrad@kola-entertainments.de> * Added copyright header * Fixed a bug to show the heatmap for the actual user instead of the currently logged in * Added integration test for heatmaps * Added a heatmap on the dashboard * Fixed timestamp parsing * Hide heatmap on mobile * optimized postgresql group by query * Improved sqlite group by statement
		
			
				
	
	
		
			312 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// https://github.com/DKirwan/calendar-heatmap
 | 
						|
 | 
						|
function calendarHeatmap() {
 | 
						|
  // defaults
 | 
						|
  var width = 750;
 | 
						|
  var height = 110;
 | 
						|
  var legendWidth = 150;
 | 
						|
  var selector = 'body';
 | 
						|
  var SQUARE_LENGTH = 11;
 | 
						|
  var SQUARE_PADDING = 2;
 | 
						|
  var MONTH_LABEL_PADDING = 6;
 | 
						|
  var now = moment().endOf('day').toDate();
 | 
						|
  var yearAgo = moment().startOf('day').subtract(1, 'year').toDate();
 | 
						|
  var startDate = null;
 | 
						|
  var counterMap= {};
 | 
						|
  var data = [];
 | 
						|
  var max = null;
 | 
						|
  var colorRange = ['#D8E6E7', '#218380'];
 | 
						|
  var tooltipEnabled = true;
 | 
						|
  var tooltipUnit = 'contribution';
 | 
						|
  var legendEnabled = true;
 | 
						|
  var onClick = null;
 | 
						|
  var weekStart = 1; //0 for Sunday, 1 for Monday
 | 
						|
  var locale = {
 | 
						|
    months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
 | 
						|
    days: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
 | 
						|
    No: 'No',
 | 
						|
    on: 'on',
 | 
						|
    Less: 'Less',
 | 
						|
    More: 'More'
 | 
						|
  };
 | 
						|
  var v = Number(d3.version.split('.')[0]);
 | 
						|
 | 
						|
  // setters and getters
 | 
						|
  chart.data = function (value) {
 | 
						|
    if (!arguments.length) { return data; }
 | 
						|
    data = value;
 | 
						|
 | 
						|
    counterMap= {};
 | 
						|
 | 
						|
    data.forEach(function (element, index) {
 | 
						|
        var key= moment(element.date).format( 'YYYY-MM-DD' );
 | 
						|
        var counter= counterMap[key] || 0;
 | 
						|
        counterMap[key]= counter + element.count;
 | 
						|
    });
 | 
						|
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.max = function (value) {
 | 
						|
    if (!arguments.length) { return max; }
 | 
						|
    max = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.selector = function (value) {
 | 
						|
    if (!arguments.length) { return selector; }
 | 
						|
    selector = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.startDate = function (value) {
 | 
						|
    if (!arguments.length) { return startDate; }
 | 
						|
    yearAgo = value;
 | 
						|
    now = moment(value).endOf('day').add(1, 'year').toDate();
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.colorRange = function (value) {
 | 
						|
    if (!arguments.length) { return colorRange; }
 | 
						|
    colorRange = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.tooltipEnabled = function (value) {
 | 
						|
    if (!arguments.length) { return tooltipEnabled; }
 | 
						|
    tooltipEnabled = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.tooltipUnit = function (value) {
 | 
						|
    if (!arguments.length) { return tooltipUnit; }
 | 
						|
    tooltipUnit = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.legendEnabled = function (value) {
 | 
						|
    if (!arguments.length) { return legendEnabled; }
 | 
						|
    legendEnabled = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.onClick = function (value) {
 | 
						|
    if (!arguments.length) { return onClick(); }
 | 
						|
    onClick = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  chart.locale = function (value) {
 | 
						|
    if (!arguments.length) { return locale; }
 | 
						|
    locale = value;
 | 
						|
    return chart;
 | 
						|
  };
 | 
						|
 | 
						|
  function chart() {
 | 
						|
 | 
						|
    d3.select(chart.selector()).selectAll('svg.calendar-heatmap').remove(); // remove the existing chart, if it exists
 | 
						|
 | 
						|
    var dateRange = ((d3.time && d3.time.days) || d3.timeDays)(yearAgo, now); // generates an array of date objects within the specified range
 | 
						|
    var monthRange = ((d3.time && d3.time.months) || d3.timeMonths)(moment(yearAgo).startOf('month').toDate(), now); // it ignores the first month if the 1st date is after the start of the month
 | 
						|
    var firstDate = moment(dateRange[0]);
 | 
						|
    if (chart.data().length == 0) {
 | 
						|
      max = 0;
 | 
						|
    } else if (max === null) {
 | 
						|
      max = d3.max(chart.data(), function (d) { return d.count; }); // max data value
 | 
						|
    }
 | 
						|
 | 
						|
    // color range
 | 
						|
    var color = ((d3.scale && d3.scale.linear) || d3.scaleLinear)()
 | 
						|
      .range(chart.colorRange())
 | 
						|
      .domain([0, max]);
 | 
						|
 | 
						|
    var tooltip;
 | 
						|
    var dayRects;
 | 
						|
 | 
						|
    drawChart();
 | 
						|
 | 
						|
    function drawChart() {
 | 
						|
      var svg = d3.select(chart.selector())
 | 
						|
        .style('position', 'relative')
 | 
						|
        .append('svg')
 | 
						|
        .attr('width', width)
 | 
						|
        .attr('class', 'calendar-heatmap')
 | 
						|
        .attr('height', height)
 | 
						|
        .style('padding', '36px');
 | 
						|
 | 
						|
      dayRects = svg.selectAll('.day-cell')
 | 
						|
        .data(dateRange);  //  array of days for the last yr
 | 
						|
 | 
						|
      var enterSelection = dayRects.enter().append('rect')
 | 
						|
        .attr('class', 'day-cell')
 | 
						|
        .attr('width', SQUARE_LENGTH)
 | 
						|
        .attr('height', SQUARE_LENGTH)
 | 
						|
        .attr('fill', function(d) { return color(countForDate(d)); })
 | 
						|
        .attr('x', function (d, i) {
 | 
						|
          var cellDate = moment(d);
 | 
						|
          var result = cellDate.week() - firstDate.week() + (firstDate.weeksInYear() * (cellDate.weekYear() - firstDate.weekYear()));
 | 
						|
          return result * (SQUARE_LENGTH + SQUARE_PADDING);
 | 
						|
        })
 | 
						|
        .attr('y', function (d, i) {
 | 
						|
          return MONTH_LABEL_PADDING + formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING);
 | 
						|
        });
 | 
						|
 | 
						|
      if (typeof onClick === 'function') {
 | 
						|
        (v === 3 ? enterSelection : enterSelection.merge(dayRects)).on('click', function(d) {
 | 
						|
          var count = countForDate(d);
 | 
						|
          onClick({ date: d, count: count});
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      if (chart.tooltipEnabled()) {
 | 
						|
        (v === 3 ? enterSelection : enterSelection.merge(dayRects)).on('mouseover', function(d, i) {
 | 
						|
          tooltip = d3.select(chart.selector())
 | 
						|
            .append('div')
 | 
						|
            .attr('class', 'day-cell-tooltip')
 | 
						|
            .html(tooltipHTMLForDate(d))
 | 
						|
            .style('left', function () { return Math.floor(i / 7) * SQUARE_LENGTH + 'px'; })
 | 
						|
            .style('top', function () {
 | 
						|
              return formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING) + MONTH_LABEL_PADDING * 2 + 'px';
 | 
						|
            });
 | 
						|
        })
 | 
						|
        .on('mouseout', function (d, i) {
 | 
						|
          tooltip.remove();
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      if (chart.legendEnabled()) {
 | 
						|
        var colorRange = [color(0)];
 | 
						|
        for (var i = 3; i > 0; i--) {
 | 
						|
          colorRange.push(color(max / i));
 | 
						|
        }
 | 
						|
 | 
						|
        var legendGroup = svg.append('g');
 | 
						|
        legendGroup.selectAll('.calendar-heatmap-legend')
 | 
						|
            .data(colorRange)
 | 
						|
            .enter()
 | 
						|
          .append('rect')
 | 
						|
            .attr('class', 'calendar-heatmap-legend')
 | 
						|
            .attr('width', SQUARE_LENGTH)
 | 
						|
            .attr('height', SQUARE_LENGTH)
 | 
						|
            .attr('x', function (d, i) { return (width - legendWidth) + (i + 1) * 13; })
 | 
						|
            .attr('y', height + SQUARE_PADDING)
 | 
						|
            .attr('fill', function (d) { return d; });
 | 
						|
 | 
						|
        legendGroup.append('text')
 | 
						|
          .attr('class', 'calendar-heatmap-legend-text calendar-heatmap-legend-text-less')
 | 
						|
          .attr('x', width - legendWidth - 13)
 | 
						|
          .attr('y', height + SQUARE_LENGTH)
 | 
						|
          .text(locale.Less);
 | 
						|
 | 
						|
        legendGroup.append('text')
 | 
						|
          .attr('class', 'calendar-heatmap-legend-text calendar-heatmap-legend-text-more')
 | 
						|
          .attr('x', (width - legendWidth + SQUARE_PADDING) + (colorRange.length + 1) * 13)
 | 
						|
          .attr('y', height + SQUARE_LENGTH)
 | 
						|
          .text(locale.More);
 | 
						|
      }
 | 
						|
 | 
						|
      dayRects.exit().remove();
 | 
						|
      var monthLabels = svg.selectAll('.month')
 | 
						|
          .data(monthRange)
 | 
						|
          .enter().append('text')
 | 
						|
          .attr('class', 'month-name')
 | 
						|
          .text(function (d) {
 | 
						|
            return locale.months[d.getMonth()];
 | 
						|
          })
 | 
						|
          .attr('x', function (d, i) {
 | 
						|
            var matchIndex = 0;
 | 
						|
            dateRange.find(function (element, index) {
 | 
						|
              matchIndex = index;
 | 
						|
              return moment(d).isSame(element, 'month') && moment(d).isSame(element, 'year');
 | 
						|
            });
 | 
						|
 | 
						|
            return Math.floor(matchIndex / 7) * (SQUARE_LENGTH + SQUARE_PADDING);
 | 
						|
          })
 | 
						|
          .attr('y', 0);  // fix these to the top
 | 
						|
 | 
						|
      locale.days.forEach(function (day, index) {
 | 
						|
        index = formatWeekday(index);
 | 
						|
        if (index % 2) {
 | 
						|
          svg.append('text')
 | 
						|
            .attr('class', 'day-initial')
 | 
						|
            .attr('transform', 'translate(-8,' + (SQUARE_LENGTH + SQUARE_PADDING) * (index + 1) + ')')
 | 
						|
            .style('text-anchor', 'middle')
 | 
						|
            .attr('dy', '2')
 | 
						|
            .text(day);
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    function pluralizedTooltipUnit (count) {
 | 
						|
      if ('string' === typeof tooltipUnit) {
 | 
						|
        return (tooltipUnit + (count === 1 ? '' : 's'));
 | 
						|
      }
 | 
						|
      for (var i in tooltipUnit) {
 | 
						|
        var _rule = tooltipUnit[i];
 | 
						|
        var _min = _rule.min;
 | 
						|
        var _max = _rule.max || _rule.min;
 | 
						|
        _max = _max === 'Infinity' ? Infinity : _max;
 | 
						|
        if (count >= _min && count <= _max) {
 | 
						|
          return _rule.unit;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    function tooltipHTMLForDate(d) {
 | 
						|
      var dateStr = moment(d).format('ddd, MMM Do YYYY');
 | 
						|
      var count = countForDate(d);
 | 
						|
      return '<span><strong>' + (count ? count : locale.No) + ' ' + pluralizedTooltipUnit(count) + '</strong> ' + locale.on + ' ' + dateStr + '</span>';
 | 
						|
    }
 | 
						|
 | 
						|
    function countForDate(d) {
 | 
						|
        var key= moment(d).format( 'YYYY-MM-DD' );
 | 
						|
        return counterMap[key] || 0;
 | 
						|
    }
 | 
						|
 | 
						|
    function formatWeekday(weekDay) {
 | 
						|
      if (weekStart === 1) {
 | 
						|
        if (weekDay === 0) {
 | 
						|
          return 6;
 | 
						|
        } else {
 | 
						|
          return weekDay - 1;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      return weekDay;
 | 
						|
    }
 | 
						|
 | 
						|
    var daysOfChart = chart.data().map(function (day) {
 | 
						|
      return day.date.toDateString();
 | 
						|
    });
 | 
						|
 | 
						|
  }
 | 
						|
 | 
						|
  return chart;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
// polyfill for Array.find() method
 | 
						|
/* jshint ignore:start */
 | 
						|
if (!Array.prototype.find) {
 | 
						|
  Array.prototype.find = function (predicate) {
 | 
						|
    if (this === null) {
 | 
						|
      throw new TypeError('Array.prototype.find called on null or undefined');
 | 
						|
    }
 | 
						|
    if (typeof predicate !== 'function') {
 | 
						|
      throw new TypeError('predicate must be a function');
 | 
						|
    }
 | 
						|
    var list = Object(this);
 | 
						|
    var length = list.length >>> 0;
 | 
						|
    var thisArg = arguments[1];
 | 
						|
    var value;
 | 
						|
 | 
						|
    for (var i = 0; i < length; i++) {
 | 
						|
      value = list[i];
 | 
						|
      if (predicate.call(thisArg, value, i, list)) {
 | 
						|
        return value;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return undefined;
 | 
						|
  };
 | 
						|
}
 | 
						|
/* jshint ignore:end */
 |