FundingSource.tsx 6.04 KiB
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 DataPage from '../components/DataPage';
import Filter from "../components/graphing/Filter"
import {ExportType, Sections} from '../helpers/constants';
import ColorBadge from '../components/ColorBadge';
import DownloadDataButton from "../components/DownloadDataButton";
export const chartOptions = {
maintainAspectRatio: false,
layout: {
padding: {
right: 60
}
},
animation: {
duration: 0,
},
plugins: {
legend: {
display: false
}
},
scales: {
x: {
position: "top" as const,
ticks: {
callback: (value: string | number) => {
if (typeof value === "number") {
return `${value}%`;
}
return value;
},
},
},
xBottom: {
ticks: {
callback: (value: string | number) => {
if (typeof value === "number") {
return `${value}%`;
}
return value;
}
},
grid: {
drawOnChartArea: false
},
afterDataLimits: function (axis) {
const indices = Object.keys(ChartJS.instances)
// initial values should be far outside possible range
let max = -999999
let min = 999999
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 FundingSourceLegend() {
return (
<div className="d-flex justify-content-center bold-grey-12pt">
<Row xs="auto" className="border rounded-3 border-1 my-5 justify-content-center">
<Col className="d-flex align-items-center">
<ColorBadge key={0} index={0} />Client Institutions
</Col>
<Col className="d-flex align-items-center">
<ColorBadge key={1} index={1} />Commercial
</Col>
<Col className="d-flex align-items-center">
<ColorBadge key={2} index={2} />European Funding
</Col>
<Col className="d-flex align-items-center">
<ColorBadge key={3} index={3} />Gov/Public Bodies
</Col>
<Col className="d-flex align-items-center">
<ColorBadge key={4} index={4} />Other
</Col>
</Row>
</div>
);
}
interface inputProps {
filterSelection: FilterSelection
setFilterSelection: React.Dispatch<React.SetStateAction<FilterSelection>>
}
function FundingSourcePage({ filterSelection, setFilterSelection }: inputProps) {
const [fundingSourceData, setFundingSourceData] = useState<FundingSource[]>([]);
const { years, nrens } = useMemo(
() => getYearsAndNrens(fundingSourceData),
[fundingSourceData]
);
const fundingSourceDataset = createFundingSourceDataset(fundingSourceData);
fundingSourceDataset.datasets.forEach(dataset => {
dataset.hidden = !filterSelection.selectedYears.includes(parseInt(dataset.stack));
});
// remove the datapoints and labels for the nrens that aren't selected
// unfortunately we cannot just hide them because graph.js doesn't want
// to create a stack from a single dataset
fundingSourceDataset.datasets.forEach(dataset => {
dataset.data = dataset.data.filter((e, i) => {
return filterSelection.selectedNrens.includes(fundingSourceDataset.labels[i]);
});
});
fundingSourceDataset.labels = fundingSourceDataset.labels.filter((e) => filterSelection.selectedNrens.includes(e));
useEffect(() => {
loadDataWithFilterSelectionFallback('/api/funding/', setFundingSourceData, setFilterSelection);
}, [setFilterSelection]);
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
// set a minimum height of 20rem, additional years need some more space
const height = numNrens * numYears * heightPerBar + 5;
return (
<DataPage title="Income Source Of NRENs per Year"
description='The graph shows the percentage share of their income that individual NRENs derived from different sources.
On hovering over the graphs will give income share in that year. This can be used to compare selecting multiple years to see how the
share has changed between the years.'
category={Sections.Organisation} filter={filterNode}>
<>
<Row>
<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/>
<div className="chart-container" style={{'height': `${height}rem`}}>
<Bar
plugins={[ChartDataLabels]}
data={fundingSourceDataset}
options={chartOptions}
/>
</div>
<FundingSourceLegend/>
</div>
</>
</DataPage>
);
}
export default FundingSourcePage;