Use hue-map package for custom colour palettes

This commit is contained in:
Ben Grant 2022-08-20 23:45:17 +10:00
parent 913f14d8b9
commit 6083fc556a
9 changed files with 61 additions and 12 deletions

View file

@ -14,6 +14,7 @@
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"gapi-script": "^1.2.0", "gapi-script": "^1.2.0",
"goober": "^2.1.10", "goober": "^2.1.10",
"hue-map": "^0.1.0",
"i18next": "^21.9.0", "i18next": "^21.9.0",
"i18next-browser-languagedetector": "^6.1.5", "i18next-browser-languagedetector": "^6.1.5",
"i18next-http-backend": "^1.4.1", "i18next-http-backend": "^1.4.1",

View file

@ -53,6 +53,9 @@
}, },
"language": { "language": {
"label": "Language" "label": "Language"
},
"colormap": {
"label": "Heatmap colors"
} }
}, },
"video": { "video": {

View file

@ -4,6 +4,7 @@ import dayjs from 'dayjs'
import localeData from 'dayjs/plugin/localeData' import localeData from 'dayjs/plugin/localeData'
import customParseFormat from 'dayjs/plugin/customParseFormat' import customParseFormat from 'dayjs/plugin/customParseFormat'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import createPalette from 'hue-map'
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores' import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
@ -50,6 +51,7 @@ const AvailabilityViewer = ({
const [tooltip, setTooltip] = useState(null) const [tooltip, setTooltip] = useState(null)
const timeFormat = useSettingsStore(state => state.timeFormat) const timeFormat = useSettingsStore(state => state.timeFormat)
const highlight = useSettingsStore(state => state.highlight) const highlight = useSettingsStore(state => state.highlight)
const colormap = useSettingsStore(state => state.colormap)
const [filteredPeople, setFilteredPeople] = useState([]) const [filteredPeople, setFilteredPeople] = useState([])
const [touched, setTouched] = useState(false) const [touched, setTouched] = useState(false)
const [tempFocus, setTempFocus] = useState(null) const [tempFocus, setTempFocus] = useState(null)
@ -65,6 +67,13 @@ const AvailabilityViewer = ({
setTouched(people.length <= 1) setTouched(people.length <= 1)
}, [people]) }, [people])
const [palette, setPalette] = useState([])
useEffect(() => setPalette(createPalette({
map: colormap === 'crabfit' ? [[0, [247,158,0,0]], [1, [247,158,0,255]]] : colormap,
steps: tempFocus !== null ? 2 : Math.min(max, filteredPeople.length)+1,
})), [tempFocus, filteredPeople, max, colormap])
const heatmap = useMemo(() => ( const heatmap = useMemo(() => (
<Container> <Container>
<TimeLabels> <TimeLabels>
@ -104,7 +113,8 @@ const AvailabilityViewer = ({
key={i} key={i}
$time={time} $time={time}
className="time" className="time"
$peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length} $peopleCount={focusCount !== null && focusCount !== peopleHere.length ? null : peopleHere.length}
$palette={palette}
aria-label={peopleHere.join(', ')} aria-label={peopleHere.join(', ')}
$maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)} $maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
$minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)} $minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
@ -129,9 +139,7 @@ const AvailabilityViewer = ({
})} })}
</Times> </Times>
</Date> </Date>
{last && dates.length !== i+1 && ( {last && dates.length !== i+1 && <Spacer />}
<Spacer />
)}
</Fragment> </Fragment>
) )
})} })}
@ -151,6 +159,7 @@ const AvailabilityViewer = ({
timeFormat, timeFormat,
timeLabels, timeLabels,
times, times,
palette,
]) ])
return ( return (

View file

@ -86,15 +86,15 @@ export const Time = styled('div')`
border-top: 2px dotted var(--text); border-top: 2px dotted var(--text);
`} `}
background-color: ${props => `#F79E00${Math.round((props.$peopleCount/props.$maxPeople)*255).toString(16)}`}; background-color: ${props => props.$palette[props.$peopleCount] ?? 'transparent'};
${props => props.$highlight && props.$peopleCount === props.$maxPeople && props.$peopleCount > 0 && ` ${props => props.$highlight && props.$peopleCount === props.$maxPeople && props.$peopleCount > 0 && `
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
transparent, transparent,
transparent 4.3px, transparent 4.3px,
var(--shadow) 4.3px, rgba(0,0,0,.5) 4.3px,
var(--shadow) 8.6px rgba(0,0,0,.5) 8.6px
); );
`} `}

View file

