Compare commits

...

4 Commits

Author SHA1 Message Date
65e8899426 Cleanup imports
All checks were successful
Build and Publish Docker Container / build (push) Successful in 3m17s
2024-02-25 19:58:31 +00:00
ec91f6df42 Fix sizing of the heatmap and make goal configurable 2024-02-25 19:57:46 +00:00
f2dd382cf3 Improve the heatmap 2024-02-25 19:53:58 +00:00
058466294b Initial import of AI heatmap 2024-02-25 19:24:58 +00:00
2 changed files with 136 additions and 31 deletions

131
src/components/HeatMap.tsx Normal file
View File

@ -0,0 +1,131 @@
import React from "react";
import * as dFns from "date-fns";
interface DateValue {
date: Date;
count: number;
}
interface Props {
startDate: Date;
endDate: Date;
data: DateValue[];
className?: string;
goal: number;
}
function GitHubCalendarHeatmap({
startDate,
endDate,
data,
className,
goal
}: 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): string => {
const values = [
"#eeeeee",
"#d6e685",
"#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 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);
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);
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;

View File

@ -1,25 +1,9 @@
"use client";
import {Card, Title} from "@tremor/react";
import * as R from 'ramda';
import * as dFns from 'date-fns';
import CalendarHeatmap from 'react-calendar-heatmap';
import 'react-calendar-heatmap/dist/styles.css';
import '../../app/calendar-styles.css'
import {Tooltip} from 'react-tooltip';
import {Data} from "@/data/fetchData";
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;
}
import HeatMap from "@/components/HeatMap";
function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
const timeEntries = data.timeEntries;
@ -68,19 +52,9 @@ export function CalendarOverviewCard({
return <Card className="col-span-1">
<Tooltip id="calendar-tooltip"/>
<Title>Overview</Title>
<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
}}
/>
<Title>Semester Overview</Title>
<div className="m-2">
<HeatMap startDate={initialDate} endDate={endDate} data={calendarData} goal={goal}/>
</div>
</Card>
}