diff --git a/components/charts/axis-tick-by-date.js b/components/charts/axis-tick-by-date.js new file mode 100644 index 000000000..dc28c804d --- /dev/null +++ b/components/charts/axis-tick-by-date.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types' + +function AxisTickByDate({x, y, payload}) { + const [year, month, day] = payload.value.split('-') + + return ( + + + { + day ? + `${day}/${month}/${year}` : + (new Date(year, month - 1, day || 1)).toLocaleDateString('fr-FR', {year: 'numeric', month: 'short'}) + } + + + ) +} + +AxisTickByDate.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, +} + +export default AxisTickByDate diff --git a/components/charts/chart.js b/components/charts/chart.js new file mode 100644 index 000000000..9eb4e2339 --- /dev/null +++ b/components/charts/chart.js @@ -0,0 +1,140 @@ +import PropTypes from 'prop-types' + +import { + AreaChart, + Area, + BarChart, + Bar, + LineChart, + Line, + ScatterChart, + Scatter, + CartesianGrid, + XAxis, + YAxis, + LabelList, + ResponsiveContainer, + Tooltip, + Legend +} from 'recharts' + +import {defaultTheme} from './color-theme' +import AxisTickByDate from './axis-tick-by-date' + +const renderColorfulLegendText = (value, entry) => { + const {color} = entry + return {value} +} + +const yAxisTickFormatter = value => { + return Number.isNaN(value) ? + value : + `${value.toLocaleString( + undefined, {minimumFractionDigits: 0} + ).replace(/000$/g, 'K')}` +} + +const defaultArea = { + type: 'monotone', + dataKey: 'download BAL', + stackId: '1', + strokeWidth: 0.5, +} + +const typeComponents = { + area: { + chart: AreaChart, + axis: Area, + }, + bar: { + chart: BarChart, + axis: Bar, + }, + line: { + chart: LineChart, + axis: Line, + }, + scatter: { + chart: ScatterChart, + axis: Scatter, + }, +} + +function CartesianChart({type, data, axisDef, yAxisMaxKeyName, totalKeyName}) { + const dataList = Object.entries(axisDef).map(([dataKey, areaItem], index) => ({ + ...defaultArea, + dataKey, + stroke: defaultTheme[index]?.[0], + fill: defaultTheme[index]?.[1], + ...(areaItem || {}), + })) + + const {chart: Chart, axis: Axis} = typeComponents[type] + + return ( + + + + } + /> + + + + + <> + {dataList.map((areaItem, index, arr) => ( + + {totalKeyName && index === arr.length - 1 && } + + ))} + + + + + + + ) +} + +CartesianChart.propTypes = { + type: PropTypes.oneOf(['area', 'bar', 'line', 'scatter']), + data: PropTypes.array, + axisDef: PropTypes.object, + yAxisMaxKeyName: PropTypes.string, + totalKeyName: PropTypes.string, +} + +export default CartesianChart diff --git a/components/charts/color-theme.js b/components/charts/color-theme.js new file mode 100644 index 000000000..42384a011 --- /dev/null +++ b/components/charts/color-theme.js @@ -0,0 +1,51 @@ +export const defaultTheme = [ + ['#6C6C6C', '#b3b3b3'], + ['#5C5C5C', '#a6a6a6'], + ['#4F4F4F', '#999999'], + ['#444444', '#8c8c8c'], +] + +export const colorthemes = { + ecume: [ + ['#869ECE', '#ced6ea'], + ['#465F9D', '#8b9eda'], + ['#273962', '#5576c2'], + ['#222940', '#536190'], + ], + wisteria: [ + ['#A695E7', '#e2d8f5'], + ['#8C7CE5', '#c6bdf0'], + ['#6F5FD5', '#a99ee6'], + ['#4D3DBD', '#7d6fc6'], + ], + azure: [ + ['#3C91E6', '#cbe0f7'], + ['#1D7ECC', '#8dc0f3'], + ['#0E6AA2', '#6aa8e9'], + ['#0A4C6A', '#4e8ad5'], + ], + forest: [ + ['#6DBA8E', '#c8e5d6'], + ['#3FAE6B', '#9ed8b9'], + ['#2D8E53', '#6cc39b'], + ['#1E673C', '#4da77f'], + ], + emerald: [ + ['#4DBA87', '#c8e5d6'], + ['#2FAE64', '#9ed8b9'], + ['#1D8E4F', '#6cc39b'], + ['#0E673A', '#4da77f'], + ], + glicyne: [ + ['#F5C242', '#f9e7c7'], + ['#F3B824', '#f9d99f'], + ['#F1A806', '#f9cb77'], + ['#F09A00', '#f9c05e'], + ], + rubi: [ + ['#E65A5A', '#f7c6c6'], + ['#CC3F3F', '#f3a8a8'], + ['#A22D2D', '#e98b8b'], + ['#6A1E1E', '#d56f6f'], + ], +} diff --git a/components/charts/index.js b/components/charts/index.js new file mode 100644 index 000000000..153beab6e --- /dev/null +++ b/components/charts/index.js @@ -0,0 +1,2 @@ +export {default as CartesianChart} from './chart' +export {defaultTheme, colorthemes} from './color-theme' diff --git a/components/color-line.js b/components/color-line.js new file mode 100644 index 000000000..fcb8cabf0 --- /dev/null +++ b/components/color-line.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types' + +function ColorLine({color, className}) { + return ( + <> + + + + + + ) +} + +ColorLine.propTypes = { + color: PropTypes.string, + className: PropTypes.string, +} + +export default ColorLine diff --git a/components/key-numbers-block/index.js b/components/key-numbers-block/index.js new file mode 100644 index 000000000..6158e42f3 --- /dev/null +++ b/components/key-numbers-block/index.js @@ -0,0 +1,2 @@ +export {default} from './key-numbers-block' +export {default as KeyNumberItem} from './key-number-item' diff --git a/components/key-numbers-block/key-number-item.js b/components/key-numbers-block/key-number-item.js new file mode 100644 index 000000000..29af8bf52 --- /dev/null +++ b/components/key-numbers-block/key-number-item.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types' + +function KeyNumberItem({value = '', unit, label = '', description, className}) { + return ( + <> +
+ {value} + {label} + {description && {description}} +
+ + + + ) +} + +export const KeyNumberItemPropTypes = { + value: PropTypes.string.isRequired, + unit: PropTypes.string, + label: PropTypes.string.isRequired, + description: PropTypes.string, + className: PropTypes.string, +} + +KeyNumberItem.propTypes = KeyNumberItemPropTypes + +export default KeyNumberItem diff --git a/components/key-numbers-block/key-numbers-block.js b/components/key-numbers-block/key-numbers-block.js new file mode 100644 index 000000000..aa85c8856 --- /dev/null +++ b/components/key-numbers-block/key-numbers-block.js @@ -0,0 +1,80 @@ +import PropTypes from 'prop-types' + +import theme from '@/styles/theme' + +import KeyNumberItem, {KeyNumberItemPropTypes} from './key-number-item' + +function KeyNumbersBlock({data = [], className, hasSeparator}) { + return ( + <> +
+ {data.map( + ({large, hasSeparator, className = '', ...keyNumberProps}) => ( + + ))} +
+ + + + ) +} + +KeyNumbersBlock.propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({ + ...KeyNumberItemPropTypes, + large: PropTypes.bool, + hasSeparator: PropTypes.bool, + })), + className: PropTypes.string, + hasSeparator: PropTypes.bool, +} + +export default KeyNumbersBlock diff --git a/components/section.js b/components/section.js index e0fc57815..8cddc9c0b 100644 --- a/components/section.js +++ b/components/section.js @@ -2,9 +2,9 @@ import PropTypes from 'prop-types' import Container from './container' -function Section({title, subtitle, children, background, ...props}) { +function Section({title, subtitle, children, background, className, ...props}) { return ( -
+
{title &&

{title}

} {subtitle &&

{subtitle}

} @@ -18,6 +18,7 @@ Section.propTypes = { title: PropTypes.string, subtitle: PropTypes.string, children: PropTypes.node, + className: PropTypes.string, style: PropTypes.object, background: PropTypes.oneOf([ 'white', diff --git a/components/star-line.js b/components/star-line.js new file mode 100644 index 000000000..1a9156844 --- /dev/null +++ b/components/star-line.js @@ -0,0 +1,48 @@ +/* eslint-disable react/no-array-index-key */ +import PropTypes from 'prop-types' +import {fr} from '@codegouvfr/react-dsfr' + +function Star({isFill, isHalf}) { + const className = isFill ? 'ri-star-s-fill' : (isHalf ? 'ri-star-half-s-line' : 'ri-star-s-line') + return +} + +Star.propTypes = { + isFill: PropTypes.bool, + isHalf: PropTypes.bool, +} + +function StarLine({score = 0, color, className}) { + const fullScore = 5 + const minScore = Math.floor(score) + const halfScore = (score - minScore) >= 0.25 && (score - minScore) < 0.75 ? 1 : 0 + const intagerScore = (minScore + 0.75) < score ? minScore + 1 : minScore + const emptyScore = fullScore - intagerScore - halfScore + return ( + <> + + {(Array.from({length: intagerScore})).map((_, i) => )} + {(Array.from({length: halfScore})).map((_, i) => )} + {(Array.from({length: emptyScore})).map((_, i) => )} + + + + ) +} + +StarLine.propTypes = { + score: PropTypes.number, + color: PropTypes.string, + className: PropTypes.string, +} + +export default StarLine diff --git a/components/wip-section.js b/components/wip-section.js new file mode 100644 index 000000000..fb91173e7 --- /dev/null +++ b/components/wip-section.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types' +import {fr} from '@codegouvfr/react-dsfr' + +import Section from '@/components/section' + +function WipSection({children}) { + return ( + <> +
+ {' '} + {children} +
+ + + + + ) +} + +WipSection.propTypes = { + children: PropTypes.node.isRequired, +} + +export default WipSection diff --git a/package.json b/package.json index b19dac33f..9f16db041 100644 --- a/package.json +++ b/package.json @@ -54,9 +54,11 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-feather": "^2.0.10", + "recharts": "^2.8.0", "send": "^0.18.0", "sharp": "^0.29.3", "styled-components": "^6.0.7", + "swr": "^2.2.4", "underscore.string": "^3.3.6", "vt-pbf": "^3.1.3" }, diff --git a/pages/api/stats/[key-stat].js b/pages/api/stats/[key-stat].js new file mode 100644 index 000000000..f3ddbea8f --- /dev/null +++ b/pages/api/stats/[key-stat].js @@ -0,0 +1,170 @@ +import { + matomoToVisitData, + matomoDailyToData, + matomoMonthlyToData, + geocoderSearchToData, +} from '@/views/stats/helper' + +const { + NEXT_PUBLIC_MATOMO_URL: MATOMO_URL, + NEXT_PUBLIC_MATOMO_SITE_ID: MATOMO_ID, + MATOMO_TOKEN_AUTH, +} = process.env + +const fakeDataQualityScore = { + 0: 39, + 1: 9, + 2: 22, + 3: 19, + 4: 2, + 5: 9 +} + +const matomoToFakeGeocodeurData = (matomoData = {}) => Object.entries(matomoData) + .map(([dataDate, dataStat]) => { + const filtredDataStat = dataStat.filter((dataStatItem = {}) => /^download/i.test(dataStatItem.label)) + return [dataDate, filtredDataStat] + }) + .map(([dataDate, dataStat]) => { + const convertToFakeLabel = { + 'download BAL': ['nbSearch', 'nbReverseSearch'], + 'Download BAL': ['nbSearch', 'nbReverseSearch'], + 'download CSV historique adresses': ['nbSearchResult', 'nbReverseSearchResult'], + 'Download CSV historique adresses': ['nbSearchResult', 'nbReverseSearchResult'], + 'download CSV historique lieux-dits': ['nbSearchEmptyResult', 'nbReverseSearchEmptyResult'], + 'Download CSV historique lieux-dits': ['nbSearchEmptyResult', 'nbReverseSearchEmptyResult'], + } + const finalDataStat = dataStat + .map((dataStatItem = {}) => { + const {subtable} = dataStatItem + const otherFields = Object.fromEntries( + subtable.flatMap((subtableItem = {}) => { + const {label: subtableLabel, nb_events: subtableNbEvents} = subtableItem + return [ + [convertToFakeLabel[subtableLabel]?.[0] || subtableLabel, (subtableNbEvents * 0)], + [convertToFakeLabel[subtableLabel]?.[1] || subtableLabel, (subtableNbEvents * 0)] + ] + }) + ) + + return { + label: 'API Geocodeur Adresse', + period: dataDate, + ...otherFields, + } + }) + return finalDataStat[0] + }) + +const getQualityData = dataQuality => { + const StateBanDefs = [ + {color: '#e2d8f5', description: 'Adresses composés à partir de sources multiples (autre que BAL)'}, + {color: '#f9e7c7', description: 'Adresses issues des Bases Adresses Locales'}, + {color: '#f7c6c6', description: 'Adresses certifiées'}, + {color: '#cccccc', description: 'Total des adresses au sein de la Base Adresse National'}, + ] + const QualityDefs = [ + {color: '#869ECE', description: 'Adresses issues de l\'assemblage de sources non certifiable'}, + {color: '#A695E7', description: 'Adresses issues de l\'assemblage de sources certifiées'}, + {color: '#3C91E6', description: 'Adresses issues d\'une Base Adresse Locale'}, + {color: '#0E6AA2', description: 'Adresses certifiées ou referençants une parcelle cadastrale'}, + {color: '#F5C242', description: 'Adresses identifiables et suivies au sein d\'une Base Adresse Locale'}, + {color: '#E65A5A', description: 'Adresses certifiées et referençants une parcelle cadastrale'}, + ] + const DataQuality = [ + Object + .entries(dataQuality) + .reduce((acc, [key, value]) => { // eslint-disable-line unicorn/no-array-reduce + let name + switch (key) { + case '0': + case '1': + name = '0' + break + case '2': + case '3': + case '4': + name = '1' + break + case '5': + name = '2' + break + default: + name = 0 + } + + acc[name] = (acc[name] || 0) + value + console.log('acc', acc) + return acc + }, []) + .map((value, index) => [`${index}`, value]) + .map( + ([name, value]) => ({ + name, value, + color: StateBanDefs[Number(name)].color, + description: StateBanDefs[Number(name)].description + }) + ), + Object + .entries(fakeDataQualityScore) + .map( + ([name, value]) => ({ + name, value, + color: QualityDefs[Number(name)].color, + description: QualityDefs[Number(name)].description + }) + ), + ] + return DataQuality +} + +const getMonthlyUsageData = dataMonthlyUsage => ({ + period: dataMonthlyUsage.period, + value: dataMonthlyUsage ? [ + {value: `${dataMonthlyUsage?.value?.nb_events || ''}`, label: 'Téléchargements', description: 'des données BAN sur nos serveurs.*', large: true}, + {value: '??', label: 'Recherche', description: 'effectuées sur notre API Geocodage-BAN.**'}, + {value: '??', label: 'Exploitants', description: 'utilisant les données de la BAN sur leurs outils.***', hasSeparator: true}, + ] : [] +}) + +const URL_GET_STATS_DAILY_DOWNLOAD = `${MATOMO_URL}/index.php?idSite=${MATOMO_ID}&module=API&format=JSON&period=day&date=previous30&method=Events.getCategory&expanded=1&filter_limit=10&token_auth=${MATOMO_TOKEN_AUTH}` +const URL_GET_STATS_MONTHLY_DOWNLOAD = `${MATOMO_URL}/index.php?idSite=${MATOMO_ID}&module=API&format=JSON&period=month&date=previous30&method=Events.getCategory&label=download&format_metrics=1&expanded=1&token_auth=${MATOMO_TOKEN_AUTH}` +const URL_GET_STATS_GEOCODER = `${MATOMO_URL}/index.php?idSite=${MATOMO_ID}&module=API&format=JSON&period=day&date=previous30&method=Events.getCategory&expanded=1&filter_limit=10&token_auth=${MATOMO_TOKEN_AUTH}` +const URL_GET_STAT_VISIT = `${MATOMO_URL}/index.php?idSite=${MATOMO_ID}&module=API&format=JSON&period=month&date=previous12&method=API.get&filter_limit=100&format_metrics=1&expanded=1&token_auth=${MATOMO_TOKEN_AUTH}` + +const APIs = { + 'daily-download': {url: URL_GET_STATS_DAILY_DOWNLOAD, converter: matomoDailyToData}, + 'monthly-usage': {url: URL_GET_STATS_MONTHLY_DOWNLOAD, converter: data => getMonthlyUsageData(matomoMonthlyToData(data))}, + geocoder: {url: URL_GET_STATS_GEOCODER, converter: data => geocoderSearchToData(matomoToFakeGeocodeurData(data))}, + visit: {url: URL_GET_STAT_VISIT, converter: matomoToVisitData}, + quality: {data: fakeDataQualityScore, converter: getQualityData}, +} + +export default async function handler(req, res) { + const {'key-stat': keyStat} = req.query + + try { + const {url, data: dataRaw, converter = d => d} = APIs?.[keyStat] || {} + if (!url && !dataRaw) { + throw new Error('API not found', {status: 404, cause: {details: 'API not found', status: 404}}) + } + + if (dataRaw) { + const data = converter(dataRaw) + return res.status(200).json(data) + } + + const response = await fetch(url) + const {status} = response + if (status !== 200) { + throw new Error('API not found', {cause: {details: 'Error while fetching API', status}}) + } + + const data = converter(await response.json()) + res.status(status).json(data) + } catch (error) { + const {message, cause} = error + console.error('Error on Front-end Stat API :', message, cause, error) + res.status(cause?.status || 500).send(message || 'Internal server error') + } +} diff --git a/pages/budget.js b/pages/budget.js new file mode 100644 index 000000000..905b02292 --- /dev/null +++ b/pages/budget.js @@ -0,0 +1,255 @@ +import {Fragment} from 'react' +import Link from 'next/link' + +import Page from '@/layouts/main' +import Section from '@/components/section' +import WipSection from '@/components/wip-section' + +const budgetFormatter = value => { + return Number.isNaN(value) ? + value : + `${value.toLocaleString( + undefined, {minimumFractionDigits: 0} + ).replace(/000$/g, 'K')}` +} + +const keyLegend = { + BAN: 'Base Adresses Nationale (BAN)', + BAL: 'Base Adresses Locales (BAL)', +} + +const dataBudget = { + 2023: { + BAN: { + info: 'En 2023 le budget de l’équipe est évalué à 1 192 000 €. L’IGN et la DINUM partagent le financement de cette équipe dédiée à la construction de la Base Adresse Nationale.', + values: { + Développement: [0, 0], + Déploiement: [0, 0], + 'Logiciel hébergement': [0, 0], + 'Total HT': [0, 0], + 'Total TTC': [0, 0], + }, + }, + BAL: { + info: 'En 2023 le budget de l’équipe est évalué à XXX €. L’ANCT et la DINUM partagent le financement de cette équipe dédiée au projet Base Adresse Local.', + values: { + Développement: [0, 0], + Déploiement: [0, 0], + 'Logiciel hébergement': [0, 0], + 'Total HT': [0, 0], + 'Total TTC': [0, 0], + }, + } + } +} + +function StatsPage() { + return ( + + + La page que vous visitez est actuellement en cours de construction. + Les informations qui y sont présentées sont susceptibles d’évoluer, d’être incomplètes ou erronées. + Nous vous remercions de votre compréhension. + + +
+

Budget

+

+ La Base Adresse Nationale (BAN) est un service public numérique, c’est pourquoi nous sommes transparents + sur les ressources allouées et la manière dont elles sont employées. +

+ +

+ Ces données étant en cours de construction, elles peuvent, dans certains cas, être incomplètes.
+ Nous vous remercions de votre compréhension.
+

+
+ +
+

Principes

+

+ Nous suivons le manifeste beta.gouv dont + nous rappelons les principes ici : +

+ +
    +
  • Les besoins des utilisateurs sont prioritaires sur les besoins de l’administration
  • +
  • Le mode de gestion de l’équipe repose sur la confiance
  • +
  • L’équipe adopte une approche itérative et d’amélioration en continu
  • +
+ +
+ +
+

Budget consommé

+

● Année : {2023}

+ +
+ { + Object.entries(dataBudget['2023']).map(([projectName, projectData]) => ( + +

+ {projectData.info} +

+ + + + + + + + + + + + { + Object.entries(projectData.values) + .map(([label, values]) => { + return ( + + + + + + + ) + }) + } + +
{keyLegend?.[projectName] || projectName} + 1er semestre2ème semestretotal
{label} + {budgetFormatter(values[0])}{budgetFormatter(values[1])}{budgetFormatter(values[0] + values[1])}
+
+ ) + ) + } +
+
+ +
+

Description des catégories

+ +

Déploiement et développement

+

+ Les coûts de développement représentent la grande majorité de notre budget. Nous sommes une petite équipe + de développeurs composée d’agents publics et de freelances, pluridisciplinaires aussi bien sur les aspects techniques, + stratégiques et métiers. Les rémunérations des intervenants en freelance suivent + la grille + de beta.gouv. +

+ +

Logiciel et hébergement

+

+ Notre modèle open-source nous permet d’accéder gratuitement à la majorité des outils que nous utilisons + (hébergement de code, serveurs de tests, etc.). Le site est hébergé + chez OVH. +

+ +

Portage

+

+ La marge du porteur attributaire + du marché + public de la DINUM, + ainsi que les coûts liés à la société + spécialement créée pour effectuer le portage des indépendants qui travaillent pour le service (administration, + comptabilité, facturation, impôts, etc.). +

+ +
+
+
+

A propos de la TVA

+

+ Contrairement aux entreprises du secteur privé, les administrations ne peuvent pas récupérer la TVA supportée sur + leurs achats dans le cadre de leur activité.
+ Le montant TTC inclut la TVA au taux de 20%.
+ La TVA est collectée et reversée à l’État et diminue donc le montant du budget utilisable sur le projet. +

+ +
+
+
+
+ + + +
+ ) +} + +export default StatsPage diff --git a/pages/stats.js b/pages/stats.js new file mode 100644 index 000000000..2ac39bed4 --- /dev/null +++ b/pages/stats.js @@ -0,0 +1,251 @@ +import {useMemo, useEffect} from 'react' +import useSWR from 'swr' +import Link from 'next/link' +import {fr} from '@codegouvfr/react-dsfr' + +import Page from '@/layouts/main' +import Section from '@/components/section' +import WipSection from '@/components/wip-section' +import KeyNumbersBlock from '@/components/key-numbers-block' +import {CartesianChart as Chart} from '@/components/charts' +import QualityChartBlock from '@/views/stats/quality-chart-block' +import { + getDataDef, + getBanStatsData, + fetcher, +} from '@/views/stats/helper' +import { + defDatadailyDownload, + defDataGeocoder, + defDataBanVisit, +} from '@/views/stats/stats-config-data' + +const URL_GET_STATS_DOWNLOAD_DAY = './api/stats/daily-download' +const URL_GET_STATS_DOWNLOAD_MONTH = './api/stats/monthly-usage' +const URL_GET_STATS_GEOCODER = './api/stats/geocoder' +const URL_GET_STATS_VISIT = './api/stats/visit' +const URL_GET_STATS_QUALITY = './api/stats/quality' +const QUALITY_CHART_WRAPPER_SIZE = 260 + +function StatsPage() { + const {data: dataDailyDownload, error: errorDataDailyDownload} = useSWR(URL_GET_STATS_DOWNLOAD_DAY, fetcher) + const {data: dataMonthlyUsage, error: errorDataMonthlyUsage} = useSWR(URL_GET_STATS_DOWNLOAD_MONTH, fetcher) + const {data: dataGeocoder, error: errorDataGeocoder} = useSWR(URL_GET_STATS_GEOCODER, fetcher) + const {data: dataBanVisit, error: errorDataBanVisit} = useSWR(URL_GET_STATS_VISIT, fetcher) + const {data: dataBanQuality, error: errorDataBanQuality} = useSWR(URL_GET_STATS_QUALITY, fetcher) + const {data: dataStateBan, error: errorBanStats} = useSWR('BAN__GET_STATE__API', getBanStatsData) + + const axisDefDailyDownload = useMemo(() => getDataDef(defDatadailyDownload, 'glicyne'), []) + const axisDefDataGeocoder = useMemo(() => getDataDef(defDataGeocoder), []) + const axisDefDataBanVisit = useMemo(() => getDataDef(defDataBanVisit), []) + + useEffect(() => { + [ + errorDataDailyDownload, + errorDataMonthlyUsage, + errorDataGeocoder, + errorDataBanVisit, + errorDataBanQuality, + errorBanStats, + ].filter(Boolean).forEach(err => console.error(`API CALL ERROR: ${err}`)) + }, [ + errorDataDailyDownload, + errorDataMonthlyUsage, + errorDataGeocoder, + errorDataBanVisit, + errorDataBanQuality, + errorBanStats, + ]) + + return ( + + + La page que vous visitez est actuellement en cours de construction. + Les informations qui y sont présentées sont susceptibles d’évoluer, d’être incomplètes ou erronées. + Nous vous remercions de votre compréhension. + + +
+

Statistiques

+

+ Vous trouverez ici les statistiques d’utilisation de la Base Adresse Nationale (BAN). + Ces données ont pour objectif de vous permettre de suivre l’évolution de la BAN et + de vous aider à prendre des décisions sur son utilisation. + Elles nous permettent également de mieux comprendre vos besoins et + d’orienter nos actions pour améliorer la BAN. +

+ +

+ Il est à noter que ces statistiques ne sont pas exhaustives et qu’elles sont en constante évolution. + De plus, elles ne prennent pas en compte les utilisations de la BAN par les organismes partenaires. +

+ +

+ Ces données étant en cours de construction, elles peuvent, dans certains cas, être incomplètes.
+ Nous vous remercions de votre compréhension.
+

+
+ +
+

Les usages de la BAN en chiffre

+

{dataMonthlyUsage?.period && `● Periode : ${dataMonthlyUsage.period}`}

+

+ Les données affichées + ci-dessous sont incomplètes. +

+ + +
    +
  • + * Moyenne mensuelle sur la periode. +
  • +
  • + ** Anciennement “API Adresse”. Moyenne mensuelle sur la periode. +
  • +
  • + *** Total référencé sur la Base Adresses Nationale durant la periode. +
  • +
+
+ +
+

Exploitation directe de la BAN :
Nombre de recherches d’adresses

+

Consommation des données de la BAN depuis l’API Geocodage-BAN (anciennement “API Adresse”).

+

+ Ces données + ne sont pas encore disponibles. +

+ +
+ +
+ +
    +
  • + Recherches quotidiennes sur les 30 derniers jours. +
  • +
  • + * L’API Geocodage-BAN (anciennement “API Adresse”) met à disposition les adresses présentes dans la BAN. + Les adresses retournées par l’API ne sont actuellement disponibles qu’en français. + L’API ne fournit pas encore les lieux-dits. +
  • +
+
+ +
+

Exploitation indirecte de la BAN :
Nombre de téléchargements des données BAN

+

Téléchargement des données de la BAN depuis nos serveurs, pour une utilisation gérée par l’exploitant.

+

+ Les données affichées + ci-dessous sont incomplètes. +

+ +
+ +
+ +
    +
  • Téléchargements quotidiens sur les 30 derniers jours.
  • +
+
+ +
+

État de la Base Adresses Nationale (BAN)

+

+ La Base Adresse Nationale (BAN) est constituée de plusieurs sources de données, de natures différentes. + La récente loi dite 3DS impose aux communes de mettre en place une Base Adresse Locale (BAL), + seule source certifiable et officielle, qui à terme constituera l’intégralité de la BAN. +

+ + {dataStateBan && ( + + )} + +
+ Plus d'informations sur l'état du déploiement des Bases Adresses Locales +
+
+ + {dataBanQuality && ( +
+

Répartition qualitative des données de la BAN

+ +

+ Afin de déterminer la qualité des adresses de la Base Adresse Nationale (BAN), un système de notation a été mis en place.
+ Ce système est basé sur la qualité des sources qui ont permis de constituer la BAN. +

+ +

+ Chaque adresse de la BAN est notée entre 0 et 5 étoiles selon des critères cumulatifs prédéfinis.
+ Le graphique ci-dessous représente la répartition actuelle des différentes sources de la BAN, et + la moyenne des notes de chaque adresse qui la constitue au niveau national. +

+ + +
+ )} + +
+

Attrait du sujet “Adresse”

+

Quantité de visites sur le site web.

+ +
+ +
+ +
    +
  • Valeurs totales mensuelles des 12 derniers mois.
  • +
+
+ +
+

À propos des statistiques

+

+ Les données de statistiques sont issues de Matomo, un outil libre de mesure d’audience web. + Ces données sont anonymisées, hébergées en France par la société OVH, et conservées dans le but d’améliorer les services proposés par l’état. + En aucun cas, ces données ne sont partagées avec des tiers ou utilisées à d’autres fins que l’amélioration de la plateforme. +

+

+ Pour plus d'informations, vous pouvez consulter + nos + « Conditions Générales d’Utilisation » + . +

+
+ + + +
+ ) +} + +export default StatsPage diff --git a/views/stats/helper.js b/views/stats/helper.js new file mode 100644 index 000000000..f7ecb643c --- /dev/null +++ b/views/stats/helper.js @@ -0,0 +1,129 @@ +import { + colorthemes as customColor, + defaultTheme, +} from '@/components/charts' +import {getStats} from '@/lib/api-ban' + +const colorName = 'glicyne' + +const legendTranslate = { + interopKeyTotal: 'Clé d\'interoperabilité conservée', + interopKeyRemove: 'Clé d\'interoperabilité supprimmé', + interopKeyNew: 'Nouvelle clé d\'interoperabilité', + addrTotal: 'Total des adresses', + + 'download BAL': 'Fichiers BAL', + 'Download BAL': 'Fichiers BAL', + 'download CSV historique adresses': 'CSV historique adresses', + 'Download CSV historique adresses': 'CSV historique adresses', + 'download CSV historique lieux-dits': 'CSV historique lieux-dits', + 'Download CSV historique lieux-dits': 'CSV historique lieux-dits', + + nbSearch: 'Recherche', + nbSearchResult: 'Résultats de recherche', + nbSearchEmptyResult: 'Recherche sans résultat', + nbReverseSearch: 'Recherche inversée', + nbReverseSearchEmptyResult: 'Recherche inversée sans résultat', + nbReverseSearchResult: 'Résultats de recherche inversée', + + nbVisits: 'Visites', + nbUniqVisitors: 'Visiteurs uniques', +} + +export const matomoToVisitData = (matomoData = {}) => Object.entries(matomoData) + .map(([dataDate, {nb_visits: nbVisits, nb_uniq_visitors: nbUniqVisitors}]) => { + return [dataDate, {nbVisits, nbUniqVisitors}] + }) + .map(([dataDate, dataStat]) => { + return { + period: dataDate, + label: 'visit', + ...Object + .fromEntries( + Object + .entries(dataStat) + .map(([key, value]) => [legendTranslate[key] || key, value]) + ) + } + }) + +export const matomoDailyToData = (matomoData = {}) => Object.entries(matomoData) + .map(([dataDate, dataStat]) => { + const filtredDataStat = dataStat.filter((dataStatItem = {}) => /^download/i.test(dataStatItem.label)) + return [dataDate, filtredDataStat] + }) + .map(([dataDate, dataStat]) => { + const finalDataStat = dataStat + .map((dataStatItem = {}) => { + const {label, nb_events: nbEvents, subtable} = dataStatItem + const otherFields = Object.fromEntries( + subtable.map((subtableItem = {}) => { + const {label: subtableLabel, nb_events: subtableNbEvents} = subtableItem + return [legendTranslate[subtableLabel] || subtableLabel, subtableNbEvents] + }) + ) + + return { + label, + period: dataDate, + total: nbEvents, + ...otherFields, + } + }) + return finalDataStat[0] + }) + +export const matomoMonthlyToData = (matomoMonthlyData = {}) => { + const [date = '', value = []] = Object + .entries(matomoMonthlyData) + .reverse()?.[0] || [] + const [year, month] = date.split('-') + const period = (new Date(year, month - 1, 1)).toLocaleDateString('fr-FR', {year: 'numeric', month: 'long'}) + return { + period, + value: value?.[0] || {}, + } +} + +export const geocoderSearchToData = (geocoderData = []) => { + const data = geocoderData + .map(({period, ...value}) => { + const translatedValues = Object.fromEntries( + Object.entries(value) + .map(([key, value]) => { + return [legendTranslate[key] || key, value] + }) + ) + return { + period, + ...translatedValues, + } + } + ) + return data +} + +export const getDataDef = (dataDef = [], themeColor = null) => dataDef + .reduce((acc, {dataKey, colors, ...dataItem}, index) => { // eslint-disable-line unicorn/no-array-reduce + const theme = (themeColor ? customColor[colorName]?.[index] : defaultTheme?.[index]) || defaultTheme[0] + return { + ...acc, + [legendTranslate[dataKey] || dataKey]: { + stroke: colors?.[0] || theme?.[0], + fill: colors?.[1] || theme?.[1], + ...dataItem + }, + } + }, {}) + +export const getBanStatsData = async () => { + const {ban: dataBanStats, bal: dataBalStats} = await getStats() || {} + return dataBanStats && dataBalStats ? [ + {value: `${(Number(((dataBanStats.nbAdresses) * 0.000001).toFixed(2)))}`, unit: 'M', label: 'Total des adresses au sein de la Base Adresse National'}, + {value: `${(Number(((dataBalStats.nbAdresses) * 0.000001).toFixed(2)))}`, unit: 'M', label: 'Adresses issues des Bases Adresses Locales', hasSeparator: true}, + {value: `${(Number(((dataBanStats.nbAdresses - dataBalStats.nbAdresses) * 0.000001).toFixed(2)))}`, unit: 'M', label: 'Adresses issues des autres sources'}, + {value: `${(Number(((dataBalStats.nbAdressesCertifiees) * 0.000001).toFixed(2)))}`, unit: 'M', label: 'Adresses certifiées', hasSeparator: true}, + ] : [] +} + +export const fetcher = async (...args) => (await fetch(...args))?.json() diff --git a/views/stats/quality-chart-block/index.js b/views/stats/quality-chart-block/index.js new file mode 100644 index 000000000..d9854353e --- /dev/null +++ b/views/stats/quality-chart-block/index.js @@ -0,0 +1 @@ +export {default} from './quality-chart-block' diff --git a/views/stats/quality-chart-block/quality-chart-block.js b/views/stats/quality-chart-block/quality-chart-block.js new file mode 100644 index 000000000..59c88ea55 --- /dev/null +++ b/views/stats/quality-chart-block/quality-chart-block.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types' +import {numFormater} from '@/lib/format-numbers' +import theme from '@/styles/theme' + +import QualityPieCharts from './quality-pie-charts' +import StarLine from '@/components/star-line' +import ColorLine from '@/components/color-line' + +function QualityChartBlock({data, chartSize = 200}) { + return ( + <> +
+
+ +
+ +
+
+ {data[1].map(({name, value, color, description}) => ( +
+ +  {numFormater(value)}%  + + {description} +
+ ))} +
+ +
+ +
+ {data[0].map(({name, value, color, description}) => ( +
+ +  {numFormater(value)}%  + + {description} +
+ ))} + +
+
+ +
+ + + + ) +} + +QualityChartBlock.propTypes = { + data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }))).isRequired, + chartSize: PropTypes.number, +} + +export default QualityChartBlock diff --git a/views/stats/quality-chart-block/quality-pie-charts.js b/views/stats/quality-chart-block/quality-pie-charts.js new file mode 100644 index 000000000..a819e446a --- /dev/null +++ b/views/stats/quality-chart-block/quality-pie-charts.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types' +import {ResponsiveContainer, PieChart, Pie, Cell} from 'recharts' + +function QualityPieCharts({data, size = 200, pieThicknesses = [16, 20], pieGap: propsPieGap = 4, color = '#aaa'}) { + const pieGap = propsPieGap * pieThicknesses.length + const pieOuterRadius = size / 2 + const paddingAngle = propsPieGap / 2 + + return ( + + + + {data[0].map(({name, color}) => ( + + ))} + + + + {data[1].map(({name, color}) => ( + + ))} + + + + ) +} + +QualityPieCharts.propTypes = { + data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + description: PropTypes.string, + }))).isRequired, + size: PropTypes.number, + pieThicknesses: PropTypes.arrayOf(PropTypes.number), + pieGap: PropTypes.number, + color: PropTypes.string, +} + +export default QualityPieCharts diff --git a/views/stats/stats-config-data.js b/views/stats/stats-config-data.js new file mode 100644 index 000000000..966721a1e --- /dev/null +++ b/views/stats/stats-config-data.js @@ -0,0 +1,57 @@ +import {colorthemes as customColor} from '@/components/charts' + +export const defDatadailyDownload = [ + { + dataKey: 'download BAL', + }, + { + dataKey: 'download CSV historique adresses', + strokeDasharray: 5, + }, + { + dataKey: 'download CSV historique lieux-dits', + }, +] + +export const defDataGeocoder = [ + { + dataKey: 'nbSearch', + colors: customColor.glicyne[0], + }, + { + dataKey: 'nbSearchResult', + strokeDasharray: 3, + colors: customColor.glicyne[2], + }, + { + dataKey: 'nbSearchEmptyResult', + strokeDasharray: 5, + colors: customColor.glicyne[3], + }, + { + dataKey: 'nbReverseSearch', + colors: customColor.ecume[0], + }, + { + dataKey: 'nbReverseSearchResult', + strokeDasharray: 3, + colors: customColor.ecume[3], + }, + { + dataKey: 'nbReverseSearchEmptyResult', + strokeDasharray: 5, + colors: customColor.ecume[2], + }, +] + +export const defDataBanVisit = [ + { + dataKey: 'Visites', + colors: customColor.glicyne[3], + }, + { + dataKey: 'Visiteurs uniques', + strokeDasharray: 3, + colors: customColor.azure[0], + }, +] diff --git a/yarn.lock b/yarn.lock index d681419eb..06c796151 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1244,6 +1244,13 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== +"@babel/runtime@^7.1.2": + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" + integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.20.13": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" @@ -1987,6 +1994,57 @@ "@types/keygrip" "*" "@types/node" "*" +"@types/d3-array@^3.0.3": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.8.tgz#a5d0687a12b48142c6f124d5e3796054e91bcea5" + integrity sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ== + +"@types/d3-color@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.1.tgz#43a2aa7836fdae19ce32fabe97742e787f4b2e08" + integrity sha512-CSAVrHAtM9wfuLJ2tpvvwCU/F22sm7rMHNN+yh9D6O6hyAms3+O0cgMpC1pm6UEUMOntuZC8bMt74PteiDUdCg== + +"@types/d3-ease@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" + integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.2.tgz#b5928cca26fc20dbfe689ff37d62f7bac434c74e" + integrity sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" + integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== + +"@types/d3-scale@^4.0.2": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.5.tgz#daa4faa5438315a37a1f5eb1bcdc5aeb3d3e5a2d" + integrity sha512-w/C++3W394MHzcLKO2kdsIn5KKNTOqeQVzyPSGPLzQbkPw/jpeaGtSRlakcKevGgGsjJxGsbqS0fPrVFDbHrDA== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.3.tgz#20eee7aad70f2562041af18e305fec6b48fd511d" + integrity sha512-cHMdIq+rhF5IVwAV7t61pcEXfEHsEsrbBUPkFGBwTXuxtTAkBBrnrNA8++6OWm3jwVsXoZYQM8NEekg6CPJ3zw== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.1.tgz#f0c8f9037632cc4511ae55e7e1459dcb95fb3619" + integrity sha512-5j/AnefKAhCw4HpITmLDTPlf4vhi8o/dES+zbegfPb7LaGfNyqkLxBR6E+4yvTAgnJLmhe80EXFMzUs38fw4oA== + +"@types/d3-timer@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" + integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== + "@types/eslint@^7.2.13": version "7.29.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" @@ -2834,6 +2892,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.2.5: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + clean-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7" @@ -2846,7 +2909,7 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== -client-only@0.0.1: +client-only@0.0.1, client-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== @@ -3087,6 +3150,11 @@ css-to-react-native@^3.2.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" +css-unit-converter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" + integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== + csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" @@ -3097,6 +3165,77 @@ csstype@^3.1.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + date-fns@^2.29.3: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" @@ -3136,6 +3275,11 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" @@ -3265,6 +3409,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-scroll-into-view@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.0.1.tgz#32abb92f0d8feca6215162aef43e4b449ab8d99c" @@ -3823,6 +3974,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +eventemitter3@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3937,6 +4093,11 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-equals@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" + integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ== + fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" @@ -4663,6 +4824,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -5278,7 +5444,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6086,6 +6252,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" @@ -6322,11 +6493,41 @@ react-feather@^2.0.10: dependencies: prop-types "^15.7.2" -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-resize-detector@^8.0.4: + version "8.1.0" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-8.1.0.tgz#1c7817db8bc886e2dbd3fbe3b26ea8e56be0524a" + integrity sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w== + dependencies: + lodash "^4.17.21" + +react-smooth@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.4.tgz#95187126265970a1490e2aea5690365203ee555f" + integrity sha512-OkFsrrMBTvQUwEJthE1KXSOj79z57yvEWeFefeXPib+RmQEI9B1Ub1PgzlzzUyBOvl/TjXt5nF2hmD4NsgAh8A== + dependencies: + fast-equals "^5.0.0" + react-transition-group "2.9.0" + +react-transition-group@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -6369,6 +6570,28 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.8.0.tgz#90c95136e2cb6930224c94a51adce607701284fc" + integrity sha512-nciXqQDh3aW8abhwUlA4EBOBusRHLNiKHfpRZiG/yjups1x+auHb2zWPuEcTn/IMiN47vVMMuF8Sr+vcQJtsmw== + dependencies: + classnames "^2.2.5" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^8.0.4" + react-smooth "^2.0.2" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + victory-vendor "^36.6.8" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -6377,6 +6600,14 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" @@ -7100,6 +7331,14 @@ svg-arc-to-cubic-bezier@^3.0.0, svg-arc-to-cubic-bezier@^3.2.0: resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6" integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g== +swr@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07" + integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ== + dependencies: + client-only "^0.0.1" + use-sync-external-store "^1.2.0" + table@^6.0.9: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" @@ -7494,6 +7733,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -7527,6 +7771,26 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +victory-vendor@^36.6.8: + version "36.6.11" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.6.11.tgz#acae770717c2dae541a54929c304ecab5ab6ac2a" + integrity sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + vite-compatible-readable-stream@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz#27267aebbdc9893c0ddf65a421279cbb1e31d8cd"