Skip to content

Commit

Permalink
Merge pull request #323 from NASA-IMPACT/feature/download-csv
Browse files Browse the repository at this point in the history
Adds an option to download chart data as CSV
  • Loading branch information
nerik authored Nov 17, 2022
2 parents c41ef54 + 517c790 commit f8c0e20
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 27 deletions.
100 changes: 73 additions & 27 deletions app/scripts/components/analysis/results/chart-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useRef, useMemo, useState } from 'react';
import React, {
useCallback,
useRef,
useMemo,
useState,
MouseEvent
} from 'react';
import { format } from 'date-fns';
import { reverse } from 'd3';
import { useTheme } from 'styled-components';
Expand All @@ -11,6 +17,7 @@ import {
CollecticonCircleInformation,
CollecticonDownload2
} from '@devseed-ui/collecticons';
import { Dropdown, DropMenu, DropTitle } from '@devseed-ui/dropdown';

import { TimeseriesData } from './timeseries-data';
import {
Expand All @@ -32,6 +39,8 @@ import { ChartLoading } from '$components/common/loading-skeleton';
import { dateFormatter } from '$components/common/chart/utils';
import { Tip } from '$components/common/tip';
import { composeVisuallyDisabled } from '$utils/utils';
import { exportCsv } from '$components/common/chart/analysis/utils';
import DropMenuItemButton from '$styles/drop-menu-item-button';

interface ChartCardProps {
title: React.ReactNode;
Expand Down Expand Up @@ -64,22 +73,37 @@ export default function ChartCard(props: ChartCardProps) {
const [brushIndex, setBrushIndex] = useState({ startIndex: 0, endIndex: 0 });
const noDownloadReason = getNoDownloadReason(chartData);

const onExportClick = useCallback(() => {
if (!chartData.data?.timeseries.length) {
return;
}
const onExportClick = useCallback(
(e: MouseEvent, type: 'image' | 'text') => {
e.preventDefault();
if (!chartData.data?.timeseries.length) {
return;
}

const { startIndex, endIndex } = brushIndex;
// The indexes expect the data to be ascending, so we have to reverse the
// data.
const data = reverse(chartData.data.timeseries);
const dFormat = 'yyyy-MM-dd';
const startDate = format(new Date(data[startIndex].date), dFormat);
const endDate = format(new Date(data[endIndex].date), dFormat);
const { startIndex, endIndex } = brushIndex;
// The indexes expect the data to be ascending, so we have to reverse the
// data.
const data = reverse(chartData.data.timeseries);
const dFormat = 'yyyy-MM-dd';
const startDate = format(new Date(data[startIndex].date), dFormat);
const endDate = format(new Date(data[endIndex].date), dFormat);

const filename = `chart.${id}.${startDate}-${endDate}`;
chartRef.current?.saveAsImage(filename);
}, [id, chartData.data, brushIndex]);
const filename = `chart.${id}.${startDate}-${endDate}`;

if (type === 'image') {
chartRef.current?.saveAsImage(filename);
} else {
exportCsv(
filename,
data,
data[startIndex].date,
data[endIndex].date,
activeMetrics
);
}
},
[id, chartData.data, brushIndex, activeMetrics]
);

const theme = useTheme();

Expand Down Expand Up @@ -109,19 +133,41 @@ export default function ChartCard(props: ChartCardProps) {
</CardHeadline>
<CardActions>
<Toolbar size='small'>
<Tip
content={noDownloadReason}
disabled={!noDownloadReason}
hideOnClick={false}
<Dropdown
alignment='right'
triggerElement={(props) => (
<Tip
content={noDownloadReason}
disabled={!noDownloadReason}
hideOnClick={false}
>
<ChartDownloadButton
{...props}
variation='base-text'
visuallyDisabled={!!noDownloadReason}
>
<CollecticonDownload2 title='Download' meaningful />
</ChartDownloadButton>
</Tip>
)}
>
<ChartDownloadButton
variation='base-text'
onClick={onExportClick}
visuallyDisabled={!!noDownloadReason}
>
<CollecticonDownload2 title='Download' meaningful />
</ChartDownloadButton>
</Tip>
<DropTitle>Select a file format</DropTitle>
<DropMenu>
<li>
<DropMenuItemButton
onClick={(e) => onExportClick(e, 'image')}
>
Image (JPG)
</DropMenuItemButton>
</li>
<li>
<DropMenuItemButton onClick={(e) => onExportClick(e, 'text')}>
Text (CSV)
</DropMenuItemButton>
</li>
</DropMenu>
</Dropdown>

<VerticalDivider variation='dark' />
<ToolbarIconButton variation='base-text'>
<CollecticonCircleInformation title='More info' meaningful />
Expand Down
26 changes: 26 additions & 0 deletions app/scripts/components/common/chart/analysis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { RefObject, Component } from 'react';
import FileSaver from 'file-saver';
import { unparse } from 'papaparse';
import {
chartAspectRatio,
brushHeight
} from '$components/common/chart/constant';
import { TimeseriesDataUnit } from '$components/analysis/results/timeseries-data';
import { DataMetric } from '$components/analysis/results/analysis-head-actions';

const URL = window.URL;

Expand Down Expand Up @@ -202,3 +206,25 @@ export async function exportImage({
throw Error('No SVG specified');
}
}

export function exportCsv(
filename: string,
data: TimeseriesDataUnit[],
startDate: string,
endDate: string,
activeMetrics: DataMetric[]
) {
const startTimestamp = +new Date(startDate);
const endTimestamp = +new Date(endDate);
const filtered = data.filter((row) => {
const timestamp = +new Date(row.date);
return timestamp >= startTimestamp && timestamp <= endTimestamp;
});
const csv = unparse(filtered, {
columns: ['date', ...activeMetrics.map((m) => m.id)]
});
FileSaver.saveAs(
new Blob([csv], { type: 'text/csv;charset=utf-8' }),
`${filename}.csv`
);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"mapbox-gl": "^2.9.2",
"mapbox-gl-compare": "^0.4.0",
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
"papaparse": "^5.3.2",
"polished": "^4.1.3",
"prop-types": "^15.7.2",
"pure-react-carousel": "^1.28.1",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9533,6 +9533,11 @@ p-try@^2.0.0:
resolved "http://ec2-3-82-125-171.compute-1.amazonaws.com:4873/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==

papaparse@^5.3.2:
version "5.3.2"
resolved "http://ec2-3-82-125-171.compute-1.amazonaws.com:4873/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467"
integrity sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==

"parcel-resolver-thematics@link:./parcel-resolver-thematics":
version "0.0.0"
uid ""
Expand Down

0 comments on commit f8c0e20

Please sign in to comment.