Skip to content
Snippets Groups Projects
Commit 595a57d0 authored by Remco Tukker's avatar Remco Tukker
Browse files

cleanup of the budget page code

parent 334763c5
No related branches found
No related tags found
1 merge request!4cleanup of the budget page code
...@@ -4,21 +4,6 @@ export interface Nren { ...@@ -4,21 +4,6 @@ export interface Nren {
tags: string[] 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 { export interface Budget {
BUDGET: string, BUDGET: string,
BUDGET_YEAR: number, BUDGET_YEAR: number,
...@@ -37,16 +22,22 @@ export interface FundingSource { ...@@ -37,16 +22,22 @@ export interface FundingSource {
id: number id: number
} }
export interface FilterOptions {
availableNrens: string[]
availableYears: number[]
}
export interface FilterSelection { export interface FilterSelection {
selectedNrens: string[] selectedNrens: string[]
selectedYears: number[] selectedYears: number[]
} }
export interface BasicDataset {
labels: string[];
datasets: {
backgroundColor: string;
borderColor?: string;
data: (number | null)[];
label: string;
hidden: boolean;
}[];
}
export interface FundingSourceDataset { export interface FundingSourceDataset {
labels: string[], labels: string[],
...@@ -83,16 +74,6 @@ export interface ChargingStructureDataset { ...@@ -83,16 +74,6 @@ export interface ChargingStructureDataset {
}[] }[]
} }
export interface DataEntrySection {
name: string,
description: string,
items: {
id: number,
title: string,
url: string
}[]
}
export interface Service { export interface Service {
compendium_id: number, compendium_id: number,
country_code: string, country_code: string,
......
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { Bar } from 'react-chartjs-2'; import { Bar } from 'react-chartjs-2';
import { DataSetProps } from './types';
import { import {
...@@ -12,6 +11,7 @@ import { ...@@ -12,6 +11,7 @@ import {
Tooltip, Tooltip,
Legend, Legend,
} from 'chart.js'; } from 'chart.js';
import {BasicDataset} from "../../Schema";
ChartJS.register( ChartJS.register(
...@@ -23,9 +23,9 @@ ChartJS.register( ...@@ -23,9 +23,9 @@ ChartJS.register(
Legend Legend
); );
function BarGraph(data: DataSetProps): ReactElement { function BarGraph(data: BasicDataset): ReactElement {
return ( return (
<Bar data={data.data} options={{}} /> <Bar data={data} options={{}} />
); );
} }
......
import React, {ReactElement} from 'react'; import React, {ReactElement} from 'react';
import {Button, Row} from 'react-bootstrap'; import {Button, Row} from 'react-bootstrap';
import {FilterOptions, FilterSelection} from "../../Schema"; import {FilterSelection} from "../../Schema";
interface inputProps { interface inputProps {
filterOptions: FilterOptions filterOptions: { availableNrens: string[], availableYears: number[] }
filterSelection: FilterSelection filterSelection: FilterSelection
setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>> setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
} }
...@@ -55,7 +55,7 @@ function Filter({ filterOptions, filterSelection, setFilterSelection }: inputPro ...@@ -55,7 +55,7 @@ function Filter({ filterOptions, filterSelection, setFilterSelection }: inputPro
return ( return (
<> <>
<Row> <Row>
{filterOptions.availableYears.map((year) => ( {filterOptions.availableYears.sort().map((year) => (
<div key={year} onClick={() => (handleYearClick(year))}> <div key={year} onClick={() => (handleYearClick(year))}>
<input <input
type="checkbox" type="checkbox"
...@@ -67,7 +67,7 @@ function Filter({ filterOptions, filterSelection, setFilterSelection }: inputPro ...@@ -67,7 +67,7 @@ function Filter({ filterOptions, filterSelection, setFilterSelection }: inputPro
))} ))}
</Row> </Row>
<Row> <Row>
{filterOptions.availableNrens.map((nren) => ( {filterOptions.availableNrens.sort().map((nren) => (
<div key={nren} onClick={() => (handleNrenClick(nren))}> <div key={nren} onClick={() => (handleNrenClick(nren))}>
<input <input
type="checkbox" type="checkbox"
......
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { DataSetProps } from './types';
import { import {
...@@ -13,6 +12,7 @@ import { ...@@ -13,6 +12,7 @@ import {
Tooltip, Tooltip,
Legend, Legend,
} from 'chart.js'; } from 'chart.js';
import {BasicDataset} from "../../Schema";
ChartJS.register( ChartJS.register(
...@@ -38,9 +38,14 @@ const options = { ...@@ -38,9 +38,14 @@ const options = {
}, },
}, },
}; };
function LineGraph(data: DataSetProps): ReactElement {
interface inputProps {
data: BasicDataset
}
function LineGraph({data}: inputProps): ReactElement {
return ( return (
<Line data={data.data} options={options} /> <Line data={data} options={options} />
); );
} }
......
export type DatasetEntry = {
backgroundColor: string;
borderColor?: string;
data: (number | null)[];
label: string;
};
export type DataSetProps = {
data: {
datasets: DatasetEntry[];
labels: string[];
};
};
...@@ -4,20 +4,9 @@ import { ...@@ -4,20 +4,9 @@ import {
FundingSource, FundingSource,
FundingSourceDataset, FundingSourceDataset,
ChargingStructure, ChargingStructure,
ChargingStructureDataset ChargingStructureDataset, Budget, BasicDataset
} from "../Schema"; } 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 = [ const DEFAULT_CHARGING_STRUCTURE_DATA = [
{ {
"NREN": "", "NREN": "",
...@@ -26,13 +15,28 @@ const DEFAULT_CHARGING_STRUCTURE_DATA = [ ...@@ -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() { function getColorMap() {
const rgbToHex = (r: number, g: number, b: number) => '#' + [r, g, b].map(x => { const rgbToHex = (r: number, g: number, b: number) => '#' + [r, g, b].map(x => {
const hex = x.toString(16) const hex = x.toString(16)
return hex.length === 1 ? '0' + hex : hex return hex.length === 1 ? '0' + hex : hex
}).join('') }).join('')
let colorMap = new Map<string, string>(); const colorMap = new Map<string, string>();
colorMap.set("CLIENT INSTITUTIONS", rgbToHex(157, 40, 114)) colorMap.set("CLIENT INSTITUTIONS", rgbToHex(157, 40, 114))
colorMap.set("COMMERCIAL", rgbToHex(241, 224, 79)) colorMap.set("COMMERCIAL", rgbToHex(241, 224, 79))
colorMap.set("EUROPEAN FUNDING", rgbToHex(219, 42, 76)) colorMap.set("EUROPEAN FUNDING", rgbToHex(219, 42, 76))
...@@ -42,7 +46,7 @@ function getColorMap() { ...@@ -42,7 +46,7 @@ function getColorMap() {
} }
function CreateDataLookup(data: FundingSource[]) { function CreateDataLookup(data: FundingSource[]) {
let dataLookup = new Map<string, Map<string, number>>(); const dataLookup = new Map<string, Map<string, number>>();
data.forEach((item: FundingSource) => { data.forEach((item: FundingSource) => {
const lookupKey = `${item.NREN}/${item.YEAR}` const lookupKey = `${item.NREN}/${item.YEAR}`
...@@ -62,7 +66,7 @@ function CreateDataLookup(data: FundingSource[]) { ...@@ -62,7 +66,7 @@ function CreateDataLookup(data: FundingSource[]) {
} }
export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) => { export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) => {
const data = fundingSourcesData ?? DEFAULT_FUNDING_SOURCE_DATA; const data = fundingSourcesData;
const dataLookup = CreateDataLookup(data) const dataLookup = CreateDataLookup(data)
const labelsYear = [...new Set(data.map((item: FundingSource) => item.YEAR))]; const labelsYear = [...new Set(data.map((item: FundingSource) => item.YEAR))];
...@@ -111,9 +115,43 @@ export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) ...@@ -111,9 +115,43 @@ export const createFundingSourceDataset = (fundingSourcesData: FundingSource[])
return dataResponse; 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[]) { function createChargingStructureDataLookup(data: ChargingStructure[]) {
let dataLookup = new Map<string, (string|null)>(); const dataLookup = new Map<string, (string|null)>();
data.forEach((item: ChargingStructure) => { data.forEach((item: ChargingStructure) => {
const lookupKey = `${item.NREN}/${item.YEAR}` const lookupKey = `${item.NREN}/${item.YEAR}`
dataLookup.set(lookupKey, item.FEE_TYPE) dataLookup.set(lookupKey, item.FEE_TYPE)
......
import React, { ReactElement, useEffect, useState } from 'react'; import React, {ReactElement, useEffect, useState} from 'react';
import { Col, Container, Row } from "react-bootstrap"; import { Col, Container, Row } from "react-bootstrap";
import LineGraph from "../components/graphing/LineGraph"; 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 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 return response.json() as Promise<T>;
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;
} }
interface inputProps { interface inputProps {
filterSelection: FilterSelection filterSelection: FilterSelection
setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>> setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
...@@ -27,181 +27,37 @@ interface inputProps { ...@@ -27,181 +27,37 @@ interface inputProps {
function DataAnalysis({ filterSelection, setFilterSelection }: inputProps): ReactElement { 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 [budgetResponse, setBudget] = useState<Budget[]>();
const [dataEntrySection, setDataEntrySection] = useState<DataEntrySection>();
const [selectedDataEntry, setSelectedDataEntry] = useState<number>(0);
const [filterOptions, setFilterOptions] = useState<FilterOptions>({availableYears: [], availableNrens: []});
useEffect(() => { const nrens = new Set((budgetResponse ?? []).map((item) => item.NREN));
// hardcode selected section for now const budgetData = createBudgetDataset(budgetResponse ?? []);
const dataEntrySectionResponse: DataEntrySection = {
description: "Org", budgetData.datasets.forEach(dataset => {
items: [ dataset.hidden = !filterSelection.selectedNrens.includes(dataset.label);
{ });
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);
}, [])
useEffect(() => { useEffect(() => {
const loadData = async () => { 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) { api<Budget[]>('/api/budget/', {})
const budget = barResponse.find(function (entry, index) { .then((budget: Budget[]) => {
if (entry.BUDGET_YEAR == year && entry.NREN == nren) { setBudget(budget)
return Number(entry.BUDGET);
}
})
return budget !== undefined ? Number(budget.BUDGET) : null;
}
const datasetPerYear = labelsYear.map(function (year) { // filter fallback for when nothing is selected (all nrens)
const randomColor = stringToColour(year); const nrens = new Set(budget.map((item) => item.NREN));
return { setFilterSelection(previous => {
backgroundColor: randomColor, const visibleNrens = previous.selectedNrens.filter(nren => nrens.has(nren));
borderColor: randomColor, const newSelectedNrens = visibleNrens.length ? previous.selectedNrens : [...nrens];
data: labelsNREN.map(nren => dataForNRENForYear(year, nren)), return { selectedYears: previous.selectedYears, selectedNrens: newSelectedNrens };
label: year.toString() });
} })
}) .catch(error => {
console.log(`Error fetching from API: ${error}`);
const datasetPerNREN = labelsNREN.map(function (nren) { });
const randomColor = stringToColour(nren); };
return { loadData();
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);
}
}
const budgetAPIResponse: BudgetMatrix = budgetMatrixResponse !== undefined
? budgetMatrixResponse : empty_bar_response;
return ( return (
<div> <div>
<h1>Data Analysis</h1> <h1>Data Analysis</h1>
...@@ -209,35 +65,23 @@ function DataAnalysis({ filterSelection, setFilterSelection }: inputProps): Reac ...@@ -209,35 +65,23 @@ function DataAnalysis({ filterSelection, setFilterSelection }: inputProps): Reac
<Row> <Row>
<Col> <Col>
<Row> <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&apos;s budget.
</Row> </Row>
<Row>{budgetMatrixResponse?.description}</Row>
</Col> </Col>
<Col xs={3}> <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 <Filter
filterOptions={filterOptions} filterOptions={{availableYears: [], availableNrens: [...nrens]}}
filterSelection={filterSelection} filterSelection={filterSelection}
setFilterSelection={setFilterSelection} setFilterSelection={setFilterSelection}
/> />
</Col> </Col>
</Row> </Row>
</Container> </Container>
</div> </div>
); );
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment