Skip to content

Commit

Permalink
draft: replace chartjs with a smaller package for the heatmap.
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon-Laux committed Oct 19, 2023
1 parent a8f3912 commit 381802a
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 184 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"prettier-plugin-tailwindcss": "^0.2.3",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-contribution-calendar": "^1.3.5",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.0",
"vite-plugin-zip-pack": "^1.0.5",
Expand Down
3 changes: 3 additions & 0 deletions public/DependencyLicenses.txt
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,9 @@
│ │ ├─ URL: https://github.com/reactchartjs/react-chartjs-2.git
│ │ ├─ VendorName: Jeremy Ayerst
│ │ └─ VendorUrl: https://github.com/reactchartjs/react-chartjs-2
│ ├─ react-contribution-calendar@1.3.5
│ │ ├─ URL: git+https://github.com/SeiwonPark/react-contribution-calendar.git
│ │ └─ VendorUrl: https://github.com/SeiwonPark/react-contribution-calendar#readme
│ ├─ react-dom@18.2.0
│ │ ├─ URL: https://github.com/facebook/react.git
│ │ └─ VendorUrl: https://reactjs.org/
Expand Down
274 changes: 90 additions & 184 deletions src/pages/StatisticsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
import { Chart, ChartProps } from "react-chartjs-2";
import {
Chart as ChartJS,
Title,
CategoryScale,
TimeScale,
Tooltip,
} from "chart.js";
import "chartjs-adapter-luxon";

ChartJS.register(Title, CategoryScale, TimeScale, Tooltip);

import { MatrixController, MatrixElement } from "chartjs-chart-matrix";

ChartJS.register(MatrixController, MatrixElement);

