From 9a42c579379ef3c8087a19a10f2fa7cfc1f9719c Mon Sep 17 00:00:00 2001
From: Ian Galpin <ian.galpin@geant.org>
Date: Tue, 6 Dec 2022 13:13:35 +0000
Subject: [PATCH] Added line graphs and re-organised the webapp layout

---
 compendium_v2/db/survey.py                   |  40 +++++++
 compendium_v2/routes/data_entry.py           |   5 +-
 setup.py                                     |   2 +-
 webapp/src/App.tsx                           |   6 +-
 webapp/src/Schema.tsx                        |   9 ++
 webapp/src/components/graphing/BarGraph.tsx  |  32 ++++++
 webapp/src/components/graphing/LineGraph.tsx |  46 ++++++++
 webapp/src/components/graphing/types.ts      |  14 +++
 webapp/src/{ => pages}/About.tsx             |   0
 webapp/src/{ => pages}/AnnualReport.tsx      |   2 +-
 webapp/src/{ => pages}/DataAnalysis.tsx      | 112 +++++++++++--------
 webapp/src/styles.scss                       |   2 +-
 12 files changed, 219 insertions(+), 51 deletions(-)
 create mode 100644 webapp/src/components/graphing/BarGraph.tsx
 create mode 100644 webapp/src/components/graphing/LineGraph.tsx
 create mode 100644 webapp/src/components/graphing/types.ts
 rename webapp/src/{ => pages}/About.tsx (100%)
 rename webapp/src/{ => pages}/AnnualReport.tsx (97%)
 rename webapp/src/{ => pages}/DataAnalysis.tsx (51%)

diff --git a/compendium_v2/db/survey.py b/compendium_v2/db/survey.py
index 90e6d101..dfe57ab7 100644
--- a/compendium_v2/db/survey.py
+++ b/compendium_v2/db/survey.py
@@ -54,3 +54,43 @@ def get_budget_by_year():
         response_data['datasets'].append(dataset)
 
     return response_data
+
+
+def get_budget_by_nren():
+    budget_data = db_survey.session.execute(
+        db_survey.select(AnnualBudgetEntry)
+        .filter(AnnualBudgetEntry.region_name == 'Western Europe')
+    ).scalars()
+
+    annual_data = {
+
+    }
+    seen_years = set()
+
+    for line_item in budget_data:
+        li_year = line_item.year
+        li_country_code = line_item.country_code
+        if li_country_code not in annual_data:
+            annual_data[li_country_code] = {}
+        seen_years.add(li_year)
+        annual_data[li_country_code][li_year] = line_item.budget
+
+    sorted_years = sorted(seen_years)
+    response_data = {
+        'labels': sorted_years,
+        'datasets': []
+    }
+
+    for country in sorted(annual_data.keys()):
+        dataset = {
+            'label': country,
+            'data': []
+        }
+        for year in sorted_years:
+            budget_amount = annual_data[country].get(year)
+            dataset['data'].append(float(budget_amount)
+                                   if budget_amount else None)
+
+        response_data['datasets'].append(dataset)
+
+    return response_data
diff --git a/compendium_v2/routes/data_entry.py b/compendium_v2/routes/data_entry.py
index c2a03b59..f38e8f62 100644
--- a/compendium_v2/routes/data_entry.py
+++ b/compendium_v2/routes/data_entry.py
@@ -5,7 +5,7 @@ from flask import Blueprint, abort, jsonify, url_for
 from compendium_v2.db import db
 from compendium_v2.db.models import (DataEntryItem, DataEntrySection,
                                      DataSourceType)
-from compendium_v2.db.survey import get_budget_by_year
+from compendium_v2.db.survey import get_budget_by_nren, get_budget_by_year
 from compendium_v2.routes import common
 
 routes = Blueprint('data-entry', __name__)
@@ -131,11 +131,14 @@ def load_data(data_source_id: DataSourceType) -> Any:
     response_data = {}
     if data_source_id == DataSourceType.BUDGETS_BY_YEAR:
         response_data = get_budget_by_year()
+    if data_source_id == DataSourceType.BUDGETS_BY_NREN:
+        response_data = get_budget_by_nren()
 
     # Enrich response data
     # Add the colour formatting
     for index, dataset in enumerate(response_data['datasets']):
         dataset['backgroundColor'] = col_pal[index % len(col_pal)]
+        dataset['borderColor'] = col_pal[index % len(col_pal)]
 
     return response_data
 
diff --git a/setup.py b/setup.py
index 6ceebadc..49655b8e 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='compendium-v2',
-    version="0.3",
+    version='0.3',
     author='GEANT',
     author_email='swd@geant.org',
     description='Flask and React project for displaying '
diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index 8abd7629..c945027a 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -1,8 +1,8 @@
 import React, {ReactElement} from 'react';
-import AnnualReport from './AnnualReport';
-import DataAnalysis from './DataAnalysis';
+import AnnualReport from './pages/AnnualReport';
+import DataAnalysis from './pages/DataAnalysis';
 import Navigation from './Navigation';
-import About from './About';
+import About from './pages/About';
 import {BrowserRouter as Router,Switch, Route} from 'react-router-dom'
 
 
diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx
index d34bfc25..905dd1d3 100644
--- a/webapp/src/Schema.tsx
+++ b/webapp/src/Schema.tsx
@@ -26,6 +26,15 @@ export interface BudgetMatrix{
     title:string
 }
 
+export interface DataEntrySection {
+    name: string,
+    description: string,
+    items: {
+        id: number,
+        title: string,
+        url: string
+    }[]
+}
 
 export interface Service {
     compendium_id: number,
diff --git a/webapp/src/components/graphing/BarGraph.tsx b/webapp/src/components/graphing/BarGraph.tsx
new file mode 100644
index 00000000..38f49b0f
--- /dev/null
+++ b/webapp/src/components/graphing/BarGraph.tsx
@@ -0,0 +1,32 @@
+import React, {ReactElement, useEffect, useState} from 'react';
+import {Bar} from 'react-chartjs-2';
+import {DataSetProps} from './types';
+
+
+import {
+    Chart as ChartJS,
+    CategoryScale,
+    LinearScale,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend,
+} from 'chart.js';
+
+
+ChartJS.register(
+    CategoryScale,
+    LinearScale,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend
+);
+
+function BarGraph(data: DataSetProps): ReactElement {
+    return (
+        <Bar data={data.data} options={{}}/>
+    );
+}
+
+export default BarGraph;
diff --git a/webapp/src/components/graphing/LineGraph.tsx b/webapp/src/components/graphing/LineGraph.tsx
new file mode 100644
index 00000000..be9d1e02
--- /dev/null
+++ b/webapp/src/components/graphing/LineGraph.tsx
@@ -0,0 +1,46 @@
+import React, {ReactElement, useEffect, useState} from 'react';
+import {Line} from 'react-chartjs-2';
+import {DataSetProps} from './types';
+
+
+import {
+ Chart as ChartJS,
+  CategoryScale,
+  LinearScale,
+  PointElement,
+  LineElement,
+  Title,
+  Tooltip,
+  Legend,
+} from 'chart.js';
+
+
+ChartJS.register(
+  CategoryScale,
+  LinearScale,
+  PointElement,
+  LineElement,
+  Title,
+  Tooltip,
+  Legend
+);
+
+const options = {
+  responsive: true,
+  plugins: {
+    legend: {
+      position: 'top' as const,
+    },
+    title: {
+      display: true,
+      text: '',
+    },
+  },
+};
+function LineGraph(data: DataSetProps): ReactElement {
+    return (
+        <Line data={data.data} options={options}/>
+    );
+}
+
+export default LineGraph;
diff --git a/webapp/src/components/graphing/types.ts b/webapp/src/components/graphing/types.ts
new file mode 100644
index 00000000..6ebb08f1
--- /dev/null
+++ b/webapp/src/components/graphing/types.ts
@@ -0,0 +1,14 @@
+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/About.tsx b/webapp/src/pages/About.tsx
similarity index 100%
rename from webapp/src/About.tsx
rename to webapp/src/pages/About.tsx
diff --git a/webapp/src/AnnualReport.tsx b/webapp/src/pages/AnnualReport.tsx
similarity index 97%
rename from webapp/src/AnnualReport.tsx
rename to webapp/src/pages/AnnualReport.tsx
index 23c73eab..90f744aa 100644
--- a/webapp/src/AnnualReport.tsx
+++ b/webapp/src/pages/AnnualReport.tsx
@@ -1,5 +1,5 @@
 import React, {useState, useEffect, ReactElement} from 'react';
-import {Nren, Service, ServiceMatrix} from "./Schema";
+import {Nren, Service, ServiceMatrix} from "../Schema";
  
 
 // const api_url = window.location.origin;
diff --git a/webapp/src/DataAnalysis.tsx b/webapp/src/pages/DataAnalysis.tsx
similarity index 51%
rename from webapp/src/DataAnalysis.tsx
rename to webapp/src/pages/DataAnalysis.tsx
index 2f9f41b9..5030d6d0 100644
--- a/webapp/src/DataAnalysis.tsx
+++ b/webapp/src/pages/DataAnalysis.tsx
@@ -1,28 +1,12 @@
 import React, {ReactElement, useEffect, useState} from 'react';
-import {Accordion, Col, Container, Form, Row} from "react-bootstrap";
-import {Bar} from 'react-chartjs-2';
-
-
-import {
-    Chart as ChartJS,
-    CategoryScale,
-    LinearScale,
-    BarElement,
-    Title,
-    Tooltip,
-    Legend,
-} from 'chart.js';
-
-import {BudgetMatrix} from "./Schema";
-
-ChartJS.register(
-    CategoryScale,
-    LinearScale,
-    BarElement,
-    Title,
-    Tooltip,
-    Legend
-);
+import {Accordion, Col, Container, Form, ListGroup, Row} from "react-bootstrap";
+import BarGraph from "../components/graphing/BarGraph";
+import LineGraph from "../components/graphing/LineGraph";
+import {DataSetProps} from "../components/graphing/types";
+
+
+import {BudgetMatrix, DataEntrySection} from "../Schema";
+
 
 export const options = {
     // indexAxis: 'y' as const,
@@ -42,7 +26,6 @@ function DataAnalysis(): ReactElement {
     function api<T>(url: string, options: RequestInit): Promise<T> {
         return fetch(url, options)
             .then((response) => {
-                console.log(response)
                 if (!response.ok) {
                     return response.text().then((message) => {
                         console.error(`Failed to load datax: ${message}`, response.status);
@@ -55,12 +38,31 @@ function DataAnalysis(): ReactElement {
     }
 
     const [budgetResponse, setBudgetMatrix] = useState<BudgetMatrix>();
+    const [dataEntrySection, setDataEntrySection] = useState<DataEntrySection>();
+    const [selectedDataEntry, setSelectedDataEntry] = useState<number>(0);
+
     // const [services, setServices] = useState<Service[][]>();
 
     useEffect(() => {
         // let timeoutRef = 0;
+        const getDataEntries = () => {
+            api<DataEntrySection>('/api/data-entries/sections/1', {
+                referrerPolicy: "unsafe-url",
+                headers: {
+                    "Access-Control-Allow-Origin": "*",
+                    "Content-Type": "text/plain"
+                }
+            }).then( (dataEntrySectionResponse: DataEntrySection) => {
+                setDataEntrySection(dataEntrySectionResponse);
+            })
+        }
         const loadData = () => {
-            api<BudgetMatrix>('/api/data-entries/item/2',{
+            if (selectedDataEntry == 0) {
+                getDataEntries();
+                return;
+            }
+
+            api<BudgetMatrix>('/api/data-entries/item/' + selectedDataEntry,{
                 referrerPolicy: "unsafe-url",
                 headers: {
                     "Access-Control-Allow-Origin": "*",
@@ -68,15 +70,21 @@ function DataAnalysis(): ReactElement {
                 }
             })
                 .then((budgetMatrix :BudgetMatrix)=>{
-                    console.log('got response==>data');
-                    console.log(budgetMatrix.data);
                     options.plugins.title.text = budgetMatrix.title;
                     setBudgetMatrix(budgetMatrix)
                 })
 
         }
         loadData()
-    }, []);
+    }, [selectedDataEntry]);
+
+    const renderEntries = () => {
+        return (
+            <ul>
+
+            </ul>
+        );
+    };
     const empty_bar_response = {
     'data': {
         'datasets': [
@@ -94,6 +102,25 @@ function DataAnalysis(): ReactElement {
 }
 
 const barResponse: BudgetMatrix = budgetResponse !== undefined ? budgetResponse : empty_bar_response;
+    const data: DataSetProps = {
+        data: {
+            datasets: [
+                {
+                    backgroundColor: '#114466',
+                    borderColor: '#114466',
+                    data: [10, 13, 18,21,16,12],
+                    label: '2016'
+                },
+                {
+                    backgroundColor: '#FF4466',
+                    borderColor: '#FF4466',
+                    data: [15, null, 13,11,11,44],
+                    label: '2017'
+                },
+            ],
+            labels: ['AB', 'BC', 'CD', 'DE', 'EF', 'FE']
+        }
+    }
 
     return (
         <div>
@@ -102,8 +129,10 @@ const barResponse: BudgetMatrix = budgetResponse !== undefined ? budgetResponse
                 <Row>
                     <Col>
                         <Row>
-                            <Bar data={barResponse.data} options={options}/>
-
+                            <BarGraph data={barResponse.data} />
+                        </Row>
+                        <Row>
+                            <LineGraph data={barResponse.data} />
                         </Row>
                         <Row>{budgetResponse?.description}</Row>
 
@@ -111,20 +140,15 @@ const barResponse: BudgetMatrix = budgetResponse !== undefined ? budgetResponse
                     <Col xs={3}>
                         <Accordion defaultActiveKey="0">
                             <Accordion.Item eventKey="0">
-                                <Accordion.Header>NRENs</Accordion.Header>
+                                <Accordion.Header>Items</Accordion.Header>
                                 <Accordion.Body>
-                                    <Form>
-                                        <Form.Check
-                                            type={'checkbox'}
-                                            label='JISC'/>
-                                        <Form.Check
-                                            type={'checkbox'}
-                                            label='SURF'/>
-                                        <Form.Check
-                                            type={'checkbox'}
-                                            label='URAN'/>
-
-                                    </Form>
+                                    <ListGroup>
+                                        {
+                                            dataEntrySection?.items.map((item) => (
+                                                <ListGroup.Item key={item.id} action onClick={() => setSelectedDataEntry(item.id)}>{item.title}</ListGroup.Item>
+                                            ))
+                                        }
+                                    </ListGroup>
                                 </Accordion.Body>
                             </Accordion.Item>
                         </Accordion>
diff --git a/webapp/src/styles.scss b/webapp/src/styles.scss
index 5d084e11..85b5b785 100644
--- a/webapp/src/styles.scss
+++ b/webapp/src/styles.scss
@@ -1,5 +1,5 @@
 table {
-    min-width: 650;
+    min-width: 650px;
 }
 
 thead {
-- 
GitLab