160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
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 (
|
|
<svg width="100%" height="100%" viewBox={`0 0 ${totalWidth} ${totalHeight}`} className={className}>
|
|
{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 (
|
|
<rect
|
|
key={date.toISOString()}
|
|
x={x}
|
|
y={y}
|
|
width={squareSize}
|
|
height={squareSize}
|
|
fill={color}
|
|
data-tooltip-id={`calendar-tooltip`}
|
|
data-tooltip-content={count ? `${dFns.format(date, 'EEE do')}: ${count.toFixed(2)} hours` : `${dFns.format(date, 'EEE do')}`}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{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 (
|
|
<rect
|
|
key={`week-total-${weekIndex}`}
|
|
x={x}
|
|
y={y}
|
|
width={squareSize}
|
|
height={squareSize}
|
|
fill={color}
|
|
data-tooltip-id={`calendar-tooltip`}
|
|
data-tooltip-content={count ? `Week total ${count.toFixed(2)} hours` : ``}
|
|
/>
|
|
);
|
|
})}
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export default GitHubCalendarHeatmap;
|