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}
-
- {selection.map((filter, index) => {
- if (!filter?.field) return null;
- const current = base.find((_filter) => _filter.field === filter.field);
- if (!current) return null;
- const filterValue = getFilterValue(filter.value);
- if (!filterValue) return null;
- return (
- -
- {current.label}: {filterValue}
-
- );
- })}
-
-
-
-
-
- {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}
- />
-
-
-
-
-
-
-
-
-
-
- {!!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"