From e8072fe8e1146378436e7a4fd3d0dcbcf25b93e8 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Thu, 23 Apr 2020 17:44:41 +0200 Subject: [PATCH] Page /stats (#969) Co-authored-by: Elodie Quandalle --- netlify.toml | 5 - package.json | 2 + source/components/BarChart.tsx | 92 ++++ source/components/Distribution.tsx | 97 +---- source/components/MoreInfosOnUs.tsx | 21 +- source/components/StackedBarChart.tsx | 39 +- source/components/utils/colors.tsx | 3 +- source/engine/date.ts | 1 + source/locales/en.yaml | 49 +-- source/scripts/fetch-releases.js | 34 +- source/scripts/fetch-stats.js | 333 ++++++++++++++ source/scripts/prepare.js | 1 + source/scripts/utils.js | 13 + source/sites/mon-entreprise.fr/App.tsx | 2 + .../layout/Footer/Footer.tsx | 2 +- .../layout/Footer/Privacy.tsx | 4 +- .../pages/Simulateurs/ArtisteAuteur.tsx | 3 +- .../pages/Simulateurs/Home.tsx | 191 ++++---- .../pages/Stats/LazyStats.tsx | 12 + .../mon-entreprise.fr/pages/Stats/Stats.tsx | 412 ++++++++++++++++++ source/sites/mon-entreprise.fr/sitePaths.ts | 1 + yarn.lock | 163 ++++++- 22 files changed, 1212 insertions(+), 268 deletions(-) create mode 100644 source/components/BarChart.tsx create mode 100644 source/scripts/fetch-stats.js create mode 100644 source/scripts/utils.js create mode 100644 source/sites/mon-entreprise.fr/pages/Stats/LazyStats.tsx create mode 100644 source/sites/mon-entreprise.fr/pages/Stats/Stats.tsx diff --git a/netlify.toml b/netlify.toml index 94caa20b3..db8e2fd81 100644 --- a/netlify.toml +++ b/netlify.toml @@ -129,11 +129,6 @@ status = 200 # Mon-entreprise.fr PRODUCTION settings -[[redirects]] - from = "https://mon-entreprise.fr/stats" - to = "https://mon-entreprise.glitch.me/" - status = 200 - [[redirects]] from = "https://mon-entreprise.fr/robots.txt" to = "/robots.infrance.txt" diff --git a/package.json b/package.json index 799fe14a5..16f00de7f 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-syntax-highlighter": "^10.1.1", "react-to-print": "^2.5.1", "react-transition-group": "^2.2.1", + "recharts": "^1.8.5", "reduce-reducers": "^1.0.4", "redux": "^4.0.4", "redux-thunk": "^2.3.0", @@ -130,6 +131,7 @@ "@types/react-router-dom": "^5.1.0", "@types/react-router-hash-link": "^1.2.1", "@types/react-syntax-highlighter": "^11.0.4", + "@types/recharts": "^1.8.9", "@types/styled-components": "^4.1.19", "@types/webpack": "^4.41.10", "@types/webpack-env": "^1.14.1", diff --git a/source/components/BarChart.tsx b/source/components/BarChart.tsx new file mode 100644 index 000000000..466a01cf0 --- /dev/null +++ b/source/components/BarChart.tsx @@ -0,0 +1,92 @@ +import Value from 'Components/Value' +import React, { useContext } from 'react' +import emoji from 'react-easy-emoji' +import { animated, config, useSpring } from 'react-spring' +import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting' +import { ThemeColorsContext } from 'Components/utils/colors' + +const ANIMATION_SPRING = config.gentle + +let ChartItemBar = ({ styles, color, numberToPlot, unit }) => ( +
+ +
+ + {numberToPlot} + +
+
+) +let BranchIcône = ({ icône }) => ( +
+ {emoji(icône)} +
+) + +type BarChartBranchProps = { + value: number + title: React.ReactNode + icon?: string + maximum: number + description?: string + unit?: string +} + +export default function BarChartBranch({ + value, + title, + icon, + maximum, + description, + unit +}: BarChartBranchProps) { + const [intersectionRef, brancheInViewport] = useDisplayOnIntersecting({ + threshold: 0.5 + }) + const { color } = useContext(ThemeColorsContext) + const numberToPlot = brancheInViewport ? value : 0 + const styles = useSpring({ + config: ANIMATION_SPRING, + to: { + flex: numberToPlot / maximum, + opacity: numberToPlot ? 1 : 0 + } + }) as { flex: number; opacity: number } // TODO: problème avec les types de react-spring ? + + return ( + + {icon && } +
+

+ {title} +
+ {description && {description}} +

+ +
+
+ ) +} diff --git a/source/components/Distribution.tsx b/source/components/Distribution.tsx index 99a42cfe1..b8883db83 100644 --- a/source/components/Distribution.tsx +++ b/source/components/Distribution.tsx @@ -1,16 +1,12 @@ -import { ThemeColorsContext } from 'Components/utils/colors' -import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting' -import Value from 'Components/Value' -import React, { useContext } from 'react' -import emoji from 'react-easy-emoji' +import React from 'react' import { useSelector } from 'react-redux' -import { animated, config, useSpring } from 'react-spring' import { DottedName } from 'Rules' import { parsedRulesSelector } from 'Selectors/analyseSelectors' import répartitionSelector from 'Selectors/repartitionSelectors' import './Distribution.css' import './PaySlip' import RuleLink from './RuleLink' +import BarChartBranch from './BarChart' export default function Distribution() { const distribution = useSelector(répartitionSelector) as any @@ -28,7 +24,7 @@ export default function Distribution() { key={brancheDottedName} dottedName={brancheDottedName} value={partPatronale + partSalariale} - distribution={distribution} + maximum={distribution.maximum} /> ) )} @@ -40,91 +36,26 @@ export default function Distribution() { type DistributionBranchProps = { dottedName: DottedName value: number - distribution: { maximum: number; total: number } + maximum: number icon?: string } -const ANIMATION_SPRING = config.gentle export function DistributionBranch({ dottedName, value, icon, - distribution + maximum }: DistributionBranchProps) { const rules = useSelector(parsedRulesSelector) - const [intersectionRef, brancheInViewport] = useDisplayOnIntersecting({ - threshold: 0.5 - }) - const { color } = useContext(ThemeColorsContext) - const branche = rules[dottedName] - const montant = brancheInViewport ? value : 0 - const styles = useSpring({ - config: ANIMATION_SPRING, - to: { - flex: montant / distribution.maximum, - opacity: montant ? 1 : 0 - } - }) as { flex: number; opacity: number } // TODO: problème avec les types de react-spring ? - + const branch = rules[dottedName] return ( - - -
-

