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