Skip to content

Commit

Permalink
vinvoor: improve heatmap
Browse files Browse the repository at this point in the history
  • Loading branch information
Topvennie committed Jul 29, 2024
1 parent 8d97c69 commit d233aca
Show file tree
Hide file tree
Showing 12 changed files with 542 additions and 683 deletions.
16 changes: 14 additions & 2 deletions vinvoor/src/components/BrowserView.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { FC, useEffect } from "react";

interface BrowserViewProps {
onMobileView?: () => void;
onBrowserView?: () => void;
children: React.ReactNode;
}

export const BrowserView: FC<BrowserViewProps> = ({ children }) => {
export const BrowserView: FC<BrowserViewProps> = ({
onMobileView,
onBrowserView,
children,
}) => {
const theme = useTheme();
const isMobileView = useMediaQuery(theme.breakpoints.down("md"));

// Only run callbacks after the component has rendered
useEffect(() => {
if (isMobileView) onMobileView?.();
else onBrowserView?.();
}, [isMobileView]);

return isMobileView ? null : <>{children}</>;
};
2 changes: 1 addition & 1 deletion vinvoor/src/navbar/NavBarLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface NavBarLogoProps {
}

const CLICK_AMOUNT = 10;
const CLICK_TIME_MS = 900;
const CLICK_TIME_MS = 1000;

let pressedAmount = 0;
let startTimePress = 0;
Expand Down
100 changes: 42 additions & 58 deletions vinvoor/src/overview/Overview.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Box, Paper, Stack, Switch, Typography } from "@mui/material";
import Grid from "@mui/material/Grid";
import { createContext, useEffect, useRef, useState } from "react";
import { createContext, useLayoutEffect, useRef, useState } from "react";
import { Tooltip } from "react-tooltip";
import { BrowserView } from "../components/BrowserView";
import { LoadingSkeleton } from "../components/LoadingSkeleton";
import { useFetch } from "../hooks/useFetch";
import { convertScanJSON, Scan } from "../types/scans";
import { CheckIn } from "./checkin/CheckIn";
import { Days } from "./days/Days";
import { Heatmap, HeatmapVariant } from "./heatmap/Heatmap";
import { HeatmapNew } from "./heatmap/HeatmapNew";
import { Heatmap } from "./heatmap/Heatmap";
import { HeatmapVariant } from "./heatmap/types";
import { Streak } from "./streak/Streak";

interface ScanContextProps {
Expand All @@ -29,22 +29,15 @@ export const Overview = () => {
);
const [checked, setChecked] = useState<boolean>(false);
const daysRef = useRef<HTMLDivElement>(null);
const heatmapSwitchRef = useRef<HTMLDivElement>(null);
const [heatmapSwitchHeight, setHeatmapSwitchHeight] = useState<number>(0);
const [paperHeight, setPaperHeight] = useState<number>(0);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
};

