diff --git a/compendium_v2/db/survey.py b/compendium_v2/db/survey.py index 90e6d101eb1a12bebb9d1820bb6692a9d3633226..dfe57ab7eb45a948ca3deaecba357e9ffa2724a2 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 c2a03b59dd2505ce3f94fa16636d6db7ca42e036..f38e8f62ed1ae880895b7c6baa4736f2c12703d9 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/tox.ini b/tox.ini index 74a219d33af3ae0bd808ebf0d7fe2a9f42b5893d..c5dbcc55dec8f70bcfc98c56e032d85697c82e49 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py39 [flake8] -exclude = ./.tox,./webapp,./compendium_v2/migrations +exclude = ./.tox,./webapp,./compendium_v2/migrations,./setup.py [testenv] deps = diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 8abd76294b6d89bf4f5a22e1816e95b329d801de..c945027a18785c3002d931423f68ea47134b0fe3 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 d34bfc256089a18b486b60bfc814fe39ed77b5a6..905dd1d3487017a6ca18571013a1993976a15895 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 0000000000000000000000000000000000000000..38f49b0f65633cf74f6952c94cf571031ba10c2a --- /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 0000000000000000000000000000000000000000..be9d1e026838498672e334e073deadbc1938f8dc --- /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 0000000000000000000000000000000000000000..6ebb08f1be0a0fc55689111165f301214510848d --- /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 23c73eabefa2cd690e720fca4243aee48fe1dc05..90f744aa561c339a65605b92c6476155aa505bfd 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 2f9f41b9511aee0b603cc9cea44f925f3e0ac5e8..5030d6d0c2920bb708653082a0aff02dde63e2fa 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 5d084e11e4766b1594c2f8d052158dbab10ca91c..85b5b785a869758b7b4b78c6c29b2282d0417432 100644 --- a/webapp/src/styles.scss +++ b/webapp/src/styles.scss @@ -1,5 +1,5 @@ table { - min-width: 650; + min-width: 650px; } thead { diff --git a/webapp/webpack.config.ts b/webapp/webpack.config.ts index 89fd4c2bfbb727ed06139a1017cba593e1288843..58a216d123fa7646f3d7a9f98b52956ab7f98153 100644 --- a/webapp/webpack.config.ts +++ b/webapp/webpack.config.ts @@ -80,7 +80,11 @@ const config: webpack.Configuration = { compress: true, port: 4000, // Allow SPA urls to work with dev-server - historyApiFallback: true + historyApiFallback: true, + proxy: { + '/api': 'http://127.0.0.1:5000' + } + }, plugins: [ new ForkTsCheckerWebpackPlugin({