Skip to content
Snippets Groups Projects
Commit ffeaffcb authored by Mohammad Torkashvand's avatar Mohammad Torkashvand :lion_face:
Browse files

Merge branch 'feature/COMP-247-download-charts-as-image' into 'develop'

Download a chart as an image (CSV, PNG, JPEG)

See merge request !67
parents d19bf275 c7990ed1
No related branches found
No related tags found
1 merge request!67Download a chart as an image (CSV, PNG, JPEG)
Showing
with 247 additions and 90 deletions
......@@ -14,6 +14,7 @@
"chart.js": "^4.2.1",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.26.1",
"dom-to-image": "^2.6.0",
"install": "^0.13.0",
"npm": "^9.2.0",
"react": "^18.2.0",
......@@ -5250,6 +5251,11 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/dom-to-image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz",
"integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA=="
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
......@@ -19029,6 +19035,11 @@
"entities": "^2.0.0"
}
},
"dom-to-image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz",
"integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA=="
},
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
......
......
......@@ -47,6 +47,7 @@
"chart.js": "^4.2.1",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.26.1",
"dom-to-image": "^2.6.0",
"install": "^0.13.0",
"npm": "^9.2.0",
"react": "^18.2.0",
......
......
......@@ -3,6 +3,7 @@ import React, { ReactElement } from "react";
import SidebarProvider from "./helpers/SidebarProvider";
import UserProvider from "./shared/UserProvider";
import FilterSelectionProvider from "./helpers/FilterSelectionProvider";
import ChartContainerProvider from "./helpers/ChartContainerProvider";
function Providers({ children }): ReactElement {
......@@ -10,7 +11,9 @@ function Providers({ children }): ReactElement {
<SidebarProvider>
<UserProvider>
<FilterSelectionProvider>
<ChartContainerProvider>
{children}
</ChartContainerProvider>
</FilterSelectionProvider>
</UserProvider>
</SidebarProvider>
......
......
import React, { useContext, useState } from 'react';
import domtoimage from 'dom-to-image';
import { ImageType } from "../helpers/constants";
import { ChartContainerContext } from "../helpers/ChartContainerProvider";
interface DownloadImageChartProps {
filename: string;
}
const DownloadImageChartButton: React.FC<DownloadImageChartProps> = ({ filename }) => {
const chartContainerRef = useContext(ChartContainerContext);
const [exportType, setExportType] = useState<ImageType>(ImageType.PNG);
const downloadChartAsImage = async () => {
if (chartContainerRef?.current) {
const scale = 1;
const commonStyle = {
transform: `scale(${scale})`,
'transform-origin': 'top left',
background: 'white'
};
let dataUrl;
switch (exportType) {
case ImageType.JPEG:
dataUrl = await domtoimage.toJpeg(chartContainerRef.current, {
quality: 0.95,
style: commonStyle
});
break;
case ImageType.SVG:
dataUrl = await domtoimage.toSvg(chartContainerRef.current, {
style: commonStyle
});
break;
case ImageType.PNG:
default:
dataUrl = await domtoimage.toPng(chartContainerRef.current, {
style: commonStyle
});
break;
}
const link = document.createElement('a');
link.href = typeof dataUrl === 'string' ? dataUrl : URL.createObjectURL(dataUrl);
link.download = `${filename}.${exportType}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
return (
<>
<select value={exportType} onChange={(e) => setExportType(e.target.value as ImageType)}>
<option value={ImageType.PNG}>PNG</option>
<option value={ImageType.JPEG}>JPEG</option>
<option value={ImageType.SVG}>SVG</option>
</select>
<button onClick={downloadChartAsImage}>Download Chart</button>
</>
);
}
export default DownloadImageChartButton;
import React from 'react';
import ChartContainer from "./graphing/ChartContainer";
interface InputProps {
......@@ -13,12 +14,11 @@ const WithLegend: React.FC<InputProps> = ({ children, location }) => {
const bottom = location === "bottom" || location === "both";
return (
<>
<ChartContainer>
{top && <div style={{ paddingLeft: '33%', 'paddingTop': '2.5rem', 'paddingBottom': '1.5rem' }} id={'legendtop'} />}
{children}
{bottom && <div style={{ paddingLeft: '33%', 'paddingTop': '1.5rem' }} id={'legendbottom'} />}
</>
</ChartContainer>
);
};
......
......
import React, { useContext, ReactNode } from 'react';
import { ChartContainerContext } from '../../helpers/ChartContainerProvider';
interface ChartContainerProps {
children: ReactNode;
}
const ChartContainer: React.FC<ChartContainerProps> = ({ children }) => {
const containerRef = useContext(ChartContainerContext);
return (
<div ref={containerRef}>
{children}
</div>
);
};
export default ChartContainer;
import React, { createContext, useRef, ReactNode, RefObject } from 'react';
const ChartContainerContext = createContext<RefObject<HTMLDivElement> | null>(null);
interface ChartContainerProviderProps {
children: ReactNode;
}
const ChartContainerProvider: React.FC<ChartContainerProviderProps> = ({ children }) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
return (
<ChartContainerContext.Provider value={chartContainerRef}>
{children}
</ChartContainerContext.Provider>
);
};
export { ChartContainerContext };
export default ChartContainerProvider;
......@@ -10,3 +10,9 @@ export enum ExportType {
CSV = "CSV",
EXCEL = "EXCEL",
}
export enum ImageType {
PNG = "png",
JPEG = "jpeg",
SVG = "svg",
}
\ No newline at end of file
......@@ -9,6 +9,8 @@ import LineGraph from "../components/graphing/LineGraph";
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";
function BudgetPage(): ReactElement {
......@@ -44,10 +46,14 @@ function BudgetPage(): ReactElement {
<>
<Row>
<DownloadDataButton data={budgetResponse} filename="budget_data.csv" exportType={ExportType.CSV}/>
<DownloadDataButton data={budgetResponse} filename="budget_data.xlsx" exportType={ExportType.EXCEL}/>
<DownloadDataButton data={budgetResponse} filename="budget_data.xlsx"
exportType={ExportType.EXCEL}/>
</Row>
<Row>
<DownloadImageChartButton filename="budget_data"/>
<ChartContainer>
<LineGraph data={budgetData}/>
</ChartContainer>
</Row>
</>
</DataPage>
......
......
......@@ -10,6 +10,8 @@ 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";
ChartJS.register(
CategoryScale,
......@@ -57,6 +59,8 @@ function ChargingStructurePage(): React.ReactElement {
<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>
<DownloadImageChartButton filename="charging_mechanism_of_nrens_per_year"/>
<ChartContainer>
<Table className="charging-struct-table" striped bordered responsive>
<colgroup>
<col span={1} style={{width: "10%"}}/>
......@@ -83,15 +87,16 @@ function ChargingStructurePage(): React.ReactElement {
{["flat_fee", "usage_based_fee", "combination", "no_charge", "other"].map(discriminator => (
<td key={discriminator}>
{Array.from(nrenMap.entries()).map(([year, chargingMechanism]) => (
<ColorPill key={year} year={year} active={chargingMechanism == discriminator}/>
<ColorPill key={year} year={year}
active={chargingMechanism == discriminator}/>
))}
</td>
))}
</tr>
))}
</tbody>
</Table>
</ChartContainer>
</>
</DataPage>
);
......
......
......@@ -8,6 +8,8 @@ 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";
function getJSXFromMap(data: Map<string, Map<number, ECProject[]>>) {
......@@ -62,6 +64,8 @@ function ECProjects() {
<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>
<DownloadImageChartButton filename="nren_involvement_in_european_commission_projects"/>
<ChartContainer>
<Table borderless className='compendium-table'>
<thead>
<tr>
......@@ -74,6 +78,7 @@ function ECProjects() {
{getJSXFromMap(projectDataByYear)}
</tbody>
</Table>
</ChartContainer>
</>
</DataPage>
)
......
......
......@@ -12,6 +12,9 @@ import {ExportType, Sections} from '../helpers/constants';
import ColorBadge from '../components/ColorBadge';
import DownloadDataButton from "../components/DownloadDataButton";
import { FilterSelectionContext } from '../helpers/FilterSelectionProvider';
import DownloadImageChartButton from "../components/DownloadImageChartButton";
import { ChartContainerContext } from "../helpers/ChartContainerProvider";
import ChartContainer from "../components/graphing/ChartContainer";
export const chartOptions = {
maintainAspectRatio: false,
......@@ -156,7 +159,8 @@ function FundingSourcePage() {
<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>
<DownloadImageChartButton filename="income_source_of_nren_per_year"/>
<ChartContainer>
<FundingSourceLegend/>
<div className="chart-container" style={{'height': `${height}rem`}}>
<Bar
......@@ -166,7 +170,7 @@ function FundingSourcePage() {
/>
</div>
<FundingSourceLegend/>
</div>
</ChartContainer>
</>
</DataPage>
);
......
......
......@@ -8,6 +8,8 @@ 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";
function getJSXFromMap(data: Map<string, Map<number, Organisation[]>>) {
......@@ -56,6 +58,8 @@ function ParentOrganisation() {
<DownloadDataButton data={organisationData} filename="nren_parent_organisations.csv" exportType={ExportType.CSV}/>
<DownloadDataButton data={organisationData} filename="nren_parent_organisations.xlsx" exportType={ExportType.EXCEL}/>
</Row>
<DownloadImageChartButton filename='nren_parent_organisations'/>
<ChartContainer>
<Table borderless className='compendium-table'>
<thead>
<tr>
......@@ -68,6 +72,7 @@ function ParentOrganisation() {
{getJSXFromMap(organisationDataset)}
</tbody>
</Table>
</ChartContainer>
</>
</DataPage>
......
......
......@@ -12,6 +12,7 @@ import htmlLegendPlugin from '../plugins/HTMLLegendPlugin';
import {Row} from "react-bootstrap";
import DownloadDataButton from "../components/DownloadDataButton";
import { FilterSelectionContext } from '../helpers/FilterSelectionProvider';
import DownloadImageChartButton from "../components/DownloadImageChartButton";
ChartJS.register(
CategoryScale,
......@@ -163,6 +164,7 @@ function StaffGraph({ roles = false }: inputProps) {
<DownloadDataButton data={staffData} filename={filename} exportType={ExportType.CSV}/>
<DownloadDataButton data={staffData} filename={filename} exportType={ExportType.EXCEL}/>
</Row>
<DownloadImageChartButton filename={filename}/>
<WithLegend>
<div className="chart-container" style={{'height': `${height}rem`}}>
<Bar
......
......
......@@ -8,7 +8,8 @@ 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";
function getJSXFromMap(data: Map<string, Map<number, Organisation[]>>) {
return Array.from(data.entries()).map(([nren, nrenMap]) => {
......@@ -62,6 +63,8 @@ function SubOrganisation() {
<DownloadDataButton data={organisationData} filename="nren_suborganisations.csv" exportType={ExportType.CSV}/>
<DownloadDataButton data={organisationData} filename="nren_suborganisations.xlsx" exportType={ExportType.EXCEL}/>
</Row>
<DownloadImageChartButton filename="nren_suborganisations"/>
<ChartContainer>
<Table borderless className='compendium-table'>
<thead>
<tr>
......@@ -74,6 +77,7 @@ function SubOrganisation() {
{getJSXFromMap(organisationDataset)}
</tbody>
</Table>
</ChartContainer>
</>
</DataPage>
);
......
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment