Page /stats (#969)

Co-authored-by: Elodie Quandalle <elodie.quandalle@gmail.com>
pull/984/head
Maxime Quandalle 2020-04-23 17:44:41 +02:00 committed by GitHub
parent 4c5cf52bd8
commit e8072fe8e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1212 additions and 268 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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 }) => (
<div className="distribution-chart__bar-container">
<animated.div
className="distribution-chart__bar"
style={{
backgroundColor: color,
flex: styles.flex
}}
/>
<div
css={`
font-weight: bold;
margin-left: 1rem;
color: var(--textColorOnWhite);
`}
>
<Value maximumFractionDigits={0} unit={unit}>
{numberToPlot}
</Value>
</div>
</div>
)
let BranchIcône = ({ icône }) => (
<div className="distribution-chart__legend">
<span className="distribution-chart__icon">{emoji(icône)}</span>
</div>
)
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 (
<animated.div
ref={intersectionRef}
className="distribution-chart__item"
style={{ opacity: styles.opacity }}
>
{icon && <BranchIcône icône={icon} />}
<div className="distribution-chart__item-content">
<p className="distribution-chart__counterparts">
<span className="distribution-chart__branche-name">{title}</span>
<br />
{description && <small>{description}</small>}
</p>
<ChartItemBar
{...{
styles,
color,
numberToPlot,
unit
}}
/>
</div>
</animated.div>
)
}

View File

@ -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 (
<animated.div
ref={intersectionRef}
className="distribution-chart__item"
style={{ opacity: styles.opacity }}
>
<BranchIcône icône={(icon ?? branche.icons) as string} />
<div className="distribution-chart__item-content">
<p className="distribution-chart__counterparts">
<span className="distribution-chart__branche-name">
<RuleLink {...branche} />
</span>
<br />
<small>{branche.summary}</small>
</p>
<ChartItemBar
{...{
styles,
color,
montant,
total: distribution.total
}}
/>
</div>
</animated.div>
<BarChartBranch
value={value}
maximum={maximum}
title={<RuleLink {...branch} />}
icon={icon ?? branch.icons}
description={branch.summary}
unit="€"
/>
)
}
type ChartItemBarProps = {
styles: React.CSSProperties
color: string
montant: number
}
let ChartItemBar = ({ styles, color, montant }: ChartItemBarProps) => (
<div className="distribution-chart__bar-container">
<animated.div
className="distribution-chart__bar"
style={{
backgroundColor: color,
...styles
}}
/>
<div
css={`
font-weight: bold;
margin-left: 1rem;
color: var(--textColorOnWhite);
`}
>
<Value maximumFractionDigits={0} unit="€">
{montant}
</Value>
</div>
</div>
)
let BranchIcône = ({ icône }: { icône: string }) => (
<div className="distribution-chart__legend">
<span className="distribution-chart__icon">{emoji(icône)}</span>
</div>
)

View File

@ -34,17 +34,16 @@ export default function MoreInfosOnUs() {
<div className="ui__ small simple button">Découvrir</div>
</Link>
)}
<a
href="https://mon-entreprise.fr/stats"
className="ui__ interactive card box"
>
<div className="ui__ big box-icon">{emoji('📊')}</div>
<h3>Les statistiques</h3>
<p className="ui__ notice" css="flex: 1">
Quel est notre impact ?
</p>
<div className="ui__ small simple button">Découvrir</div>
</a>
{!pathname.startsWith(sitePaths.stats) && (
<Link className="ui__ interactive card box" to={sitePaths.stats}>
<div className="ui__ big box-icon">{emoji('📊')}</div>
<h3>Les statistiques</h3>
<p className="ui__ notice" css="flex: 1">
Quel est notre impact ?
</p>
<div className="ui__ small simple button">Découvrir</div>
</Link>
)}
{!pathname.startsWith(sitePaths.budget) && (
<Link className="ui__ interactive card box" to={sitePaths.budget}>
<div className="ui__ big box-icon">{emoji('💶')}</div>

View File

@ -81,15 +81,19 @@ export function roundedPercentages(values: Array<number>) {
}
type StackedBarChartProps = {
data: Array<{ color?: string } & EvaluatedRule<DottedName>>
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) {
// <BarItem /> 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 }) => (
<BarItem
style={{
width: `${percentage}%`,
backgroundColor: color || 'green'
}}
key={dottedName}
key={key}
/>
))}
</BarStack>
<BarStackLegend>
{dataWithPercentage.map(({ percentage, color, ...rule }) => (
<BarStackLegendItem key={rule.dottedName}>
{dataWithPercentage.map(({ key, percentage, color, legend }) => (
<BarStackLegendItem key={key}>
<SmallCircle style={{ backgroundColor: color }} />
<RuleLink {...rule}>{capitalise0(rule.title)}</RuleLink>
<strong>{legend}</strong>
<strong>{percentage} %</strong>
</BarStackLegendItem>
))}
@ -125,3 +129,20 @@ export default function StackedBarChart({ data }: StackedBarChartProps) {
</animated.div>
)
}
type StackedRulesChartProps = {
data: Array<{ color?: string } & EvaluatedRule<DottedName>>
}
export default function StackedRulesChart({ data }: StackedRulesChartProps) {
return (
<StackedBarChart
data={data.map(rule => ({
...rule,
key: rule.dottedName,
value: rule.nodeValue,
legend: <RuleLink {...rule}>{capitalise0(rule.title)}</RuleLink>
}))}
/>
)
}

View File

@ -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])
]
}