import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import { useStore } from "../store";
import { DateTime, Duration } from "luxon";
import {
Expand All @@ -25,6 +9,7 @@ import { NavigationContext } from "../App";
import { WeekView } from "../components/StatsWeekView";
import { QuickStats } from "../components/TrackPageStats";
import { MonthView } from "../components/StatsMonthViewDays";
import { ContributionCalendar } from "react-contribution-calendar";

export function StatisticsPage() {
const now = DateTime.now();
Expand Down Expand Up @@ -108,40 +93,44 @@ export function StatisticsPage() {
</button>
<hr />
</div>
<div className="divider">Current Year</div>
<div className="divider">Heatmap</div>
<ActivityMap />
</div>
);
}

const maxFeasableMinutesPerDay = 60 * 8;
// const maxPossibleMinutesPerDay = 60 * 24;

const day_size = 12;
const free_space = 70; // space for other stuff

function ActivityMap() {
type dataType = {
x: string;
y: string;
d: string;
v: number;
type dataType2 = {
[date: string]: {
level: number;
data: {
// this is not a lib feature yet
custom_tooltip: string;
};
};
};
const [data, setData] = useState<dataType[] | null>(null);
const [data2, setData2] = useState<dataType2[] | null>(null);

const entries = useStore((store) => store.getTrackedEntries());

const [timeSpan] = useState([
DateTime.now()
.minus(Duration.fromObject({ year: 1 }))
.toMillis(),
DateTime.now().endOf("day").toMillis(),
const [timeSpan, setTimeSpan] = useState([
DateTime.now().minus(Duration.fromObject({ year: 1 })),
DateTime.now().endOf("day"),
]);
console.log(entries.length);

useEffect(() => {
console.log(entries.length, timeSpan[0], timeSpan[1]);

// idea for later: move work into webworker?
setData(null);
setData2(null);

const dataEntries: dataType[] = [];
let working_day = DateTime.fromMillis(timeSpan[0]);
const end = DateTime.fromMillis(timeSpan[1]);
const data2Entries: dataType2[] = [];
let working_day = timeSpan[0];
const end = timeSpan[1];
// this is for improving performance a little bit
const entries_in_span = getEntriesTouchingTimeframe(
entries,
Expand All @@ -165,167 +154,84 @@ function ActivityMap() {
.map((e) => e.duration || 0)
.reduce((previous, current) => previous + current, 0);

dataEntries.push({
x: startOfDay.toISODate() || "",
y: String(startOfDay.weekday),
v: Math.floor(timeSpentThatDay / 60000), // convert to minutes per day
d: startOfDay.toISODate() || "",
const minutes = Math.floor(timeSpentThatDay / 60000); // convert to minutes per day

const level = Math.min(
Math.max(Math.floor((minutes / maxFeasableMinutesPerDay) * 4), 0),
4
);

data2Entries.push({
[startOfDay.toFormat("yyyy-MM-dd")]: {
level,
data: {
custom_tooltip:
startOfDay.toFormat("yyyy-MM-dd") +
"\n" +
Duration.fromObject({ minutes })
.shiftTo("hours", "minutes")
.toHuman(),
},
},
});

working_day = working_day.plus({ days: 1 });
}
console.log(dataEntries);
// console.log(dataEntries);

setData(dataEntries);
setData2(data2Entries);
}, [`${entries.length}`, timeSpan[0], timeSpan[1]]);

const { options, chartData } = useMemo(() => {
const scales: any = {
y: {
type: "time",
offset: true,
time: {
unit: "day",
round: "day",
isoWeekday: 1,
parser: "E",
displayFormats: {
day: "EEE",
},
},
reverse: true,
position: "right",
ticks: {
maxRotation: 0,
autoSkip: true,
padding: 1,
font: {
size: 9,
},
},
grid: {
display: false,
drawBorder: false,
tickLength: 0,
},
},
x: {
type: "time",
position: "bottom",
offset: true,
time: {
unit: "week",
round: "week",
isoWeekday: 1,
displayFormats: {
week: "MMM dd",
},
},
ticks: {
maxRotation: 0,
autoSkip: true,
font: {
size: 9,
},
},
grid: {
display: false,
drawBorder: false,
tickLength: 0,
},
},
};
const options: ChartProps<
"matrix",
{ x: string; y: string; d: string; v: number }[],
unknown
>["options"] = {
responsive: true,
aspectRatio: 5,
plugins: {
tooltip: {
displayColors: false,
callbacks: {
title(items) {
const context = items[0];
if (context) {
return (
context.dataset.data[context.dataIndex] as any as dataType
).d;
}
},
label(context) {
const { v } = context.dataset.data[
context.dataIndex
] as any as dataType;
return Duration.fromObject({ minutes: v })
.shiftTo("hours", "minutes")
.toHuman();
},
},
},
},
scales: scales,
layout: {
padding: {
top: 10,
},
},
};
const container = useRef<HTMLDivElement | null>(null);
const [availableWidth, setAvailableWidth] = useState<number | null>(null);

const maxFeasableMinutesPerDay = 60 * 8;
// const maxPossibleMinutesPerDay = 60 * 24;

const chartData: ChartProps<
"matrix",
{ x: string; y: string; d: string; v: number }[],
unknown
>["data"] = {
datasets: [
{
data: data || [],
backgroundColor(c) {
const minutes = (c.dataset.data[c.dataIndex] as any as dataType).v;
const alpha = minutes / maxFeasableMinutesPerDay;
let green = 255;
if (alpha > 1) {
green = 255 - 255 * (alpha - 1);
}
return `rgba(0,${green},200, ${alpha})`;
},
borderColor(c) {
const minutes = (c.dataset.data[c.dataIndex] as any as dataType).v;
const alpha = minutes / maxFeasableMinutesPerDay;
let green = 255;
if (alpha > 1) {
green = 255 - 255 * (alpha - 1);
}
return `rgba(0,${green},200, ${alpha})`;
},
borderWidth: 1,
hoverBackgroundColor: "lightblue",
hoverBorderColor: "blue",
width(c) {
const a = c.chart.chartArea || {};
return (a.right - a.left) / 53 - 1;
},
height(c) {
const a = c.chart.chartArea || {};
return (a.bottom - a.top) / 7 - 1;
},
},
],
useEffect(() => {
const update = () => {
if (container.current) {
console.log("hi");
// TODO on reszie does not work yet

const availableWidth = container.current.getBoundingClientRect().width;
const weeks = Math.floor(
Math.max((availableWidth || 0) - free_space, 0) / day_size
);
setTimeSpan([DateTime.now().minus({ weeks }), DateTime.now()]);
setAvailableWidth(availableWidth);
}
};
if (container.current) {
update();
window.addEventListener("resize", update);
return window.removeEventListener("resize", update);
}
}, [container.current]);

return { options, chartData };
}, [data]);
const start = timeSpan[0].toFormat("yyyy-MM-dd");
const end = timeSpan[1].toFormat("yyyy-MM-dd");

return (
<div>
{data === null && <div>Loading Data...</div>}
{data !== null && (
<Chart options={options} type={"matrix"} data={chartData} />
)}
{data2 === null && <div>Loading Data...</div>}
<div className="w-full py-4" ref={container}>
{availableWidth && data2 !== null && (
<ContributionCalendar
data={data2}
start={start}
end={end}
daysOfTheWeek={["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]}
textColor="grey"
startsOnSunday={true}
includeBoundary={false}
theme="dark_winter"
cx={day_size}
cy={day_size}
cr={1}
onCellClick={(e, data) => console.log(data)}
scroll={false}
style={{}}
/>
)}
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,11 @@ react-chartjs-2@^5.2.0:
resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz"
integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==

react-contribution-calendar@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/react-contribution-calendar/-/react-contribution-calendar-1.3.5.tgz#e6f45069d422ca014cef970ac652ee0191a5c998"
integrity sha512-aSsjEuLV9hxDcZoIOEnacTkBHUzGihbIJ5Sd4JvwUCqzlF1sr+dc7lDy0ZX5+jThW89DLKFtWIpxCB6HeBXOcw==

react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
Expand Down

0 comments on commit 381802a

Please sign in to comment.