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&apos;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