After having trouble with a few Charting libraries I decided to implement my own GitHub style heatmap with react and React-Bootstrap. It actually works. Since it does not need to be extensible it only has the features that it needs. However I was wondering if the code quality can be improved. Selecting a tile inside the heatmap takes 4ms which is a bit slow. It is usable even in this condition but I was wondering how I could improve it. Also some parts regarding the labels feels a bit hardcoded but I did not know better. Any suggestion about any part of the code would be welcome.
import { useEffect } from "react";
import { useState } from "react";
import { Stack } from "react-bootstrap";
import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Tooltip from "react-bootstrap/Tooltip";
import CommitByDayList from "./CommitByDayList";
const TILE_SIZE = "13px";
const COLORS = ["#ededed", "#acd5f2", "#7fa8d1", "#49729b", "#254e77"];
function getColor(count) {
if (count === 0) {
return COLORS[0];
} else if (count < 10) {
return COLORS[1];
} else if (count < 20) {
return COLORS[2];
} else if (count < 30) {
return COLORS[3];
} else {
return COLORS[4];
}
}
function removeTimeFromDate(date) {
return date.split("T")[0];
}
function get_today() {
return removeTimeFromDate(new Date().toISOString());
}
function arrayRotate(arr, count) {
const len = arr.length;
arr.push(...arr.splice(0, ((-count % len) + len) % len));
return arr;
}
const today = get_today();
function Tile({ date, count, idx, setDate, setTileIndex, selected }) {
const color = getColor(count);
console.timeEnd("setdate");
return (
<OverlayTrigger
placement="top"
overlay={(props) => (
<Tooltip {...props}>
{
<div>
<p className="mb-0 border-bottom">{date}</p>
<p className="mb-0">{count}</p>
</div>
}
</Tooltip>
)}
>
<div
onClick={() => {
console.time("setdate");
setDate(selected ? today : date);
setTileIndex(selected ? -1 : idx);
}}
style={{
width: TILE_SIZE,
height: TILE_SIZE,
backgroundColor: selected ? "red" : color,
}}
className="border border-1 border-light"
></div>
</OverlayTrigger>
);
}
export default function CustomHeatmap({ data, repoDict, setDate }) {
const [tileIndex, setTileIndex] = useState(-1);
const [chartData, setChartData] = useState(new Map());
function countFrequencies(arr) {
const map = arr.reduce(
(acc, e) => acc.set(e, (acc.get(e) || 0) + 1),
new Map()
);
return map;
}
function createYearArray() {
let date = new Date();
let this_week = date.getDay();
date.setDate(date.getDate() + 1 - (7 * 52 + this_week));
let dates = [];
for (let week = 0; week < 52; week++) {
let days = [];
for (let day = 0; day < 7; day++) {
days.push(date.toISOString().split("T")[0]);
date.setDate(date.getDate() + 1);
}
dates.push(days);
}
let temp = [];
for (let index = 0; index < this_week; index++) {
temp.push(date.toISOString().split("T")[0]);
date.setDate(date.getDate() + 1);
}
dates.push(temp);
return dates;
}
useEffect(() => {
if (data) {
const freqs = countFrequencies(
data.map((data) => data.commitDate.split("T")[0])
);
setChartData(freqs);
}
}, [data]);
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const monthsShifted = arrayRotate(
monthNames,
-1 * (new Date().getMonth() + 1)
);
const dates = createYearArray();
return (
<div className="d-flex justify-content-start">
<div className=" d-flex flex-column" style={{}}>
<Stack direction="horizontal">
{/* Week labels */}
<Stack className="me-2 mt-1">
{days.map((day) => (
<div style={{ height: TILE_SIZE, fontSize: "smaller" }}>
<p className="font-monospace">{day}</p>
</div>
))}
</Stack>
{/* Heatmap Tiles */}
{dates.map((week, outer_idx) => (
<Stack className="pt-1">
{week.map((week_date, inner_idx) => (
<Tile
idx={inner_idx + outer_idx * 7}
date={week_date}
count={chartData.get(week_date) ?? 0}
setDate={setDate}
setTileIndex={setTileIndex}
selected={tileIndex === inner_idx + outer_idx * 7}
></Tile>
))}
</Stack>
))}
</Stack>
{/* Month Labels */}
<Stack direction="horizontal">
{/* This is only to pad the month label names according to the month label names */}
<div className="me-2 invisible">
<p className="font-monospace"></p>...
</div>
{monthsShifted.map((name) => (
<p className="mx-auto">{name.slice(0, 3)}</p>
))}
</Stack>
</div>
</div>
);
}
Here is what it looks like enter image description here
Tileis relatively simple, but the effect of rerendering everything must accumulate - you can wrap it withReact.memoand likely (measure first!) get a speedup. Also, what's your primary objective here - performance or engineering/readability? I can write some "doesn't look right to me" things regarding code style, but can hardly suggest anything beyond memoization for performance. \$\endgroup\$