useEffect(() => {
if (daysRef.current) {
setPaperHeight(daysRef.current.clientHeight);
}

if (heatmapSwitchRef.current) {
setHeatmapSwitchHeight(heatmapSwitchRef.current.clientHeight);
}
useLayoutEffect(() => {
if (daysRef.current)
setPaperHeight(daysRef.current.getBoundingClientRect().height);
});

return (
Expand All @@ -62,40 +55,47 @@ export const Overview = () => {
<Paper
elevation={4}
sx={{
padding: 1,
padding: 2,
width: "100%",
height: { xs: "auto", lg: paperHeight },
display: "flex",
justifyContent: "center",
}}
>
<BrowserView>
<Stack
direction="row"
spacing={1}
alignItems="center"
justifyContent="flex-end"
ref={heatmapSwitchRef}
<Stack
direction="column"
sx={{}}
width="100%"
height="100%"
>
<BrowserView
onMobileView={() => setChecked(false)}
>
<Typography>Months</Typography>
<Switch
checked={checked}
onChange={handleChange}
/>
<Typography>Days</Typography>
</Stack>
</BrowserView>
<Heatmap
startDate={new Date("2024-05-01")}
endDate={new Date("2024-09-30")}
variant={
checked
? HeatmapVariant.DAYS
: HeatmapVariant.MONTHS
}
maxHeight={
paperHeight - heatmapSwitchHeight - 10
}
/>
<Tooltip id="heatmap" />
<Stack
direction="row"
spacing={1}
alignItems="center"
justifyContent="flex-end"
>
<Typography>Months</Typography>
<Switch
checked={checked}
onChange={handleChange}
/>
<Typography>Days</Typography>
</Stack>
</BrowserView>
<Heatmap
startDate={new Date("2024-05-01")}
endDate={new Date("2024-09-30")}
variant={
checked
? HeatmapVariant.DAYS
: HeatmapVariant.MONTHS
}
/>
<Tooltip id="heatmap" />
</Stack>
</Paper>
</Grid>
<Grid item xs={12} md={4} sx={{ display: "flex" }}>
Expand All @@ -107,22 +107,6 @@ export const Overview = () => {
<Days />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper
elevation={4}
sx={{ padding: 2, width: "100%" }}
>
<HeatmapNew
startDate={new Date("2024-05-01")}
endDate={new Date("2024-09-30")}
variant={
checked
? HeatmapVariant.DAYS
: HeatmapVariant.MONTHS
}
/>
</Paper>
</Grid>
</Grid>
) : (
<Box
Expand Down
195 changes: 195 additions & 0 deletions vinvoor/src/overview/heatmap/Day.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { useTheme } from "@mui/material";
import { FC, useContext, useMemo } from "react";
import { ScanContext } from "../Overview";
import "./heatmap.css";
import { Rect } from "./Rect";
import { DayData, HeatmapVariant } from "./types";
import {
DATE_FORMATTER,
DAYS_IN_WEEK,
formatData,
getColumnCountMonths,
getMondayIndexedDay,
isDayVariant,
MILLISECONDS_IN_DAY,
styleMonth,
WEEKS_IN_MONTH,
} from "./utils";

interface DayProps {
startDate: Date;
endDate: Date;
columnCount: number;
transform: string;
isSmallView: boolean;
variant: HeatmapVariant;
}

export const Day: FC<DayProps> = ({
startDate,
endDate,
columnCount,
transform,
isSmallView,
variant,
}) => {
const theme = useTheme();
const { scans } = useContext(ScanContext);

const data = useMemo<DayData>(() => {
const normalizedScans = [...scans];
normalizedScans.forEach((scan) => scan.scanTime.setHours(0, 0, 0, 0));
const formattedScans = formatData(normalizedScans);

const start = new Date(
startDate.getTime() -
startDate.getDay() * MILLISECONDS_IN_DAY +
MILLISECONDS_IN_DAY
);

const startDates = [
...Array(getColumnCountMonths(startDate, endDate)),
].map((_, idx) => {
const newStartDate = new Date(startDate);
if (idx === 0) {
while (newStartDate.getDay() !== 1) {
newStartDate.setDate(newStartDate.getDate() - 1);
}
} else {
newStartDate.setMonth(newStartDate.getMonth() + idx);
newStartDate.setDate(1);
while (newStartDate.getDay() !== 1) {
newStartDate.setDate(newStartDate.getDate() + 1);
}
}

return newStartDate;
});

const endWeek = new Date(
endDate.getTime() +
MILLISECONDS_IN_DAY *
(DAYS_IN_WEEK -
(getMondayIndexedDay(endDate) % DAYS_IN_WEEK))
);

return {
data: formattedScans,
start,
endWeek,
startDates,
};
}, [scans, startDate, endDate]);

return (
<g transform={transform}>
{[...Array(columnCount)].map((_, idx) => {
return (
<g key={idx}>
{isDayVariant(variant)
? [...Array(DAYS_IN_WEEK)].map((_, cidx) => {
const currentDate = new Date(
data.start.getTime() +
MILLISECONDS_IN_DAY *
(idx * DAYS_IN_WEEK + cidx)
);

if (
currentDate.getTime() <
startDate.getTime()
)
return null;

if (currentDate.getTime() > endDate.getTime())
return null;

let colors = theme.heatmap.colorInActive;
if (data.data[currentDate.getTime()])
colors = theme.heatmap.colorActive;

const dataTooltipContent = `${
data.data[currentDate.getTime()]
? "Present"
: "Absent"
} on ${DATE_FORMATTER.format(currentDate)}`;

return (
<Rect
key={cidx}
idx={idx}
cidx={cidx}
isSmallView={isSmallView}
colors={colors}
dataTooltipContent={
dataTooltipContent
}
/>
);
})
: [...Array(WEEKS_IN_MONTH)].map((_, cidx) => {
const currentDate = new Date(
data.startDates[idx].getTime() +
MILLISECONDS_IN_DAY *
cidx *
DAYS_IN_WEEK
);

// Week is no longer in the month
if (
currentDate.getMonth() >
startDate.getMonth() + idx &&
getMondayIndexedDay(currentDate) <=
currentDate.getDate() - 1
)
return null;

// Week is after end date
if (
currentDate.getTime() >=
data.endWeek.getTime()
)
return null;

const count = [...Array(DAYS_IN_WEEK)]
.map(
(_, i) =>
new Date(
currentDate.getTime() +
i * MILLISECONDS_IN_DAY
)
)
.filter(
(date) =>
date.getTime() <=
endDate.getTime() &&
data.data[date.getTime()]
).length;

const colors =
styleMonth[Math.min(count, 5)](theme); // Can be higher than 5 if multiple scans in a day or scanned during the weekend

const dataTooltipContent = `${count} scan${
count !== 1 ? "s" : ""
} in the week of ${DATE_FORMATTER.format(
currentDate
)}`;

return (
<Rect
key={cidx}
idx={idx}
cidx={cidx}
isSmallView={isSmallView}
colors={colors}
dataTooltipContent={
dataTooltipContent
}
/>
);
})}
</g>
);
})}
</g>
);
};
Loading

0 comments on commit d233aca

Please sign in to comment.