3
\$\begingroup\$

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

Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Aug 26, 2024 at 8:35
\$\endgroup\$
1
  • 1
    \$\begingroup\$ You mention 4ms, but your component rerenders all tiles on click - 4ms is excellent, to be honest (I can't reproduce that in CodeSandbox, though, my timings are less optimistic by orders of magnitude). Your Tile is relatively simple, but the effect of rerendering everything must accumulate - you can wrap it with React.memo and 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\$ Commented Aug 26, 2024 at 21:41

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.