From 76eef86e2c6cf36796f10c416a2aaeaf42ff78f5 Mon Sep 17 00:00:00 2001 From: Bjarke Madsen <bjarke@nordu.net> Date: Tue, 28 Mar 2023 17:11:42 +0200 Subject: [PATCH] Refactor FundingSource page --- webapp/src/Schema.tsx | 18 +- webapp/src/helpers/dataconversion.tsx | 102 +++++++++++ webapp/src/pages/FundingSource.tsx | 246 +++++--------------------- 3 files changed, 158 insertions(+), 208 deletions(-) create mode 100644 webapp/src/helpers/dataconversion.tsx diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx index d2f88d70..380237d6 100644 --- a/webapp/src/Schema.tsx +++ b/webapp/src/Schema.tsx @@ -27,26 +27,22 @@ export interface Budget { } export interface FundingSource { - CLIENT_INSTITUTIONS: string, - COMMERCIAL: string, - EUROPEAN_FUNDING: string, - GOV_PUBLIC_BODIES: string, + CLIENT_INSTITUTIONS: number, + COMMERCIAL: number, + EUROPEAN_FUNDING: number, + GOV_PUBLIC_BODIES: number, + OTHER: number, NREN: string, - OTHER: string, YEAR: number, id: number } -export interface FS { - data: [FundingSource] -} - -export interface FundingGraphMatrix { +export interface FundingSourceDataset { labels: string[], datasets: { label: string, - data: string[], + data: number[], backgroundColor: string borderRadius: number, borderSkipped: boolean, diff --git a/webapp/src/helpers/dataconversion.tsx b/webapp/src/helpers/dataconversion.tsx new file mode 100644 index 00000000..a3bb36ac --- /dev/null +++ b/webapp/src/helpers/dataconversion.tsx @@ -0,0 +1,102 @@ +import { cartesianProduct } from 'cartesian-product-multiple-arrays'; + +import { + FundingSource, + FundingSourceDataset +} from "../Schema"; + +const DEFAULT_FUNDING_SOURCE_DATA = [ + { + "CLIENT_INSTITUTIONS": "0.0", + "COMMERCIAL": "0.0", + "EUROPEAN_FUNDING": "0.0", + "GOV_PUBLIC_BODIES": "0.0", + "NREN": "", + "OTHER": "0.0", + "YEAR": 0, + "id": 0 + }] + +function getColorMap() { + const rgbToHex = (r: number, g: number, b: number) => '#' + [r, g, b].map(x => { + const hex = x.toString(16) + return hex.length === 1 ? '0' + hex : hex + }).join('') + + let colorMap = new Map<string, string>(); + colorMap.set("CLIENT INSTITUTIONS", rgbToHex(157, 40, 114)) + colorMap.set("COMMERCIAL", rgbToHex(241, 224, 79)) + colorMap.set("EUROPEAN FUNDING", rgbToHex(219, 42, 76)) + colorMap.set("GOV/PUBLIC_BODIES", rgbToHex(237, 141, 24)) + colorMap.set("OTHER", rgbToHex(137, 166, 121)) + return colorMap +} + +function CreateDataLookup(data: FundingSource[]) { + let dataLookup = new Map<string, Map<string, number>>(); + + data.forEach((item: FundingSource) => { + const lookupKey = `${item.NREN}/${item.YEAR}` + + let fundingSourceMap = dataLookup.get(lookupKey) + if (!fundingSourceMap) { + fundingSourceMap = new Map<string, number>(); + } + fundingSourceMap.set("CLIENT INSTITUTIONS", item.CLIENT_INSTITUTIONS) + fundingSourceMap.set("COMMERCIAL", item.COMMERCIAL) + fundingSourceMap.set("EUROPEAN FUNDING", item.EUROPEAN_FUNDING) + fundingSourceMap.set("GOV/PUBLIC_BODIES", item.GOV_PUBLIC_BODIES) + fundingSourceMap.set("OTHER", item.OTHER) + dataLookup.set(lookupKey, fundingSourceMap) + }) + return dataLookup +} + +export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) => { + const data = fundingSourcesData ?? DEFAULT_FUNDING_SOURCE_DATA; + const dataLookup = CreateDataLookup(data) + + const labelsYear = [...new Set(data.map((item: FundingSource) => item.YEAR))]; + const labelsNREN = [...new Set(data.map((item: FundingSource) => item.NREN))]; + const fundingSources = [ + "CLIENT INSTITUTIONS", + "COMMERCIAL", + "EUROPEAN FUNDING", + "GOV/PUBLIC_BODIES", + "OTHER" + ] + const fundingSourcesPerYear = cartesianProduct(fundingSources, labelsYear) + + const colorMap = getColorMap(); + const fundingSourceDataset = fundingSourcesPerYear.map(function ([fundingSource, year]) { + + const color = colorMap.get(fundingSource)!; + return { + backgroundColor: color, + label: fundingSource + "(" + year + ")", + data: labelsNREN.map(nren => { + // ensure that the data is in the same order as the labels + const lookupKey = `${nren}/${year}` + const dataForYear = dataLookup.get(lookupKey) + if (!dataForYear) { + return 0 + } + return dataForYear.get(fundingSource) ?? 0 + }), + stack: year, + borderRadius: 10, + borderSkipped: false, + barPercentage: 0.5, + borderWidth: 0.5, + categoryPercentage: 0.8 + + } + }) + + const dataResponse: FundingSourceDataset = { + // datasets: datasetFunding, + datasets: fundingSourceDataset, + labels: labelsNREN.map(l => l.toString()) + } + return dataResponse; +} \ No newline at end of file diff --git a/webapp/src/pages/FundingSource.tsx b/webapp/src/pages/FundingSource.tsx index 1514cf6d..cdc3db32 100644 --- a/webapp/src/pages/FundingSource.tsx +++ b/webapp/src/pages/FundingSource.tsx @@ -1,7 +1,6 @@ -import React, { ReactElement, useEffect, useState } from 'react'; -import { ChartOptions, scales, Tick } from 'chart.js'; +import React, { useEffect, useState } from 'react'; import { Bar } from 'react-chartjs-2'; -import { cartesianProduct } from 'cartesian-product-multiple-arrays'; +import { createFundingSourceDataset } from "../helpers/dataconversion"; import { Chart as ChartJS, @@ -13,14 +12,9 @@ import { Legend, } from 'chart.js'; import { - Budget, - BudgetMatrix, - DataEntrySection, FundingSource, - FS, FundingGraphMatrix + FundingSourceDataset } from "../Schema"; -// import _default from "chart.js/dist/plugins/plugin.tooltip"; -// import numbers = _default.defaults.animations.numbers; ChartJS.register( @@ -32,15 +26,34 @@ ChartJS.register( Legend ); -export const option = { +const EMPTY_DATASET = { + datasets: [ + { + backgroundColor: '', + data: [], + label: '', + borderRadius: 0, + borderSkipped: false, + barPercentage: 0, + borderWidth: 0, + stack: '0', + categoryPercentage: 0.5 + }], + labels: [] +} + +export const chartOptions = { + maintainAspectRatio: false, plugins: { legend: { + display: false, labels: { boxWidth: 20, boxHeight: 30, pointStyle: "rectRounded", borderRadius: 6, useBorderRadius: true, + }, }, }, @@ -60,209 +73,48 @@ export const option = { stacked: true, }, }, - indexAxis: 'y', + indexAxis: "y" as const }; - - - -function FundingSourcePage(): ReactElement { - function api<T>(url: string, options: RequestInit | undefined = undefined): Promise<T> { - return fetch(url, options) - .then((response) => { - if (!response.ok) { - return response.text().then((message) => { - console.error(`Failed to load datax: ${message}`, response.status); - throw new Error("The data could not be loaded, check the logs for details."); - }); - } - - return response.json() as Promise<T>; - }) +async function getData(): Promise<FundingSource[]> { + try { + const response = await fetch('/api/funding/'); + return response.json(); + } catch (error) { + console.error(`Failed to load data: ${error}`); + throw error; } +} - const [fundingMatrixResponse, setFundingMatrixResponse] = useState<FundingGraphMatrix>(); - const [fundingSourceResponse, setFundingSource] = useState<FundingSource[]>(); - const [dataEntrySection, setDataEntrySection] = useState<DataEntrySection>(); - const [selectedDataEntry, setSelectedDataEntry] = useState<number>(0); +function FundingSourcePage() { + const [fundingSourceDataset, setDataset] = useState<FundingSourceDataset>(); + const [fundingSourceData, setFundingSourceData] = useState<FundingSource[]>(); useEffect(() => { const loadData = async () => { - if (fundingSourceResponse == undefined) { - api<FS>('/api/funding/', {}) - .then((fundingSources: FS) => { - console.log('fundingSource:', fundingSources) - const entry = dataEntrySection?.items.find(i => i.id == selectedDataEntry) - console.log(selectedDataEntry, dataEntrySection, entry) - if (entry) - console.log("hello") - // options.plugins.title.text = entry.title; - setFundingSource(fundingSources.data) - convertToFundingSourcePerYearDataResponse(fundingSources.data) - }) - .catch(error => { - console.log(`Error fetching from API: ${error}`); - }) - } else { - convertToFundingSourcePerYearDataResponse(fundingSourceResponse) - } - + const _fundingData = await getData() + setFundingSourceData(_fundingData) } loadData() }, []) - const empty_funding_source_response = [ - { - "CLIENT_INSTITUTIONS": "0.0", - "COMMERCIAL": "0.0", - "EUROPEAN_FUNDING": "0.0", - "GOV_PUBLIC_BODIES": "0.0", - "NREN": "", - "OTHER": "0.0", - "YEAR": 0, - "id": 0 - }] - - const convertToFundingSourcePerYearDataResponse = (fundingSourcesResponse: FundingSource[]) => { - const fsResponse = fundingSourcesResponse != undefined ? fundingSourcesResponse : empty_funding_source_response; - const labelsYear = [...new Set(fsResponse.map((item: FundingSource) => item.YEAR))]; - const labelsNREN = [...new Set(fsResponse.map((item: FundingSource) => item.NREN))]; - const fundingComposition = [ - "CLIENT INSTITUTIONS", - "COMMERCIAL", - "EUROPEAN FUNDING", - "GOV/PUBLIC_BODIES", - "OTHER" - ] - const dataSetKey = cartesianProduct(fundingComposition, labelsYear) - console.log("Nrens : ", labelsNREN) - console.log("Years : ", labelsYear) - console.log(dataSetKey); - - function getRandomColor() { - const red = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); // generates a value between 00 and ff - const green = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); - const blue = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); - return `#${red}${green}${blue}`; - } - - const rgbToHex = (r: number, g: number, b: number) => '#' + [r, g, b].map(x => { - const hex = x.toString(16) - return hex.length === 1 ? '0' + hex : hex - }).join('') - - let colorMap = new Map<string, string>(); - colorMap.set("CLIENT INSTITUTIONS", rgbToHex(157, 40, 114)) - colorMap.set("COMMERCIAL", rgbToHex(241, 224, 79)) - colorMap.set("EUROPEAN FUNDING", rgbToHex(219, 42, 76)) - colorMap.set("GOV/PUBLIC_BODIES", rgbToHex(237, 141, 24)) - colorMap.set("OTHER", rgbToHex(137, 166, 121)) - - - const datasetFunding = dataSetKey.map(function (entry) { - - // const randomColor = getRandomColor(); - const color: string = colorMap.get(entry[0])!; - console.log(color) - return { - backgroundColor: color, - label: entry[0] + "(" + entry[1] + ")",//composition+year - data: labelsNREN.map(nren => dataPerCompositionPerYear(entry[1], nren, entry[0])), - stack: entry[1], - borderRadius: 10, - borderSkipped: false, - barPercentage: 0.5, - borderWidth: 0.5, - categoryPercentage: 0.8 - - } - }) - - function dataPerCompositionPerYear(year: number, nren: string, composition: string) { - let compValue = "" - fsResponse.find(function (entry, index) { - if (entry.YEAR == year && entry.NREN == nren) { - if (composition === "CLIENT INSTITUTIONS") - compValue = String(entry.CLIENT_INSTITUTIONS); - if (composition === "COMMERCIAL") - compValue = entry.COMMERCIAL; - if (composition === "EUROPEAN FUNDING") - compValue = entry.EUROPEAN_FUNDING; - if (composition === "GOV/PUBLIC_BODIES") - compValue = entry.GOV_PUBLIC_BODIES; - if (composition === "OTHER") - compValue = entry.OTHER; - } - }) - console.log(compValue) - return compValue; + useEffect(() => { + if (fundingSourceData != undefined) { + const dataset = createFundingSourceDataset(fundingSourceData); + setDataset(dataset); } - console.log(datasetFunding) + }, [fundingSourceData]) - const dataResponse: FundingGraphMatrix = { - // datasets: datasetFunding, - datasets: datasetFunding, - labels: labelsNREN.map(l => l.toString()) - } - setFundingMatrixResponse(dataResponse); - } - const empty_bar_response = { - datasets: [ - { - backgroundColor: '', - data: [], - label: '', - borderRadius: 0, - borderSkipped: false, - barPercentage: 0, - borderWidth: 0, - stack: '0', - categoryPercentage: 0.5 - }], - labels: [] - } - const fundingAPIResponse: FundingGraphMatrix = fundingMatrixResponse !== undefined - ? fundingMatrixResponse : empty_bar_response; + const dataset: FundingSourceDataset = fundingSourceDataset ?? EMPTY_DATASET; return ( <div className='center' > - <div className="chart-container" style={{ position: 'relative', height: '300vh', 'width': '80vw' }}> - <h1>Income Source</h1> - <Bar data={fundingAPIResponse} - //height={200} - options={{ - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - labels: { - boxWidth: 20, - boxHeight: 30, - pointStyle: "rectRounded", - borderRadius: 6, - useBorderRadius: true, + <h1 >Funding Source</h1> - }, - }, - }, - scales: { - x: { - stacked: true, - ticks: { - callback: (value: string | number) => { - if (typeof value === 'number') { - return value.toFixed(2); - } - return value; - }, - }, - }, - y: { - stacked: true, - }, - }, - indexAxis: "y", - }} - ></Bar> + <div className="chart-container" style={{ 'minHeight': '100vh', 'width': '60vw', }}> + <Bar + data={dataset} + options={chartOptions} + /> </div> </div> -- GitLab