Use hue-map package for custom colour palettes
This commit is contained in:
parent
913f14d8b9
commit
6083fc556a
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"label": "Language"
|
"label": "Language"
|
||||||
|
},
|
||||||
|
"colormap": {
|
||||||
|
"label": "Heatmap colors"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"video": {
|
"video": {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
`}
|
`}
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue