import React from "react"; import * as dFns from "date-fns"; import * as R from 'ramda'; interface DateValue { date: Date; count: number; } interface Props { startDate: Date; endDate: Date; data: DateValue[]; className?: string; goal: number; penaliseOvertime?: boolean; } function GitHubCalendarHeatmap({ startDate, endDate, data, className, goal, penaliseOvertime = false, }: Props) { const numDays = dFns.differenceInDays(endDate, startDate); // Calculate the number of weeks in the range const numWeeks = Math.ceil(numDays / 7); // Calculate the size of each square in the heatmap and the padding between each square const squareSize = 15; const padding = 2; const weekTotalPadding = 10; // Extra padding for the week total row const totalWidth = numWeeks * (squareSize + padding); const totalHeight = 8 * (squareSize + padding) + weekTotalPadding; // Added an extra row for week total // Goal times for the daily and weekly squares const dayGoalTime = goal; const weekGoalTime = 5 * dayGoalTime; const getColorForValue = (value: number, goalTime: number, zeroColor: string): string => { const buckets = [ { min: -Infinity, max: 0, color: zeroColor, }, { min: 0, max: 0.40, color: '#d6e685', }, { min: 0.40, max: 0.50, color: '#8cc665', }, { min: 0.50, max: 0.90, color: '#44a340', }, { min: 0.90, // If penalising overtime, the max is 1.125, otherwise it's infinity (ie this is the last bucket) max: penaliseOvertime ? 1.125 : Infinity, color: '#1e6823', }, { min: 1.125, max: 1.25, color: '#f59e0b' }, { min: 1.25, max: Infinity, color: '#ef4444' }, ]; const linearValue = value / goalTime; return R.find(minMax => minMax.min < linearValue && linearValue <= minMax.max, buckets)!.color; }; const getWeekTotal = (weekIndex: number): number => { const weekStart = dFns.addWeeks(dFns.startOfWeek(startDate, {weekStartsOn: 1}), weekIndex); return data.filter(dv => dFns.isSameWeek(dv.date, weekStart, {weekStartsOn: 1})) .reduce((total, {count}) => total + count, 0); }; const dateValues: DateValue[] = dFns.eachDayOfInterval({ start: startDate, end: endDate, }).map((date) => { const foundData = data.find( ({date: dataDate}) => dFns.isEqual(dFns.startOfDay(date), dFns.startOfDay(dataDate)) ); return { date, count: foundData ? foundData.count : 0, } }); return ( {dateValues.map(({ date, count }) => { const weekIndex = dFns.differenceInCalendarWeeks(date, startDate, {weekStartsOn: 1}); const dayOfWeek = (date.getDay() + 6) % 7; const x = weekIndex * (squareSize + padding); const y = dayOfWeek * (squareSize + padding); const color = getColorForValue(count, dayGoalTime, dFns.isWeekend(date) ? '#d4d4d4' : '#eeeeee'); return ( ); })} {Array(numWeeks).fill(0) .map((_, weekIndex) => { const x = weekIndex * (squareSize + padding); const y = 7 * (squareSize + padding) + weekTotalPadding; // Position for the week total const count = getWeekTotal(weekIndex); const color = getColorForValue(count, weekGoalTime, '#eeeeee'); return ( ); })} ); } export default GitHubCalendarHeatmap;