diff --git a/compendium-frontend/src/App.tsx b/compendium-frontend/src/App.tsx index 56e412d763f61ebf70b0ab8a08564d27b537c266..dc9c25adbec4fd4ba092152c3bb2599b2e70f49a 100644 --- a/compendium-frontend/src/App.tsx +++ b/compendium-frontend/src/App.tsx @@ -13,6 +13,7 @@ import ParentOrganisation from "./pages/ParentOrganisation"; import ECProjects from "./pages/ECProjects"; import Providers from "./Providers"; import PolicyPage from "./pages/Policy"; +import TrafficVolumePage from "./pages/TrafficVolumePerNren"; const router = createBrowserRouter([ @@ -25,6 +26,7 @@ const router = createBrowserRouter([ { path: "/parentorganisation", element: <ParentOrganisation />}, { path: "/ec-projects", element: <ECProjects />}, { path: "/policy", element: <PolicyPage />}, + { path: "/traffic-volume", element: <TrafficVolumePage />}, { path: "/data", element: <CompendiumData />}, { path: "*", element: <Landing />}, ]); diff --git a/compendium-frontend/src/Schema.tsx b/compendium-frontend/src/Schema.tsx index 2ea12a60f494dbdfe65a764534c6e6ffc78ad6b2..b7e39efdc80884be722b123802bbdcc683b25107 100644 --- a/compendium-frontend/src/Schema.tsx +++ b/compendium-frontend/src/Schema.tsx @@ -18,6 +18,13 @@ export interface Budget extends NrenAndYearDatapoint { budget: string } +export interface TrafficVolume extends NrenAndYearDatapoint { + from_customers: number, + to_customers: number, + from_external: number, + to_external: number +} + export interface FundingSource extends NrenAndYearDatapoint { client_institutions: number, commercial: number, diff --git a/compendium-frontend/src/components/DataPage.tsx b/compendium-frontend/src/components/DataPage.tsx index c3af795939265d30774e4d8ef1bf3f092ca16c90..4c6a812bb730c53fe0dccb39be630979c2f61a09 100644 --- a/compendium-frontend/src/components/DataPage.tsx +++ b/compendium-frontend/src/components/DataPage.tsx @@ -9,6 +9,7 @@ import PolicySidebar from "./PolicySidebar"; import { Chart as ChartJS } from 'chart.js'; import { usePreview } from "../helpers/usePreview"; +import NetworkSidebar from "./NetworkSidebar"; ChartJS.defaults.font.size = 16; ChartJS.defaults.font.family = 'Open Sans'; @@ -30,6 +31,7 @@ function DataPage({ title, description, filter, children, category }: inputProps <> {category === Sections.Organisation && <OrganizationSidebar />} {category === Sections.Policy && <PolicySidebar />} + {category === Sections.Network && <NetworkSidebar />} <PageHeader type={'data'} /> { preview && <Row className="preview-banner"> <span>You are viewing a preview of the website which includes pre-published survey data. <a href={locationWithoutPreview}>Click here</a> to deactivate preview mode.</span> diff --git a/compendium-frontend/src/components/NetworkSidebar.tsx b/compendium-frontend/src/components/NetworkSidebar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1274242afcc736e29c95897df673f32ff8dee36d --- /dev/null +++ b/compendium-frontend/src/components/NetworkSidebar.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Row } from 'react-bootstrap'; +import Sidebar from './SideBar'; + +const PolicySidebar = () => { + return ( + <Sidebar> + <h5>Network</h5> + <Row> + <Link to="/traffic-volume" className="link-text-underline"> + <span>Traffic volume</span> + </Link> + </Row> + </Sidebar> + ) +} + +export default PolicySidebar \ No newline at end of file diff --git a/compendium-frontend/src/components/SectionNavigation.tsx b/compendium-frontend/src/components/SectionNavigation.tsx index 01c2a950a3fdd9067276aac6f0893e8e81cbe4c1..1636ac7dc260813d5c55831d2afd6d328b8ed1c9 100644 --- a/compendium-frontend/src/components/SectionNavigation.tsx +++ b/compendium-frontend/src/components/SectionNavigation.tsx @@ -33,10 +33,9 @@ const SectionNavigation = ({ activeCategory }: inputProps) => { <span>{Sections.ConnectedUsers}</span> </Button> <Button - onClick={() => navigate(activeCategory === Sections.Network ? '.' : '.')} + onClick={() => navigate(activeCategory === Sections.Network ? '.' : '/traffic-volume')} variant={'nav-box'} - active={activeCategory === Sections.Network} - disabled={true}> + active={activeCategory === Sections.Network}> <span>{Sections.Network}</span> </Button> <Button diff --git a/compendium-frontend/src/helpers/dataconversion.tsx b/compendium-frontend/src/helpers/dataconversion.tsx index c5a53aa8f1e758b209c741b0109fd300c7a79a5a..a8611c879ee69f2956115970f112acfb9c751aad 100644 --- a/compendium-frontend/src/helpers/dataconversion.tsx +++ b/compendium-frontend/src/helpers/dataconversion.tsx @@ -1,7 +1,7 @@ import { cartesianProduct } from 'cartesian-product-multiple-arrays'; import { FundingSource, FundingSourceDataset, ChargingStructure, - Budget, BasicDataset, NrenStaff, NrenStaffDataset, Organisation, ECProject, Policy + Budget, BasicDataset, NrenStaff, NrenStaffDataset, Organisation, ECProject, Policy, TrafficVolume } from "../Schema"; // create a color from a string, credits https://stackoverflow.com/a/16348977 @@ -54,6 +54,50 @@ function CreateDataLookup(data: FundingSource[]) { return dataLookup } +export const createTrafficVolumeDataset = (fundingSourcesData: TrafficVolume[]) => { + const data = fundingSourcesData; + const dataLookup = new Map<string, number>(); + data.forEach((item: TrafficVolume) => { + const lookupKey = `${item.nren}/${item.year}`; + console.log(lookupKey ); + dataLookup.set(lookupKey, item.from_customers); // we ignore the rest of the data for now.. + }) + + const labelsYear = [...new Set(data.map((item: TrafficVolume) => item.year))]; + const labelsNREN = [...new Set(data.map((item: TrafficVolume) => item.nren))]; + + const sets = labelsYear.map(year => { + return { + backgroundColor: 'rgba(40, 40, 250, 0.8)', + data: labelsNREN.map((nren) => dataLookup.get(`${nren}/${year}`) ?? null), + label: year.toString(), + borderSkipped: true, + barPercentage: 0.8, + borderWidth: 0.5, + categoryPercentage: 0.8, + hidden: false, + datalabels: { + display: true, + color: 'grey', + formatter: function(value, context) { + return context.dataset.label; + }, + anchor: 'start', + align: 'end', + offset: function(context) { + return context.chart.chartArea.width; + } + } + } + }); + + const dataResponse: BasicDataset = { + datasets: sets, + labels: labelsNREN.map(l => l.toString()) + } + return dataResponse; +} + export const createFundingSourceDataset = (fundingSourcesData: FundingSource[]) => { const data = fundingSourcesData; const dataLookup = CreateDataLookup(data) diff --git a/compendium-frontend/src/pages/CompendiumData.tsx b/compendium-frontend/src/pages/CompendiumData.tsx index 2b20e067713a9f3fd27a6505ad70595a3fc579d7..504ecd7c48fea1e09828a91e1f70f31b43777ed5 100644 --- a/compendium-frontend/src/pages/CompendiumData.tsx +++ b/compendium-frontend/src/pages/CompendiumData.tsx @@ -95,7 +95,11 @@ function CompendiumData(): ReactElement { </CollapsibleBox> <CollapsibleBox title={Sections.Network} startCollapsed> <div className="collapsible-column"> - <h5>Coming Soon</h5> + <Row> + <Link to="/traffic-volume" className="link-text-underline"> + <span>Total yearly traffic volume per NREN</span> + </Link> + </Row> </div> </CollapsibleBox> <CollapsibleBox title={Sections.Services} startCollapsed> diff --git a/compendium-frontend/src/pages/TrafficVolumePerNren.tsx b/compendium-frontend/src/pages/TrafficVolumePerNren.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5cd86d80df332dfe0175c3160cf583ff43ef58a2 --- /dev/null +++ b/compendium-frontend/src/pages/TrafficVolumePerNren.tsx @@ -0,0 +1,127 @@ +import React, { useContext } from 'react'; +import { Bar } from 'react-chartjs-2'; +import { Col, Row } from "react-bootstrap"; +import { Chart as ChartJS } from 'chart.js'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; + +import { TrafficVolume } from "../Schema"; +import { createTrafficVolumeDataset } from "../helpers/dataconversion"; +import DataPage from '../components/DataPage'; +import Filter from "../components/graphing/Filter" +import { ExportType, Sections } from '../helpers/constants'; +import DownloadDataButton from "../components/DownloadDataButton"; +import { FilterSelectionContext } from '../helpers/FilterSelectionProvider'; +import DownloadImageChartButton from "../components/DownloadImageChartButton"; +import ChartContainer from "../components/graphing/ChartContainer"; +import { useData } from '../helpers/useData'; + +export const chartOptions = { + maintainAspectRatio: false, + layout: { + padding: { + right: 60 + } + }, + animation: { + duration: 0, + }, + plugins: { + legend: { + display: false + } + }, + scales: { + x: { + position: "top" as const + }, + xBottom: { + grid: { + drawOnChartArea: false + }, + afterDataLimits: function (axis) { + const indices = Object.keys(ChartJS.instances) + + // initial values should be far outside possible range + let max = -99999999 + let min = 99999999 + + for (const index of indices) { + if (ChartJS.instances[index] && axis.chart.scales.xBottom) { + min = Math.min(ChartJS.instances[index].scales.x.min, min); + max = Math.max(ChartJS.instances[index].scales.x.max, max); + } + } + + axis.chart.scales.xBottom.options.min = min; + axis.chart.scales.xBottom.options.max = max; + axis.chart.scales.xBottom.min = min; + axis.chart.scales.xBottom.max = max; + }, + }, + y: { + ticks: { + autoSkip: false + }, + } + }, + indexAxis: "y" as const, +}; + +function TrafficVolumePage() { + const { filterSelection, setFilterSelection } = useContext(FilterSelectionContext); + const { data: trafficVolumeData, years, nrens } = useData<TrafficVolume>('/api/traffic/', setFilterSelection); + + const trafficVolumeDataset = createTrafficVolumeDataset(trafficVolumeData); + + trafficVolumeDataset.datasets.forEach(dataset => { + dataset.hidden = !filterSelection.selectedYears.includes(parseInt(dataset.label)); + }); + + // remove the datapoints and labels for the nrens that aren't selected + // unfortunately we cannot just hide them because chart.js doesn't want + // to create a stack from a single dataset + trafficVolumeDataset.datasets.forEach(dataset => { + dataset.data = dataset.data.filter((e, i) => { + return filterSelection.selectedNrens.includes(trafficVolumeDataset.labels[i]); + }); + }); + trafficVolumeDataset.labels = trafficVolumeDataset.labels.filter((e) => filterSelection.selectedNrens.includes(e)); + + const filterNode = <Filter + filterOptions={{ availableYears: [...years], availableNrens: [...nrens.values()] }} + filterSelection={filterSelection} + setFilterSelection={setFilterSelection} + /> + + const numNrens = filterSelection.selectedNrens.length; + const numYears = filterSelection.selectedYears.length; + const heightPerBar = 2; // every added bar should give this much additional height + + console.log(trafficVolumeDataset) + + // set a minimum height of 20rem, additional years need some more space + const height = numNrens * numYears * heightPerBar + 5; + return ( + <DataPage title="Traffic Volume Of NRENs per Year" + description='Total yearly traffic volume in terabyte per NREN' + category={Sections.Network} filter={filterNode}> + <> + <Row> + <DownloadDataButton data={trafficVolumeData} filename="traffic_volume_of_nren_per_year.csv" exportType={ExportType.CSV} /> + <DownloadDataButton data={trafficVolumeData} filename="traffic_volume_of_nren_per_year.xlsx" exportType={ExportType.EXCEL} /> + </Row> + <DownloadImageChartButton filename="traffic_volume_of_nren_per_year" /> + <ChartContainer> + <div className="chart-container" style={{ 'height': `${height}rem` }}> + <Bar + plugins={[ChartDataLabels]} + data={trafficVolumeDataset} + options={chartOptions} + /> + </div> + </ChartContainer> + </> + </DataPage> + ); +} +export default TrafficVolumePage;