Speed up rendering of table by reducing temporal calls

This commit is contained in:
Benji Grant 2023-06-09 01:41:33 +10:00
parent 085dc389ca
commit f72204c796
7 changed files with 157 additions and 134 deletions

View file

@ -0,0 +1,13 @@
import { createPalette } from 'hue-map'
import { useStore } from '/src/stores'
import useSettingsStore from '/src/stores/settingsStore'
export const usePalette = (min: number, max: number) => {
const colormap = useStore(useSettingsStore, state => state.colormap)
return createPalette({
map: (colormap === undefined || colormap === 'crabfit') ? [[0, [247, 158, 0, 0]], [1, [247, 158, 0, 255]]] : colormap,
steps: Math.max((max - min) + 1, 2),
}).format()
}

View file

@ -1,8 +1,7 @@
'use client'
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
import { Fragment, useMemo, useRef, useState } from 'react'
import { Temporal } from '@js-temporal/polyfill'
import { createPalette } from 'hue-map'
import Content from '/src/components/Content/Content'
import Legend from '/src/components/Legend/Legend'
@ -10,9 +9,10 @@ import { PersonResponse } from '/src/config/api'
import { useTranslation } from '/src/i18n/client'
import { useStore } from '/src/stores'
import useSettingsStore from '/src/stores/settingsStore'
import { calculateAvailability, calculateColumns, calculateRows, convertTimesToDates, makeClass, relativeTimeFormat } from '/src/utils'
import { calculateAvailability, calculateTable, makeClass, relativeTimeFormat } from '/src/utils'
import styles from './AvailabilityViewer.module.scss'
import { usePalette } from '/hooks/usePalette'
interface AvailabilityViewerProps {
times: string[]
@ -23,9 +23,8 @@ interface AvailabilityViewerProps {
const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps) => {
const { t, i18n } = useTranslation('event')
const timeFormat = useStore(useSettingsStore, state => state.timeFormat)
const timeFormat = useStore(useSettingsStore, state => state.timeFormat) ?? '12h'
const highlight = useStore(useSettingsStore, state => state.highlight)
const colormap = useStore(useSettingsStore, state => state.colormap)
const [filteredPeople, setFilteredPeople] = useState(people.map(p => p.name))
const [tempFocus, setTempFocus] = useState<string>()
const [focusCount, setFocusCount] = useState<number>()
@ -39,82 +38,56 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
people: string[]
}>()
// Calculate rows and columns
const [dates, rows, columns] = useMemo(() => {
const dates = convertTimesToDates(times, timezone)
return [dates, calculateRows(dates), calculateColumns(dates)]
}, [times, timezone])
// Calculate table
const { rows, columns } = useMemo(() =>
calculateTable(times, i18n.language, timeFormat, timezone),
[times, i18n.language, timeFormat, timezone])
// Calculate availabilities
const { availabilities, min, max } = useMemo(() => calculateAvailability(dates, people
.filter(p => filteredPeople.includes(p.name))
.map(p => ({
...p,
availability: convertTimesToDates(p.availability, timezone),
}))
), [dates, filteredPeople, people, timezone])
const { availabilities, min, max } = useMemo(() =>
calculateAvailability(times, people.filter(p => filteredPeople.includes(p.name))),
[times, filteredPeople, people])
// Create the colour palette
const palette = usePalette(min, max)
// Is specific dates or just days of the week
const isSpecificDates = useMemo(() => times[0].length === 13, [times])
// Create the colour palette
const [palette, setPalette] = useState<string[]>([])
useEffect(() => {
setPalette(createPalette({
map: colormap !== 'crabfit' ? colormap : [[0, [247, 158, 0, 0]], [1, [247, 158, 0, 255]]],
steps: Math.max((max - min) + 1, 2),
}).format())
}, [min, max, colormap])
const heatmap = useMemo(() => (
<div className={styles.heatmap}>
<div className={styles.timeLabels}>
{rows.map((row, i) =>
<div className={styles.timeSpace} key={i}>
{row && row.minute === 0 && <label className={styles.timeLabel}>
{row.toLocaleString(i18n.language, { hour: 'numeric', hour12: timeFormat === '12h' })}
</label>}
</div>
)}
</div>
{columns.map((column, i) => <Fragment key={i}>
const heatmap = useMemo(() => columns.map((column, x) => <Fragment key={x}>
{column ? <div className={styles.dateColumn}>
{isSpecificDates && <label className={styles.dateLabel}>{column.toLocaleString(i18n.language, { month: 'short', day: 'numeric' })}</label>}
<label className={styles.dayLabel}>{column.toLocaleString(i18n.language, { weekday: 'short' })}</label>
{isSpecificDates && <label className={styles.dateLabel}>{column.header.dateLabel}</label>}
<label className={styles.dayLabel}>{column.header.weekdayLabel}</label>
<div
className={styles.times}
data-border-left={i === 0 || columns.at(i - 1) === null}
data-border-right={i === columns.length - 1 || columns.at(i + 1) === null}
data-border-left={x === 0 || columns.at(x - 1) === null}
data-border-right={x === columns.length - 1 || columns.at(x + 1) === null}
>
{rows.map((row, i) => {
if (i === rows.length - 1) return null
{column.cells.map((cell, y) => {
if (y === column.cells.length - 1) return null
if (!row || rows.at(i + 1) === null || dates.every(d => !d.equals(column.toZonedDateTime({ timeZone: timezone, plainTime: row })))) {
return <div
if (!cell) return <div
className={makeClass(styles.timeSpace, styles.grey)}
key={i}
key={y}
title={t<string>('greyed_times')}
/>
}
const date = column.toZonedDateTime({ timeZone: timezone, plainTime: row })
let peopleHere = availabilities.find(a => a.date.equals(date))?.people ?? []
let peopleHere = availabilities.find(a => a.date === cell.serialized)?.people ?? []
if (tempFocus) {
peopleHere = peopleHere.filter(p => p === tempFocus)
}
return <div
key={i}
key={y}
className={makeClass(
styles.time,
(focusCount === undefined || focusCount === peopleHere.length) && highlight && (peopleHere.length === max || tempFocus) && peopleHere.length > 0 && styles.highlight,
)}
style={{
backgroundColor: (focusCount === undefined || focusCount === peopleHere.length) ? palette[tempFocus && peopleHere.length ? max : peopleHere.length] : 'transparent',
...date.minute !== 0 && date.minute !== 30 && { borderTopColor: 'transparent' },
...date.minute === 30 && { borderTopStyle: 'dotted' },
...cell.minute !== 0 && cell.minute !== 30 && { borderTopColor: 'transparent' },
...cell.minute === 30 && { borderTopStyle: 'dotted' },
}}
aria-label={peopleHere.join(', ')}
onMouseEnter={e => {
@ -124,9 +97,7 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
x: Math.round(cellBox.x - wrapperBox.x + cellBox.width / 2),
y: Math.round(cellBox.y - wrapperBox.y + cellBox.height) + 6,
available: `${peopleHere.length} / ${filteredPeople.length} ${t('available')}`,
date: isSpecificDates
? date.toLocaleString(i18n.language, { dateStyle: 'long', timeStyle: 'short', hour12: timeFormat === '12h' })
: `${date.toLocaleString(i18n.language, { timeStyle: 'short', hour12: timeFormat === '12h' })}, ${date.toLocaleString(i18n.language, { weekday: 'long' })}`,
date: cell.label,
people: peopleHere,
})
}}
@ -135,24 +106,17 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
})}
</div>
</div> : <div className={styles.columnSpacer} />}
</Fragment>)}
</div>
), [
</Fragment>), [
availabilities,
dates,
isSpecificDates,
rows,
columns,
highlight,
max,
t,
timeFormat,
palette,
tempFocus,
focusCount,
filteredPeople,
i18n.language,
timezone,
])
return <>
@ -197,7 +161,19 @@ const AvailabilityViewer = ({ times, timezone, people }: AvailabilityViewerProps
<div className={styles.wrapper} ref={wrapperRef}>
<div>
<div className={styles.heatmap}>
{useMemo(() => <div className={styles.timeLabels}>
{rows.map((row, i) =>
<div className={styles.timeSpace} key={i}>
{row && <label className={styles.timeLabel}>
{row.label}
</label>}
</div>
)}
</div>, [rows])}
{heatmap}
</div>
{tooltip && <div
className={styles.tooltip}

View file

@ -1,20 +1,5 @@
import { useState, useEffect } from 'react'
import { loadGapiInsideDOM } from 'gapi-script'
import { useTranslation } from 'react-i18next'
import { Button, Center } from '/src/components'
import { Loader } from '../Loading/Loading.styles'
import {
CalendarList,
CheckboxInput,
CheckboxLabel,
CalendarLabel,
Info,
Options,
Title,
Icon,
LinkButton,
} from './GoogleCalendar.styles'
import { useEffect, useState } from 'react'
import googleLogo from '/src/res/google.svg'
@ -52,9 +37,6 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
const importAvailability = () => {
setFreeBusyLoading(true)
gtag('event', 'google_cal_sync', {
'event_category': 'event',
})
window.gapi.client.calendar.freebusy.query({
timeMin,
timeMax,

View file

@ -1,23 +1,23 @@
import { useState, useEffect } from 'react'
import { PublicClientApplication } from '@azure/msal-browser'
import { Client } from '@microsoft/microsoft-graph-client'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Center } from '/src/components'
import { Loader } from '../Loading/Loading.styles'
import outlookLogo from '/src/res/outlook.svg'
import {
CalendarLabel,
CalendarList,
CheckboxInput,
CheckboxLabel,
CalendarLabel,
Icon,
Info,
LinkButton,
Options,
Title,
Icon,
LinkButton,
} from '../GoogleCalendar/GoogleCalendar.styles'
import outlookLogo from '/src/res/outlook.svg'
import { Loader } from '../Loading/Loading.styles'
const scopes = ['Calendars.Read', 'Calendars.Read.Shared']

View file

@ -1,12 +1,10 @@
import { Temporal } from '@js-temporal/polyfill'
interface Person {
name: string
availability: Temporal.ZonedDateTime[]
availability: string[]
}
interface Availability {
date: Temporal.ZonedDateTime
date: string
/** Names of everyone who is available at this date */
people: string[]
}
@ -24,12 +22,12 @@ interface AvailabilityInfo {
* where each person has a name and availability array, and returns the
* group availability for each date passed in.
*/
export const calculateAvailability = (dates: Temporal.ZonedDateTime[], people: Person[]): AvailabilityInfo => {
export const calculateAvailability = (dates: string[], people: Person[]): AvailabilityInfo => {
let min = Infinity
let max = -Infinity
const availabilities: Availability[] = dates.map(date => {
const names = people.flatMap(p => p.availability.some(d => d.equals(date)) ? [p.name] : [])
const names = people.flatMap(p => p.availability.some(d => d === date) ? [p.name] : [])
if (names.length < min) {
min = names.length
}

View file

@ -0,0 +1,53 @@
import { calculateColumns } from '/src/utils/calculateColumns'
import { calculateRows } from '/src/utils/calculateRows'
import { convertTimesToDates } from '/src/utils/convertTimesToDates'
import { serializeTime } from '/src/utils/serializeTime'
/**
* Take rows and columns and turn them into a data structure representing an availability table
*/
export const calculateTable = (
/** As `HHmm-DDMMYYYY` or `HHmm-d` strings */
times: string[],
locale: string,
timeFormat: '12h' | '24h',
timezone: string,
) => {
const dates = convertTimesToDates(times, timezone)
const rows = calculateRows(dates)
const columns = calculateColumns(dates)
// Is specific dates or just days of the week
const isSpecificDates = times[0].length === 13
return {
rows: rows.map(row => row && row.minute === 0 ? {
label: row.toLocaleString(locale, { hour: 'numeric', hour12: timeFormat === '12h' }),
string: row.toString(),
} : null),
columns: columns.map(column => column ? {
header: {
dateLabel: column.toLocaleString(locale, { month: 'short', day: 'numeric' }),
weekdayLabel: column.toLocaleString(locale, { weekday: 'short' }),
string: column.toString(),
},
cells: rows.map(row => {
if (!row) return null
const date = column.toZonedDateTime({ timeZone: timezone, plainTime: row })
const serialized = serializeTime(date, isSpecificDates)
// Cell not in dates
if (!times.includes(serialized)) return null
return {
serialized,
minute: date.minute,
label: isSpecificDates
? date.toLocaleString(locale, { dateStyle: 'long', timeStyle: 'short', hour12: timeFormat === '12h' })
: `${date.toLocaleString(locale, { timeStyle: 'short', hour12: timeFormat === '12h' })}, ${date.toLocaleString(locale, { weekday: 'long' })}`,
}
})
} : null)
}
}

View file

@ -5,6 +5,7 @@ export * from './convertTimesToDates'
export * from './calculateAvailability'
export * from './calculateRows'
export * from './calculateColumns'
export * from './calculateTable'
export * from './getWeekdayNames'
export * from './relativeTimeFormat'
export * from './expandTimes'