- - - -
- {branche.summary} -

- -
-
+ } + icon={icon ?? branch.icons} + description={branch.summary} + unit="€" + /> ) } - -type ChartItemBarProps = { - styles: React.CSSProperties - color: string - montant: number -} - -let ChartItemBar = ({ styles, color, montant }: ChartItemBarProps) => ( -
- -
- - {montant} - -
-
-) - -let BranchIcône = ({ icône }: { icône: string }) => ( -
- {emoji(icône)} -
-) diff --git a/source/components/MoreInfosOnUs.tsx b/source/components/MoreInfosOnUs.tsx index 8ff719af2..3dbcee0d9 100644 --- a/source/components/MoreInfosOnUs.tsx +++ b/source/components/MoreInfosOnUs.tsx @@ -34,17 +34,16 @@ export default function MoreInfosOnUs() {
Découvrir
)} - -
{emoji('📊')}
-

Les statistiques

-

- Quel est notre impact ? -

-
Découvrir
-
+ {!pathname.startsWith(sitePaths.stats) && ( + +
{emoji('📊')}
+

Les statistiques

+

+ Quel est notre impact ? +

+
Découvrir
+ + )} {!pathname.startsWith(sitePaths.budget) && (
{emoji('💶')}
diff --git a/source/components/StackedBarChart.tsx b/source/components/StackedBarChart.tsx index e453bbf0d..bfc8c6bde 100644 --- a/source/components/StackedBarChart.tsx +++ b/source/components/StackedBarChart.tsx @@ -81,15 +81,19 @@ export function roundedPercentages(values: Array) { } type StackedBarChartProps = { - data: Array<{ color?: string } & EvaluatedRule> + data: Array<{ + color?: string + value: number | undefined + legend: React.ReactNode + key: string + }> } -export default function StackedBarChart({ data }: StackedBarChartProps) { +export function StackedBarChart({ data }: StackedBarChartProps) { const [intersectionRef, displayChart] = useDisplayOnIntersecting({ threshold: 0.5 }) - data = data.filter(d => d.nodeValue != undefined) - const percentages = roundedPercentages(data.map(d => d.nodeValue as number)) + const percentages = roundedPercentages(data.map(d => d.value ?? 0)) const dataWithPercentage = data.map((data, index) => ({ ...data, percentage: percentages[index] @@ -103,21 +107,21 @@ export default function StackedBarChart({ data }: StackedBarChartProps) { // has a border so we don't want to display empty bars // (even with width 0). .filter(({ percentage }) => percentage !== 0) - .map(({ dottedName, color, percentage }) => ( + .map(({ key, color, percentage }) => ( ))} - {dataWithPercentage.map(({ percentage, color, ...rule }) => ( - + {dataWithPercentage.map(({ key, percentage, color, legend }) => ( + - {capitalise0(rule.title)} + {legend} {percentage} % ))} @@ -125,3 +129,20 @@ export default function StackedBarChart({ data }: StackedBarChartProps) { ) } + +type StackedRulesChartProps = { + data: Array<{ color?: string } & EvaluatedRule> +} + +export default function StackedRulesChart({ data }: StackedRulesChartProps) { + return ( + ({ + ...rule, + key: rule.dottedName, + value: rule.nodeValue, + legend: {capitalise0(rule.title)} + }))} + /> + ) +} diff --git a/source/components/utils/colors.tsx b/source/components/utils/colors.tsx index b7d2ea75c..97f7acbee 100644 --- a/source/components/utils/colors.tsx +++ b/source/components/utils/colors.tsx @@ -50,7 +50,8 @@ const deriveAnalogousPalettes = (hex: string) => { const [h, s, l] = convert.hex.hsl(hex.split('#')[1]) return [ generateDarkenVariations(4, [(h - 45) % 360, 0.75 * s, l]), - generateDarkenVariations(4, [(h + 45) % 360, 0.75 * s, l]) + generateDarkenVariations(4, [(h + 45) % 360, 0.75 * s, l]), + generateDarkenVariations(4, [(h + 90) % 360, 0.75 * s, l]) ] } diff --git a/source/engine/date.ts b/source/engine/date.ts index 3c4273321..605cc9960 100644 --- a/source/engine/date.ts +++ b/source/engine/date.ts @@ -5,6 +5,7 @@ export function normalizeDateString(dateString: string): string { } return normalizeDate(+year, +month, +day) } + const pad = (n: number): string => (+n < 10 ? `0${n}` : '' + n) export function normalizeDate( year: number, diff --git a/source/locales/en.yaml b/source/locales/en.yaml index 2b9334452..8930e38fc 100644 --- a/source/locales/en.yaml +++ b/source/locales/en.yaml @@ -8,6 +8,7 @@ Accueil: Home Aide à la déclaration de revenus au titre de l'année 2019: Help with your 2019 income tax return Alors: Then Année d'activité: Years of activity +Artiste-auteur: Artist-author Assimilé salarié: '"Assimilé-salarié"' Au-delà du dernier plafond: Beyond the last ceiling Au-dessus de: Above @@ -24,7 +25,9 @@ Choisir plus tard: Choose later Code d'intégration: Integration Code Commencer: Get started 'Commerçant, artisan, ou libéral ?': 'Trader, craftsman, or liberal?' +Comparaison statuts: Status comparison Continuer: Continue +Coronavirus: Coronavirus Cotisations: Contributions Cotisations sociales: Social contributions 'Covid-19 : Découvrez les mesures de soutien aux entreprises': 'Covid-19: Find out about business support measures' @@ -61,8 +64,8 @@ Gérant minoritaire: Managing director Habituellement: Usually Imprimer: Print Impôts: Taxes -'Indemnité chômage partiel prise en charge par l''état :': 'State-paid short-time working allowance :' -Indépendant: Independent +"Indemnité chômage partiel prise en charge par l'état :": 'State-paid short-time working allowance :' +Indépendant: 'Indépendant' International: International Intégrer l'interface de simulation: Integrate the simulation interface Intégrer la bibliothèque de calcul: Integrate the calculation library @@ -78,7 +81,7 @@ Mon entreprise: My company Mon revenu: My income Montant: Amount Montant des cotisations: Amount of contributions -'Nom de l''entreprise ou SIREN ': Company name or SIREN code +"Nom de l'entreprise ou SIREN ": Company name or SIREN code Non: 'No' Nous n'avons rien trouvé: We didn't find any matching registered company. Oui: 'Yes' @@ -132,6 +135,7 @@ Saisissez votre domaine d'activité: Enter your business area Salaire: Salary Salaire net: Net Salary Salaire net et brut: Net and gross salary +Salarié: Employee Sans responsabilité limitée: Without limited liability Si: If Simulateur de salaire: Employee salary simulation @@ -143,7 +147,7 @@ Taux: Rate Taux calculé: Calculated rate Taux moyen: Average rate Total des retenues: Total withheld -'Total payé par l''entreprise :': 'Total paid by the company :' +"Total payé par l'entreprise :": 'Total paid by the company :' Tout effacer: Delete all Tranche de l'assiette: Scale bracket Un seul associé: Only one partner @@ -927,6 +931,7 @@ path: index: /simulators indépendant: /independant salarié: /salaried + stats: /stats économieCollaborative: index: /sharing-economy votreSituation: /your-situation @@ -1019,20 +1024,6 @@ simlateurs: the year 2020 based on your projected income. simulateurs: accueil: - artiste-auteur: >- - <0>Artist-author<1>Estimating the social security contributions of an - artist or author - assimilé: | - <0>"Assimilé-salarié" - <1>Calculate the income of an officer of a minority SAS, SASU or SARL - auto: | - <0>Auto-entrepreneur - <1>Calculate the income (or turnover) of an auto-entrepreneur - comparaison: > - <0>Status comparison - - <1>Simulate the differences between the plans (contributions, retirement, - maternity, illness, etc.) description: >- <0>All the simulators on this site are: @@ -1040,14 +1031,6 @@ simulateurs: to increase the number of devices taken into account <2>Developed in partnership with the Urssaf (the contribution collector entity in France)<2> - indépendant: | - <0>"Indépendant" - <1>Calculate the income of a majority manager of EURL, EI, or SARL - salarié: > - <0>Employee - - <1>Calculate the net, gross, or total salary of an employee, trainee, or - similar titre: Available simulators artiste-auteur: titre: Estimate my artist/author contributions @@ -1099,6 +1082,20 @@ simulateurs: défaut: 'Refine the simulation by answering the following questions:' faible: Low accuracy moyenne: Medium accuracy + résumé: + artiste-auteur: Estimating the social security contributions of an artist or author + assimilé: | + Calculate the income of an officer of a minority SAS, SASU or SARL + auto: | + Calculate the income (or turnover) of an auto-entrepreneur + comparaison: > + Simulate the differences between the plans (contributions, retirement, + maternity, illness, etc.) + indépendant: | + Calculate the income of a majority manager of EURL, EI, or SARL + salarié: > + Calculate the net, gross, or total salary of an employee, trainee, or + similar salarié: page: description: >- diff --git a/source/scripts/fetch-releases.js b/source/scripts/fetch-releases.js index 4494c7786..a32f9c8e9 100644 --- a/source/scripts/fetch-releases.js +++ b/source/scripts/fetch-releases.js @@ -9,8 +9,7 @@ // "public repo" authorization when generating the access token. require('dotenv').config() require('isomorphic-fetch') -const fs = require('fs') -const path = require('path') +var { createDataDir, writeInDataDir } = require('./utils.js') // We use the GitHub API V4 in GraphQL to download the releases. A GraphQL // explorer can be found here : https://developer.github.com/v4/explorer/ @@ -48,17 +47,15 @@ const fakeData = [ } ] -const dataDir = path.resolve(__dirname, '../data/') - async function main() { createDataDir() - writeReleasesInDataDir(await fetchReleases()) -} - -function createDataDir() { - if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir) - } + const releases = await fetchReleases() + // The last release name is fetched on all pages (to display the banner) + // whereas the full release data is used only in the dedicated page, that why + // we deduplicate the releases data in two separated files that can be + // bundled/fetched separately. + writeInDataDir('releases.json', releases) + writeInDataDir('last-release.json', { lastRelease: releases[0].name }) } async function fetchReleases() { @@ -84,19 +81,4 @@ async function fetchReleases() { } } -function writeReleasesInDataDir(releases) { - // The last release name is fetched on all pages (to display the banner) - // whereas the full release data is used only in the dedicated page, that why - // we deduplicate the releases data in two separated files that can be - // bundled/fetched separately. - fs.writeFileSync( - path.join(dataDir, 'releases.json'), - JSON.stringify(releases) - ) - fs.writeFileSync( - path.join(dataDir, 'last-release.json'), - JSON.stringify({ lastRelease: releases[0].name }) - ) -} - main() diff --git a/source/scripts/fetch-stats.js b/source/scripts/fetch-stats.js new file mode 100644 index 000000000..6ffcf13e6 --- /dev/null +++ b/source/scripts/fetch-stats.js @@ -0,0 +1,333 @@ +// This script uses the Matomo API which requires an access token +// Once you have your access token you can put it in a `.env` file at the root +// of the project to enable it during development. For instance: +// +// MATOMO_TOKEN=f4336c82cb1e494752d06e610614eab12b65f1d1 +// +// Matomo API documentation: +// https://developer.matomo.org/api-reference/reporting-api + +require('dotenv').config() +require('isomorphic-fetch') +const querystring = require('querystring') +const { createDataDir, writeInDataDir } = require('./utils.js') +const R = require('ramda') + +const apiURL = params => { + const query = querystring.stringify({ + period: 'month', + date: 'last1', + method: 'API.get', + format: 'JSON', + module: 'API', + idSite: 39, + language: 'fr', + apiAction: 'get', + token_auth: process.env.MATOMO_TOKEN, + ...params + }) + return `https://stats.data.gouv.fr/index.php?${query}` +} + +async function main() { + createDataDir() + const stats = { + simulators: await fetchSimulatorsMonth(), + monthlyVisits: await fetchMonthlyVisits(), + dailyVisits: await fetchDailyVisits(), + statusChosen: await fetchStatusChosen(), + feedback: await fetchFeedback(), + channelType: await fetchChannelType() + } + writeInDataDir('stats.json', stats) +} + +function xMonthAgo(x = 0) { + const date = new Date() + if (date.getMonth() - x > 0) { + date.setMonth(date.getMonth() - x) + } else { + date.setMonth(12 + date.getMonth() - x) + date.setFullYear(date.getFullYear() - 1) + } + return date.toISOString().substring(0, 7) +} + +async function fetchSimulatorsMonth() { + const getDataFromXMonthAgo = async x => { + const date = xMonthAgo(x) + return { date, visites: await fetchSimulators(`${date}-01`) } + } + return { + currentMonth: await getDataFromXMonthAgo(0), + oneMonthAgo: await getDataFromXMonthAgo(1), + twoMonthAgo: await getDataFromXMonthAgo(2) + } +} + +async function fetchSimulators(dt) { + try { + const response = await fetch( + apiURL({ + period: 'month', + date: `${dt}`, + method: 'Actions.getPageUrls', + filter_limits: -1 + }) + ) + const firstLevelData = await response.json() + + const coronavirusPage = firstLevelData.find( + page => page.label === '/coronavirus' + ) + + // Visits on simulators pages + const idSubTableSimulateurs = firstLevelData.find( + page => page.label === 'simulateurs' + ).idsubdatatable + + const responseSimulateurs = await fetch( + apiURL({ + date: `${dt}`, + method: 'Actions.getPageUrls', + search_recursive: 1, + filter_limits: -1, + idSubtable: idSubTableSimulateurs + }) + ) + + const dataSimulateurs = await responseSimulateurs.json() + const resultSimulateurs = dataSimulateurs + .filter(({ label }) => + [ + '/salarié', + '/auto-entrepreneur', + '/artiste-auteur', + '/indépendant', + '/comparaison-régimes-sociaux', + '/assimilé-salarié' + ].includes(label) + ) + + /// Two '/salarié' pages are reported on Matomo, one of which has very few + /// visitors. We delete it manually. + .filter( + x => + x.label != '/salarié' || + x.nb_visits != + dataSimulateurs + .filter(x => x.label == '/salarié') + .reduce((a, b) => Math.min(a, b.nb_visits), 1000) + ) + + // Add iframes + const idTableIframes = firstLevelData.find(page => page.label == 'iframes') + .idsubdatatable + const responseIframes = await fetch( + apiURL({ + date: `${dt}`, + method: 'Actions.getPageUrls', + search_recursive: 1, + filter_limits: -1, + idSubtable: idTableIframes + }) + ) + const dataIframes = await responseIframes.json() + const resultIframes = dataIframes.filter(x => + [ + '/simulateur-embauche?couleur=', + '/simulateur-autoentrepreneur?couleur=' + ].includes(x.label) + ) + + const groupSimulateursIframesVisits = ({ label }) => + label.startsWith('/simulateur-embauche') + ? '/salarié' + : label.startsWith('/simulateur-autoentrepreneur') + ? '/auto-entrepreneur' + : label + + const sumVisits = (acc, { nb_visits }) => acc + nb_visits + const results = R.reduceBy( + sumVisits, + 0, + groupSimulateursIframesVisits, + [...resultSimulateurs, ...resultIframes, coronavirusPage].filter( + x => x !== undefined + ) + ) + return Object.entries(results) + .map(([label, nb_visits]) => ({ label, nb_visits })) + .sort((a, b) => b.nb_visits - a.nb_visits) + } catch (e) { + console.log('fail to fetch Simulators Visits') + return null + } +} + +// We had a tracking bug in 2019, in which every click on Safari+iframe counted +// as a visit, so the numbers are manually corrected. +const visitsIn2019 = { + '2019-01': 119541, + '2019-02': 99065, + '2019-03': 122931, + '2019-04': 113454, + '2019-05': 118637, + '2019-06': 152981, + '2019-07': 141079, + '2019-08': 127326, + '2019-09': 178474, + '2019-10': 198260, + '2019-11': 174515, + '2019-12': 116305 +} + +async function fetchMonthlyVisits() { + try { + const response = await fetch( + apiURL({ + period: 'month', + date: 'previous12', + method: 'VisitsSummary.getUniqueVisitors' + }) + ) + const data = await response.json() + const result = Object.entries({ ...data, ...visitsIn2019 }) + .sort(([t1], [t2]) => (t1 > t2 ? 1 : -1)) + .map(([date, visiteurs]) => ({ date, visiteurs })) + return result + } catch (e) { + console.log('fail to fetch Monthly Visits') + return null + } +} + +async function fetchDailyVisits() { + try { + const response = await fetch( + apiURL({ + period: 'day', + date: 'previous30', + method: 'VisitsSummary.getUniqueVisitors' + }) + ) + const data = await response.json() + return Object.entries(data).map(([date, visiteurs]) => ({ + date, + visiteurs + })) + } catch (e) { + console.log('fail to fetch Daily Visits') + return null + } +} + +async function fetchStatusChosen() { + try { + const response = await fetch( + apiURL({ + method: 'Events.getAction', + label: 'status chosen', + date: 'previous1' + }) + ) + const data = await response.json() + const response2 = await fetch( + apiURL({ + method: 'Events.getNameFromActionId', + idSubtable: Object.values(data)[0][0].idsubdatatable, + date: 'previous1' + }) + ) + const data2 = await response2.json() + const result = Object.values(data2)[0].map(({ label, nb_visits }) => ({ + label, + nb_visits + })) + return result + } catch (e) { + console.log('fail to fetch Status Chosen') + return null + } +} + +async function fetchFeedback() { + try { + const APIcontent = await fetch( + apiURL({ + method: 'Events.getCategory', + label: 'Feedback > @rate%20page%20usefulness', + date: 'previous5' + }) + ) + const APIsimulator = await fetch( + apiURL({ + method: 'Events.getCategory', + label: 'Feedback > @rate%20simulator', + date: 'previous5' + }) + ) + const feedbackcontent = await APIcontent.json() + const feedbacksimulator = await APIsimulator.json() + + let content = 0 + let simulator = 0 + let j = 0 + // The weights are defined by taking the coefficients of an exponential + // smoothing with alpha=0.8 and normalizing them. The current month is not + // considered. + const weights = [0.0015, 0.0076, 0.0381, 0.1905, 0.7623] + for (const i in feedbackcontent) { + content += feedbackcontent[i][0].avg_event_value * weights[j] + simulator += feedbacksimulator[i][0].avg_event_value * weights[j] + j += 1 + } + return { + content: Math.round(content * 10), + simulator: Math.round(simulator * 10) + } + } catch (e) { + console.log('fail to fetch feedbacks') + return null + } +} + +async function fetchChannelType() { + try { + const response = await fetch( + apiURL({ + period: 'month', + date: 'last3', + method: 'Referrers.getReferrerType' + }) + ) + + const data = await response.json() + + const result = R.map( + date => + date + .filter(x => + ['Sites web', 'Moteurs de recherche', 'Entrées directes'].includes( + x.label + ) + ) + .map(({ label, nb_visits }) => ({ + label, + nb_visits + })), + data + ) + const dates = Object.keys(result).sort((t1, t2) => t1 - t2) + return { + currentMonth: { date: dates[0], visites: result[dates[0]] }, + oneMonthAgo: { date: dates[1], visites: result[dates[1]] }, + twoMonthAgo: { date: dates[2], visites: result[dates[2]] } + } + } catch (e) { + console.log('fail to fetch channel type') + return null + } +} + +main() diff --git a/source/scripts/prepare.js b/source/scripts/prepare.js index 09e33f836..307b42c34 100644 --- a/source/scripts/prepare.js +++ b/source/scripts/prepare.js @@ -1,2 +1,3 @@ require('./dottednames.js') require('./fetch-releases.js') +require('./fetch-stats.js') diff --git a/source/scripts/utils.js b/source/scripts/utils.js new file mode 100644 index 000000000..ab64a1666 --- /dev/null +++ b/source/scripts/utils.js @@ -0,0 +1,13 @@ +const path = require('path') +const fs = require('fs') +const dataDir = path.resolve(__dirname, '../data/') + +exports.createDataDir = () => { + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir) + } +} + +exports.writeInDataDir = (filename, data) => { + fs.writeFileSync(path.join(dataDir, filename), JSON.stringify(data, null, 2)) +} diff --git a/source/sites/mon-entreprise.fr/App.tsx b/source/sites/mon-entreprise.fr/App.tsx index 62dcab7b1..d0580c878 100644 --- a/source/sites/mon-entreprise.fr/App.tsx +++ b/source/sites/mon-entreprise.fr/App.tsx @@ -36,6 +36,7 @@ import Integration from './pages/integration/index' import Landing from './pages/Landing/Landing' import Nouveautés from './pages/Nouveautés/Nouveautés' import Simulateurs from './pages/Simulateurs' +import Stats from './pages/Stats/LazyStats' import ÉconomieCollaborative from './pages/ÉconomieCollaborative' import redirects from './redirects' import { constructLocalizedSitePath } from './sitePaths' @@ -131,6 +132,7 @@ const App = () => { /> + diff --git a/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx b/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx index c90d90447..74e389deb 100644 --- a/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx +++ b/source/sites/mon-entreprise.fr/layout/Footer/Footer.tsx @@ -66,7 +66,7 @@ const Footer = () => { {' • '} Nouveautés {' • '} - Stats + Stats {' • '} Budget diff --git a/source/sites/mon-entreprise.fr/layout/Footer/Privacy.tsx b/source/sites/mon-entreprise.fr/layout/Footer/Privacy.tsx index e53c23fde..812c44a2d 100644 --- a/source/sites/mon-entreprise.fr/layout/Footer/Privacy.tsx +++ b/source/sites/mon-entreprise.fr/layout/Footer/Privacy.tsx @@ -2,7 +2,7 @@ import Overlay from 'Components/Overlay' import React, { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -export default function Privacy() { +export default function Privacy({ label }: { label?: string }) { const [opened, setOpened] = useState(false) const { i18n } = useTranslation() @@ -16,7 +16,7 @@ export default function Privacy() { return ( <> {opened && ( diff --git a/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx b/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx index ed6559fc3..6b4d3fd84 100644 --- a/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx +++ b/source/sites/mon-entreprise.fr/pages/Simulateurs/ArtisteAuteur.tsx @@ -205,7 +205,6 @@ function RepartitionCotisations() { value: useRule(branch.dottedName).nodeValue as number })) const maximum = Math.max(...cotisations.map(x => x.value)) - const total = cotisations.map(x => x.value).reduce((a = 0, b) => a + b) return (

@@ -215,7 +214,7 @@ function RepartitionCotisations() { {cotisations.map(cotisation => ( ))} diff --git a/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx b/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx index 0d7da2463..7724e7184 100644 --- a/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx +++ b/source/sites/mon-entreprise.fr/pages/Simulateurs/Home.tsx @@ -5,9 +5,83 @@ import { Helmet } from 'react-helmet' import { Trans, useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' -export default function Simulateurs() { +export function useSimulatorsMetadata() { + const { t } = useTranslation() const sitePaths = useContext(SitePathsContext) - const { t, i18n } = useTranslation() + + type SimulatorMetaData = { + name: string + icône: string + description?: string + sitePath: string + } + + return [ + { + name: t('Assimilé salarié'), + icône: '☂️', + description: t( + 'simulateurs.résumé.assimilé', + "Calculer le revenu d'un dirigeant de SAS, SASU ou SARL minoritaire" + ), + sitePath: sitePaths.simulateurs['assimilé-salarié'] + }, + { + name: t('Indépendant'), + icône: '👩‍🔧', + description: t( + 'simulateurs.résumé.indépendant', + "Calculer le revenu d'un dirigeant de EURL, EI, ou SARL majoritaire" + ), + sitePath: sitePaths.simulateurs.indépendant + }, + { + name: t('Auto-entrepreneur'), + icône: '🚶‍♂️', + description: t( + 'simulateurs.résumé.auto', + "Calculer le revenu (ou le chiffre d'affaires) d'un auto-entrepreneur" + ), + sitePath: sitePaths.simulateurs['auto-entrepreneur'] + }, + { + name: t('Salarié'), + icône: '🤝', + description: t( + 'simulateurs.résumé.salarié', + "Calculer le salaire net, brut, ou total d'un salarié, stagiaire,ou assimilé" + ), + sitePath: sitePaths.simulateurs.salarié + }, + { + name: t('Artiste-auteur'), + icône: '👩‍🎨', + description: t( + 'simulateurs.résumé.artiste-auteur', + "Estimer les cotisations sociales d'un artiste ou auteur" + ), + sitePath: sitePaths.simulateurs['artiste-auteur'] + }, + { + name: t('Comparaison statuts'), + icône: '📊', + description: t( + 'simulateurs.résumé.comparaison', + 'Simulez les différences entre les régimes (cotisations,retraite, maternité, maladie, etc.)' + ), + sitePath: sitePaths.simulateurs.comparaison + }, + { + name: t('Coronavirus'), + icône: '👨‍🔬', + sitePath: sitePaths.coronavirus + } + ] as Array +} + +export default function Simulateurs() { + const { t } = useTranslation() + const simulatorsMetadata = useSimulatorsMetadata() const titre = t('simulateurs.accueil.titre', 'Simulateurs disponibles') return ( <> @@ -27,101 +101,24 @@ export default function Simulateurs() { // dernière ligne. style={{ maxWidth: 1100, margin: 'auto' }} > - -
{emoji('☂️')}
- -

Assimilé salarié

-

- Calculer le revenu d'un dirigeant de SAS, SASU ou SARL - minoritaire -

-
- - -
{emoji('👩‍🔧')}
- -

Indépendant

-

- Calculer le revenu d'un dirigeant de EURL, EI, ou SARL - majoritaire -

-
- - -
{emoji('🚶‍♂️')}
- -

Auto-entrepreneur

-

- Calculer le revenu (ou le chiffre d'affaires) d'un - auto-entrepreneur -

-
- - -
{emoji('🤝')}
- -

Salarié

-

- Calculer le salaire net, brut, ou total d'un salarié, stagiaire, - ou assimilé -

-
- - -
{emoji('👩‍🎨')}
- -

Artiste-auteur

-

- Estimer les cotisations sociales d'un artiste ou auteur -

-
- - -
{emoji('📊')}
- -

Comparaison statuts

-

- Simulez les différences entre les régimes (cotisations, - retraite, maternité, maladie, etc.) -

-
- + {simulatorsMetadata + .filter(({ name }) => name !== 'Coronavirus') + .map(({ name, description, sitePath, icône }) => ( + +
{emoji(icône)}
+

{name}

+

+ {description} +

+ + ))}

diff --git a/source/sites/mon-entreprise.fr/pages/Stats/LazyStats.tsx b/source/sites/mon-entreprise.fr/pages/Stats/LazyStats.tsx new file mode 100644 index 000000000..a2c2dcb2f --- /dev/null +++ b/source/sites/mon-entreprise.fr/pages/Stats/LazyStats.tsx @@ -0,0 +1,12 @@ +import React, { Suspense } from 'react' +let Stats = React.lazy(() => import('./Stats')) + +export default function LazyStats() { + return ( + Chargement de la page stats

} + > + +
+ ) +} diff --git a/source/sites/mon-entreprise.fr/pages/Stats/Stats.tsx b/source/sites/mon-entreprise.fr/pages/Stats/Stats.tsx new file mode 100644 index 000000000..5b73e8e21 --- /dev/null +++ b/source/sites/mon-entreprise.fr/pages/Stats/Stats.tsx @@ -0,0 +1,412 @@ +import BarChartBranch from 'Components/BarChart' +import MoreInfosOnUs from 'Components/MoreInfosOnUs' +import { StackedBarChart } from 'Components/StackedBarChart' +import { ThemeColorsContext } from 'Components/utils/colors' +import { ScrollToTop } from 'Components/utils/Scroll' +import { groupWith } from 'ramda' +import React, { useContext, useState } from 'react' +import emoji from 'react-easy-emoji' +import { Link } from 'react-router-dom' +import { + CartesianGrid, + Legend, + Line, + LineChart, + ReferenceArea, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from 'recharts' +import DefaultTooltipContent from 'recharts/lib/component/DefaultTooltipContent' +import styled from 'styled-components' +import { + formatPercentage, + formatValue +} from '../../../../../source/engine/format' +import statsJson from '../../../../data/stats.json' +import { capitalise0 } from '../../../../utils' +import Privacy from '../../layout/Footer/Privacy' +import { useSimulatorsMetadata } from '../Simulateurs/Home' + +const stats: StatsData = statsJson as any + +const monthPeriods = ['currentMonth', 'oneMonthAgo', 'twoMonthAgo'] as const +type MonthPeriod = typeof monthPeriods[number] + +type Periodicity = 'daily' | 'monthly' + +type StatsData = { + feedback: { + simulator: number + content: number + } + statusChosen: Array<{ + label: string + nb_visits: number + }> + dailyVisits: Array<{ + date: string + visiteurs: number + }> + monthlyVisits: Array<{ + date: string + visiteurs: number + }> + simulators: Record< + MonthPeriod, + { + date: string + visites: Array<{ label: string; nb_visits: number }> + } + > + channelType: Record< + MonthPeriod, + { + date: string + visites: Array<{ label: string; nb_visits: number }> + } + > +} + +export default function Stats() { + const [choice, setChoice] = useState('monthly') + const [choicesimulators, setChoicesimulators] = useState( + 'oneMonthAgo' + ) + const { palettes } = useContext(ThemeColorsContext) + const simulatorsMetadata = useSimulatorsMetadata() + return ( + <> + +

+ Statistiques <>{emoji('📊')} +

+

+ Découvrez nos statistiques d'utilisation mises à jour quotidiennement. +
+ Les données recueillies sont anonymisées.{' '} + +

+
+ +

Nombre de visites

+ + {emoji('🗓')}{' '} + + +
+ + + + + + +
+
+ +

Nombre d'utilisation des simulateurs

+ { + setChoicesimulators(event.target.value as MonthPeriod) + }} + value={choicesimulators} + /> +
+ + {stats.simulators[choicesimulators].visites.map( + ({ label, nb_visits }) => { + const details = simulatorsMetadata.find(({ sitePath }) => + sitePath.includes(label) + ) + if (!details) { + return null + } + return ( + + {details.name}{' '} + + {emoji('📎')} + + + } + icon={details.icône} + maximum={stats.simulators[choicesimulators].visites.reduce( + (a, b) => Math.max(a, b.nb_visits), + 0 + )} + unit="visiteurs" + /> + ) + } + )} +
+
+ +

Origine du trafic

+ { + setChoicesimulators(event.target.value as MonthPeriod) + }} + value={choicesimulators} + /> +
+ + ({ + value: data.nb_visits, + key: data.label, + legend: capitalise0(data.label), + color: palettes[i][0] + })) + .reverse()} + /> +
+
+

Avis des visiteurs

+ + + + +

+ Ces indicateurs sont calculés à partir des boutons de retours affichés + en bas de toutes les pages. +

+
+
+

Statut choisi le mois dernier

+ {stats.statusChosen.map(x => ( + Math.max(a, b.nb_visits), + 0 + )} + unit="visiteurs" + /> + ))} +
+ + + + ) +} + +const weekEndDays = groupWith( + (a, b) => { + const dayAfterA = new Date(a) + dayAfterA.setDate(dayAfterA.getDate() + 1) + return dayAfterA.toISOString().substring(0, 10) === b + }, + stats.dailyVisits + .map(({ date }) => new Date(date)) + .filter(date => date.getDay() === 0 || date.getDay() === 6) + .map(date => date.toISOString().substring(0, 10)) +) + +function PeriodSelector(props: React.ComponentProps<'select'>) { + const formatDate = (date: string) => + new Date(date).toLocaleString('default', { + month: 'long', + year: 'numeric' + }) + return ( + + {emoji('🗓')}{' '} + + + ) +} + +const Indicators = styled.div` + display: flex; + flex-direction: row; + justify-content: space-around; + margin: 2rem 0; +` + +type IndicatorProps = { + main?: string + subTitle?: string +} + +function Indicator({ main, subTitle }: IndicatorProps) { + return ( +
+
+ {main} +
+
{subTitle}
+
+ ) +} + +type LineChartVisitsProps = { + periodicity: Periodicity +} + +function LineChartVisits({ periodicity }: LineChartVisitsProps) { + const { color } = useContext(ThemeColorsContext) + const data = periodicity === 'daily' ? stats.dailyVisits : stats.monthlyVisits + + return ( + + + + formatDate(tickItem, periodicity)} + /> + + formatValue({ value: tickItem, language: 'fr' }) + } + /> + {periodicity === 'daily' ? ( + + ) : null} + } /> + {weekEndDays.map(days => + days.length === 2 ? ( + + ) : ( + + ) + )} + + + + ) +} + +function formatDate(date: string | Date, periodicity?: Periodicity) { + if (periodicity === 'monthly') { + return new Date(date).toLocaleString('default', { + month: 'short', + year: '2-digit' + }) + } else { + return new Date(date).toLocaleString('default', { + day: '2-digit', + month: '2-digit' + }) + } +} + +type CustomTooltipProps = { + active?: boolean + periodicity: Periodicity + payload?: any +} + +const CustomTooltip = ({ + active, + periodicity, + payload +}: CustomTooltipProps) => { + if (!active) { + return null + } + return ( + + ) +} + +const SectionTitle = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 2rem; + + h2 { + margin: 0; + } +` diff --git a/source/sites/mon-entreprise.fr/sitePaths.ts b/source/sites/mon-entreprise.fr/sitePaths.ts index 04129e035..ab719905b 100644 --- a/source/sites/mon-entreprise.fr/sitePaths.ts +++ b/source/sites/mon-entreprise.fr/sitePaths.ts @@ -127,6 +127,7 @@ export const constructLocalizedSitePath = (language: string) => { }, nouveautés: t('path.nouveautés', '/nouveautés'), budget: t('path.budget', '/budget'), + stats: t('path.stats', '/stats'), documentation: { index: t('path.documentation.index', '/documentation'), rule: (dottedName: DottedName) => '/' + encodeRuleName(dottedName) diff --git a/yarn.lock b/yarn.lock index 51f6db0f4..c7746e802 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1228,6 +1228,18 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/d3-path@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" + integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA== + +"@types/d3-shape@*": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.2.tgz#a41d9d6b10d02e221696b240caf0b5d0f5a588ec" + integrity sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w== + dependencies: + "@types/d3-path" "*" + "@types/history@*": version "4.7.5" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860" @@ -1411,6 +1423,20 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/recharts-scale@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/recharts-scale/-/recharts-scale-1.0.0.tgz#348c9220d6d9062c44a9d585d686644a97f7e25d" + integrity sha512-HR/PrCcxYb2YHviTqH7CMdL1TUhUZLTUKzfrkMhxm1HTa5mg/QtP8XMiuSPz6dZ6wecazAOu8aYZ5DqkNlgHHQ== + +"@types/recharts@^1.8.9": + version "1.8.9" + resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.9.tgz#2439f1138253500a1fadc1ae3534ce895a0dd0a3" + integrity sha512-J4sZYDfdbFf1aLzenOksdd2s8D/GWh8tftgaE3oMDkvgfOwba0d8DxP0hnE2+WzsSy4fSvHWqrKZv5r2hVH47A== + dependencies: + "@types/d3-shape" "*" + "@types/react" "*" + "@types/recharts-scale" "*" + "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -3469,7 +3495,7 @@ core-js@^0.8.3: resolved "https://registry.yarnpkg.com/core-js/-/core-js-0.8.4.tgz#c22665f1e0d1b9c3c5e1b08dabd1f108695e4fcf" integrity sha1-wiZl8eDRucPF4bCNq9HxCGleT88= -core-js@^2.4.0: +core-js@^2.4.0, core-js@^2.6.10: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== @@ -3771,6 +3797,69 @@ cypress@^3.6.1: url "0.11.0" yauzl "2.10.0" +d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf" + integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg== + +d3-format@1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.4.tgz#356925f28d0fd7c7983bfad593726fce46844030" + integrity sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw== + +d3-interpolate@1, d3-interpolate@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" + integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-scale@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-shape@^1.2.0: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.2.3.tgz#0c9a12ee28342b2037e5ea1cf0b9eb4dd75f29cb" + integrity sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA== + dependencies: + d3-time "1" + +d3-time@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + daggy@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/daggy/-/daggy-1.4.0.tgz#178b4722866e1c8fac7b91d4bbf171d3767c1573" @@ -3838,6 +3927,11 @@ decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decimal.js-light@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.0.tgz#ca7faf504c799326df94b0ab920424fdfc125348" + integrity sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg== + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -7259,6 +7353,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" @@ -7324,12 +7423,17 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: +lodash@4.17.15, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -9229,7 +9333,7 @@ quick-temp@^0.1.3: rimraf "^2.5.4" underscore.string "~3.3.4" -raf@^3.4.1: +raf@^3.4.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -9477,6 +9581,16 @@ react-redux@^7.0.3: prop-types "^15.7.2" react-is "^16.9.0" +react-resize-detector@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c" + integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ== + dependencies: + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + prop-types "^15.6.0" + resize-observer-polyfill "^1.5.0" + react-router-dom@^5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" @@ -9520,6 +9634,16 @@ react-side-effect@^1.1.0: dependencies: shallowequal "^1.0.1" +react-smooth@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.5.tgz#94ae161d7951cdd893ccb7099d031d342cb762ad" + integrity sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w== + dependencies: + lodash "~4.17.4" + prop-types "^15.6.0" + raf "^3.4.0" + react-transition-group "^2.5.0" + react-spring@=8.0.27: version "8.0.27" resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" @@ -9567,7 +9691,7 @@ react-transition-group@^1.2.0: prop-types "^15.5.6" warning "^3.0.0" -react-transition-group@^2.2.1: +react-transition-group@^2.2.1, react-transition-group@^2.5.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== @@ -9658,7 +9782,31 @@ recast@~0.11.12: private "~0.1.5" source-map "~0.5.0" -reduce-css-calc@^1.2.6: +recharts-scale@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.3.tgz#040b4f638ed687a530357292ecac880578384b59" + integrity sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" + integrity sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg== + dependencies: + classnames "^2.2.5" + core-js "^2.6.10" + d3-interpolate "^1.3.0" + d3-scale "^2.1.0" + d3-shape "^1.2.0" + lodash "^4.17.5" + prop-types "^15.6.0" + react-resize-detector "^2.3.0" + react-smooth "^1.0.5" + recharts-scale "^0.4.2" + reduce-css-calc "^1.3.0" + +reduce-css-calc@^1.2.6, reduce-css-calc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= @@ -9977,6 +10125,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"