From 595a57d0787e3f03597efa8bfd4ecc1b680f3adb Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Wed, 19 Apr 2023 00:01:37 +0200 Subject: [PATCH] cleanup of the budget page code --- webapp/src/Schema.tsx | 41 +-- webapp/src/components/graphing/BarGraph.tsx | 6 +- webapp/src/components/graphing/Filter.tsx | 8 +- webapp/src/components/graphing/LineGraph.tsx | 11 +- webapp/src/components/graphing/types.ts | 13 - webapp/src/helpers/dataconversion.tsx | 70 ++++-- webapp/src/pages/DataAnalysis.tsx | 248 ++++--------------- 7 files changed, 126 insertions(+), 271 deletions(-) delete mode 100644 webapp/src/components/graphing/types.ts diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx index 00f39a39..e6f7c4c3 100644 --- a/webapp/src/Schema.tsx +++ b/webapp/src/Schema.tsx @@ -4,21 +4,6 @@ export interface Nren { tags: string[] } -export interface BudgetMatrix { - data: { - labels: string[], - datasets: { - label: string, - data: (number | null)[], - backgroundColor: string - }[] - }, - description: string, - id: string, - settings: Record<string, unknown>, - title: string -} - export interface Budget { BUDGET: string, BUDGET_YEAR: number, @@ -37,16 +22,22 @@ export interface FundingSource { id: number } -export interface FilterOptions { - availableNrens: string[] - availableYears: number[] -} - export interface FilterSelection { selectedNrens: string[] selectedYears: number[] } +export interface BasicDataset { + labels: string[]; + datasets: { + backgroundColor: string; + borderColor?: string; + data: (number | null)[]; + label: string; + hidden: boolean; + }[]; +} + export interface FundingSourceDataset { labels: string[], @@ -83,16 +74,6 @@ export interface ChargingStructureDataset { }[] } -export interface DataEntrySection { - name: string, - description: string, - items: { - id: number, - title: string, - url: string - }[] -} - export interface Service { compendium_id: number, country_code: string, diff --git a/webapp/src/components/graphing/BarGraph.tsx b/webapp/src/components/graphing/BarGraph.tsx index 46689a07..791b2a04 100644 --- a/webapp/src/components/graphing/BarGraph.tsx +++ b/webapp/src/components/graphing/BarGraph.tsx @@ -1,6 +1,5 @@ import React, { ReactElement } from 'react'; import { Bar } from 'react-chartjs-2'; -import { DataSetProps } from './types'; import { @@ -12,6 +11,7 @@ import { Tooltip, Legend, } from 'chart.js'; +import {BasicDataset} from "../../Schema"; ChartJS.register( @@ -23,9 +23,9 @@ ChartJS.register( Legend ); -function BarGraph(data: DataSetProps): ReactElement { +function BarGraph(data: BasicDataset): ReactElement { return ( - <Bar data={data.data} options={{}} /> + <Bar data={data} options={{}} /> ); } diff --git a/webapp/src/components/graphing/Filter.tsx b/webapp/src/components/graphing/Filter.tsx index 85323f13..3391b9f7 100644 --- a/webapp/src/components/graphing/Filter.tsx +++ b/webapp/src/components/graphing/Filter.tsx @@ -1,9 +1,9 @@ import React, {ReactElement} from 'react'; import {Button, Row} from 'react-bootstrap'; -import {FilterOptions, FilterSelection} from "../../Schema"; +import {FilterSelection} from "../../Schema"; interface inputProps { - filterOptions: FilterOptions + filterOptions: { availableNrens: string[], availableYears: number[] } filterSelection: FilterSelection setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>> } @@ -55,7 +55,7 @@ function Filter({ filterOptions, filterSelection, setFilterSelection }: inputPro return ( <> <Row> - {filterOptions.availableYears.map((year) => ( + {filterOptions.availableYears.sort().map((year) => ( <div key={year} onClick={() => (handleYearClick(year))}> <input type="checkbox" @@ -67,7 +67,7 @@ function Filter({ filterOptions, filterSelection, setFilterSelection }: inputPro ))} </Row> <Row> - {filterOptions.availableNrens.map((nren) => ( + {filterOptions.availableNrens.sort().map((nren) => ( <div key={nren} onClick={() => (handleNrenClick(nren))}> <input type="checkbox" diff --git a/webapp/src/components/graphing/LineGraph.tsx b/webapp/src/components/graphing/LineGraph.tsx index 61cf7be2..a6fe1661 100644 --- a/webapp/src/components/graphing/LineGraph.tsx +++ b/webapp/src/components/graphing/LineGraph.tsx @@ -1,6 +1,5 @@ import React, { ReactElement } from 'react'; import { Line } from 'react-chartjs-2'; -import { DataSetProps } from './types'; import { @@ -13,6 +12,7 @@ import { Tooltip, Legend, } from 'chart.js'; +import {BasicDataset} from "../../Schema"; ChartJS.register( @@ -38,9 +38,14 @@ const options = { }, }, }; -function LineGraph(data: DataSetProps): ReactElement { + +interface inputProps { + data: BasicDataset +} + +function LineGraph({data}: inputProps): ReactElement { return ( - <Line data={data.data} options={options} /> + <Line data={data} options={options} /> ); } diff --git a/webapp/src/components/graphing/types.ts b/webapp/src/components/graphing/types.ts deleted file mode 100644 index cd3c2cb1..00000000 --- a/webapp/src/components/graphing/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type DatasetEntry = { - backgroundColor: string; - borderColor?: string; - data: (number | null)[]; - label: string; -}; - -export type DataSetProps = { - data: { - datasets: DatasetEntry[]; - labels: string[]; - }; -}; diff --git a/webapp/src/helpers/dataconversion.tsx b/webapp/src/helpers/dataconversion.tsx index 6512b6bc..82179113 100644 --- a/webapp/src/helpers/dataconversion.tsx +++ b/webapp/src/helpers/dataconversion.tsx @@ -4,20 +4,9 @@ import { FundingSource, FundingSourceDataset, ChargingStructure, - ChargingStructureDataset + ChargingStructureDataset, Budget, BasicDataset } 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 - }] const DEFAULT_CHARGING_STRUCTURE_DATA = [ { "NREN": "", @@ -26,13 +15,28 @@ const DEFAULT_CHARGING_STRUCTURE_DATA = [ } ] +// create a color from a string, credits https://stackoverflow.com/a/16348977 +const stringToColour = function(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let colour = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xFF; + const valueAsString = '00' + value.toString(16); + colour += valueAsString.substring(valueAsString.length - 2); + } + return colour; +} + 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>(); + const 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)) @@ -42,7 +46,7 @@ function getColorMap() { } function CreateDataLookup(data: FundingSource[]) { - let dataLookup = new Map<string, Map<string, number>>(); + const dataLookup = new Map<string, Map<string, number>>(); data.forEach((item: FundingSource) => { const lookupKey = `${item.NREN}/${item.YEAR}` @@ -62,7 +66,7 @@ function CreateDataLookup(data: FundingSource[]) { } export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) => { - const data = fundingSourcesData ?? DEFAULT_FUNDING_SOURCE_DATA; + const data = fundingSourcesData; const dataLookup = CreateDataLookup(data) const labelsYear = [...new Set(data.map((item: FundingSource) => item.YEAR))]; @@ -111,9 +115,43 @@ export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) return dataResponse; } +function createBudgetDataLookup(budgetEntries: Budget[]) { + const dataLookup = new Map<string, number>(); + budgetEntries.forEach((item: Budget) => { + const lookupKey = `${item.NREN}/${item.BUDGET_YEAR}`; + dataLookup.set(lookupKey, Number(item.BUDGET)); + }) + return dataLookup; +} + +export function createBudgetDataset(budgetEntries: Budget[]): BasicDataset { + const labelsYear = [...new Set(budgetEntries.map((item) => item.BUDGET_YEAR))].sort(); + const labelsNREN = [...new Set(budgetEntries.map((item) => item.NREN))].sort(); + + const dataLookup = createBudgetDataLookup(budgetEntries); + + const sets = labelsNREN.map(nren => { + const randomColor = stringToColour(nren); + return { + backgroundColor: stringToColour(randomColor), + borderColor: stringToColour(randomColor), + data: labelsYear.map((year) => dataLookup.get(`${nren}/${year}`) ?? null), + label: nren, + hidden: false + } + }); + + const budgetAPIResponse = { + datasets: sets, + labels: labelsYear.map(year => year.toString()) + } + + return budgetAPIResponse; +} + function createChargingStructureDataLookup(data: ChargingStructure[]) { - let dataLookup = new Map<string, (string|null)>(); + const dataLookup = new Map<string, (string|null)>(); data.forEach((item: ChargingStructure) => { const lookupKey = `${item.NREN}/${item.YEAR}` dataLookup.set(lookupKey, item.FEE_TYPE) diff --git a/webapp/src/pages/DataAnalysis.tsx b/webapp/src/pages/DataAnalysis.tsx index dd5bb9f8..29cb56db 100644 --- a/webapp/src/pages/DataAnalysis.tsx +++ b/webapp/src/pages/DataAnalysis.tsx @@ -1,25 +1,25 @@ -import React, { ReactElement, useEffect, useState } from 'react'; +import React, {ReactElement, useEffect, useState} from 'react'; import { Col, Container, Row } from "react-bootstrap"; import LineGraph from "../components/graphing/LineGraph"; -import {BudgetMatrix, DataEntrySection, Budget, FilterSelection, FilterOptions} from "../Schema"; +import {Budget, FilterSelection} from "../Schema"; import Filter from "../components/graphing/Filter"; +import {createBudgetDataset} from "../helpers/dataconversion"; + +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."); + }); + } -// create a color from a string, credits https://stackoverflow.com/a/16348977 -const stringToColour = function(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let colour = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xFF; - colour += ('00' + value.toString(16)).substr(-2); - } - return colour; + return response.json() as Promise<T>; + }) } - interface inputProps { filterSelection: FilterSelection setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>> @@ -27,181 +27,37 @@ interface inputProps { function DataAnalysis({ filterSelection, setFilterSelection }: inputProps): 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>; - }) - } - const [budgetMatrixResponse, setBudgetMatrixResponse] = useState<BudgetMatrix>(); const [budgetResponse, setBudget] = useState<Budget[]>(); - const [dataEntrySection, setDataEntrySection] = useState<DataEntrySection>(); - const [selectedDataEntry, setSelectedDataEntry] = useState<number>(0); - const [filterOptions, setFilterOptions] = useState<FilterOptions>({availableYears: [], availableNrens: []}); - useEffect(() => { - // hardcode selected section for now - const dataEntrySectionResponse: DataEntrySection = { - description: "Org", - items: [ - { - id: 0, - title: "NREN Budgets per year, in Millions EUR", - url: "/api/data-entries/item/2" - } - // { - // id:3, - // title:"NREN Budgets per NREN, in Millions EUR", - // url:"/api/data-entries/item/3" - // } - ], - name: "Organisation" - } - setDataEntrySection(dataEntrySectionResponse); - setSelectedDataEntry(dataEntrySectionResponse.items[0].id); - }, []) + const nrens = new Set((budgetResponse ?? []).map((item) => item.NREN)); + const budgetData = createBudgetDataset(budgetResponse ?? []); + + budgetData.datasets.forEach(dataset => { + dataset.hidden = !filterSelection.selectedNrens.includes(dataset.label); + }); useEffect(() => { const loadData = async () => { - console.log("budgetResponse " + budgetResponse) - if (budgetResponse == undefined) { - api<Budget[]>('/api/budget/', {}) - .then((budget: Budget[]) => { - console.log('budget.data :', budget) - console.log('budget :', budget) - const entry = dataEntrySection?.items.find(i => i.id == selectedDataEntry) - console.log(selectedDataEntry, dataEntrySection, entry) - setBudget(budget) - console.log("budgetResponse after api " + budgetResponse) - convertToBudgetPerYearDataResponse(budget) - }) - .catch(error => { - console.log(`Error fetching from API: ${error}`); - }) - } else { - convertToBudgetPerYearDataResponse(budgetResponse) - } - - } - loadData() - }, [dataEntrySection, selectedDataEntry, filterSelection]); - - const empty_bar_response = { - data: { - datasets: [ - { - backgroundColor: '', - data: [], - label: '' - }], - labels: [] - }, - description: "", - id: "", - settings: {}, - title: "" - } - - const empty_budget_response = [{ - BUDGET: "", - BUDGET_YEAR: 0, - NREN: "", - id: 0 - }] - - - const convertToBudgetPerYearDataResponse = (budgetResponse: Budget[]) => { - const barResponse = budgetResponse != undefined ? budgetResponse : empty_budget_response; - console.log("barResponse " + barResponse); - console.log(barResponse.map((item) => item.BUDGET_YEAR)); - const labelsYear = [...new Set(barResponse.map((item) => item.BUDGET_YEAR))]; - const labelsNREN = [...new Set(barResponse.map((item) => item.NREN))]; - - setFilterOptions({ availableYears: [], availableNrens: labelsNREN }); - function dataForNRENForYear(year: number, nren: string) { - const budget = barResponse.find(function (entry, index) { - if (entry.BUDGET_YEAR == year && entry.NREN == nren) { - return Number(entry.BUDGET); - } - }) - return budget !== undefined ? Number(budget.BUDGET) : null; - } + api<Budget[]>('/api/budget/', {}) + .then((budget: Budget[]) => { + setBudget(budget) - const datasetPerYear = labelsYear.map(function (year) { - const randomColor = stringToColour(year); - return { - backgroundColor: randomColor, - borderColor: randomColor, - data: labelsNREN.map(nren => dataForNRENForYear(year, nren)), - label: year.toString() - } - }) - - const datasetPerNREN = labelsNREN.map(function (nren) { - const randomColor = stringToColour(nren); - return { - backgroundColor: randomColor, - borderColor: randomColor, - data: labelsYear.map(year => dataForNRENForYear(year, nren)), - label: nren, - hidden: !filterSelection.selectedNrens.includes(nren) - } - }) - - if (selectedDataEntry == 0) { - const dataResponse: BudgetMatrix = { - data: { - datasets: datasetPerNREN, - labels: labelsYear.map(l => l.toString()) - }, - description: "The numbers are based on 30 NRENs that " + - "reported their budgets continuously throughout this" + - " period. This means that some larger NRENs are not" + - " included and therefore the actual total budgets will" + - " have been higher. (For comparison, the total budget" + - " according to the 2021 survey results based on the data" + - " for all responding NRENs that year is €555 M). The" + - " percentage change is based on the previous year's" + - " budget.", - id: "3", - settings: {}, - title: 'NREN Budgets per NREN, in Millions EUR' - } - setBudgetMatrixResponse(dataResponse); - } - else { - const dataResponse: BudgetMatrix = { - data: { - datasets: datasetPerYear, - labels: labelsNREN.map(l => l.toString()) - }, - description: - "The numbers are based on 30 NRENs that reported their " + - "budgets continuously throughout this period. This " + - "means that some larger NRENs are not included and " + - "therefore the actual total budgets will have been " + - "higher. (For comparison, the total budget according to" + - " the 2021 survey results based on the data for all" + - " responding NRENs that year is €555 M). The percentage" + - " change is based on the previous year’s budget.", - id: "2", - settings: {}, - title: 'NREN Budgets per year, in Millions EUR' - } - setBudgetMatrixResponse(dataResponse); - } - } + // filter fallback for when nothing is selected (all nrens) + const nrens = new Set(budget.map((item) => item.NREN)); + setFilterSelection(previous => { + const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren)); + const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens]; + return { selectedYears: previous.selectedYears, selectedNrens: newSelectedNrens }; + }); + }) + .catch(error => { + console.log(`Error fetching from API: ${error}`); + }); + }; + loadData(); + }, []); - const budgetAPIResponse: BudgetMatrix = budgetMatrixResponse !== undefined - ? budgetMatrixResponse : empty_bar_response; return ( <div> <h1>Data Analysis</h1> @@ -209,35 +65,23 @@ function DataAnalysis({ filterSelection, setFilterSelection }: inputProps): Reac <Row> <Col> <Row> - <LineGraph data={budgetAPIResponse.data} /> + <LineGraph data={budgetData} /> + </Row> + <Row> +The numbers are based on 30 NRENs that reported their budgets continuously throughout this +period. This means that some larger NRENs are not included and therefore the actual total budgets will +have been higher. (For comparison, the total budget according to the 2021 survey results based on the data +for all responding NRENs that year is €555 M). The percentage change is based on the previous year's budget. </Row> - <Row>{budgetMatrixResponse?.description}</Row> - </Col> <Col xs={3}> - {/*<Accordion defaultActiveKey="0">*/} - {/* <Accordion.Item eventKey="0">*/} - {/* <Accordion.Header>Items</Accordion.Header>*/} - {/* <Accordion.Body>*/} - {/* <ListGroup>*/} - {/* {*/} - {/* dataEntrySection?.items.map((item) => (*/} - {/* <ListGroup.Item key={item.id} action active={item.id == selectedDataEntry} onClick={() => setSelectedDataEntry(item.id)}>{item.title}</ListGroup.Item>*/} - {/* ))*/} - {/* }*/} - {/* </ListGroup>*/} - {/* </Accordion.Body>*/} - {/* </Accordion.Item>*/} - {/*</Accordion>*/} <Filter - filterOptions={filterOptions} + filterOptions={{availableYears: [], availableNrens: [...nrens]}} filterSelection={filterSelection} setFilterSelection={setFilterSelection} /> </Col> </Row> - - </Container> </div> ); -- GitLab