@ -1,4 +1,6 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import createPalette from 'hue-map'
import { useSettingsStore } from '/src/stores' import { useSettingsStore } from '/src/stores'
@ -17,8 +19,16 @@ const Legend = ({
}) => { }) => {
const { t } = useTranslation('event') const { t } = useTranslation('event')
const highlight = useSettingsStore(state => state.highlight) const highlight = useSettingsStore(state => state.highlight)
const colormap = useSettingsStore(state => state.colormap)
const setHighlight = useSettingsStore(state => state.setHighlight) const setHighlight = useSettingsStore(state => state.setHighlight)
const [palette, setPalette] = useState([])
useEffect(() => setPalette(createPalette({
map: colormap === 'crabfit' ? [[0, [247,158,0,0]], [1, [247,158,0,255]]] : colormap,
steps: max+1-min,
})), [min, max, colormap])
return ( return (
<Wrapper> <Wrapper>
<Label>{min}/{total} {t('event:available')}</Label> <Label>{min}/{total} {t('event:available')}</Label>
@ -31,7 +41,7 @@ const Legend = ({
{[...Array(max+1-min).keys()].map(i => i+min).map(i => {[...Array(max+1-min).keys()].map(i => i+min).map(i =>
<Grade <Grade
key={i} key={i}
$color={`#F79E00${Math.round((i/(max))*255).toString(16)}`} $color={palette[i]}
$highlight={highlight && i === max && max > 0} $highlight={highlight && i === max && max > 0}
onMouseOver={() => onSegmentFocus(i)} onMouseOver={() => onSegmentFocus(i)}
/> />

View file

@ -43,10 +43,10 @@ export const Grade = styled('div')`
${props => props.$highlight && ` ${props => props.$highlight && `
background-image: repeating-linear-gradient( background-image: repeating-linear-gradient(
45deg, 45deg,
var(--primary), transparent,
var(--primary) 4.5px, transparent 4.5px,
var(--shadow) 4.5px, rgba(0,0,0,.5) 4.5px,
var(--shadow) 9px rgba(0,0,0,.5) 9px
); );
`} `}
` `

View file

@ -3,6 +3,7 @@ import { useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Settings as SettingsIcon } from 'lucide-react' import { Settings as SettingsIcon } from 'lucide-react'
import { maps } from 'hue-map'
import { ToggleField, SelectField } from '/src/components' import { ToggleField, SelectField } from '/src/components'
@ -138,6 +139,24 @@ const Settings = () => {
onChange={value => store.setHighlight(value === 'On')} onChange={value => store.setHighlight(value === 'On')}
/> />
<SelectField
label={t('options.colormap.label')}
name="colormap"
id="colormap"
options={{
'crabfit': 'Crab Fit (classic)',
...Object.fromEntries(Object.keys(maps).map(palette => [
palette,
palette.split('-')
.map(w => w[0].toLocaleUpperCase() + w.substring(1).toLocaleLowerCase())
.join(' '),
])),
}}
small
value={store.colormap}
onChange={event => store.setColormap(event.target.value)}
/>
<SelectField <SelectField
label={t('options.language.label')} label={t('options.language.label')}
name="language" name="language"

View file

@ -7,11 +7,13 @@ const useSettingsStore = create(persist(
timeFormat: '12h', timeFormat: '12h',
theme: 'System', theme: 'System',
highlight: false, highlight: false,
colormap: 'crabfit',
setWeekStart: weekStart => set({ weekStart }), setWeekStart: weekStart => set({ weekStart }),
setTimeFormat: timeFormat => set({ timeFormat }), setTimeFormat: timeFormat => set({ timeFormat }),
setTheme: theme => set({ theme }), setTheme: theme => set({ theme }),
setHighlight: highlight => set({ highlight }), setHighlight: highlight => set({ highlight }),
setColormap: colormap => set({ colormap }),
}), }),
{ name: 'crabfit-settings' }, { name: 'crabfit-settings' },
)) ))

View file

@ -2185,6 +2185,11 @@ html-parse-stringify@^3.0.1:
dependencies: dependencies:
void-elements "3.1.0" void-elements "3.1.0"
hue-map@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/hue-map/-/hue-map-0.1.0.tgz#75aad75552d37cd748955cbafce7d83e3ff289ab"
integrity sha512-WC3Uzgymj+M55zdU6kOzbFEItp/3OGeVvzvc2O2DC8/zV8I51T+YVoWQEwfnPgR08GlZTO9fUEr90E5gdMtlgA==
i18next-browser-languagedetector@^6.1.5: i18next-browser-languagedetector@^6.1.5:
version "6.1.5" version "6.1.5"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.5.tgz#ed8c9319a8d246995d8ec8fccb5bf5f4248d0fb1" resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.5.tgz#ed8c9319a8d246995d8ec8fccb5bf5f4248d0fb1"