diff --git a/dashboard/package.json b/dashboard/package.json index 0d2792bf6..8d991c03f 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -8,6 +8,7 @@ "@nivo/bar": "^0.80.0", "@nivo/core": "^0.80.0", "@nivo/pie": "^0.80.0", + "@nivo/stream": "^0.84.0", "@react-hookz/web": "^23.0.0", "@sentry/browser": "^6.17.5", "@sentry/react": "^7.69.0", diff --git a/dashboard/src/components/EvolutiveStatsSelector.js b/dashboard/src/components/EvolutiveStatsSelector.js deleted file mode 100644 index 480ab0eaf..000000000 --- a/dashboard/src/components/EvolutiveStatsSelector.js +++ /dev/null @@ -1,349 +0,0 @@ -import React from 'react'; -import SelectCustom from './SelectCustom'; -import { components } from 'react-select'; -import { dayjsInstance, isOnSameDay } from '../services/date'; -import DatePicker from './DatePicker'; - -const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInURLParams = false }) => { - selection = !!selection.length ? selection : [{ field: null, type: null, value: null }]; - const onAddFilter = () => onChange([...selection, {}], saveInURLParams); - const filterFields = base.filter((_filter) => _filter.field !== 'alertness').map((f) => ({ label: f.label, field: f.field, type: f.type })); - - function getFilterOptionsByField(field, base, index) { - if (!field) return []; - const current = base.find((filter) => filter.field === field); - if (!current) { - onChange( - selection.filter((_f, i) => i !== index), - saveInURLParams - ); - return []; - } - if (['yes-no'].includes(current.type)) return ['Oui', 'Non', 'Non renseigné']; - if (['boolean'].includes(current.type)) return ['Oui', 'Non']; - if (current?.field === 'outOfActiveList') return current.options; - if (current?.options?.length) return [...current?.options, 'Non renseigné']; - return ['Non renseigné']; - } - - function getFilterValue(filterValue) { - if (typeof filterValue === 'object') { - if (filterValue?.date != null) { - if (filterValue.comparator === 'unfilled') return 'Non renseigné'; - if (filterValue.comparator === 'before') return `Avant le ${dayjsInstance(filterValue.date).format('DD/MM/YYYY')}`; - if (filterValue.comparator === 'after') return `Après le ${dayjsInstance(filterValue.date).format('DD/MM/YYYY')}`; - if (filterValue.comparator === 'equals') return `Le ${dayjsInstance(filterValue.date).format('DD/MM/YYYY')}`; - return ''; - } - if (filterValue?.number != null) { - if (filterValue.comparator === 'unfilled') return 'Non renseigné'; - if (filterValue.comparator === 'between') return `Entre ${filterValue.number} et ${filterValue.number2}`; - if (filterValue.comparator === 'equals') return `Égal à ${filterValue.number}`; - if (filterValue.comparator === 'lower') return `Inférieur à ${filterValue.number}`; - if (filterValue.comparator === 'greater') return `Supérieur à ${filterValue.number}`; - } - return ''; - } - return filterValue; - } - - return ( - <> -
-

{title}

- -
-
-
-
-

{title}

-
-
-
- {selection.map((filter, index) => { - // filter: field, value, type - const filterValues = getFilterOptionsByField(filter.field, base, index); - const onChangeField = (newField) => { - onChange( - selection.map((_filter, i) => (i === index ? { field: newField?.field, value: null, type: newField?.type } : _filter)), - saveInURLParams - ); - }; - const onChangeValue = (newValue) => { - onChange( - selection.map((f, i) => (i === index ? { field: filter.field, value: newValue, type: filter.type } : f)), - saveInURLParams - ); - }; - const onRemoveFilter = () => { - onChange( - selection.filter((_f, i) => i !== index), - saveInURLParams - ); - }; - - return ( -
-
-

{index === 0 ? 'Indicateur' : 'ET indicateur'}

-
-
- filterFields.find((_filter) => _filter.field === _option.field)?.label} - getOptionValue={(_option) => _option.field} - isClearable={true} - isMulti={false} - /> -
-
-

de

-
-
- -
-
-

à

-
-
- -
-
- {!!selection.filter((_filter) => Boolean(_filter.field)).length && ( - - )} -
-
- ); - })} -
-
-
- -
-
- - ); -}; - -const dateOptions = [ - { - label: 'Avant', - value: 'before', - }, - { - label: 'Après', - value: 'after', - }, - { - label: 'Date exacte', - value: 'equals', - }, - { - label: 'Non renseigné', - value: 'unfilled', - }, -]; - -const numberOptions = [ - { - label: 'Inférieur à', - value: 'lower', - }, - { - label: 'Supérieur à', - value: 'greater', - }, - { - label: 'Égal à', - value: 'equals', - }, - { - label: 'Entre', - value: 'between', - }, - { - label: 'Non renseigné', - value: 'unfilled', - }, -]; - -const ValueSelector = ({ field, filterValues, value, onChangeValue, base }) => { - const [comparator, setComparator] = React.useState(null); - if (!field) return <>; - const current = base.find((filter) => filter.field === field); - if (!current) return <>; - const { type, field: name } = current; - - if (['text', 'textarea'].includes(type)) { - return ( - { - e.preventDefault(); - onChangeValue(e.target.value); - }} - /> - ); - } - - if (['date-with-time', 'date'].includes(type)) { - return ( -
-
- opt.value === value?.comparator)} - isClearable={!value} - onChange={(e) => { - if (!e) return setComparator(null); - setComparator(e.value); - onChangeValue({ date: value?.date, comparator: e.value }); - }} - /> -
- {value?.comparator !== 'unfilled' && ( -
- onChangeValue({ date: date.target.value, comparator })} - /> -
- )} -
- ); - } - - if (['number'].includes(type)) { - return ( -
-
- opt.value === value?.comparator)} - isClearable={!value} - onChange={(e) => { - if (!e) return setComparator(null); - setComparator(e.value); - onChangeValue({ number: value?.number, comparator: e.value }); - }} - /> -
- {value?.comparator !== 'unfilled' && ( -
- { - onChangeValue({ number: e.target.value, number2: value?.number2, comparator }); - }} - /> -
- )} - {value?.comparator === 'between' && ( - <> -
et
-
- { - onChangeValue({ number2: e.target.value, number: value?.number, comparator }); - }} - /> -
- - )} -
- ); - } - - if (['enum', 'multi-choice'].includes(type) && name !== 'outOfActiveList') { - try { - return ( - ({ label: _value, value: _value }))} - value={value?.map((_value) => ({ label: _value, value: _value })) || []} - getOptionLabel={(f) => f.label} - getOptionValue={(f) => f.value} - onChange={(newValue) => onChangeValue(newValue?.map((option) => option.value))} - isClearable={!value?.length} - isMulti - components={{ - MultiValueContainer: (props) => { - if (props.selectProps?.values?.length <= 1) { - return ; - } - const lastValue = props.selectProps?.value?.[props.selectProps?.value?.length - 1]?.value; - const isLastValue = props?.data?.value === lastValue; - return ( - <> - - {!isLastValue && OU} - - ); - }, - }} - /> - ); - } catch (e) { - console.log(e); - } - return null; - } - - return ( - ({ label: _value, value: _value }))} - value={value ? { label: value, value } : null} - getOptionLabel={(f) => f.label} - getOptionValue={(f) => f.value} - onChange={(f) => onChangeValue(f.value)} - isClearable={!value} - /> - ); -}; - -export default EvolutiveStatsSelector; diff --git a/dashboard/src/components/EvolutiveStatsSelector.tsx b/dashboard/src/components/EvolutiveStatsSelector.tsx index bb5eaad66..20976db6e 100644 --- a/dashboard/src/components/EvolutiveStatsSelector.tsx +++ b/dashboard/src/components/EvolutiveStatsSelector.tsx @@ -4,37 +4,28 @@ import { components } from 'react-select'; import { dayjsInstance } from '../services/date'; import DatePicker from './DatePicker'; import type { FilterField } from '../types/field'; - -type IndicatorValue = any; - -type Indicator = { - field: string | null; - fromValue: IndicatorValue; - toValue: IndicatorValue; - type: string | null; -}; - -type Selection = Array; - -type IndicatorsBase = Array; +import type { IndicatorValue, IndicatorsSelection, IndicatorsBase } from '../types/evolutivesStats'; +import { useRecoilValue } from 'recoil'; +import { evolutiveStatsIndicatorsBaseSelector } from '../recoil/evolutiveStats'; interface EvolutiveStatsSelectorProps { - onChange: (selection: Selection, saveInURLParams: boolean) => void; - base: IndicatorsBase; - selection: Selection; + onChange: (selection: IndicatorsSelection, saveInURLParams: boolean) => void; + selection: IndicatorsSelection; title?: string; saveInURLParams?: boolean; } -const emptySelection = { field: null, type: null, fromValue: null, toValue: null }; -const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInURLParams = false }: EvolutiveStatsSelectorProps) => { +const emptySelection = { fieldName: null, type: null, fromValue: null, toValue: null }; +const EvolutiveStatsSelector = ({ onChange, selection, title = '', saveInURLParams = false }: EvolutiveStatsSelectorProps) => { + const indicatorsBase = useRecoilValue(evolutiveStatsIndicatorsBaseSelector); + selection = !!selection.length ? selection : [emptySelection]; const onAddIndicator = () => onChange([...selection, emptySelection], saveInURLParams); - const selectCustomOptions = base.map((_indicator) => ({ label: _indicator.label, value: _indicator.field })) || []; + const selectCustomOptions = indicatorsBase.map((_indicator) => ({ label: _indicator.label, value: _indicator.name })) || []; - function getFilterOptionsByField(field: FilterField['field'] | null, base: IndicatorsBase, index: number) { - if (!field) return []; - let current = base.find((indicator) => indicator.field === field); + function getFilterOptionsByField(fieldName: FilterField['name'] | null, base: IndicatorsBase, index: number) { + if (!fieldName) return []; + let current = base.find((field) => field.name === fieldName); if (!current) { onChange( selection.filter((_f, i) => i !== index), @@ -44,8 +35,14 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU } if (['yes-no'].includes(current.type)) return ['Oui', 'Non', 'Non renseigné']; if (['boolean'].includes(current.type)) return ['Oui', 'Non']; - if (current?.field === 'outOfActiveList') return current.options; - if (current?.options?.length) return [...current?.options, 'Non renseigné']; + if (current?.name === 'outOfActiveList') return current.options || []; + if (current?.options?.length) { + return [...current?.options, 'Non renseigné'].filter((option) => { + if (!option) return false; + if (option.includes('Choisissez un genre')) return false; + return true; + }); + } return ['Non renseigné']; } @@ -79,8 +76,8 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU {title}
    {selection.map((indicator, index) => { - if (!indicator?.field) return null; - const current = base.find((_indicator) => _indicator.field === indicator.field); + if (!indicator?.fieldName) return null; + const current = indicatorsBase.find((field) => field.name === indicator.fieldName); if (!current) return null; const indicatorFromValue = getIndicatorValue(indicator.fromValue); if (!indicatorFromValue) return null; @@ -101,7 +98,7 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU
    {selection.map((indicator, index) => { // indicator: field, value, type - const indicatorValues = getFilterOptionsByField(indicator.field, base, index); + const indicatorValues = getFilterOptionsByField(indicator.fieldName, indicatorsBase, index); const onChangeFromValue = (newValue: any) => { onChange( selection.map((f, i) => (i === index ? { ...f, fromValue: newValue } : f)), @@ -121,10 +118,10 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU ); }; - const value = selectCustomOptions.find((opt) => opt.value === indicator.field); + const value = selectCustomOptions.find((opt) => opt.value === indicator.fieldName); return ( - +

    {index === 0 ? 'Indicateur' : 'ET'}

    @@ -133,11 +130,11 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU options={selectCustomOptions} value={value} onChange={(option) => { - const newField = base.find((_indicator) => _indicator.field === option?.value); + const newField = indicatorsBase.find((field) => field.name === option?.value); if (!newField) return; onChange( selection.map((_indicator, i) => - i === index ? { field: newField.field, fromValue: null, toValue: null, type: newField.type } : _indicator + i === index ? { fieldName: newField.name, fromValue: null, toValue: null, type: newField.type } : _indicator ), saveInURLParams ); @@ -149,10 +146,10 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU
    @@ -161,15 +158,15 @@ const EvolutiveStatsSelector = ({ onChange, base, selection, title = '', saveInU
- {!!selection.filter((_indicator) => Boolean(_indicator.field)).length && ( + {!!selection.filter((_indicator) => Boolean(_indicator.fieldName)).length && (
@@ -239,19 +236,19 @@ const numberOptions = [ ]; interface ValueSelectorProps { - field: string | null; + fieldName: string | null; indicatorValues: Array; value: any; onChangeValue: (newValue: any) => void; base: IndicatorsBase; } -const ValueSelector = ({ field, indicatorValues, value, onChangeValue, base }: ValueSelectorProps) => { +const ValueSelector = ({ fieldName, indicatorValues, value, onChangeValue, base }: ValueSelectorProps) => { const [comparator, setComparator] = useState(null); - if (!field) return <>; - const current = base.find((indicator) => indicator.field === field); + if (!fieldName) return <>; + const current = base.find((field) => field.name === fieldName); if (!current) return <>; - const { type, field: name } = current; + const { type, name } = current; if (['text', 'textarea'].includes(type)) { return ( @@ -357,27 +354,11 @@ const ValueSelector = ({ field, indicatorValues, value, onChangeValue, base }: V return ( ({ label: _value, value: _value }))} - value={value?.map((_value: any) => ({ label: _value, value: _value })) || []} + value={{ label: value, value }} getOptionLabel={(f) => f.label} getOptionValue={(f) => f.value} - onChange={(newValue) => onChangeValue(newValue?.map((option) => option.value))} + onChange={(newValue) => onChangeValue(newValue?.value)} isClearable={!value?.length} - isMulti - components={{ - MultiValueContainer: (props) => { - if (props.selectProps?.value?.length <= 1) { - return ; - } - const lastValue = props.selectProps?.value?.[props.selectProps?.value?.length - 1]?.value; - const isLastValue = props?.data?.value === lastValue; - return ( - <> - - {!isLastValue && OU} - - ); - }, - }} /> ); } catch (e) { diff --git a/dashboard/src/components/EvolutiveStatsViewer.tsx b/dashboard/src/components/EvolutiveStatsViewer.tsx index ff3c99a1a..eca5a86a2 100644 --- a/dashboard/src/components/EvolutiveStatsViewer.tsx +++ b/dashboard/src/components/EvolutiveStatsViewer.tsx @@ -1,11 +1,89 @@ -export default function EvolutiveStatsViewer() { +import { useRecoilValue } from 'recoil'; +import { evolutiveStatsIndicatorsBaseSelector, evolutiveStatsPersonSelector } from '../recoil/evolutiveStats'; +import type { PersonPopulated } from '../types/person'; +import type { IndicatorsSelection } from '../types/evolutivesStats'; +import { dayjsInstance } from '../services/date'; +import { ResponsiveStream } from '@nivo/stream'; +import { useMemo } from 'react'; + +interface EvolutiveStatsViewerProps { + evolutiveStatsIndicators: IndicatorsSelection; + period: { + startDate: string; + endDate: string; + }; + persons: Array; +} + +export default function EvolutiveStatsViewer({ evolutiveStatsIndicators, period, persons }: EvolutiveStatsViewerProps) { + const startDate = period.startDate; + const endDate = period.endDate; + const evolutiveStatsPerson = useRecoilValue( + evolutiveStatsPersonSelector({ + persons, + startDate: period.startDate ? dayjsInstance(period.startDate).format('YYYY-MM-DD') : null, + }) + ); + const indicatorsBase = useRecoilValue(evolutiveStatsIndicatorsBaseSelector); + console.log({ + evolutiveStatsPerson, + startDate, + endDate, + evolutiveStatsIndicators, + }); + + if (!evolutiveStatsIndicators.length) return null; + + const indicator = evolutiveStatsIndicators[0]; + + if (!indicator.fieldName) return null; + if (!startDate) return null; + if (!endDate) return null; + if (!endDate) return null; + + const startDateFormatted = dayjsInstance(startDate); + const endDateFormatted = dayjsInstance(endDate); + + if (startDateFormatted.isSame(endDateFormatted)) return null; + + const fieldStart = indicator.fromValue; + const fieldEnd = indicator.toValue; + + if (fieldStart == null || fieldEnd == null) { + return ( + <> +

+ Évolution du champ {indicatorsBase.find((field) => field.name === indicator.fieldName)?.label} entre le{' '} + {startDateFormatted.format('DD/MM/YYYY')} et le {endDateFormatted.format('DD/MM/YYYY')} +

+ + + + ); + } + + const valueStart = evolutiveStatsPerson[indicator.fieldName][fieldStart][startDateFormatted.format('YYYYMMDD')]; + const valueEnd = evolutiveStatsPerson[indicator.fieldName][fieldEnd][endDateFormatted.format('YYYYMMDD')]; + + console.log({ + fieldStart, + fieldEnd, + valueStart, + valueEnd, + }); + return (
-
Au 31/12/2023
+
Au {startDateFormatted.format('DD/MM/YYYY')}
-

45

-

des personnes SDF

+

{valueStart}

+

{fieldStart}

@@ -19,12 +97,112 @@ export default function EvolutiveStatsViewer() {
-
Au 31/12/2023
+
Au {endDateFormatted.format('DD/MM/YYYY')}
-

45

-

des personnes SDF

+

{valueEnd}

+

{fieldEnd}

); } + +function MyResponsiveStream({ indicator, evolutiveStatsPerson, startDateFormatted, endDateFormatted }: any) { + const chartData = useMemo(() => { + if (!indicator.fieldName) return { data: [], legend: [], keys: [] }; + const data = []; + const legend = []; + const fieldData = evolutiveStatsPerson[indicator.fieldName]; + const daysDiff = endDateFormatted.diff(startDateFormatted, 'days'); + const spacing = Math.round(Math.max(1, daysDiff / 12)); + for (let i = 0; i < daysDiff; i += spacing) { + const date = startDateFormatted.add(i, 'days'); + legend.push(date.format('DD/MM/YYYY')); + const dateValue: any = {}; + for (const option of Object.keys(fieldData)) { + const value = fieldData[option][date.format('YYYYMMDD')]; + dateValue[option] = value; + } + data.push(dateValue); + } + // end date + legend.push(endDateFormatted.format('DD/MM/YYYY')); + const lastDateValue: Record = {}; + for (const option of Object.keys(fieldData)) { + const value = fieldData[option][endDateFormatted.format('YYYYMMDD')]; + lastDateValue[option] = value; + } + data.push(lastDateValue); + const keys = Object.entries(lastDateValue) + .sort((a, b) => b[1] - a[1]) + .map((entry) => entry[0]); + return { data, legend, keys }; + }, [startDateFormatted, endDateFormatted, evolutiveStatsPerson, indicator.fieldName]); + + return ( +
+ chartData.legend[index], + }} + axisLeft={{ + // orient: 'left', + tickSize: 5, + tickPadding: 5, + tickRotation: 0, + legend: '', + legendOffset: -40, + }} + curve="basis" + enableGridX={true} + enableGridY={true} + offsetType="diverging" + colors={{ scheme: 'set2' }} + fillOpacity={0.85} + borderColor={{ theme: 'background' }} + dotSize={8} + dotColor={{ from: 'color' }} + dotBorderWidth={2} + dotBorderColor={{ + from: 'color', + modifiers: [['darker', 0.7]], + }} + legends={[ + { + anchor: 'bottom-right', + direction: 'column', + translateX: 100, + itemWidth: 80, + itemHeight: 20, + itemTextColor: '#999999', + symbolSize: 12, + symbolShape: 'circle', + effects: [ + { + on: 'hover', + style: { + itemTextColor: '#000000', + }, + }, + ], + }, + ]} + /> +
+ ); +} diff --git a/dashboard/src/recoil/evolutiveStats.ts b/dashboard/src/recoil/evolutiveStats.ts new file mode 100644 index 000000000..ce254a7d1 --- /dev/null +++ b/dashboard/src/recoil/evolutiveStats.ts @@ -0,0 +1,178 @@ +import { selector, selectorFamily } from 'recoil'; +import { capture } from '../services/sentry'; +import type { PersonInstance } from '../types/person'; +import type { CustomOrPredefinedField } from '../types/field'; +import type { EvolutiveStatsPersonFields, EvolutiveStatOption, EvolutiveStatDateYYYYMMDD } from '../types/evolutivesStats'; +import { dayjsInstance } from '../services/date'; +import { personFieldsIncludingCustomFieldsSelector, personsState } from './persons'; + +export const evolutiveStatsIndicatorsBaseSelector = selector({ + key: 'evolutiveStatsIndicatorsBaseSelector', + get: ({ get }) => { + const now = Date.now(); + const allFields = get(personFieldsIncludingCustomFieldsSelector); + const indicatorsBase = allFields.filter((f) => { + if (f.name === 'history') return false; + if (f.name === 'documents') return false; + switch (f.type) { + case 'text': + case 'textarea': + case 'number': + case 'date': + case 'date-with-time': + case 'multi-choice': + case 'boolean': + return false; + case 'yes-no': + case 'enum': + default: + return f.filterable; + } + }); + + return indicatorsBase; + }, +}); + +export const evolutiveStatsPersonSelector = selectorFamily({ + key: 'evolutiveStatsPersonSelector', + get: + ({ startDate, persons }: { startDate: string | null; persons: Array }) => + ({ get }) => { + const now = Date.now(); + const indicatorsBase = get(evolutiveStatsIndicatorsBaseSelector); + const fieldsMap: Record = indicatorsBase.reduce((acc, field) => { + acc[field.name] = field; + return acc; + }, {} as Record); + const personsFieldsInHistoryObject: EvolutiveStatsPersonFields = {}; + + function getValuesOptionsByField(field: CustomOrPredefinedField): Array { + if (!field) return []; + const current = fieldsMap[field.name]; + if (!current) return []; + if (['yes-no'].includes(current.type)) return ['Oui', 'Non', 'Non renseigné']; + if (['boolean'].includes(current.type)) return ['Oui', 'Non']; + if (current?.name === 'outOfActiveList') return current.options ?? ['Oui', 'Non']; + if (current?.options?.length) { + return [...current?.options, 'Non renseigné'].filter((option) => { + if (option.includes('Choisissez un genre')) return false; + return true; + }); + } + return ['Non renseigné']; + } + + function getValueByField(fieldName: CustomOrPredefinedField['name'], value: any): string { + if (!fieldName) return ''; + const current = fieldsMap[fieldName]; + if (!current) return ''; + if (['yes-no'].includes(current.type)) { + if (value === 'Oui') return 'Oui'; + return 'Non'; + } + if (['boolean'].includes(current.type)) { + if (value === true) return 'Oui'; + return 'Non'; + } + if (current?.name === 'outOfActiveList') { + if (value === true) return 'Oui'; + return 'Non'; + } + if (value == null || value === '') return 'Non renseigné'; // we cover the case of undefined, null, empty string + if (value.includes('Choisissez un genre')) return 'Non renseigné'; + return value; + } + + // we take the years since the history began, let's say early 2023 + const dates: Record = {}; + const minimumDateForEvolutiveStats = dayjsInstance(startDate ?? '2022-09-23').format('YYYYMMDD'); + let date = minimumDateForEvolutiveStats; + const today = dayjsInstance().format('YYYYMMDD'); + while (date !== today) { + dates[date] = 0; + date = dayjsInstance(date).add(1, 'day').format('YYYYMMDD'); + } + + for (const field of indicatorsBase) { + const options = getValuesOptionsByField(field); + personsFieldsInHistoryObject[field.name] = {}; + for (const option of options) { + personsFieldsInHistoryObject[field.name][option] = { + ...dates, + }; + } + } + + for (const person of persons) { + const followedSince = dayjsInstance(person.followedSince || person.createdAt).format('YYYYMMDD'); + const minimumDate = followedSince < minimumDateForEvolutiveStats ? minimumDateForEvolutiveStats : followedSince; + let currentDate = today; + let currentPerson = structuredClone(person); + for (const field of indicatorsBase) { + const value = getValueByField(field.name, currentPerson[field.name]); + if (value === '') continue; + try { + if (!personsFieldsInHistoryObject[field.name][value][currentDate]) { + personsFieldsInHistoryObject[field.name][value][currentDate] = 0; + } + personsFieldsInHistoryObject[field.name][value][currentDate]++; + } catch (error) { + capture(error, { extra: { person, field, value, currentDate } }); + return personsFieldsInHistoryObject; + } + } + const history = person.history; + if (!!history?.length) { + const reversedHistory = [...history].reverse(); + for (const historyItem of reversedHistory) { + let historyDate = dayjsInstance(historyItem.date).format('YYYYMMDD'); + while (currentDate > historyDate && currentDate > minimumDate) { + currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD'); + for (const field of indicatorsBase) { + const value = getValueByField(field.name, currentPerson[field.name]); + if (value === '') continue; + if (!personsFieldsInHistoryObject[field.name][value][currentDate]) { + personsFieldsInHistoryObject[field.name][value][currentDate] = 0; + } + personsFieldsInHistoryObject[field.name][value][currentDate]++; + } + } + for (const historyChangeField of Object.keys(historyItem.data)) { + const oldValue = getValueByField(historyChangeField, historyItem.data[historyChangeField].oldValue); + const historyNewValue = getValueByField(historyChangeField, historyItem.data[historyChangeField].newValue); + const currentPersonValue = getValueByField(historyChangeField, currentPerson[historyChangeField]); + if (historyNewValue !== currentPersonValue) { + capture(new Error('Incoherent history'), { + extra: { + person, + currentPerson, + historyItem, + historyChangeField, + oldValue, + historyNewValue, + currentPersonValue, + }, + }); + } + if (oldValue === '') continue; + currentPerson[historyChangeField] = oldValue; + } + } + } + while (currentDate >= minimumDate) { + currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD'); + for (const field of indicatorsBase) { + const value = getValueByField(field.name, currentPerson[field.name]); + if (value === '') continue; + if (!personsFieldsInHistoryObject[field.name][value][currentDate]) { + personsFieldsInHistoryObject[field.name][value][currentDate] = 0; + } + personsFieldsInHistoryObject[field.name][value][currentDate]++; + } + } + } + console.log('finito evolutiveStatsPersonSelector', Date.now() - now, 'ms'); + return personsFieldsInHistoryObject; + }, +}); diff --git a/dashboard/src/recoil/persons.ts b/dashboard/src/recoil/persons.ts index 9207b2ba9..5c256101c 100644 --- a/dashboard/src/recoil/persons.ts +++ b/dashboard/src/recoil/persons.ts @@ -5,8 +5,6 @@ import { toast } from 'react-toastify'; import { capture } from '../services/sentry'; import type { PersonInstance } from '../types/person'; import type { PredefinedField, CustomField, CustomOrPredefinedField } from '../types/field'; -import type { EvolutiveStatsPersonFields, EvolutiveStatOption, EvolutiveStatDateYYYYMMDD } from '../types/evolutivesStats'; -import { dayjsInstance } from '../services/date'; const collectionName = 'person'; export const personsState = atom({ @@ -101,107 +99,6 @@ export const allowedPersonFieldsInHistorySelector = selector({ }, }); -export const evolutiveStatsPersonSelector = selector({ - key: 'evolutiveStatsPersonSelector', - get: ({ get }) => { - const allFields = get(personFieldsIncludingCustomFieldsSelector); - const fields = allFields.filter((f) => { - if (f.name === 'history') return false; - if (['text', 'textarea', 'number', 'date', 'date-with-time'].includes(f.type)) return false; - // remains 'yes-no' | 'enum' | 'multi-choice' | 'boolean' - return f.filterable; - }); - const personsFieldsInHistoryObject: EvolutiveStatsPersonFields = {}; - const persons = get(personsState); - - function getValuesOptionsByField(field: CustomOrPredefinedField): Array { - if (!field) return []; - const current = fields.find((_field) => _field.name === field.name); - if (!current) return []; - if (['yes-no'].includes(current.type)) return ['Oui', 'Non', 'Non renseigné']; - if (['boolean'].includes(current.type)) return ['Oui', 'Non']; - if (current?.name === 'outOfActiveList') return current.options ?? ['Oui', 'Non']; - if (current?.options?.length) return [...current?.options, 'Non renseigné']; - return ['Non renseigné']; - } - - // we take the years since the history began, let's say early 2023 - const dates: Record = {}; - let date = dayjsInstance('2023-01-01').format('YYYYMMDD'); - const today = dayjsInstance().format('YYYYMMDD'); - while (date !== today) { - dates[date] = 0; - date = dayjsInstance(date).add(1, 'day').format('YYYYMMDD'); - } - - for (const field of fields) { - const options = getValuesOptionsByField(field); - personsFieldsInHistoryObject[field.name] = {}; - for (const option of options) { - personsFieldsInHistoryObject[field.name][option] = { - ...dates, - }; - } - } - - for (const person of persons) { - const minimumDate = dayjsInstance(person.followedSince || person.createdAt).format('YYYYMMDD'); - let currentDate = today; - let currentPerson = structuredClone(person); - for (const field of fields) { - const value = currentPerson[field.name]; - if (value == null || value === '') { - // we cover the case of undefined, null, empty string - continue; - } - personsFieldsInHistoryObject[field.name][value][currentDate]++; - } - const history = person.history; - if (!!history?.length) { - const reversedHistory = [...history].reverse(); - for (const historyItem of reversedHistory) { - let historyDate = dayjsInstance(historyItem.date).format('YYYYMMDD'); - while (currentDate !== historyDate) { - currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD'); - for (const field of fields) { - const value = currentPerson[field.name]; - if (value == null || value === '') { - // we cover the case of undefined, null, empty string - continue; - } - personsFieldsInHistoryObject[field.name][value][currentDate]++; - } - } - for (const historyChangeField of Object.keys(historyItem.data)) { - const oldValue = historyItem.data[historyChangeField].oldValue; - if (historyItem.data[historyChangeField].newValue !== currentPerson[historyChangeField]) { - capture(new Error('Incoherent history'), { - extra: { - person, - historyItem, - historyChangeField, - }, - }); - } - currentPerson[historyChangeField] = oldValue; - } - } - } - while (currentDate !== minimumDate) { - currentDate = dayjsInstance(currentDate).subtract(1, 'day').format('YYYYMMDD'); - for (const field of fields) { - const value = currentPerson[field.name]; - if (value == null || value === '') { - // we cover the case of undefined, null, empty string - continue; - } - personsFieldsInHistoryObject[field.name][value][currentDate]++; - } - } - } - }, -}); - export const filterPersonsBaseSelector = selector({ key: 'filterPersonsBaseSelector', get: ({ get }) => { diff --git a/dashboard/src/scenes/stats/PersonsStats.js b/dashboard/src/scenes/stats/PersonsStats.js index adef32fa1..eb148d251 100644 --- a/dashboard/src/scenes/stats/PersonsStats.js +++ b/dashboard/src/scenes/stats/PersonsStats.js @@ -27,11 +27,12 @@ export default function PersonStats({ filterBase, filterPersons, setFilterPersons, - evolutiveStatsIndicators, - setEvolutiveStatsIndicators, personsForStats, personFields, + period, evolutivesStatsActivated, + evolutiveStatsIndicators, + setEvolutiveStatsIndicators, }) { const allGroups = useRecoilValue(groupsState); const customFieldsPersons = useRecoilValue(customFieldsPersonsSelector); @@ -83,7 +84,7 @@ export default function PersonStats({ selection={evolutiveStatsIndicators} onChange={setEvolutiveStatsIndicators} /> - + ) : ( <> @@ -183,6 +184,7 @@ export default function PersonStats({ {customFieldsPersons.map((section) => { return (
{ const itemsForStatsSelector = selectorFamily({ key: 'itemsForStatsSelector', get: - ({ period, filterPersons, selectedTeamsObjectWithOwnPeriod, viewAllOrganisationData, evolutivesStatsActivated }) => + ({ period, filterPersons, selectedTeamsObjectWithOwnPeriod, viewAllOrganisationData }) => ({ get }) => { const activeFilters = filterPersons.filter((f) => f.value); const filterItemByTeam = (item, key) => { @@ -372,7 +372,6 @@ const Stats = () => { filterPersons, selectedTeamsObjectWithOwnPeriod, viewAllOrganisationData, - evolutivesStatsActivated, }) ); @@ -641,6 +640,7 @@ const Stats = () => { personFields={personFields} flattenedCustomFieldsPersons={flattenedCustomFieldsPersons} evolutivesStatsActivated={evolutivesStatsActivated} + period={period} evolutiveStatsIndicators={evolutiveStatsIndicators} setEvolutiveStatsIndicators={setEvolutiveStatsIndicators} /> @@ -657,6 +657,7 @@ const Stats = () => { filterPersons={filterPersons} setFilterPersons={setFilterPersons} evolutivesStatsActivated={evolutivesStatsActivated} + period={period} evolutiveStatsIndicators={evolutiveStatsIndicators} setEvolutiveStatsIndicators={setEvolutiveStatsIndicators} /> diff --git a/dashboard/src/types/evolutivesStats.ts b/dashboard/src/types/evolutivesStats.ts index 9280ca8cd..627dd11f7 100644 --- a/dashboard/src/types/evolutivesStats.ts +++ b/dashboard/src/types/evolutivesStats.ts @@ -6,3 +6,13 @@ export type EvolutiveStatsPersonFields = Record< CustomOrPredefinedField['name'], Record> >; + +export type IndicatorValue = any; +export type Indicator = { + fieldName: string | null; + fromValue: IndicatorValue; + toValue: IndicatorValue; + type: string | null; +}; +export type IndicatorsSelection = Array; +export type IndicatorsBase = Array; diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index c051097f7..c121d2ce3 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -2367,6 +2367,25 @@ __metadata: languageName: node linkType: hard +"@nivo/axes@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/axes@npm:0.84.0" + dependencies: + "@nivo/core": "npm:0.84.0" + "@nivo/scales": "npm:0.84.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + "@types/d3-format": "npm:^1.4.1" + "@types/d3-time-format": "npm:^2.3.1" + "@types/prop-types": "npm:^15.7.2" + d3-format: "npm:^1.4.4" + d3-time-format: "npm:^3.0.0" + prop-types: "npm:^15.7.2" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 59e1db46674a409d8b510d61d752aa2446c21d54de5c300b359d55a587b82c5284119a1876e86f7ce260811a54fec1af8eddcd146a57fe333669dea1828b58d6 + languageName: node + linkType: hard + "@nivo/bar@npm:^0.80.0": version: 0.80.0 resolution: "@nivo/bar@npm:0.80.0" @@ -2404,6 +2423,49 @@ __metadata: languageName: node linkType: hard +"@nivo/colors@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/colors@npm:0.84.0" + dependencies: + "@nivo/core": "npm:0.84.0" + "@types/d3-color": "npm:^2.0.0" + "@types/d3-scale": "npm:^3.2.3" + "@types/d3-scale-chromatic": "npm:^2.0.0" + "@types/prop-types": "npm:^15.7.2" + d3-color: "npm:^3.1.0" + d3-scale: "npm:^3.2.3" + d3-scale-chromatic: "npm:^2.0.0" + lodash: "npm:^4.17.21" + prop-types: "npm:^15.7.2" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 1b1aa3d8fc4573446a03d19b423ac8529c66f4eacbb3fe9d371fe019674c40953e65e5db5f8e8fe188c99717109bd40911e0cb1623e2c53b1f22f84990da0d9c + languageName: node + linkType: hard + +"@nivo/core@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/core@npm:0.84.0" + dependencies: + "@nivo/recompose": "npm:0.84.0" + "@nivo/tooltip": "npm:0.84.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + "@types/d3-shape": "npm:^2.0.0" + d3-color: "npm:^3.1.0" + d3-format: "npm:^1.4.4" + d3-interpolate: "npm:^3.0.1" + d3-scale: "npm:^3.2.3" + d3-scale-chromatic: "npm:^3.0.0" + d3-shape: "npm:^1.3.5" + d3-time-format: "npm:^3.0.0" + lodash: "npm:^4.17.21" + peerDependencies: + prop-types: ">= 15.5.10 < 16.0.0" + react: ">= 16.14.0 < 19.0.0" + checksum: 2367b6fab4f091818fa5b2743a98002f6b13acb2b7388ad5a7f047683009a229fa7252669a14055625542105ab4c8553d96a5acea4236d919bdeb3d8f64d7e12 + languageName: node + linkType: hard + "@nivo/core@npm:^0.80.0": version: 0.80.0 resolution: "@nivo/core@npm:0.80.0" @@ -2437,6 +2499,22 @@ __metadata: languageName: node linkType: hard +"@nivo/legends@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/legends@npm:0.84.0" + dependencies: + "@nivo/colors": "npm:0.84.0" + "@nivo/core": "npm:0.84.0" + "@types/d3-scale": "npm:^3.2.3" + "@types/prop-types": "npm:^15.7.2" + d3-scale: "npm:^3.2.3" + prop-types: "npm:^15.7.2" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 99968f773ddda72a2792c3b2a576f54af08050a8533907f1ab61d32865bd7dfa2680639965b5102392ac6223f6e272c117e517c381f0f2f008562e1cc267d15b + languageName: node + linkType: hard + "@nivo/pie@npm:^0.80.0": version: 0.80.0 resolution: "@nivo/pie@npm:0.80.0" @@ -2464,6 +2542,20 @@ __metadata: languageName: node linkType: hard +"@nivo/recompose@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/recompose@npm:0.84.0" + dependencies: + "@types/prop-types": "npm:^15.7.2" + "@types/react-lifecycles-compat": "npm:^3.0.1" + prop-types: "npm:^15.7.2" + react-lifecycles-compat: "npm:^3.0.4" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: d3543d2c6049678c5d9cd77557f7fe6b77ac52f4220879f6b93777d4eb221e164326cd688b2a499204e287dbfcd888319fa867abc0f11c4bcc802ea4b5d33d4b + languageName: node + linkType: hard + "@nivo/scales@npm:0.80.0": version: 0.80.0 resolution: "@nivo/scales@npm:0.80.0" @@ -2476,6 +2568,40 @@ __metadata: languageName: node linkType: hard +"@nivo/scales@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/scales@npm:0.84.0" + dependencies: + "@types/d3-scale": "npm:^3.2.3" + "@types/d3-time": "npm:^1.1.1" + "@types/d3-time-format": "npm:^3.0.0" + d3-scale: "npm:^3.2.3" + d3-time: "npm:^1.0.11" + d3-time-format: "npm:^3.0.0" + lodash: "npm:^4.17.21" + checksum: 56174000fe17fdaba54136a249e0bb99e1f50e92d199ae22f1aec1b1bcd05e92e13c45428f02fd08eac23b5d6ac09a674b58aa9b5b0b381c1cbbff55a157ca16 + languageName: node + linkType: hard + +"@nivo/stream@npm:^0.84.0": + version: 0.84.0 + resolution: "@nivo/stream@npm:0.84.0" + dependencies: + "@nivo/axes": "npm:0.84.0" + "@nivo/colors": "npm:0.84.0" + "@nivo/core": "npm:0.84.0" + "@nivo/legends": "npm:0.84.0" + "@nivo/scales": "npm:0.84.0" + "@nivo/tooltip": "npm:0.84.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + "@types/d3-shape": "npm:^2.0.0" + d3-shape: "npm:^1.3.5" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: d058b10cc6fb4f8808691792ff2a42636dbcfd40f75ab15e5f648e60e96d64622076af1dc4a3ff84c800b40cc4efac3557837f881648d3bb41b6d60e9f2c24f6 + languageName: node + linkType: hard + "@nivo/tooltip@npm:0.80.0": version: 0.80.0 resolution: "@nivo/tooltip@npm:0.80.0" @@ -2487,6 +2613,16 @@ __metadata: languageName: node linkType: hard +"@nivo/tooltip@npm:0.84.0": + version: 0.84.0 + resolution: "@nivo/tooltip@npm:0.84.0" + dependencies: + "@nivo/core": "npm:0.84.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + checksum: 91fac6167cf0c2252fe478c6ce193ee4d20dac5704051211a404721e104576fb4b9acee4f26b9ef7187ba2712c3c60778cb12685fb41951de8fb319512fffdd4 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -2638,6 +2774,18 @@ __metadata: languageName: node linkType: hard +"@react-spring/animated@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/animated@npm:9.7.3" + dependencies: + "@react-spring/shared": "npm:~9.7.3" + "@react-spring/types": "npm:~9.7.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 75c427e810b05ef508ac81695e3410619bcc8b8b87e232eb6fa05a91155bb6a558b324937fcaacb9b2002fdffb557de97ee5f6f6b226c53f5f356f62559f89a1 + languageName: node + linkType: hard + "@react-spring/core@npm:~9.4.5": version: 9.4.5 resolution: "@react-spring/core@npm:9.4.5" @@ -2652,6 +2800,19 @@ __metadata: languageName: node linkType: hard +"@react-spring/core@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/core@npm:9.7.3" + dependencies: + "@react-spring/animated": "npm:~9.7.3" + "@react-spring/shared": "npm:~9.7.3" + "@react-spring/types": "npm:~9.7.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 91102271531eae8fc146b8847ae6dbc03ebfbab5816529b9bfdd71e6d922ea07361fcbc57b404de57dac2f719246876f94539c04e2f314b3c767ad33d8d4f984 + languageName: node + linkType: hard + "@react-spring/rafz@npm:~9.4.5": version: 9.4.5 resolution: "@react-spring/rafz@npm:9.4.5" @@ -2671,6 +2832,17 @@ __metadata: languageName: node linkType: hard +"@react-spring/shared@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/shared@npm:9.7.3" + dependencies: + "@react-spring/types": "npm:~9.7.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 76e44fe8ad63c83861a8453e26d085c69a40f0e5865ca2af7d2fecacb030e59ebe6db5f8e7ef8b1a6b6e193cc3c1c6fd3d5172b10bf216b205844e6d3e90e860 + languageName: node + linkType: hard + "@react-spring/types@npm:~9.4.5": version: 9.4.5 resolution: "@react-spring/types@npm:9.4.5" @@ -2678,6 +2850,13 @@ __metadata: languageName: node linkType: hard +"@react-spring/types@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/types@npm:9.7.3" + checksum: fcaf5fe02ae3e56a07f340dd5a0a17e9c283ff7deab8b6549ff513ef2f5ad57e0218d448b5331e422cfa739b40f0de3511e7b3f3e73ae8690496cda5bb984854 + languageName: node + linkType: hard + "@react-spring/web@npm:9.4.5": version: 9.4.5 resolution: "@react-spring/web@npm:9.4.5" @@ -2693,6 +2872,21 @@ __metadata: languageName: node linkType: hard +"@react-spring/web@npm:9.4.5 || ^9.7.2": + version: 9.7.3 + resolution: "@react-spring/web@npm:9.7.3" + dependencies: + "@react-spring/animated": "npm:~9.7.3" + "@react-spring/core": "npm:~9.7.3" + "@react-spring/shared": "npm:~9.7.3" + "@react-spring/types": "npm:~9.7.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 65c71e28ef1197d2afdc053d776b6bd1db6b5558d50849d78c7fc665c3ed1bbd08850fabfceba2223f8660915301aaea18588ebee2429e7b6de99a2640335bbe + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.2.0": version: 5.3.1 resolution: "@rollup/plugin-babel@npm:5.3.1" @@ -3243,6 +3437,80 @@ __metadata: languageName: node linkType: hard +"@types/d3-color@npm:^2.0.0": + version: 2.0.6 + resolution: "@types/d3-color@npm:2.0.6" + checksum: dd4cdb61577a57f49b464a02e5d65c3f856d922f40092569b6752f624d94571d14489e9d32816e93d96fc71494f4b575b2afcf461d247ec348ce5cde64d62a1b + languageName: node + linkType: hard + +"@types/d3-format@npm:^1.4.1": + version: 1.4.5 + resolution: "@types/d3-format@npm:1.4.5" + checksum: 9368243a1d4a96846cd4997507f23621dfd4a6f8ddeccc733b321a10dd6d0a6b6cddaa88bb4ea55ada49a9f73c2851648218ccdab81c0c018e8ce11fb46f1ac1 + languageName: node + linkType: hard + +"@types/d3-path@npm:^2": + version: 2.0.4 + resolution: "@types/d3-path@npm:2.0.4" + checksum: aa5e429dfb69e1a051530b77cf6383d062645ed313d09848485c30c0518a464aef1db0a1ea5769d31e644a16fc521f4ee2a1ebdb0a445ae0856e3c05e2d8a9f1 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:^2.0.0": + version: 2.0.4 + resolution: "@types/d3-scale-chromatic@npm:2.0.4" + checksum: a038225314ba634f48d3534e7a6f33802a424753d1c1fd8d9531a925b8b26b049f1bca87884577daf5fb6fc05ccc18dc6848b7e47d4e01fe390997481814e560 + languageName: node + linkType: hard + +"@types/d3-scale@npm:^3.2.3": + version: 3.3.5 + resolution: "@types/d3-scale@npm:3.3.5" + dependencies: + "@types/d3-time": "npm:^2" + checksum: 5e0d95ca15297b05301a329ddf36a90ee6ea6187f4bd8db175647773514d7adedbe05b790ffdf0a88c4fbae2c817300610fe694ad554e8981f434596c123ac26 + languageName: node + linkType: hard + +"@types/d3-shape@npm:^2.0.0": + version: 2.1.7 + resolution: "@types/d3-shape@npm:2.1.7" + dependencies: + "@types/d3-path": "npm:^2" + checksum: 28847bb0ebbf044f4ca31bf2042f0bd4d60e2e0b6892ebd35110739816bc08640433cb8b6abbefcfb130d5cf789a0c151cd7375097b169850c603c1fffa44fe5 + languageName: node + linkType: hard + +"@types/d3-time-format@npm:^2.3.1": + version: 2.3.4 + resolution: "@types/d3-time-format@npm:2.3.4" + checksum: 96d806ce829b809258ef86a2a34dbc508129b3b690ccfa66f1eb5564116294fb5be48eb1894cc01a0b1457a195b24554e27cce6b9fff03c7cf04652c06f1a5c1 + languageName: node + linkType: hard + +"@types/d3-time-format@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/d3-time-format@npm:3.0.4" + checksum: c6d0e5c9222f877c5875427caf7fadfe5d5fd942caff7cff5b99fae4ad586278e0e0f749bf12cbf5ef66a5339004acbfd610110c30696d21aae9d53fc3e35b2e + languageName: node + linkType: hard + +"@types/d3-time@npm:^1.1.1": + version: 1.1.4 + resolution: "@types/d3-time@npm:1.1.4" + checksum: 0f393dec4f8c42e0932c5c84efc80c966afc5a9d3e0f5573b769c9fdada4a96c085318a5abf6dbddb7a43f4b531c9f899777a5c2a18123819c0a223363e9a339 + languageName: node + linkType: hard + +"@types/d3-time@npm:^2": + version: 2.1.4 + resolution: "@types/d3-time@npm:2.1.4" + checksum: a99d4b9cc23882a6197054932db1e26d93a9d4811fdd6a97339b7781c0afa2863db01a3631ce509077502d65cdc24c282a8d8649a2dc74ef2753faf5a7eeba92 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" @@ -3440,6 +3708,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15.7.2": + version: 15.7.11 + resolution: "@types/prop-types@npm:15.7.11" + checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 + languageName: node + linkType: hard + "@types/q@npm:^1.5.1": version: 1.5.5 resolution: "@types/q@npm:1.5.5" @@ -3479,6 +3754,15 @@ __metadata: languageName: node linkType: hard +"@types/react-lifecycles-compat@npm:^3.0.1": + version: 3.0.4 + resolution: "@types/react-lifecycles-compat@npm:3.0.4" + dependencies: + "@types/react": "npm:*" + checksum: 504665a1a83be43ab43cbd3d19fd94d0de6634543f06351cce80c844628048650f2cf063048e5dc39effdf0053565ae60a04427e79d094714d6aeb84d5c9643e + languageName: node + linkType: hard + "@types/react-router-dom@npm:^5.3.3": version: 5.3.3 resolution: "@types/react-router-dom@npm:5.3.3" @@ -5986,6 +6270,13 @@ __metadata: languageName: node linkType: hard +"d3-color@npm:1 - 3, d3-color@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 536ba05bfd9f4fcd6fa289b5974f5c846b21d186875684637e22bf6855e6aba93e24a2eb3712985c6af3f502fbbfa03708edb72f58142f626241a8a17258e545 + languageName: node + linkType: hard + "d3-format@npm:1 - 2": version: 2.0.0 resolution: "d3-format@npm:2.0.0" @@ -6009,6 +6300,15 @@ __metadata: languageName: node linkType: hard +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:^3.0.1": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 988d66497ef5c190cf64f8c80cd66e1e9a58c4d1f8932d776a8e3ae59330291795d5a342f5a97602782ccbef21a5df73bc7faf1f0dc46a5145ba6243a82a0f0e + languageName: node + linkType: hard + "d3-path@npm:1": version: 1.0.9 resolution: "d3-path@npm:1.0.9" @@ -6026,6 +6326,16 @@ __metadata: languageName: node linkType: hard +"d3-scale-chromatic@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-scale-chromatic@npm:3.0.0" + dependencies: + d3-color: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + checksum: e4d23a7d2ba48ad5de1d06dcc488f7278304def0ea28a268528923b1d74971260636b5c8fe0e27bc2c51b2a3f95542c248e35028bdb0b7c19ac804eee235d340 + languageName: node + linkType: hard + "d3-scale@npm:^3.2.3": version: 3.3.0 resolution: "d3-scale@npm:3.3.0" @@ -6089,6 +6399,7 @@ __metadata: "@nivo/bar": "npm:^0.80.0" "@nivo/core": "npm:^0.80.0" "@nivo/pie": "npm:^0.80.0" + "@nivo/stream": "npm:^0.84.0" "@react-hookz/web": "npm:^23.0.0" "@sentry/browser": "npm:^6.17.5" "@sentry/cli": "npm:^2.6.0"