From 363a42f56b5120e316d962190c506933e9336ed0 Mon Sep 17 00:00:00 2001
From: Anton Angelov <123360440+tongo-angelov@users.noreply.github.com>
Date: Mon, 25 Sep 2023 14:07:03 +0300
Subject: [PATCH] Added campaign expenses chart (#1605)
* added campaign expenses chart
* added expense chart labels
---
package.json | 3 +
public/locales/bg/expenses.json | 3 +-
public/locales/en/expenses.json | 1 +
.../client/campaigns/CampaignDetails.tsx | 27 +++-
.../campaigns/CampaignPublicExpensesChart.tsx | 127 ++++++++++++++++++
yarn.lock | 38 ++++++
6 files changed, 193 insertions(+), 6 deletions(-)
create mode 100644 src/components/client/campaigns/CampaignPublicExpensesChart.tsx
diff --git a/package.json b/package.json
index 34f12cc97..1275fd58c 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,8 @@
"@tryghost/content-api": "^1.11.4",
"axios": "0.21.4",
"axios-hooks": "2.7.0",
+ "chart.js": "^4.4.0",
+ "chartjs-plugin-datalabels": "^2.2.0",
"date-fns": "2.24.0",
"dompurify": "^3.0.3",
"formik": "2.2.9",
@@ -62,6 +64,7 @@
"quill-blot-formatter": "^1.0.5",
"quill-html-edit-button": "^2.2.12",
"react": "18.2.0",
+ "react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0",
"react-gtm-module": "2.0.11",
"react-i18next": "^11.17.1",
diff --git a/public/locales/bg/expenses.json b/public/locales/bg/expenses.json
index ae7456cca..7f49246fd 100644
--- a/public/locales/bg/expenses.json
+++ b/public/locales/bg/expenses.json
@@ -80,7 +80,8 @@
"info": "Информация за разход"
},
"description": "Всички разходи",
- "reported": "Общо отчетени",
+ "reported": "Общо отчетени разходи",
+ "donations": "Общо събрани дарения",
"uploaded-documents": "Прикачени документи",
"uploaded-files": "Прикачени файлове",
"new-files": "Нови файлове",
diff --git a/public/locales/en/expenses.json b/public/locales/en/expenses.json
index af46d8032..86250e496 100644
--- a/public/locales/en/expenses.json
+++ b/public/locales/en/expenses.json
@@ -78,6 +78,7 @@
},
"description": "All expenses",
"reported": "Total reported",
+ "donations": "Total donations",
"uploaded-documents": "Uploaded documents",
"uploaded-files": "Uploaded files",
"new-files": "New files",
diff --git a/src/components/client/campaigns/CampaignDetails.tsx b/src/components/client/campaigns/CampaignDetails.tsx
index 4d14b40a2..f75c47610 100644
--- a/src/components/client/campaigns/CampaignDetails.tsx
+++ b/src/components/client/campaigns/CampaignDetails.tsx
@@ -7,7 +7,7 @@ import { CampaignResponse } from 'gql/campaigns'
import 'react-quill/dist/quill.bubble.css'
-import { Divider, Grid, Tooltip, Typography } from '@mui/material'
+import { Divider, Grid, Stack, Tooltip, Typography } from '@mui/material'
import SecurityIcon from '@mui/icons-material/Security'
import { styled } from '@mui/material/styles'
@@ -26,6 +26,7 @@ import { routes } from 'common/routes'
import { useCanEditCampaign } from 'common/hooks/campaigns'
import { moneyPublic } from 'common/util/money'
import ReceiptLongIcon from '@mui/icons-material/ReceiptLong'
+import CampaignPublicExpensesChart from './CampaignPublicExpensesChart'
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false })
const CampaignNewsSection = dynamic(() => import('./CampaignNewsSection'), { ssr: false })
@@ -144,10 +145,26 @@ export default function CampaignDetails({ campaign }: Props) {
-
- {t('expenses:reported')}:{' '}
- {moneyPublic(totalExpenses || 0, campaign.currency)}
-
+
+
+
+
+ {t('expenses:reported')}: {moneyPublic(totalExpenses || 0, campaign.currency)}
+
+
+ {t('expenses:donations')}:{' '}
+ {moneyPublic(campaign.summary.reachedAmount, campaign.currency)}
+
+
+
+
+
+
diff --git a/src/components/client/campaigns/CampaignPublicExpensesChart.tsx b/src/components/client/campaigns/CampaignPublicExpensesChart.tsx
new file mode 100644
index 000000000..b207f9485
--- /dev/null
+++ b/src/components/client/campaigns/CampaignPublicExpensesChart.tsx
@@ -0,0 +1,127 @@
+import React from 'react'
+import { observer } from 'mobx-react'
+import { useTranslation } from 'next-i18next'
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Colors,
+ Tooltip,
+ Legend,
+ TooltipItem,
+} from 'chart.js'
+import { Bar } from 'react-chartjs-2'
+import ChartDataLabels from 'chartjs-plugin-datalabels'
+
+import { useCampaignApprovedExpensesList } from 'common/hooks/expenses'
+import { fromMoney, moneyPublic, toMoney } from 'common/util/money'
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Colors,
+ Tooltip,
+ Legend,
+ ChartDataLabels,
+)
+
+type ExpenseDataset = {
+ type: string
+ total: number
+}
+
+type Props = {
+ slug: string
+ height: number
+ reachedAmount: number
+ currency?: string
+}
+
+export default observer(function CampaignPublicExpensesChart({
+ slug,
+ height,
+ reachedAmount,
+ currency,
+}: Props) {
+ const { t } = useTranslation('')
+ const { data: campaignExpenses } = useCampaignApprovedExpensesList(slug)
+
+ const expenses: ExpenseDataset[] = []
+
+ campaignExpenses?.forEach(({ type, amount }) => {
+ const exists = expenses.find((e) => e.type === type)
+ if (exists) exists.total += fromMoney(amount)
+ else expenses.push({ type, total: fromMoney(amount) })
+ })
+
+ expenses.sort((a, b) => b.total - a.total)
+
+ const options = {
+ indexAxis: 'y' as const,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ stacked: true,
+ min: 0,
+ max: fromMoney(reachedAmount),
+ },
+ y: {
+ stacked: true,
+ display: false,
+ },
+ },
+ elements: {
+ bar: {
+ borderWidth: 1,
+ },
+ },
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'bottom' as const,
+ },
+ colors: {
+ enabled: true,
+ },
+ tooltip: {
+ callbacks: {
+ label: (context: TooltipItem<'bar'>) =>
+ ` ${context.dataset.label + ':' || ''} ${moneyPublic(
+ toMoney(context.parsed.x),
+ currency,
+ )}`,
+ },
+ },
+ datalabels: {
+ formatter: function (value: number) {
+ return moneyPublic(toMoney(value), currency)
+ },
+ display: 'auto',
+ color: 'black',
+ labels: {
+ value: {
+ font: {
+ weight: 400,
+ },
+ },
+ },
+ },
+ },
+ }
+
+ const data = {
+ labels: [''],
+ datasets: expenses.map((expense) => {
+ return {
+ label: t('expenses:field-types.' + expense.type),
+ data: [expense.total],
+ }
+ }),
+ }
+
+ return
+})
diff --git a/yarn.lock b/yarn.lock
index 6c7c5de8f..aa8bab762 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1296,6 +1296,13 @@ __metadata:
languageName: node
linkType: hard
+"@kurkle/color@npm:^0.3.0":
+ version: 0.3.2
+ resolution: "@kurkle/color@npm:0.3.2"
+ checksum: 79e97b31f8f6efb28c69d373f94b0c7480226fe8ec95221f518ac998e156444a496727ce47de6d728eb5c3369288e794cba82cae34253deb0d472d3bfe080e49
+ languageName: node
+ linkType: hard
+
"@lexical/clipboard@npm:0.11.3, @lexical/clipboard@npm:^0.11.1":
version: 0.11.3
resolution: "@lexical/clipboard@npm:0.11.3"
@@ -4960,6 +4967,24 @@ __metadata:
languageName: node
linkType: hard
+"chart.js@npm:^4.4.0":
+ version: 4.4.0
+ resolution: "chart.js@npm:4.4.0"
+ dependencies:
+ "@kurkle/color": ^0.3.0
+ checksum: 5ee2d99b78608025525b5790af17178fdaa5adc3294e082deba2718029b0496109ba124f1b08dd1e4c8a04d6e842be7f384f2cfe9a11df8d1c6fe884acece52b
+ languageName: node
+ linkType: hard
+
+"chartjs-plugin-datalabels@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "chartjs-plugin-datalabels@npm:2.2.0"
+ peerDependencies:
+ chart.js: ">=3.0.0"
+ checksum: 26086a908a8e88507959b7aaf798b2d9794ea95f7a5889b8bb9f6b9f3437a7e2fdf18952d3ba403b2ff78e5b70452439fb323bd0dfe76e9d7d1dae1328dacb99
+ languageName: node
+ linkType: hard
+
"chokidar@npm:>=3.0.0 <4.0.0":
version: 3.5.3
resolution: "chokidar@npm:3.5.3"
@@ -11314,6 +11339,8 @@ __metadata:
all-contributors-cli: ^6.20.0
axios: 0.21.4
axios-hooks: 2.7.0
+ chart.js: ^4.4.0
+ chartjs-plugin-datalabels: ^2.2.0
date-fns: 2.24.0
depcheck: ^1.4.3
dompurify: ^3.0.3
@@ -11342,6 +11369,7 @@ __metadata:
quill-blot-formatter: ^1.0.5
quill-html-edit-button: ^2.2.12
react: 18.2.0
+ react-chartjs-2: ^5.2.0
react-dom: 18.2.0
react-gtm-module: 2.0.11
react-i18next: ^11.17.1
@@ -11785,6 +11813,16 @@ __metadata:
languageName: node
linkType: hard
+"react-chartjs-2@npm:^5.2.0":
+ version: 5.2.0
+ resolution: "react-chartjs-2@npm:5.2.0"
+ peerDependencies:
+ chart.js: ^4.1.1
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ checksum: ace702185be1450e5888a8bcd8b5fc1995067e3b11d236764a67f5567a3d7c32ff16923b8d48d3d39bda6e45135da6c044c9b43fbe8e1978f95aca9d2c0ce348
+ languageName: node
+ linkType: hard
+
"react-devtools-inline@npm:4.4.0":
version: 4.4.0
resolution: "react-devtools-inline@npm:4.4.0"