From e958883a89fa59ba4a1d1577306f885080896ee1 Mon Sep 17 00:00:00 2001
From: "saket.agrahari" <saket.agrahari@geant.org>
Date: Wed, 12 Apr 2023 03:27:07 +0100
Subject: [PATCH] COMP-119: charging structure be fe migration test
---
.../background_task/parse_excel_data.py | 47 ++++-
compendium_v2/db/model.py | 19 ++
...1a8f4c_adding_charging_structure_schema.py | 33 ++++
.../publishers/survey_publisher_v1.py | 13 ++
compendium_v2/routes/api.py | 2 +
compendium_v2/routes/charging.py | 68 ++++++++
test/conftest.py | 18 ++
test/data/ChargingStructureTestData.csv | 85 +++++++++
test/test_charging_structure.py | 13 ++
test/test_survey_publisher_v1.py | 3 +
webapp/src/App.tsx | 3 +
webapp/src/Schema.tsx | 20 +++
webapp/src/helpers/dataconversion.tsx | 93 +++++++++-
webapp/src/pages/ChargingStructure.tsx | 164 ++++++++++++++++++
webapp/src/pages/CompendiumData.tsx | 6 +
webapp/tsconfig.json | 4 +-
16 files changed, 586 insertions(+), 5 deletions(-)
create mode 100644 compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py
create mode 100644 compendium_v2/routes/charging.py
create mode 100644 test/data/ChargingStructureTestData.csv
create mode 100644 test/test_charging_structure.py
create mode 100644 webapp/src/pages/ChargingStructure.tsx
diff --git a/compendium_v2/background_task/parse_excel_data.py b/compendium_v2/background_task/parse_excel_data.py
index 69f54c6a..3e337765 100644
--- a/compendium_v2/background_task/parse_excel_data.py
+++ b/compendium_v2/background_task/parse_excel_data.py
@@ -2,6 +2,7 @@ import openpyxl
import os
import logging
+from compendium_v2.db.model import FeeType
from compendium_v2.environment import setup_logging
setup_logging()
@@ -14,7 +15,6 @@ EXCEL_FILE = os.path.join(
def fetch_budget_excel_data():
-
# load the xlsx file
sheet_name = "1. Budget"
wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True)
@@ -43,7 +43,6 @@ def fetch_budget_excel_data():
def fetch_funding_excel_data():
-
# load the xlsx file
wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True)
@@ -112,3 +111,47 @@ def fetch_funding_excel_data():
# For 2020
yield from create_points_for_year(8, 50, 2020, 3)
+
+
+def fetch_charging_structure_excel_data():
+ # load the xlsx file
+ wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True)
+
+ # select the active worksheet
+ sheet_name = "3. Charging mechanism"
+ ws = wb[sheet_name]
+
+ # iterate over the rows in the worksheet
+ def create_points_for_year(start_row, end_row, year, col_start):
+ for row in range(start_row, end_row):
+ # extract the data from the row
+ nren = ws.cell(row=row, column=col_start).value
+ charging_structure = ws.cell(row=row, column=col_start + 1).value
+ logger.info(
+ f'NREN: {nren}, Charging Structure: {charging_structure},'
+ f' Year: {year}')
+ if charging_structure is not None:
+ if "do not charge" in charging_structure:
+ charging_structure = FeeType.no_charge.value
+ elif "combination" in charging_structure:
+ charging_structure = FeeType.combination.value
+ elif "flat" in charging_structure:
+ charging_structure = FeeType.flat_fee.value
+ elif "usage-based" in charging_structure:
+ charging_structure = FeeType.usage_based_fee.value
+ elif "Other" in charging_structure:
+ charging_structure = FeeType.other.value
+ else:
+ charging_structure = None
+
+ logger.info(
+ f'NREN: {nren}, Charging Structure: {charging_structure},'
+ f' Year: {year}')
+
+ yield nren, year, charging_structure
+
+ # For 2021
+ yield from create_points_for_year(3, 45, 2021, 2)
+
+ # For 2019
+ yield from create_points_for_year(3, 45, 2019, 6)
diff --git a/compendium_v2/db/model.py b/compendium_v2/db/model.py
index 45d4758d..9936dfb8 100644
--- a/compendium_v2/db/model.py
+++ b/compendium_v2/db/model.py
@@ -1,5 +1,6 @@
import logging
import sqlalchemy as sa
+from enum import Enum
from typing import Any
@@ -30,3 +31,21 @@ class FundingSource(base_schema):
gov_public_bodies = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
commercial = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
other = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
+
+
+class FeeType(Enum):
+ flat_fee = "flat_fee"
+ usage_based_fee = "usage_based_fee"
+ combination = "combination"
+ no_charge = "no_charge"
+ other = "other"
+
+
+class ChargingStructure(base_schema):
+ __tablename__ = 'charging_structure'
+ nren = sa.Column(sa.String(128), primary_key=True)
+ year = sa.Column(sa.Integer, primary_key=True)
+ fee_type = sa.Column('fee_type', sa.Enum("flat_fee", "usage_based_fee",
+ "combination", "no_charge",
+ "other",
+ name="fee_type"), nullable=True)
diff --git a/compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py b/compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py
new file mode 100644
index 00000000..e200725c
--- /dev/null
+++ b/compendium_v2/migrations/versions/b123f21a8f4c_adding_charging_structure_schema.py
@@ -0,0 +1,33 @@
+"""adding charging structure schema
+
+Revision ID: b123f21a8f4c
+Revises: b70ada054046
+Create Date: 2023-04-05 22:02:02.248236
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b123f21a8f4c'
+down_revision = 'b70ada054046'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table(
+ 'charging_structure',
+ sa.Column('nren', sa.String(length=128), nullable=False),
+ sa.Column('year', sa.Integer(), nullable=False),
+ sa.Column('fee_type', sa.Enum("flat_fee", "usage_based_fee",
+ "combination", "no_charge",
+ "other",
+ name="fee_type"), nullable=True),
+ sa.PrimaryKeyConstraint('nren', 'year')
+ )
+
+
+def downgrade():
+ op.drop_table('charging_structure')
diff --git a/compendium_v2/publishers/survey_publisher_v1.py b/compendium_v2/publishers/survey_publisher_v1.py
index 6c6320f8..59716ce9 100644
--- a/compendium_v2/publishers/survey_publisher_v1.py
+++ b/compendium_v2/publishers/survey_publisher_v1.py
@@ -82,10 +82,23 @@ def db_funding_migration():
session.commit()
+def db_charging_structure_migration():
+ with db.session_scope() as session:
+ # Import the data to database
+ data = parse_excel_data.fetch_charging_structure_excel_data()
+
+ for (nren, year, charging_structure) in data:
+ charging_structure_entry = model.ChargingStructure(
+ nren=nren, year=year, fee_type=charging_structure)
+ session.merge(charging_structure_entry)
+ session.commit()
+
+
def _cli(config):
init_db(config)
db_budget_migration()
db_funding_migration()
+ db_charging_structure_migration()
@click.command()
diff --git a/compendium_v2/routes/api.py b/compendium_v2/routes/api.py
index bfb4c1b7..31acbc8d 100644
--- a/compendium_v2/routes/api.py
+++ b/compendium_v2/routes/api.py
@@ -17,10 +17,12 @@ from flask import Blueprint
from compendium_v2.routes import common
from compendium_v2.routes.budget import routes as budget_routes
from compendium_v2.routes.funding import routes as funding_routes
+from compendium_v2.routes.charging import routes as charging_routes
routes = Blueprint('compendium-v2-api', __name__)
routes.register_blueprint(budget_routes, url_prefix='/budget')
routes.register_blueprint(funding_routes, url_prefix='/funding')
+routes.register_blueprint(charging_routes, url_prefix='/charging')
logger = logging.getLogger(__name__)
diff --git a/compendium_v2/routes/charging.py b/compendium_v2/routes/charging.py
new file mode 100644
index 00000000..a0583e40
--- /dev/null
+++ b/compendium_v2/routes/charging.py
@@ -0,0 +1,68 @@
+import logging
+
+from flask import Blueprint, jsonify, current_app
+from compendium_v2 import db
+from compendium_v2.routes import common
+from compendium_v2.db import model
+from typing import Any
+
+routes = Blueprint('charging', __name__)
+
+
+@routes.before_request
+def before_request():
+ config = current_app.config['CONFIG_PARAMS']
+ dsn_prn = config['SQLALCHEMY_DATABASE_URI']
+ db.init_db_model(dsn_prn)
+
+
+logger = logging.getLogger(__name__)
+
+CHARGING_STRUCTURE_RESPONSE_SCHEMA = {
+ '$schema': 'http://json-schema.org/draft-07/schema#',
+
+ 'definitions': {
+ 'charging': {
+ 'type': 'object',
+ 'properties': {
+ 'NREN': {'type': 'string'},
+ 'YEAR': {'type': 'integer'},
+ 'FEE_TYPE': {'type': ["string", "null"]},
+ },
+ 'required': ['NREN', 'YEAR'],
+ 'additionalProperties': False
+ }
+ },
+
+ 'type': 'array',
+ 'items': {'$ref': '#/definitions/charging'}
+}
+
+
+@routes.route('/', methods=['GET'])
+@common.require_accepts_json
+def charging_structure_view() -> Any:
+ """
+ handler for /api/charging/ requests
+
+ response will be formatted as:
+
+ .. asjson::
+ compendium_v2.routes.charging.CHARGING_STRUCTURE_RESPONSE_SCHEMA
+
+ :return:
+ """
+
+ def _extract_data(entry: model.ChargingStructure):
+ return {
+ 'NREN': entry.nren,
+ 'YEAR': int(entry.year),
+ 'FEE_TYPE': entry.fee_type,
+ }
+
+ with db.session_scope() as session:
+ entries = sorted([_extract_data(entry)
+ for entry in session.query(model.ChargingStructure)
+ .all()],
+ key=lambda d: (d['NREN'], d['YEAR']))
+ return jsonify(entries)
diff --git a/test/conftest.py b/test/conftest.py
index bc36503c..34205939 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -149,3 +149,21 @@ def client(data_config_filename, mocked_db, mocked_survey_db):
os.environ['SETTINGS_FILENAME'] = data_config_filename
with compendium_v2.create_app().test_client() as c:
yield c
+
+
+@pytest.fixture
+def test_charging_structure_data():
+ with db.session_scope() as session:
+ data = _test_data_csv("ChargingStructureTestData.csv")
+ for row in data:
+ nren = row["nren"]
+ year = row["year"]
+ fee_type = row["fee_type"]
+ if fee_type == "null":
+ fee_type = None
+
+ session.add(
+ model.ChargingStructure(
+ nren=nren, year=year,
+ fee_type=fee_type)
+ )
diff --git a/test/data/ChargingStructureTestData.csv b/test/data/ChargingStructureTestData.csv
new file mode 100644
index 00000000..077661fe
--- /dev/null
+++ b/test/data/ChargingStructureTestData.csv
@@ -0,0 +1,85 @@
+nren,year,fee_type
+ACOnet,2021,usage_based_fee
+AMRES,2021,no_charge
+ANA,2021,combination
+ARNES,2021,no_charge
+ASNET-AM,2021,combination
+BASNET,2021,combination
+Belnet,2021,no_charge
+BREN,2021,no_charge
+CARNET,2021,no_charge
+CESNET,2021,flat_fee
+CYNET,2021,flat_fee
+DeIC,2021,combination
+DFN,2021,flat_fee
+EENet,2021,no_charge
+FCCN,2021,usage_based_fee
+GARR,2021,no_charge
+GRENA,2021,flat_fee
+GRNET S.A.,2021,no_charge
+HEAnet,2021,no_charge
+Jisc,2021,usage_based_fee
+LITNET,2021,no_charge
+MARnet,2021,no_charge
+MREN,2021,no_charge
+PIONIER,2021,flat_fee
+RedIRIS,2021,no_charge
+RENAM,2021,combination
+RENATER,2021,flat_fee
+RESTENA,2021,flat_fee
+RoEduNet,2021,no_charge
+SANET,2021,flat_fee
+SWITCH,2021,combination
+ULAKBIM,2021,no_charge
+ACOnet,2019,flat_fee
+AMRES,2019,no_charge
+ANA,2019,combination
+ARNES,2019,no_charge
+AzScienceNet,2019,no_charge
+BASNET,2019,combination
+BELNET,2019,flat_fee
+BREN,2019,no_charge
+CARNet,2019,no_charge
+CESNET,2019,no_charge
+DFN,2019,no_charge
+EENet,2019,no_charge
+FCCN,2019,no_charge
+Funet,2019,combination
+GARR,2019,no_charge
+GRENA,2019,flat_fee
+GRNET S.A.,2019,no_charge
+HEAnet,2019,combination
+Jisc,2019,flat_fee
+LITNET,2019,no_charge
+MARNET,2019,usage_based_fee
+MREN,2019,no_charge
+PIONIER,2019,flat_fee
+RedIRIS,2019,no_charge
+RENAM,2019,combination
+RENATER,2019,no_charge
+RESTENA,2019,no_charge
+RoEduNet,2019,no_charge
+SANET,2019,flat_fee
+SURFnet,2019,combination
+SWITCH,2019,combination
+ULAKBIM,2019,no_charge
+AzScienceNet,2021,null
+LAT,2021,null
+RHnet,2021,null
+SURFnet,2021,null
+Uninett,2021,null
+UoM/RicerkaNet,2021,null
+ASNET,2019,null
+CYNET,2019,null
+DeIC,2019,null
+KIFU (NIIF),2019,null
+LANET,2019,null
+RhNET,2019,null
+UNINETT,2019,null
+UoM,2019,null
+Funet,2021,other
+IUCC,2021,other
+KIFU,2021,other
+SUNET,2021,other
+IUCC,2019,other
+SUNET,2019,other
diff --git a/test/test_charging_structure.py b/test/test_charging_structure.py
new file mode 100644
index 00000000..c530e6df
--- /dev/null
+++ b/test/test_charging_structure.py
@@ -0,0 +1,13 @@
+import json
+import jsonschema
+from compendium_v2.routes.charging import CHARGING_STRUCTURE_RESPONSE_SCHEMA
+
+
+def test_charging_structure_response(client, test_charging_structure_data):
+ rv = client.get(
+ '/api/charging/',
+ headers={'Accept': ['application/json']})
+ assert rv.status_code == 200
+ result = json.loads(rv.data.decode('utf-8'))
+ jsonschema.validate(result, CHARGING_STRUCTURE_RESPONSE_SCHEMA)
+ assert result
diff --git a/test/test_survey_publisher_v1.py b/test/test_survey_publisher_v1.py
index 08461f1f..2f669937 100644
--- a/test/test_survey_publisher_v1.py
+++ b/test/test_survey_publisher_v1.py
@@ -20,3 +20,6 @@ def test_publisher(client, mocker, dummy_config):
assert budget_count
funding_source_count = session.query(model.FundingSource.year).count()
assert funding_source_count
+ charging_structure_count = session.query(model.ChargingStructure.year)\
+ .count()
+ assert charging_structure_count
diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index 3e2e1b9d..5dbd117b 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -7,6 +7,8 @@ import DataAnalysis from "./pages/DataAnalysis";
import AnnualReport from "./pages/AnnualReport";
import CompendiumData from "./pages/CompendiumData";
import FundingSourcePage from "./pages/FundingSource";
+import ChargingStructurePage from "./pages/ChargingStructure";
+
function App(): ReactElement {
return (
@@ -19,6 +21,7 @@ function App(): ReactElement {
<Route path="/analysis" element={<DataAnalysis />} />
<Route path="/report" element={<AnnualReport />} />
<Route path="/funding" element={<FundingSourcePage />} />
+ <Route path="/charging" element={<ChargingStructurePage />} />
<Route path="*" element={<Landing />} />
</Routes>
</Router>
diff --git a/webapp/src/Schema.tsx b/webapp/src/Schema.tsx
index 380237d6..76558fd2 100644
--- a/webapp/src/Schema.tsx
+++ b/webapp/src/Schema.tsx
@@ -53,6 +53,26 @@ export interface FundingSourceDataset {
}[]
}
+export interface ChargingStructure{
+ NREN: string,
+ YEAR: number,
+ FEE_TYPE: (string | null),
+}
+
+export interface ChargingStructureDataset {
+ labels: string[],
+ datasets: {
+ label: string,
+ data: { x: number | null; y: number | null; }[],
+ backgroundColor: string
+ borderRadius: number,
+ borderSkipped: boolean,
+ barPercentage: number,
+ borderWidth: number,
+ categoryPercentage: number,
+ }[]
+}
+
export interface DataEntrySection {
name: string,
description: string,
diff --git a/webapp/src/helpers/dataconversion.tsx b/webapp/src/helpers/dataconversion.tsx
index a3bb36ac..edfb615a 100644
--- a/webapp/src/helpers/dataconversion.tsx
+++ b/webapp/src/helpers/dataconversion.tsx
@@ -2,7 +2,9 @@ import { cartesianProduct } from 'cartesian-product-multiple-arrays';
import {
FundingSource,
- FundingSourceDataset
+ FundingSourceDataset,
+ ChargingStructure,
+ ChargingStructureDataset
} from "../Schema";
const DEFAULT_FUNDING_SOURCE_DATA = [
@@ -16,6 +18,13 @@ const DEFAULT_FUNDING_SOURCE_DATA = [
"YEAR": 0,
"id": 0
}]
+const DEFAULT_CHARGING_STRUCTURE_DATA = [
+ {
+ "NREN": "",
+ "YEAR": 0,
+ "FEE_TYPE": "",
+ }
+]
function getColorMap() {
const rgbToHex = (r: number, g: number, b: number) => '#' + [r, g, b].map(x => {
@@ -99,4 +108,84 @@ export const createFundingSourceDataset = (fundingSourcesData: FundingSource[])
labels: labelsNREN.map(l => l.toString())
}
return dataResponse;
-}
\ No newline at end of file
+}
+
+
+function createChargingStructureDataLookup(data: ChargingStructure[]) {
+ let dataLookup = new Map<string, (string|null)>();
+ data.forEach((item: ChargingStructure) => {
+ const lookupKey = `${item.NREN}/${item.YEAR}`
+ dataLookup.set(lookupKey, item.FEE_TYPE)
+ })
+ return dataLookup;
+}
+
+export function coordinateLookupForFeeStructure(feeType:string,nren:string,
+ yearIndex:number,nrenIndex:number){
+ switch(feeType){
+ case "flat_fee":
+ return {x:(0+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType}
+ case "usage_based_fee":
+ return {x:(5+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType}
+ case "combination":
+ return {x:(10+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType}
+ case "no_charge":
+ return {x:(15+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType}
+ case "other":
+ return {x:(20+((yearIndex+1)*2)),y:(nrenIndex+1),feeType:feeType}
+ default:
+ return {x:0,y:0}
+ }
+}
+
+export function lookupColorForYearIndex(yearIndex:number){
+ switch(yearIndex){
+ case 0:
+ return "rgba(244, 144, 28, 1)"
+ case 1:
+ return "rgba(140, 168, 128, 1)"
+ default:
+ return "rgba(0, 0, 0, 0)"
+ }
+}
+
+export function createChargingStructureDataset(chargingStructureData: ChargingStructure[]) {
+ const data = chargingStructureData ?? DEFAULT_CHARGING_STRUCTURE_DATA;
+ const dataLookup = createChargingStructureDataLookup(data)
+
+ const labelsNREN = [...new Set(data.map((item: ChargingStructure) => item.NREN))];
+ const labelsYear = [...new Set(data.map((item: ChargingStructure) => item.YEAR))];
+ const sizeOfYear = labelsYear.length;
+ const chargingStructureDataset = labelsYear.map(function (year,yearIndex) {
+ return {
+ label: "(" + year + ")",
+ data: labelsNREN.map((nren,nrenIndex) => {
+ // ensure that the data is in the same order as the labels
+ const lookupKey = `${nren}/${year}`
+ const fee_type =dataLookup.get(lookupKey) ?? ""
+ return coordinateLookupForFeeStructure(fee_type,nren,yearIndex,nrenIndex)
+
+ }),
+ backgroundColor: lookupColorForYearIndex(yearIndex),
+ borderColor: lookupColorForYearIndex(yearIndex),
+ pointRadius:20,
+ usePointStyle: true,
+ pointStyle: 'rectRounded',
+ pointBackgroundColor: lookupColorForYearIndex(yearIndex),
+ borderRadius: 0,
+ borderSkipped: false,
+ barPercentage: 0.5,
+ borderWidth: 0.5,
+ categoryPercentage: 0.8
+ }
+ }
+ )
+
+ const dataResponse: ChargingStructureDataset = {
+ datasets: chargingStructureDataset,
+ labels: labelsNREN.map(l => l.toString())
+ }
+ return dataResponse;
+
+
+}
diff --git a/webapp/src/pages/ChargingStructure.tsx b/webapp/src/pages/ChargingStructure.tsx
new file mode 100644
index 00000000..a90b3e7c
--- /dev/null
+++ b/webapp/src/pages/ChargingStructure.tsx
@@ -0,0 +1,164 @@
+import React, { useEffect, useState } from "react";
+import { Container, Row, Col } from "react-bootstrap";
+import Chart, { ChartOptions, Plugin, ChartTypeRegistry, ScatterDataPoint } from 'chart.js';
+import { Bubble, Scatter } from "react-chartjs-2";
+import { createChargingStructureDataset } from "../helpers/dataconversion";
+import { ChargingStructure, ChargingStructureDataset } from "../Schema";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+ scales,
+} from 'chart.js';
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend
+);
+
+const EMPTY_DATASET = { datasets: [], labels: [] };
+
+const DATA_COUNT = 7;
+const NUMBER_CFG = {count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100};
+const test_data = {
+ labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+ datasets: [{
+ label: 'Weekly Sales',
+ data: [
+ { x: 1, y: 5 },
+ { x: 3, y: 0 },
+ { x: 3, y: 2 },
+ { x: 5, y: 3 },
+ { x: 7, y: 4 }
+ ],
+ backgroundColor: 'rgba(244, 144, 28, 1)',
+ borderColor: 'rgba(244, 144, 28, 1)',
+ borderWidth: 0,
+ pointRadius: 40,
+ usePointStyle: true,
+ pointStyle: 'rectRounded',
+ },
+ {
+ label: 'Monthly Sales',
+ data: [
+ { x: 2, y: 1 },
+ { x: 1, y: 2 },
+ { x: 6, y: 3 },
+ { x: 8, y: 4 },
+ { x: 9, y: 5 }
+ ],
+ backgroundColor: 'rgba(140, 168, 128, 1)',
+ borderColor: 'rgba(140, 168, 128, 1)',
+ borderWidth: 0,
+ pointRadius: 40,
+ usePointStyle: true,
+ pointStyle: 'rectRounded',
+ }]
+ };
+
+const plugin = {
+ id: 'customCanvasBackgroundColor',
+ beforeDraw: (chart:any, args:any, options:any) => {
+ const {ctx} = chart;
+ ctx.save();
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.fillStyle = options.color || '#99ffff';
+ ctx.fillRect(0, 0, chart.width, chart.height);
+ ctx.restore();
+ }
+ };
+
+
+const options = {
+ maintainAspectRatio: false,
+ layout: {
+ padding: {
+ left: 100,
+ top: 100,
+ }
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ customCanvasBackgroundColor: {
+ color: 'lightGreen',
+ }
+ },
+ scales: {
+ x: {
+ position: "top" as const,
+ ticks: {
+ display: false,
+ stepSize: 5,
+ },
+ },
+ y: {
+ grid: {
+ display: false
+ },
+ ticks: {
+ display: false,
+ stepSize: 5,
+ },
+ },
+ }
+ }
+
+
+async function getData(): Promise<ChargingStructure[]> {
+ try {
+ const response = await fetch('/api/charging/');
+ return response.json();
+ } catch (error) {
+ console.error(`Failed to load data: ${error}`);
+ throw error;
+ }
+}
+
+function ChargingStructurePage (): React.ReactElement {
+ const [chargingStructureDataSet,setDataset]= useState<ChargingStructureDataset>(EMPTY_DATASET);
+ const [chargingStructureData, setChargingStructureData] = useState<ChargingStructure[]>([]);
+ console.log("ChargingStructurePage");
+
+
+ React.useEffect(() => {
+ const loadData = async () => {
+ const _chargingStructureData = await getData();
+ setChargingStructureData(_chargingStructureData);
+ }
+ loadData();
+ }, []);
+
+ useEffect(() => {
+ if(chargingStructureDataSet !== undefined) {
+ setDataset(createChargingStructureDataset(chargingStructureData));
+ console.log(chargingStructureDataSet);
+ }
+ }, [chargingStructureData]);
+
+ return (
+ <Container>
+ <Row>
+ <Col>
+ <h1>Charging Structure</h1>
+ </Col>
+ </Row>
+ <Row>
+ <Col>
+ <div className="chart-container" style={{ 'minHeight': '300vh', 'width': '80vw', }}>
+ <Bubble data={chargingStructureDataSet} options={options} />
+ </div>
+ </Col>
+ </Row>
+ </Container>
+ );
+};
+export default ChargingStructurePage;
diff --git a/webapp/src/pages/CompendiumData.tsx b/webapp/src/pages/CompendiumData.tsx
index 84b42507..b966c857 100644
--- a/webapp/src/pages/CompendiumData.tsx
+++ b/webapp/src/pages/CompendiumData.tsx
@@ -38,6 +38,12 @@ function CompendiumData(): ReactElement {
<span>Income Source of NRENs per Year</span>
</Link>
</Row>
+
+ <Row>
+ <Link to="/charging" state={{ graph: 'charging_structure' }}>
+ <span>Charging Mechanism of NRENs per Year</span>
+ </Link>
+ </Row>
</div>
</CollapsibleBox>
</div>
diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json
index 52b6b1da..a33ff451 100644
--- a/webapp/tsconfig.json
+++ b/webapp/tsconfig.json
@@ -12,7 +12,9 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
- "jsx": "react"
+ "jsx": "react",
+ "declaration": true,
+ "declarationDir": "dist/types"
},
"include": ["src"]
}
--
GitLab