Improve the heatmap

This commit is contained in:
Joshua Coles 2024-02-25 19:53:58 +00:00
parent 058466294b
commit f2dd382cf3
2 changed files with 48 additions and 78 deletions

View File

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import * as dFns from "date-fns"; import * as dFns from "date-fns";
import * as R from "ramda";
interface DateValue { interface DateValue {
date: Date; date: Date;
@ -13,17 +12,12 @@ interface Props {
data: DateValue[]; data: DateValue[];
} }
const GitHubCalendarHeatmap: React.FC<Props> = ({ function GitHubCalendarHeatmap({
startDate, startDate,
endDate, endDate,
data, data,
}) => { }: Props) {
// const numDays = dFns.differenceInDays(endDate, startDate); const numDays = dFns.differenceInDays(endDate, startDate);
// Calculate the number of days in the range
const numDays = Math.ceil(
(endDate.getTime() - startDate.getTime()) / (1000 * 3600 * 24)
);
// Calculate the number of weeks in the range // Calculate the number of weeks in the range
const numWeeks = Math.ceil(numDays / 7); const numWeeks = Math.ceil(numDays / 7);
@ -36,49 +30,48 @@ const GitHubCalendarHeatmap: React.FC<Props> = ({
const totalHeight = 8 * (squareSize + padding) + weekTotalPadding; // Added an extra row for week total const totalHeight = 8 * (squareSize + padding) + weekTotalPadding; // Added an extra row for week total
// Goal times for the daily and weekly squares // Goal times for the daily and weekly squares
const dayGoalTime = 24; const dayGoalTime = 8;
const weekGoalTime = 5 * dayGoalTime; const weekGoalTime = 5 * dayGoalTime;
const getColorForValue = (value: number, goalTime: number): string => { const getColorForValue = (value: number, goalTime: number): string => {
const saturation = Math.min(Math.max(value / goalTime, 0), 1) * 100; // Normalize value to range [0, 1] and then convert to percentage const values = [
const color = "#eeeeee",
saturation === 0 ? "lightgray" : `hsl(180, ${saturation}%, 50%)`; // Hue is fixed at 180 for a pleasant color. You can adjust this as needed. "#d6e685",
return color; "#8cc665",
"#44a340",
"#1e6823",
]
const linearValue = Math.round((value / goalTime) * 4);
// If we did something, but not enough to reach the first level, return 1
if (linearValue == 0 && value > 0) return values[1];
// Clamp to the granularity
if (linearValue > 4) return values[4];
return values[linearValue];
}; };
const getWeekTotal = (weekIndex: number): number => { const getWeekTotal = (weekIndex: number): number => {
const weekStart = new Date(startDate); const weekStart = dFns.addWeeks(dFns.startOfWeek(startDate, { weekStartsOn: 1 }), weekIndex);
weekStart.setDate(weekStart.getDate() + weekIndex * 7);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
return data.reduce((total, { return data.filter(dv => dFns.isSameWeek(dv.date, weekStart, { weekStartsOn: 1 }))
date, .reduce((total, {count}) => total + count, 0);
count
}) => {
if (date >= weekStart && date <= weekEnd) {
return total + count;
}
return total;
}, 0);
}; };
const dateValues: DateValue[] = []; const dateValues: DateValue[] = dFns.eachDayOfInterval({
start: startDate,
for ( end: endDate,
let date = new Date(startDate); }).map((date) => {
date <= endDate;
date.setDate(date.getDate() + 1)
) {
const foundData = data.find( const foundData = data.find(
({date: dataDate}) => ({date: dataDate}) => dFns.isEqual(dFns.startOfDay(date), dFns.startOfDay(dataDate))
dataDate.toISOString().slice(0, 10) === date.toISOString().slice(0, 10)
); );
dateValues.push({
date: new Date(date), return {
date,
count: foundData ? foundData.count : 0, count: foundData ? foundData.count : 0,
}); }
} });
return ( return (
<svg width={totalWidth} height={totalHeight}> <svg width={totalWidth} height={totalHeight}>
@ -86,10 +79,9 @@ const GitHubCalendarHeatmap: React.FC<Props> = ({
date, date,
count count
}) => { }) => {
const weekIndex = Math.floor( const weekIndex = dFns.differenceInCalendarWeeks(date, startDate, { weekStartsOn: 1 });
(date.getTime() - startDate.getTime()) / (1000 * 3600 * 24 * 7)
); const dayOfWeek = (date.getDay() + 6) % 7;
const dayOfWeek = date.getDay();
const x = weekIndex * (squareSize + padding); const x = weekIndex * (squareSize + padding);
const y = dayOfWeek * (squareSize + padding); const y = dayOfWeek * (squareSize + padding);
const color = getColorForValue(count, dayGoalTime); const color = getColorForValue(count, dayGoalTime);
@ -102,15 +94,18 @@ const GitHubCalendarHeatmap: React.FC<Props> = ({
width={squareSize} width={squareSize}
height={squareSize} height={squareSize}
fill={color} 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) {Array(numWeeks).fill(0)
.map((_, weekIndex) => { .map((_, weekIndex) => {
const x = weekIndex * (squareSize + padding); const x = weekIndex * (squareSize + padding);
const y = 7 * (squareSize + padding) + weekTotalPadding; // Position for the week total const y = 7 * (squareSize + padding) + weekTotalPadding; // Position for the week total
const color = getColorForValue(getWeekTotal(weekIndex), weekGoalTime); const count = getWeekTotal(weekIndex);
const color = getColorForValue(count, weekGoalTime);
return ( return (
<rect <rect
@ -120,11 +115,13 @@ const GitHubCalendarHeatmap: React.FC<Props> = ({
width={squareSize} width={squareSize}
height={squareSize} height={squareSize}
fill={color} fill={color}
data-tooltip-id={`calendar-tooltip`}
data-tooltip-content={count ? `Week total ${count.toFixed(2)} hours` : ``}
/> />
); );
})} })}
</svg> </svg>
); );
}; }
export default GitHubCalendarHeatmap; export default GitHubCalendarHeatmap;

View File

@ -9,19 +9,6 @@ import {Tooltip} from 'react-tooltip';
import {Data} from "@/data/fetchData"; import {Data} from "@/data/fetchData";
import HeatMap from "@/components/HeatMap"; import HeatMap from "@/components/HeatMap";
const granularity = 4;
function computeCompletionShade(value: number, dailyGoal: number) {
const linearValue = Math.round((value / dailyGoal) * granularity);
// If we did something, but not enough to reach the first level, return 1
if (linearValue == 0 && value > 0) return 1;
// Clamp to the granularity
if (linearValue > granularity) return granularity;
return linearValue;
}
function useCalendarData(data: Data, initialDate: Date, endDate: Date) { function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
const timeEntries = data.timeEntries; const timeEntries = data.timeEntries;
@ -71,19 +58,5 @@ export function CalendarOverviewCard({
<Tooltip id="calendar-tooltip"/> <Tooltip id="calendar-tooltip"/>
<Title>Overview</Title> <Title>Overview</Title>
<HeatMap startDate={initialDate} endDate={endDate} data={calendarData}/> <HeatMap startDate={initialDate} endDate={endDate} data={calendarData}/>
{/*<CalendarHeatmap*/}
{/* showWeekdayLabels={true}*/}
{/* startDate={initialDate}*/}
{/* endDate={endDate}*/}
{/* values={calendarData}*/}
{/* classForValue={value => `color-github-${computeCompletionShade(value?.count ?? 0, goal)}`}*/}
{/* tooltipDataAttrs={(value: any) => {*/}
{/* return value.date ? {*/}
{/* 'data-tooltip-id': `calendar-tooltip`,*/}
{/* 'data-tooltip-content': value.count ? `${dFns.format(value.date, 'EEE do')}: ${value.count.toFixed(2)} hours` : `${dFns.format(value.date, 'EEE do')}`*/}
{/* } : undefined*/}
{/* }}*/}
{/*/>*/}
</Card> </Card>
} }