View File

@ -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,

View File

@ -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</0><1>Estimating the social security contributions of an
artist or author</1>
assimilé: |
<0>"Assimilé-salarié"</0>
<1>Calculate the income of an officer of a minority SAS, SASU or SARL</1>
auto: |
<0>Auto-entrepreneur</0>
<1>Calculate the income (or turnover) of an auto-entrepreneur</1>
comparaison: >
<0>Status comparison</0>
<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 account</1> <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 SARL</1>
salarié: >
<0>Employee</0>
<1>Calculate the net, gross, or total salary of an employee, trainee, or
similar</1>
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: >-

View File

@ -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()

View File

@ -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 &gt; @rate%20page%20usefulness',
date: 'previous5'
})
)
const APIsimulator = await fetch(
apiURL({
method: 'Events.getCategory',
label: 'Feedback &gt; @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()

View File

@ -1,2 +1,3 @@
require('./dottednames.js')
require('./fetch-releases.js')
require('./fetch-stats.js')

13
source/scripts/utils.js Normal file
View File

@ -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))
}

View File

@ -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 = () => {
/>
<Route path={sitePaths.integration.index} component={Integration} />
<Route path={sitePaths.nouveautés} component={Nouveautés} />
<Route path={sitePaths.stats} component={Stats} />
<Route path={sitePaths.coronavirus} component={Coronavirus} />
<Route path={sitePaths.budget} component={Budget} />
<Route exact path="/dev/sitemap" component={Sitemap} />

View File

@ -66,7 +66,7 @@ const Footer = () => {
{' • '}
<Link to={sitePaths.nouveautés}>Nouveautés</Link>
{' • '}
<a href="https://mon-entreprise.fr/stats">Stats</a>
<Link to={sitePaths.stats}>Stats</Link>
{' • '}
<Link to={sitePaths.budget}>Budget</Link>
</>

View File

@ -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 (
<>
<button onClick={handleOpen} className="ui__ link-button">
<Trans>Vie privée</Trans>
{label ?? <Trans>Vie privée</Trans>}
</button>
{opened && (
<Overlay onClose={handleClose} style={{ textAlign: 'left' }}>

View File

@ -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 (
<section>
<h2>
@ -215,7 +214,7 @@ function RepartitionCotisations() {
{cotisations.map(cotisation => (
<DistributionBranch
key={cotisation.dottedName}
distribution={{ maximum, total }}
maximum={maximum}
{...cotisation}
/>
))}

View File

@ -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<SimulatorMetaData>
}
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' }}
>
<Link
className="ui__ interactive card box"
to={{
state: { fromSimulateurs: true },
pathname: sitePaths.simulateurs['assimilé-salarié']
}}
>
<div className="ui__ big box-icon">{emoji('☂️')}</div>
<Trans i18nKey="simulateurs.accueil.assimilé">
<h3>Assimilé salarié</h3>
<p className="ui__ notice" css="flex: 1">
Calculer le revenu d'un dirigeant de SAS, SASU ou SARL
minoritaire
</p>
</Trans>
</Link>
<Link
className="ui__ interactive card box"
to={{
state: { fromSimulateurs: true },
pathname: sitePaths.simulateurs.indépendant
}}
>
<div className="ui__ big box-icon">{emoji('👩‍🔧')}</div>
<Trans i18nKey="simulateurs.accueil.indépendant">
<h3>Indépendant</h3>
<p className="ui__ notice" css="flex: 1">
Calculer le revenu d'un dirigeant de EURL, EI, ou SARL
majoritaire
</p>
</Trans>
</Link>
<Link
className="ui__ interactive card box"
to={{
state: { fromSimulateurs: true },
pathname: sitePaths.simulateurs['auto-entrepreneur']
}}
>
<div className="ui__ big box-icon">{emoji('🚶‍♂️')}</div>
<Trans i18nKey="simulateurs.accueil.auto">
<h3>Auto-entrepreneur</h3>
<p className="ui__ notice" css="flex: 1">
Calculer le revenu (ou le chiffre d'affaires) d'un
auto-entrepreneur
</p>
</Trans>
</Link>
<Link
className="ui__ interactive card box"
to={{
state: { fromSimulateurs: true },
pathname: sitePaths.simulateurs.salarié
}}
>
<div className="ui__ big box-icon">{emoji('🤝')}</div>
<Trans i18nKey="simulateurs.accueil.salarié">
<h3>Salarié</h3>
<p className="ui__ notice" css="flex: 1">
Calculer le salaire net, brut, ou total d'un salarié, stagiaire,
ou assimilé
</p>
</Trans>
</Link>
<Link
className="ui__ interactive card box"
to={{
state: { fromSimulateurs: true },
pathname: sitePaths.simulateurs['artiste-auteur']
}}
>
<div className="ui__ big box-icon">{emoji('👩‍🎨')}</div>
<Trans i18nKey="simulateurs.accueil.artiste-auteur">
<h3>Artiste-auteur</h3>
<p className="ui__ notice" css="flex: 1">
Estimer les cotisations sociales d'un artiste ou auteur
</p>
</Trans>
</Link>
<Link
className="ui__ interactive card box"
to={{
state: { fromSimulateurs: true },
pathname: sitePaths.simulateurs.comparaison
}}
>
<div className="ui__ big box-icon">{emoji('📊')}</div>
<Trans i18nKey="simulateurs.accueil.comparaison">
<h3>Comparaison statuts</h3>
<p className="ui__ notice" css="flex: 1">
Simulez les différences entre les régimes (cotisations,
retraite, maternité, maladie, etc.)
</p>
</Trans>
</Link>
{simulatorsMetadata
.filter(({ name }) => name !== 'Coronavirus')
.map(({ name, description, sitePath, icône }) => (
<Link
className="ui__ interactive card box"
key={sitePath}
to={{
state: { fromSimulateurs: true },
pathname: sitePath
}}
>
<div className="ui__ big box-icon">{emoji(icône)}</div>
<h3>{name}</h3>
<p className="ui__ notice" css="flex: 1">
{description}
</p>
</Link>
))}
</div>
</section>
<section>

View File

@ -0,0 +1,12 @@
import React, { Suspense } from 'react'
let Stats = React.lazy(() => import('./Stats'))
export default function LazyStats() {
return (
<Suspense
fallback={<p className="ui__ lead">Chargement de la page stats</p>}
>
<Stats />
</Suspense>
)
}

View File

@ -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<Periodicity>('monthly')
const [choicesimulators, setChoicesimulators] = useState<MonthPeriod>(
'oneMonthAgo'
)
const { palettes } = useContext(ThemeColorsContext)
const simulatorsMetadata = useSimulatorsMetadata()
return (
<>
<ScrollToTop />
<h1>
Statistiques <>{emoji('📊')}</>
</h1>
<p>
Découvrez nos statistiques d'utilisation mises à jour quotidiennement.
<br />
Les données recueillies sont anonymisées.{' '}
<Privacy label="En savoir plus" />
</p>
<section>
<SectionTitle>
<h2>Nombre de visites</h2>
<span>
{emoji('🗓')}{' '}
<select
onChange={event => {
setChoice(event.target.value as Periodicity)
}}
value={choice}
>
<option value="monthly">les derniers mois</option>
<option value="daily">les derniers jours</option>
</select>
</span>
</SectionTitle>
<LineChartVisits periodicity={choice} />
<Indicators>
<Indicator main="1,7 million" subTitle="Visiteurs en 2019" />
<Indicator
main="52,9%"
subTitle="Convertissent en lançant une simulation"
/>
</Indicators>
</section>
<section>
<SectionTitle>
<h2>Nombre d'utilisation des simulateurs</h2>
<PeriodSelector
onChange={event => {
setChoicesimulators(event.target.value as MonthPeriod)
}}
value={choicesimulators}
/>
</SectionTitle>
{stats.simulators[choicesimulators].visites.map(
({ label, nb_visits }) => {
const details = simulatorsMetadata.find(({ sitePath }) =>
sitePath.includes(label)
)
if (!details) {
return null
}
return (
<BarChartBranch
key={label}
value={nb_visits}
title={
<>
{details.name}{' '}
<Link
className="distribution-chart__link_icone"
to={{
state: { fromSimulateurs: true },
pathname: details.sitePath
}}
title="Accéder au simulateur"
css="font-size:0.75em"
>
{emoji('📎')}
</Link>
</>
}
icon={details.icône}
maximum={stats.simulators[choicesimulators].visites.reduce(
(a, b) => Math.max(a, b.nb_visits),
0
)}
unit="visiteurs"
/>
)
}
)}
</section>
<section>
<SectionTitle>
<h2>Origine du trafic</h2>
<PeriodSelector
onChange={event => {
setChoicesimulators(event.target.value as MonthPeriod)
}}
value={choicesimulators}
/>
</SectionTitle>
<StackedBarChart
data={stats.channelType[choicesimulators].visites
.map((data, i) => ({
value: data.nb_visits,
key: data.label,
legend: capitalise0(data.label),
color: palettes[i][0]
}))
.reverse()}
/>
</section>
<section>
<h2>Avis des visiteurs</h2>
<Indicators>
<Indicator
main={formatPercentage(stats.feedback.simulator)}
subTitle="Taux de satisfaction sur les simulateurs"
/>
<Indicator
main={formatPercentage(stats.feedback.content)}
subTitle="Taux de satisfaction sur le contenu"
/>
</Indicators>
<p>
Ces indicateurs sont calculés à partir des boutons de retours affichés
en bas de toutes les pages.
</p>
</section>
<section>
<h2>Statut choisi le mois dernier</h2>
{stats.statusChosen.map(x => (
<BarChartBranch
key={x.label}
value={x.nb_visits}
title={capitalise0(x.label)}
maximum={stats.statusChosen.reduce(
(a, b) => Math.max(a, b.nb_visits),
0
)}
unit="visiteurs"
/>
))}
</section>
<MoreInfosOnUs />
</>
)
}
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 (
<span>
{emoji('🗓')}{' '}
<select {...props}>
{monthPeriods.map(monthPeriod => (
<option key={monthPeriod} value={monthPeriod}>
{formatDate(stats.simulators[monthPeriod].date)}
</option>
))}
</select>
</span>
)
}
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 (
<div
css={`
text-align: center;
width: 210px;
`}
>
<div
css={`
font-size: 2.3rem;
`}
>
{main}
</div>
<div>{subTitle}</div>
</div>
)
}
type LineChartVisitsProps = {
periodicity: Periodicity
}
function LineChartVisits({ periodicity }: LineChartVisitsProps) {
const { color } = useContext(ThemeColorsContext)
const data = periodicity === 'daily' ? stats.dailyVisits : stats.monthlyVisits
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5
}}
>
<CartesianGrid />
<XAxis
dataKey="date"
tickFormatter={tickItem => formatDate(tickItem, periodicity)}
/>
<YAxis
dataKey="visiteurs"
tickFormatter={tickItem =>
formatValue({ value: tickItem, language: 'fr' })
}
/>
{periodicity === 'daily' ? (
<Legend
payload={[
{
value: 'Week-End',
type: 'rect',
color: '#e5e5e5',
id: 'weedkend'
}
]}
/>
) : null}
<Tooltip content={<CustomTooltip periodicity={periodicity} />} />
{weekEndDays.map(days =>
days.length === 2 ? (
<ReferenceArea
key={days[0]}
x1={days[0]}
x2={days[1]}
strokeOpacity={0.3}
/>
) : (
<ReferenceArea key={days[0]} x1={days[0]} strokeOpacity={0.3} />
)
)}
<Line
type="monotone"
dataKey="visiteurs"
stroke={color}
strokeWidth={3}
animationDuration={500}
/>
</LineChart>
</ResponsiveContainer>
)
}
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 (
<DefaultTooltipContent
payload={[
{
value: formatDate(payload[0].payload.date, periodicity)
},
{
value: formatValue({
value: payload[0].payload.visiteurs,
language: 'fr'
}),
unit: ' visiteurs'
}
]}
/>
)
}
const SectionTitle = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
h2 {
margin: 0;
}
`

View File

@ -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)

163
yarn.lock
View File

@ -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"