diff --git a/compendium-frontend/src/components/DownloadCSVButton.tsx b/compendium-frontend/src/components/DownloadCSVButton.tsx deleted file mode 100644 index b4caf249e30c1fa347f7d03a277f4ebaaf3b2cc8..0000000000000000000000000000000000000000 --- a/compendium-frontend/src/components/DownloadCSVButton.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; - -interface DownloadCSVProps { - data: any[]; - filename?: string; -} - - -function createCSVRows(jsonData: any[], header: string[]): string[] { - return jsonData.map(obj => { - return header.map(fieldName => { - const value = obj[fieldName]; - - if (value === null) { - return ""; - } - - // Always wrap strings in double quotes and escape internal double quotes - if (typeof value === 'string') { - return `"${value.replace(/"/g, '""')}"`; - } - - return value; - }).join(','); - }); -} - - -function convertToCSV(jsonData: any[]): string { - if (!jsonData.length) return ""; - - const header = Object.keys(jsonData[0]); - const rows = createCSVRows(jsonData, header); - - // Combine header and rows with newline characters - return [header.join(','), ...rows].join('\r\n'); -} - - -const DownloadCSVButton: React.FC<DownloadCSVProps> = ({ data, filename = 'data.csv' }) => { - - const downloadCSV = () => { - const csv = convertToCSV(data); - const blob = new Blob([csv], { type: 'text/csv' }); - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - - return ( - <> - <button onClick={downloadCSV}>Download CSV</button> - </> - ); -} - -export default DownloadCSVButton; diff --git a/compendium-frontend/src/components/DownloadDataButton.tsx b/compendium-frontend/src/components/DownloadDataButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f5b4a128121eac958c2d439221cd58bf95fcb937 --- /dev/null +++ b/compendium-frontend/src/components/DownloadDataButton.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import * as XLSX from "xlsx"; +import {ExportType} from "../helpers/constants"; +import * as buffer from "buffer"; + + + +interface DownloadCSVProps { + data: any[]; + filename: string; + exportType: ExportType; +} + + +function createCSVRows(jsonData: any[], header: string[]): string[] { + return jsonData.map(obj => { + return header.map(fieldName => { + const value = obj[fieldName]; + + if (value === null) { + return ""; + } + + // Always wrap strings in double quotes and escape internal double quotes + if (typeof value === 'string') { + return `"${value.replace(/"/g, '""')}"`; + } + + return value; + }).join(','); + }); +} + + +function convertToCSV(jsonData: any[]): string { + if (!jsonData.length) return ""; + + const header = Object.keys(jsonData[0]); + const rows = createCSVRows(jsonData, header); + + // Combine header and rows with newline characters + return [header.join(','), ...rows].join('\r\n'); +} + +function convertToExcel(jsonData: any[], sheetName = "Sheet1"): Blob { + const ws = XLSX.utils.json_to_sheet(jsonData); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, sheetName); + const wbout = XLSX.write(wb, {bookType: 'xlsx', type: 'binary'}); + + const buffer = new ArrayBuffer(wbout.length); + const view = new Uint8Array(buffer); + for (let i = 0; i < wbout.length; i++) { + // Convert each character of the binary workbook string to an 8-bit integer and store in the Uint8Array 'view' for blob creation. + view[i] = wbout.charCodeAt(i) & 0xFF; + } + + return new Blob([buffer], {type: 'application/octet-stream'}); +} + +const DownloadDataButton: React.FC<DownloadCSVProps> = ({data, filename, exportType}) => { + + const downloadData = () => { + let blob; + + switch (exportType) { + case ExportType.EXCEL: { + const csv = convertToExcel(data); + blob = new Blob([csv], {type: 'application/octet-stream'}); + filename = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`; + break; + } + case ExportType.CSV: + default: { + const csvData = convertToCSV(data); + blob = new Blob([csvData], {type: 'text/csv'}); + filename = filename.endsWith('.csv') ? filename : `${filename}.csv`; + break; + } + } + + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + return ( + <> + <button onClick={downloadData}>Download {exportType}</button> + </> + ); +} + +export default DownloadDataButton; diff --git a/compendium-frontend/src/components/DownloadExcelButton.tsx b/compendium-frontend/src/components/DownloadExcelButton.tsx deleted file mode 100644 index f51e9e2f32bc46b53be3f953fd3ba8dee2fac606..0000000000000000000000000000000000000000 --- a/compendium-frontend/src/components/DownloadExcelButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import * as XLSX from 'xlsx'; - -interface DownloadExcelProps { - data: any[]; - filename?: string; - sheetName?: string; -} - -function convertToExcel(jsonData: any[], sheetName= "Sheet1"): Blob { - const ws = XLSX.utils.json_to_sheet(jsonData); - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, sheetName); - const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'binary' }); - - const buffer = new ArrayBuffer(wbout.length); - const view = new Uint8Array(buffer); - for (let i = 0; i < wbout.length; i++) { - // Convert each character of the binary workbook string to an 8-bit integer and store in the Uint8Array 'view' for blob creation. - view[i] = wbout.charCodeAt(i) & 0xFF; - } - - return new Blob([buffer], { type: 'application/octet-stream' }); -} - -const DownloadExcelButton: React.FC<DownloadExcelProps> = ({ data, filename = 'data.xlsx', sheetName }) => { - - const downloadExcel = () => { - const blob = convertToExcel(data, sheetName); - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - - return ( - <> - <button onClick={downloadExcel}>Download Excel</button> - </> - ); -} - -export default DownloadExcelButton; diff --git a/compendium-frontend/src/helpers/constants.ts b/compendium-frontend/src/helpers/constants.ts index aaed454cd30469e5ebfbd5cd13a46efc4281c3c8..7654b5ac3220690b240374d543a67e5422e9f9ff 100644 --- a/compendium-frontend/src/helpers/constants.ts +++ b/compendium-frontend/src/helpers/constants.ts @@ -4,4 +4,9 @@ export enum Sections { ConnectedUsers = 'CONNECTED USERS', Network = 'NETWORK', Services = 'SERVICES', +} + +export enum ExportType { + CSV = "CSV", + EXCEL = "EXCEL", } \ No newline at end of file diff --git a/compendium-frontend/src/pages/Budget.tsx b/compendium-frontend/src/pages/Budget.tsx index 7ad98bdcf50a61d7a1552f047665d1b36c3c8a05..e3eb58aba81e69008365fad859808220bab1fa4a 100644 --- a/compendium-frontend/src/pages/Budget.tsx +++ b/compendium-frontend/src/pages/Budget.tsx @@ -1,14 +1,17 @@ -import React, { ReactElement, useEffect, useMemo, useState } from 'react'; -import { Row } from "react-bootstrap"; +import React, {ReactElement, useEffect, useMemo, useState} from 'react'; +import {Row} from "react-bootstrap"; -import { Budget, FilterSelection } from "../Schema"; -import { createBudgetDataset, getYearsAndNrens, loadDataWithFilterNrenSelectionFallback } from "../helpers/dataconversion"; +import {Budget, FilterSelection} from "../Schema"; +import { + createBudgetDataset, + getYearsAndNrens, + loadDataWithFilterNrenSelectionFallback +} from "../helpers/dataconversion"; import DataPage from '../components/DataPage'; import Filter from "../components/graphing/Filter"; import LineGraph from "../components/graphing/LineGraph"; -import { Sections } from '../helpers/constants'; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import {ExportType, Sections} from '../helpers/constants'; +import DownloadDataButton from "../components/DownloadDataButton"; interface inputProps { filterSelection: FilterSelection @@ -47,8 +50,8 @@ function BudgetPage({ filterSelection, setFilterSelection }: inputProps): ReactE fluctuation of budget over years and with other NRENs.' category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={budgetResponse} filename="budget_data.csv"/> - <DownloadExcelButton data={budgetResponse} filename="budget_data.xlsx"/> + <DownloadDataButton data={budgetResponse} filename="budget_data.csv" exportType={ExportType.CSV}/> + <DownloadDataButton data={budgetResponse} filename="budget_data.xlsx" exportType={ExportType.EXCEL}/> </Row> <Row> <LineGraph data={budgetData} /> diff --git a/compendium-frontend/src/pages/ChargingStructure.tsx b/compendium-frontend/src/pages/ChargingStructure.tsx index 826df886364046115152d208812be27027d6966d..cae43e56f6a888223f3b170a05b8ff263c236650 100644 --- a/compendium-frontend/src/pages/ChargingStructure.tsx +++ b/compendium-frontend/src/pages/ChargingStructure.tsx @@ -1,15 +1,18 @@ -import React, { useMemo, useState } from "react"; +import React, {useMemo, useState} from "react"; import {Row, Table} from "react-bootstrap"; -import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; +import {BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title, Tooltip} from 'chart.js'; -import { ChargingStructure, FilterSelection } from "../Schema"; -import { createChargingStructureDataLookup, getYearsAndNrens, loadDataWithFilterSelectionFallback } from "../helpers/dataconversion"; +import {ChargingStructure, FilterSelection} from "../Schema"; +import { + createChargingStructureDataLookup, + getYearsAndNrens, + loadDataWithFilterSelectionFallback +} from "../helpers/dataconversion"; import ColorPill from "../components/ColorPill"; import DataPage from "../components/DataPage"; import Filter from "../components/graphing/Filter"; -import { Sections } from "../helpers/constants"; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import {ExportType, Sections} from "../helpers/constants"; +import DownloadDataButton from "../components/DownloadDataButton"; ChartJS.register( @@ -59,8 +62,8 @@ function ChargingStructurePage({ filterSelection, setFilterSelection }: inputPro category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={chargingStructureData} filename="charging_mechanism_of_nrens_per_year.csv"/> - <DownloadExcelButton data={chargingStructureData} filename="charging_mechanism_of_nrens_per_year.xlsx"/> + <DownloadDataButton data={chargingStructureData} filename="charging_mechanism_of_nrens_per_year.csv" exportType={ExportType.CSV}/> + <DownloadDataButton data={chargingStructureData} filename="charging_mechanism_of_nrens_per_year.xlsx" exportType={ExportType.EXCEL} /> </Row> <Table className="charging-struct-table" striped bordered responsive> <colgroup> diff --git a/compendium-frontend/src/pages/ECProjects.tsx b/compendium-frontend/src/pages/ECProjects.tsx index 0659c10115c741e8c081f0ff9d72b06f5c70088b..b2dbc05af404a22500acd484f338c8c95f581bc8 100644 --- a/compendium-frontend/src/pages/ECProjects.tsx +++ b/compendium-frontend/src/pages/ECProjects.tsx @@ -1,13 +1,16 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Row, Table} from "react-bootstrap"; -import { ECProject, FilterSelection } from "../Schema"; -import { createECProjectsDataLookup, getYearsAndNrens, loadDataWithFilterSelectionFallback } from '../helpers/dataconversion'; +import {ECProject, FilterSelection} from "../Schema"; +import { + createECProjectsDataLookup, + getYearsAndNrens, + loadDataWithFilterSelectionFallback +} from '../helpers/dataconversion'; import DataPage from '../components/DataPage'; import Filter from "../components/graphing/Filter" -import { Sections } from '../helpers/constants'; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import {ExportType, Sections} from '../helpers/constants'; +import DownloadDataButton from "../components/DownloadDataButton"; interface inputProps { @@ -63,8 +66,8 @@ function ECProjects({ filterSelection, setFilterSelection }: inputProps) { category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={projectData} filename="nren_involvement_in_european_commission_projects.csv"/> - <DownloadExcelButton data={projectData} filename="nren_involvement_in_european_commission_projects.xlsx"/> + <DownloadDataButton data={projectData} filename="nren_involvement_in_european_commission_projects.csv" exportType={ExportType.CSV}/> + <DownloadDataButton data={projectData} filename="nren_involvement_in_european_commission_projects.xlsx" exportType={ExportType.EXCEL}/> </Row> <Table borderless className='compendium-table'> <thead> diff --git a/compendium-frontend/src/pages/FundingSource.tsx b/compendium-frontend/src/pages/FundingSource.tsx index aa7fbddf758310290123521090ed3013e4db8529..599e7a72a16ae8887d112c6f8a55bed514822ad8 100644 --- a/compendium-frontend/src/pages/FundingSource.tsx +++ b/compendium-frontend/src/pages/FundingSource.tsx @@ -1,17 +1,20 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Bar } from 'react-chartjs-2'; -import { Col, Row } from "react-bootstrap"; -import { Chart as ChartJS } from 'chart.js'; +import React, {useEffect, useMemo, useState} 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 { FundingSource, FilterSelection } from "../Schema"; -import { createFundingSourceDataset, getYearsAndNrens, loadDataWithFilterSelectionFallback } from "../helpers/dataconversion"; +import {FilterSelection, FundingSource} from "../Schema"; +import { + createFundingSourceDataset, + getYearsAndNrens, + loadDataWithFilterSelectionFallback +} from "../helpers/dataconversion"; import DataPage from '../components/DataPage'; import Filter from "../components/graphing/Filter" -import { Sections } from '../helpers/constants'; +import {ExportType, Sections} from '../helpers/constants'; import ColorBadge from '../components/ColorBadge'; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import DownloadDataButton from "../components/DownloadDataButton"; export const chartOptions = { maintainAspectRatio: false, @@ -158,8 +161,8 @@ function FundingSourcePage({ filterSelection, setFilterSelection }: inputProps) category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={fundingSourceData} filename="income_source_of_nren_per_year.csv"/> - <DownloadExcelButton data={fundingSourceData} filename="income_source_of_nren_per_year.xlsx"/> + <DownloadDataButton data={fundingSourceData} filename="income_source_of_nren_per_year.csv" exportType={ExportType.CSV}/> + <DownloadDataButton data={fundingSourceData} filename="income_source_of_nren_per_year.xlsx" exportType={ExportType.EXCEL}/> </Row> <div> <FundingSourceLegend/> diff --git a/compendium-frontend/src/pages/ParentOrganisation.tsx b/compendium-frontend/src/pages/ParentOrganisation.tsx index b193d8a210af511e5e6b9209bc7bd9b79d136e24..64c6c909435f8825fd1ef3d8947e975115e639c8 100644 --- a/compendium-frontend/src/pages/ParentOrganisation.tsx +++ b/compendium-frontend/src/pages/ParentOrganisation.tsx @@ -1,13 +1,16 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Row, Table} from "react-bootstrap"; -import { Organisation, FilterSelection } from "../Schema"; -import { createOrganisationDataLookup, getYearsAndNrens, loadDataWithFilterSelectionFallback } from "../helpers/dataconversion"; +import {FilterSelection, Organisation} from "../Schema"; +import { + createOrganisationDataLookup, + getYearsAndNrens, + loadDataWithFilterSelectionFallback +} from "../helpers/dataconversion"; import DataPage from '../components/DataPage'; import Filter from "../components/graphing/Filter" -import { Sections } from '../helpers/constants'; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import {ExportType, Sections} from '../helpers/constants'; +import DownloadDataButton from "../components/DownloadDataButton"; function getJSXFromMap(data: Map<string, Map<number, Organisation[]>>) { @@ -58,8 +61,8 @@ function ParentOrganisation({ filterSelection, setFilterSelection }: inputProps) category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={organisationData} filename="nren_parent_organisations.csv"/> - <DownloadExcelButton data={organisationData} filename="nren_parent_organisations.xlsx"/> + <DownloadDataButton data={organisationData} filename="nren_parent_organisations.csv" exportType={ExportType.CSV}/> + <DownloadDataButton data={organisationData} filename="nren_parent_organisations.xlsx" exportType={ExportType.EXCEL}/> </Row> <Table borderless className='compendium-table'> <thead> diff --git a/compendium-frontend/src/pages/StaffGraph.tsx b/compendium-frontend/src/pages/StaffGraph.tsx index 6cd9a044d102fd3aec55e89a62fd67ba07eddd9b..155f1a49a447253fe3a6993e7d53c76e98bee50f 100644 --- a/compendium-frontend/src/pages/StaffGraph.tsx +++ b/compendium-frontend/src/pages/StaffGraph.tsx @@ -1,17 +1,16 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Bar } from 'react-chartjs-2'; -import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; +import React, {useEffect, useMemo, useState} from 'react'; +import {Bar} from 'react-chartjs-2'; +import {BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title, Tooltip} from 'chart.js'; -import { NrenStaff, FilterSelection } from "../Schema"; -import { createNRENStaffDataset, getYearsAndNrens, loadDataWithFilterSelectionFallback } from "../helpers/dataconversion"; +import {FilterSelection, NrenStaff} from "../Schema"; +import {createNRENStaffDataset, getYearsAndNrens, loadDataWithFilterSelectionFallback} from "../helpers/dataconversion"; import DataPage from '../components/DataPage'; import Filter from "../components/graphing/Filter" -import { Sections } from '../helpers/constants'; +import {ExportType, Sections} from '../helpers/constants'; import WithLegend from '../components/WithLegend'; import htmlLegendPlugin from '../plugins/HTMLLegendPlugin'; import {Row} from "react-bootstrap"; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import DownloadDataButton from "../components/DownloadDataButton"; ChartJS.register( CategoryScale, @@ -154,15 +153,15 @@ function StaffGraph({ filterSelection, setFilterSelection, roles = false }: inpu ? "The graph shows the roles of NREN employees. On hovering over the graph will give the percentage of employees in that role. This graph can be used to compare, selecting multiple NRENs to see the fluctuation of roles over selected year and with other NRENs." : "The graph shows the types of employment for NREN employees. On hovering over the graphs will give the percentage of employees in that type of employment. This graph can be used to compare, selecting multiple NRENs to see the fluctuation of types of employment over selected year and with other NRENs."; - const filename = roles ? "roles_of_nren_employees.csv" : "types_of_employment_for_nren.csv"; + const filename = roles ? "roles_of_nren_employees" : "types_of_employment_for_nrens"; return ( <DataPage title={title} description={description} category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={staffData} filename={filename}/> - <DownloadExcelButton data={staffData} filename={filename}/> + <DownloadDataButton data={staffData} filename={filename} exportType={ExportType.CSV}/> + <DownloadDataButton data={staffData} filename={filename} exportType={ExportType.EXCEL}/> </Row> <WithLegend> <div className="chart-container" style={{'height': `${height}rem`}}> diff --git a/compendium-frontend/src/pages/SubOrganisation.tsx b/compendium-frontend/src/pages/SubOrganisation.tsx index 3d31754fec0b5e78062cdd41a4496dd78cc31b8f..2412018f8c16e6cab75a15c65a157b558ef10206 100644 --- a/compendium-frontend/src/pages/SubOrganisation.tsx +++ b/compendium-frontend/src/pages/SubOrganisation.tsx @@ -1,13 +1,16 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Row, Table} from "react-bootstrap"; -import { Organisation, FilterSelection } from "../Schema"; -import { createOrganisationDataLookup, getYearsAndNrens, loadDataWithFilterSelectionFallback } from "../helpers/dataconversion"; +import {FilterSelection, Organisation} from "../Schema"; +import { + createOrganisationDataLookup, + getYearsAndNrens, + loadDataWithFilterSelectionFallback +} from "../helpers/dataconversion"; import DataPage from '../components/DataPage'; import Filter from "../components/graphing/Filter" -import { Sections } from '../helpers/constants'; -import DownloadCSVButton from "../components/DownloadCSVButton"; -import DownloadExcelButton from "../components/DownloadExcelButton"; +import {ExportType, Sections} from '../helpers/constants'; +import DownloadDataButton from "../components/DownloadDataButton"; function getJSXFromMap(data: Map<string, Map<number, Organisation[]>>) { @@ -63,8 +66,8 @@ function SubOrganisation({ filterSelection, setFilterSelection }: inputProps) { category={Sections.Organisation} filter={filterNode}> <> <Row> - <DownloadCSVButton data={organisationData} filename="nren_suborganisations.csv"/> - <DownloadExcelButton data={organisationData} filename="nren_suborganisations.xlsx"/> + <DownloadDataButton data={organisationData} filename="nren_suborganisations.csv" exportType={ExportType.CSV}/> + <DownloadDataButton data={organisationData} filename="nren_suborganisations.xlsx" exportType={ExportType.EXCEL}/> </Row> <Table borderless className='compendium-table'> <thead>