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) => (
-
-)
-
-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-author0><1>Estimating the social security contributions of an
- artist or author1>
- assimilé: |
- <0>"Assimilé-salarié"0>
- <1>Calculate the income of an officer of a minority SAS, SASU or SARL1>
- auto: |
- <0>Auto-entrepreneur0>
- <1>Calculate the income (or turnover) of an auto-entrepreneur1>
- comparaison: >
- <0>Status comparison0>
-
- <1>Simulate the differences between the plans (contributions, retirement,
- maternity, illness, etc.)1>
description: >-
<0>All the simulators on this site are:0>
@@ -1040,14 +1031,6 @@ simulateurs:
to increase the number of devices taken into account1> <2>Developed in
partnership with the Urssaf (the contribution collector entity in
France)<2> 1>
- indépendant: |
- <0>"Indépendant"0>
- <1>Calculate the income of a majority manager of EURL, EI, or SARL1>
- salarié: >
- <0>Employee0>
-
- <1>Calculate the net, gross, or total salary of an employee, trainee, or
- similar1>
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"