diff --git a/vinvoor/src/components/BrowserView.tsx b/vinvoor/src/components/BrowserView.tsx index 1f05829..bdee5ac 100644 --- a/vinvoor/src/components/BrowserView.tsx +++ b/vinvoor/src/components/BrowserView.tsx @@ -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 = ({ children }) => { +export const BrowserView: FC = ({ + 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}; }; diff --git a/vinvoor/src/navbar/NavBarLogo.tsx b/vinvoor/src/navbar/NavBarLogo.tsx index a89a57e..12c86b3 100644 --- a/vinvoor/src/navbar/NavBarLogo.tsx +++ b/vinvoor/src/navbar/NavBarLogo.tsx @@ -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; diff --git a/vinvoor/src/overview/Overview.tsx b/vinvoor/src/overview/Overview.tsx index 241c1a0..0e90f30 100644 --- a/vinvoor/src/overview/Overview.tsx +++ b/vinvoor/src/overview/Overview.tsx @@ -1,6 +1,6 @@ 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"; @@ -8,8 +8,8 @@ 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 { @@ -29,22 +29,15 @@ export const Overview = () => { ); const [checked, setChecked] = useState(false); const daysRef = useRef(null); - const heatmapSwitchRef = useRef(null); - const [heatmapSwitchHeight, setHeatmapSwitchHeight] = useState(0); const [paperHeight, setPaperHeight] = useState(0); const handleChange = (event: React.ChangeEvent) => { 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 ( @@ -62,40 +55,47 @@ export const Overview = () => { - - + setChecked(false)} > - Months - - Days - - - - + + Months + + Days + + + + + @@ -107,22 +107,6 @@ export const Overview = () => { - - - - - ) : ( = ({ + startDate, + endDate, + columnCount, + transform, + isSmallView, + variant, +}) => { + const theme = useTheme(); + const { scans } = useContext(ScanContext); + + const data = useMemo(() => { + 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 ( + + {[...Array(columnCount)].map((_, idx) => { + return ( + + {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 ( + + ); + }) + : [...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 ( + + ); + })} + + ); + })} + + ); +}; diff --git a/vinvoor/src/overview/heatmap/Heatmap.tsx b/vinvoor/src/overview/heatmap/Heatmap.tsx index e09ce90..f5f3c1d 100644 --- a/vinvoor/src/overview/heatmap/Heatmap.tsx +++ b/vinvoor/src/overview/heatmap/Heatmap.tsx @@ -1,282 +1,65 @@ -import { Box } from "@mui/material"; -import { Theme, useTheme } from "@mui/material/styles"; -import { FC, useContext } from "react"; -import { MILLISECONDS_IN_ONE_DAY, shiftDate } from "../../util/util"; -import { ScanContext } from "../Overview"; -import "./heatmap.css"; +import { useMediaQuery, useTheme } from "@mui/material"; +import { FC } from "react"; +import { Day } from "./Day"; +import { LabelsMonth } from "./LabelsMonth"; +import { HeatmapVariant } from "./types"; import { - dateTimeFormat, DAYS_IN_WEEK, - getColumnCount, - getEmpty, - getHeight, - getMonthLabelCoordinates, - getSquareCoordinates, - getTransformForAllWeeks, - getTransformForColumn, - getTransformForMonthLabels, - getWidth, - MONTH_LABELS, - SQUARE_SIZE, - styleMonth, + getColumnCountDays, + getColumnCountMonths, + isDayVariant, + LEFT_PAD, + RECT_SIZE, + SPACE, + TOP_PAD, + WEEKS_IN_MONTH, } from "./utils"; -export interface HeatmapItem { - date: Date; - count: number; -} - -export enum HeatmapVariant { - DAYS, - MONTHS, -} - interface HeatmapProps { startDate: Date; endDate: Date; variant: HeatmapVariant; - maxHeight: number; } -const getAllValues = ( - days: readonly Date[], - startDate: Date, - endDate: Date, - variant: HeatmapVariant -): HeatmapItem[] => { - const values: readonly HeatmapItem[] = days.map((date) => ({ - date, - count: 1, - })); - if (variant === HeatmapVariant.DAYS) { - return Array.from( - { - length: - (endDate.getTime() - startDate.getTime()) / - MILLISECONDS_IN_ONE_DAY + - 1, - }, - (_, i) => { - const date = shiftDate(startDate, i); - return ( - values.find((v) => v.date.getTime() === date.getTime()) || { - date, - count: 0, - } - ); - } - ); - } else { - return Array.from( - { - length: getColumnCount(startDate, endDate, HeatmapVariant.DAYS), - }, - (_, i) => { - const start = shiftDate(startDate, i * DAYS_IN_WEEK); - const count = Array.from({ - length: DAYS_IN_WEEK, - }).reduce((sum, _, j) => { - const date = shiftDate(start, j); - const value = values.find( - (v) => v.date.getTime() === date.getTime() - ); - return sum + (value ? value.count : 0); - }, 0); - - return { date: start, count }; - } - ); - } -}; - -const getWeeksInMonth = ( - values: HeatmapItem[], - startDate: Date -): Record => { - const startYear = values[0].date.getFullYear(); - return values.reduce( - (acc, value) => { - const index = - (value.date.getFullYear() - startYear) * 12 + - value.date.getMonth() - - startDate.getMonth(); - acc[index] = (acc[index] || 0) + 1; - return acc; - }, - { - [startDate.getMonth()]: getEmpty( - values[0].date, - HeatmapVariant.MONTHS - ), - } as { - [key: number]: number; - } - ); -}; - -const getRectStyling = ( - theme: Theme, - value: HeatmapItem, - variant: HeatmapVariant -) => { - if (variant === HeatmapVariant.DAYS) - if (value.count > 0) return theme.heatmap.colorActive; - else return theme.heatmap.colorInActive; - else return styleMonth[Math.min(value.count, 5)](theme); -}; - -const getTextStyling = (theme: Theme, variant: HeatmapVariant) => { - return { - fill: theme.palette.primary.contrastText, - fontSize: variant === HeatmapVariant.DAYS ? "1.8rem" : "0.8rem", - }; -}; - -const getTooltipDataAttrsForDate = ( - value: HeatmapItem, - variant: HeatmapVariant -) => ({ - "data-tooltip-id": "heatmap", - "data-tooltip-content": - variant === HeatmapVariant.DAYS - ? getTooltipDataAttrsForDays(value) - : getTooltipDataAttrsForMonths(value), -}); - -const getTooltipDataAttrsForDays = (value: HeatmapItem) => - `${value.count > 0 ? "Present" : "Absent"} on ${dateTimeFormat.format( - value.date - )}`; - -const getTooltipDataAttrsForMonths = (value: HeatmapItem) => - `${value.count} scan${ - value.count !== 1 ? "s" : "" - } in the week of ${dateTimeFormat.format(value.date)}`; - -export const Heatmap: FC = ({ - startDate, - endDate, - variant, - maxHeight, -}) => { +export const Heatmap: FC = ({ startDate, endDate, variant }) => { const theme = useTheme(); - const { scans } = useContext(ScanContext); - - const days = scans.map((scan) => scan.scanTime); + const isSmallView = useMediaQuery(theme.breakpoints.down("lg")); - days.forEach((date) => date.setHours(0, 0, 0, 0)); startDate.setHours(0, 0, 0, 0); endDate.setHours(0, 0, 0, 0); - const values = getAllValues(days, startDate, endDate, variant); - - const viewBox = `0 0 ${getWidth(startDate, endDate, variant)} ${getHeight( - variant - )}`; - - const weeksInMonth = - variant === HeatmapVariant.MONTHS - ? getWeeksInMonth(values, startDate) - : {}; // Amount of weeks in each month - - const columns = getColumnCount(startDate, endDate, variant); // Amount of columns of squares - const emptyStart = getEmpty(startDate, variant); // Amount of empty squares at the start - const emptyEnd = getEmpty(endDate, variant); // Amount of empty squares at the end - - let valueIndex = 0; - const renderSquare = (row: number, column: number) => { - if (column === 0 && row < emptyStart) return null; - - if (variant === HeatmapVariant.DAYS) - if (column === columns - 1 && row > emptyEnd) return null; - - const value = values[valueIndex++]; - - const [x, y] = getSquareCoordinates(row); - - return ( - - ); - }; - - const renderColumn = (column: number) => ( - - {[ - ...Array( - variant === HeatmapVariant.DAYS - ? DAYS_IN_WEEK - : weeksInMonth[column] - ).keys(), - ].map((row) => renderSquare(row, column))} - - ); - - const renderColumns = () => - [...Array(columns).keys()].map((column) => renderColumn(column)); - - const renderMonthLabels = () => { - if (variant === HeatmapVariant.DAYS) { - return [...Array(columns).keys()].map((column) => { - const endOfWeek = shiftDate(startDate, column * DAYS_IN_WEEK); - const [x, y] = getMonthLabelCoordinates(variant, column); - - return endOfWeek.getDate() >= 1 && - endOfWeek.getDate() <= DAYS_IN_WEEK ? ( - - {MONTH_LABELS[endOfWeek.getMonth()]} - - ) : null; - }); - } else { - return [...Array(columns).keys()].map((column) => { - if (column % 2 === 1) { - return null; - } - - const [x, y] = getMonthLabelCoordinates(variant, column); - - return ( - - {MONTH_LABELS[startDate.getMonth() + column]} - - ); - }); - } - }; + const columnCount = isDayVariant(variant) + ? getColumnCountDays(startDate, endDate) + : getColumnCountMonths(startDate, endDate); return ( - - - - {renderMonthLabels()} - - - {renderColumns()} - - - + + + + ); }; diff --git a/vinvoor/src/overview/heatmap/HeatmapNew.tsx b/vinvoor/src/overview/heatmap/HeatmapNew.tsx deleted file mode 100644 index c3a6a67..0000000 --- a/vinvoor/src/overview/heatmap/HeatmapNew.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import { useTheme } from "@mui/material"; -import { ApexOptions } from "apexcharts"; -import { FC, useContext } from "react"; -import Chart from "react-apexcharts"; -import { Scan } from "../../types/scans"; -import { shiftDate } from "../../util/util"; -import { ScanContext } from "../Overview"; -import { HeatmapVariant } from "./Heatmap"; - -interface HeatmapNewProps { - startDate: Date; - endDate: Date; - variant: HeatmapVariant; -} - -interface SeriesPoint { - x: string; - y: number; -} - -const WEEK_DAYS = Object.freeze({ - Sunday: 0, - Saturday: 6, - Friday: 5, - Thursday: 4, - Wednesday: 3, - Tuesday: 2, - Monday: 1, -}); -const DAYS_IN_WEEK = 7; - -const normalizeDates = (...args: Date[][]) => { - args.forEach((dates) => dates.forEach((date) => date.setHours(0, 0, 0, 0))); -}; - -const getDayData = (startDate: Date, endDate: Date, scans: readonly Scan[]) => { - const dates = [...scans].map((scan) => scan.scanTime); - normalizeDates([startDate], [endDate], dates); - - const days: SeriesPoint[][] = [[], [], [], [], [], [], []]; - let index = 0; - let week = startDate.getDay() === 1 ? -1 : 0; - - let currentDate = new Date( - startDate.setDate(startDate.getDate() - startDate.getDay() + 1) - ); - - while (currentDate <= endDate || currentDate.getDay() !== 1) { - if (currentDate.getDay() === 1) week++; - - let amount = 0; - - if (dates[index]?.getTime() === currentDate.getTime()) { - amount = 1; - index++; - } - - days[currentDate.getDay()].push({ - x: currentDate.toISOString(), - y: amount, - }); - - currentDate.setDate(currentDate.getDate() + 1); - } - - return Object.entries(WEEK_DAYS).map(([day, dayIndex]) => ({ - name: day, - data: days[dayIndex], - })) as ApexAxisChartSeries; -}; - -const getMonthData = ( - startDate: Date, - endDate: Date, - scans: readonly Scan[] -) => { - const dates = [...scans].map((scan) => scan.scanTime); - normalizeDates([startDate], [endDate], dates); - - const weeks: SeriesPoint[][] = [[], [], [], [], []]; - let index = 0; - let week = 0; - let month = 0; - - let currentDate = shiftDate(startDate, -startDate.getDay() + 1); - - console.log(currentDate); - - while (currentDate.getDate() > DAYS_IN_WEEK) - currentDate.setDate(currentDate.getDate() - DAYS_IN_WEEK); - - while ( - currentDate <= endDate || - currentDate.getDate() < DAYS_IN_WEEK || - currentDate.getDay() % DAYS_IN_WEEK !== 1 - ) { - if (currentDate.getDate() <= DAYS_IN_WEEK) { - week = 0; - month++; - } else week++; - - let amount = 0; - let endOfWeek = new Date(currentDate).setDate( - currentDate.getDate() + DAYS_IN_WEEK - ); - while ( - dates[index]?.getTime() >= currentDate.getTime() && - dates[index]?.getTime() < endOfWeek - ) { - amount++; - index++; - } - - weeks[week].push({ - x: `M${month}`, - y: amount, - }); - - currentDate.setDate(currentDate.getDate() + DAYS_IN_WEEK); - } - - return weeks.map((week, index) => ({ - name: `Week ${index + 1}`, - data: week, - })) as ApexAxisChartSeries; -}; - -export const HeatmapNew: FC = ({ - startDate, - endDate, - variant, -}) => { - const theme = useTheme(); - const { scans } = useContext(ScanContext); - - const shortMonthName = new Intl.DateTimeFormat("en-GB", { month: "short" }) - .format; - - console.log(getMonthData(startDate, endDate, scans)); - - const state = { - options: { - chart: { - animation: { - enabled: false, - }, - toolbar: { - show: false, - }, - type: "heatmap", - }, - dataLabels: { - enabled: false, - }, - grid: { - show: false, - }, - legend: { - show: false, - }, - tooltip: {}, - xaxis: { - labels: { - formatter: (value: string) => { - const date = new Date(value); - return date.getDate() <= DAYS_IN_WEEK - ? shortMonthName(date) - : ""; - }, - }, - position: "top", - axisBorder: { - show: false, - }, - axisTicks: { - show: false, - }, - tooltip: { - formatter: (value: string) => - shortMonthName(new Date(value)), - }, - }, - yaxis: { - labels: { - show: false, - }, - }, - stroke: { - width: 10, - colors: [theme.palette.background.paper], - }, - plotOptions: { - heatmap: { - radius: 10, - useFillColorAsStroke: false, - enableShades: false, - colorScale: { - ranges: - variant === HeatmapVariant.DAYS - ? [ - { - from: 0, - to: 0, - color: theme.heatmap.colorInActive - .fill, - }, - { - from: 1, - to: 1, - color: theme.heatmap.colorActive.fill, - }, - ] - : [ - { - from: 0, - to: 0, - color: theme.heatmap.color0.fill, - }, - { - from: 1, - to: 1, - color: theme.heatmap.color1.fill, - }, - { - from: 2, - to: 3, - color: theme.heatmap.color2.fill, - }, - { - from: 3, - to: 4, - color: theme.heatmap.color3.fill, - }, - ], - }, - }, - }, - } as ApexOptions, - series: - variant === HeatmapVariant.DAYS - ? getDayData(startDate, endDate, scans) - : getMonthData(startDate, endDate, scans), - }; - - return ( - - ); -}; diff --git a/vinvoor/src/overview/heatmap/LabelsMonth.tsx b/vinvoor/src/overview/heatmap/LabelsMonth.tsx new file mode 100644 index 0000000..6beb9c7 --- /dev/null +++ b/vinvoor/src/overview/heatmap/LabelsMonth.tsx @@ -0,0 +1,102 @@ +import { useTheme } from "@mui/material/styles"; +import { FC, useMemo } from "react"; +import { DayLabel, HeatmapVariant, LabelData, MonthLabel } from "./types"; +import { + DAYS_IN_WEEK, + FONT_SIZE, + getColumnCountDays, + getColumnCountMonths, + isDayVariant, + LEFT_PAD, + MILLISECONDS_IN_DAY, + MONTH_LABELS, + MONTH_RECT_Y, + RECT_SIZE, + SPACE, +} from "./utils"; + +interface LablesMonthProps { + startDate: Date; + endDate: Date; + isSmallView: boolean; + variant: HeatmapVariant; +} + +export const LabelsMonth: FC = ({ + startDate, + endDate, + isSmallView, + variant, +}) => { + const theme = useTheme(); + + const data = useMemo(() => { + const day = [ + ...Array(getColumnCountDays(startDate, endDate) * DAYS_IN_WEEK), + ] + .map((_, idx) => { + if ((idx / DAYS_IN_WEEK) % 1 === 0) { + const date = new Date( + startDate.getTime() + idx * MILLISECONDS_IN_DAY + ); + const month = date.getMonth(); + + return { + col: idx / 7, + month, + monthStr: MONTH_LABELS[month], + }; + } + return {} as DayLabel; + }) + .filter((item) => Object.keys(item).length) + .filter((item, idx, list) => + list[idx - 1] ? list[idx - 1]!.month !== item!.month : true + ); + + const month = [...Array(getColumnCountMonths(startDate, endDate))] + .map((_, idx) => { + const date = new Date(startDate); + date.setMonth(date.getMonth() + idx); + + return { monthStr: MONTH_LABELS[date.getMonth()] }; + }) + .filter((_, idx) => idx % 2 === 0); + return { + day, + month, + }; + }, [startDate, endDate]); + + return ( + <> + {(isDayVariant(variant) + ? (data.day as DayLabel[]) + : (data.month as MonthLabel[]) + ).map((item, idx) => { + return ( + + {item.monthStr} + + ); + })} + + ); +}; diff --git a/vinvoor/src/overview/heatmap/Rect.tsx b/vinvoor/src/overview/heatmap/Rect.tsx new file mode 100644 index 0000000..550f025 --- /dev/null +++ b/vinvoor/src/overview/heatmap/Rect.tsx @@ -0,0 +1,36 @@ +import { FC } from "react"; +import { HeatmapRectStyle } from "../../theme"; +import { RECT_RADIUS, RECT_SIZE, RECT_STROKE, SPACE } from "./utils"; + +interface RectProps { + idx: number; + cidx: number; + isSmallView: boolean; + colors: HeatmapRectStyle; + dataTooltipContent: string; +} + +export const Rect: FC = ({ + idx, + cidx, + isSmallView, + colors, + dataTooltipContent, +}) => { + return ( + + ); +}; diff --git a/vinvoor/src/overview/heatmap/heatmap.css b/vinvoor/src/overview/heatmap/heatmap.css index 6bb2a2f..d4f8cad 100644 --- a/vinvoor/src/overview/heatmap/heatmap.css +++ b/vinvoor/src/overview/heatmap/heatmap.css @@ -1,7 +1,3 @@ -.heatmap rect:hover { - stroke-opacity: 0; -} - @keyframes createBox { from { width: 0; @@ -12,7 +8,6 @@ } } -.rect { - /* animation: createBox 1s; */ - stroke-width: 1px; +.rect:hover { + stroke-opacity: 0; } diff --git a/vinvoor/src/overview/heatmap/types.ts b/vinvoor/src/overview/heatmap/types.ts new file mode 100644 index 0000000..3f933bc --- /dev/null +++ b/vinvoor/src/overview/heatmap/types.ts @@ -0,0 +1,31 @@ +export enum HeatmapVariant { + DAYS, + MONTHS, +} + +export interface HeatmapValue { + date: Date; + count: number; // Could be used in the future for check in and out +} + +export interface DayData { + data: Record; // Each scan + start: Date; // Start brought back to the beginning of the week + endWeek: Date; // First day of the week after the end date + startDates: Record; // Start of each week for each month +} + +export interface DayLabel { + col: number; + month: number; + monthStr: string; +} + +export interface MonthLabel { + monthStr: string; +} + +export interface LabelData { + day: DayLabel[]; + month: MonthLabel[]; +} diff --git a/vinvoor/src/overview/heatmap/utils.ts b/vinvoor/src/overview/heatmap/utils.ts index b427295..94443b9 100644 --- a/vinvoor/src/overview/heatmap/utils.ts +++ b/vinvoor/src/overview/heatmap/utils.ts @@ -1,14 +1,71 @@ -// Exports +import { Theme } from "@mui/material"; +import { Scan } from "../../types/scans"; +import { HeatmapValue, HeatmapVariant } from "./types"; + +export const getColumnCountDays = (startDate: Date, endDate: Date) => { + const startOfWeek = new Date(startDate); + startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay()); + + const endOfWeek = new Date(endDate); + if (endOfWeek.getDay() === 0) + endOfWeek.setDate(endOfWeek.getDate() - endOfWeek.getDay()); + else endOfWeek.setDate(endOfWeek.getDate() - endOfWeek.getDay() + 6); + + return Math.ceil( + (endOfWeek.getTime() - startOfWeek.getTime()) / + (DAYS_IN_WEEK * MILLISECONDS_IN_DAY) + ); +}; + +export const getColumnCountMonths = (startDate: Date, endDate: Date) => { + return ( + (endDate.getFullYear() - startDate.getFullYear()) * 12 + + endDate.getMonth() - + startDate.getMonth() + + 1 + ); +}; + +export const getMondayIndexedDay = (date: Date) => (date.getDay() + 6) % 7; + +export const formatData = (scans: Scan[]) => { + const result: Record = {}; + scans.forEach((scan) => { + result[scan.scanTime.getTime()] = { + date: scan.scanTime, + count: 1, + }; + }); + + return result; +}; -import { Theme } from "@mui/material/styles"; -import { MILLISECONDS_IN_ONE_DAY } from "../../util/util"; -import { HeatmapVariant } from "./Heatmap"; +export const isDayVariant = (variant: HeatmapVariant) => + variant === HeatmapVariant.DAYS; + +export const styleMonth = [ + (theme: Theme) => theme.heatmap.color0, + (theme: Theme) => theme.heatmap.color1, + (theme: Theme) => theme.heatmap.color2, + (theme: Theme) => theme.heatmap.color3, + (theme: Theme) => theme.heatmap.color4, + (theme: Theme) => theme.heatmap.color5, +]; // Constants -export const DAYS_IN_WEEK = 7; -export const WEEKS_IN_MONTH = 5; -export const SQUARE_SIZE = 10; +// Size + +export const RECT_SIZE = (isSmallView: boolean) => (isSmallView ? 5 : 20); +export const RECT_RADIUS = (isSmallView: boolean) => (isSmallView ? 1 : 4); +export const RECT_STROKE = (isSmallView: boolean) => (isSmallView ? 1 : 2); +export const SPACE = (isSmallView: boolean) => (isSmallView ? 2 : 10); +export const TOP_PAD = (isSmallView: boolean) => (isSmallView ? 8 : 25); +export const LEFT_PAD = (isSmallView: boolean) => (isSmallView ? 2 : 5); +export const MONTH_RECT_Y = (isSmallView: boolean) => (isSmallView ? 5 : 15); +export const FONT_SIZE = (isSmallView: boolean) => (isSmallView ? 4 : 15); + +// Month labels export const MONTH_LABELS = [ "Jan", @@ -24,105 +81,17 @@ export const MONTH_LABELS = [ "Nov", "Dec", ]; -export const dateTimeFormat = new Intl.DateTimeFormat("en-GB", { + +// Formatter + +export const DATE_FORMATTER = new Intl.DateTimeFormat("en-GB", { year: "2-digit", month: "short", day: "numeric", }); -// Labels - -export const getMonthLabelSize = (variant: HeatmapVariant) => - SQUARE_SIZE + - MONTH_LABEL_GUTTER_SIZE(variant) + - MONTH_LABEL_OFFSET(variant); - -export const getMonthLabelCoordinates = ( - variant: HeatmapVariant, - column: number -) => [ - column * getSquareSize(), - getMonthLabelSize(variant) - MONTH_LABEL_GUTTER_SIZE(variant), -]; - -// Transforms - -export const getTransformForColumn = (column: number) => - `translate(${column * getSquareSize() + GUTTERSIZE}, 0)`; +// Consts -export const getTransformForAllWeeks = (variant: HeatmapVariant) => - `translate(0, ${getMonthLabelSize(variant)})`; - -export const getTransformForMonthLabels = () => `translate(0, 0)`; - -export const getWidth = ( - startDate: Date, - endDate: Date, - variant: HeatmapVariant -) => getColumnCount(startDate, endDate, variant) * getSquareSize() + GUTTERSIZE; - -export const getHeight = (variant: HeatmapVariant) => { - if (variant === HeatmapVariant.DAYS) - return DAYS_IN_WEEK * getSquareSize() + getMonthLabelSize(variant); - else return WEEKS_IN_MONTH * getSquareSize() + getMonthLabelSize(variant); -}; - -// Coordinate - -export const getSquareCoordinates = (dayIndex: number) => [ - 0, - dayIndex * getSquareSize(), -]; - -// Utils - -export const getEmpty = (date: Date, variant: HeatmapVariant) => { - if (variant === HeatmapVariant.DAYS) - return (date.getDay() + DAYS_IN_WEEK - 1) % DAYS_IN_WEEK; - else return Math.floor((date.getDate() - 1) / DAYS_IN_WEEK); -}; - -export const getColumnCount = ( - startDate: Date, - endDate: Date, - variant: HeatmapVariant -) => { - if (variant === HeatmapVariant.DAYS) { - const startOfWeek = new Date(startDate); - startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay()); - - const endOfWeek = new Date(endDate); - if (endOfWeek.getDay() === 0) - endOfWeek.setDate(endOfWeek.getDate() - endOfWeek.getDay()); - else endOfWeek.setDate(endOfWeek.getDate() - endOfWeek.getDay() + 6); - - return Math.ceil( - (endOfWeek.getTime() - startOfWeek.getTime()) / - (DAYS_IN_WEEK * MILLISECONDS_IN_ONE_DAY) - ); - } else { - return ( - (endDate.getFullYear() - startDate.getFullYear()) * 12 + - (endDate.getMonth() - startDate.getMonth() + 1) - ); - } -}; - -export const styleMonth = [ - (theme: Theme) => theme.heatmap.color0, - (theme: Theme) => theme.heatmap.color1, - (theme: Theme) => theme.heatmap.color2, - (theme: Theme) => theme.heatmap.color3, - (theme: Theme) => theme.heatmap.color4, - (theme: Theme) => theme.heatmap.color5, -]; - -// Local functions - -const GUTTERSIZE = 5; -const MONTH_LABEL_GUTTER_SIZE = (variant: HeatmapVariant) => - variant === HeatmapVariant.DAYS ? 15 : 8; -const MONTH_LABEL_OFFSET = (variant: HeatmapVariant) => - variant === HeatmapVariant.DAYS ? 15 : 0; - -const getSquareSize = () => SQUARE_SIZE + GUTTERSIZE; +export const DAYS_IN_WEEK = 7; +export const WEEKS_IN_MONTH = 5; +export const MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; diff --git a/vinvoor/src/theme.ts b/vinvoor/src/theme.ts index 82b12e2..66d7fdc 100644 --- a/vinvoor/src/theme.ts +++ b/vinvoor/src/theme.ts @@ -169,7 +169,7 @@ export const hiddenTheme = createTheme({ }, }); -interface HeatmapRectStyle { +export interface HeatmapRectStyle { fill: string; stroke: string; }