From bb95e7d33ef339f7194802dc7f59ce0c49a0c1c3 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Mon, 14 Nov 2022 12:54:56 +0100 Subject: [PATCH 1/4] Adds an option to download chart data as CSV --- .../analysis/results/chart-card.tsx | 93 +++++++++++++------ .../components/common/chart/analysis/utils.ts | 24 +++++ package.json | 1 + yarn.lock | 5 + 4 files changed, 97 insertions(+), 26 deletions(-) diff --git a/app/scripts/components/analysis/results/chart-card.tsx b/app/scripts/components/analysis/results/chart-card.tsx index edd7b6694..7551366e3 100644 --- a/app/scripts/components/analysis/results/chart-card.tsx +++ b/app/scripts/components/analysis/results/chart-card.tsx @@ -11,6 +11,12 @@ import { CollecticonCircleInformation, CollecticonDownload2 } from '@devseed-ui/collecticons'; +import { + Dropdown, + DropMenu, + DropMenuItem, + DropTitle +} from '@devseed-ui/dropdown'; import { TimeseriesData } from './timeseries-data'; import { @@ -32,6 +38,7 @@ 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'; interface ChartCardProps { title: React.ReactNode; @@ -64,22 +71,30 @@ 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( + (type: 'image' | 'text') => { + 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}`; - const filename = `chart.${id}.${startDate}-${endDate}`; - chartRef.current?.saveAsImage(filename); - }, [id, chartData.data, brushIndex]); + if (type === 'image') { + chartRef.current?.saveAsImage(filename); + } else { + exportCsv(filename, data, data[startIndex].date, data[endIndex].date); + } + }, + [id, chartData.data, brushIndex] + ); const theme = useTheme(); @@ -109,19 +124,45 @@ export default function ChartCard(props: ChartCardProps) { - ( + + + + + + )} > - - - - + Select a file format + +
  • + onExportClick('image')} + > + Image (JPG) + +
  • +
  • + onExportClick('text')} + > + Text (CSV) + +
  • +
    + + diff --git a/app/scripts/components/common/chart/analysis/utils.ts b/app/scripts/components/common/chart/analysis/utils.ts index f02c3fa9b..8bfd187bb 100644 --- a/app/scripts/components/common/chart/analysis/utils.ts +++ b/app/scripts/components/common/chart/analysis/utils.ts @@ -1,8 +1,11 @@ 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'; const URL = window.URL || window.webkitURL || window; const chartPNGPadding = 20; @@ -142,3 +145,24 @@ export async function exportImage({ return drawOnCanvas({ chartImage, legendImage, zoomRatio }); } else throw Error('No SVG specified'); } + +export function exportCsv( + filename: string, + data: TimeseriesDataUnit[], + startDate: string, + endDate: string +) { + 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', 'min', 'mean', 'max', 'std'] + }); + FileSaver.saveAs( + new Blob([csv], { type: 'text/csv;charset=utf-8' }), + `${filename}.csv` + ); +} diff --git a/package.json b/package.json index 499d6f48b..705600375 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 5974d4011..f60f500cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 "" From 81998984c9313df2d8ba0a0b3287b25e1f1ac80f Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 16 Nov 2022 14:11:20 +0100 Subject: [PATCH 2/4] Only output active metrics in CSV --- app/scripts/components/analysis/results/chart-card.tsx | 4 ++-- app/scripts/components/common/chart/analysis/utils.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/scripts/components/analysis/results/chart-card.tsx b/app/scripts/components/analysis/results/chart-card.tsx index 7551366e3..9e76b7dab 100644 --- a/app/scripts/components/analysis/results/chart-card.tsx +++ b/app/scripts/components/analysis/results/chart-card.tsx @@ -90,10 +90,10 @@ export default function ChartCard(props: ChartCardProps) { if (type === 'image') { chartRef.current?.saveAsImage(filename); } else { - exportCsv(filename, data, data[startIndex].date, data[endIndex].date); + exportCsv(filename, data, data[startIndex].date, data[endIndex].date, activeMetrics); } }, - [id, chartData.data, brushIndex] + [id, chartData.data, brushIndex, activeMetrics] ); const theme = useTheme(); diff --git a/app/scripts/components/common/chart/analysis/utils.ts b/app/scripts/components/common/chart/analysis/utils.ts index 8bfd187bb..d319b4535 100644 --- a/app/scripts/components/common/chart/analysis/utils.ts +++ b/app/scripts/components/common/chart/analysis/utils.ts @@ -6,6 +6,7 @@ import { 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 || window.webkitURL || window; const chartPNGPadding = 20; @@ -150,7 +151,8 @@ export function exportCsv( filename: string, data: TimeseriesDataUnit[], startDate: string, - endDate: string + endDate: string, + activeMetrics: DataMetric[] ) { const startTimestamp = +new Date(startDate); const endTimestamp = +new Date(endDate); @@ -159,7 +161,7 @@ export function exportCsv( return timestamp >= startTimestamp && timestamp <= endTimestamp; }); const csv = unparse(filtered, { - columns: ['date', 'min', 'mean', 'max', 'std'] + columns: ['date', ...activeMetrics.map((m) => m.id)] }); FileSaver.saveAs( new Blob([csv], { type: 'text/csv;charset=utf-8' }), From 88cb9547f359a9a5ebf46f61a5674d4b1c6e1630 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 16 Nov 2022 14:14:31 +0100 Subject: [PATCH 3/4] Properly use links in dropdown --- .../analysis/results/chart-card.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/scripts/components/analysis/results/chart-card.tsx b/app/scripts/components/analysis/results/chart-card.tsx index 9e76b7dab..08f45c6f5 100644 --- a/app/scripts/components/analysis/results/chart-card.tsx +++ b/app/scripts/components/analysis/results/chart-card.tsx @@ -1,4 +1,4 @@ -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'; @@ -72,7 +72,8 @@ export default function ChartCard(props: ChartCardProps) { const noDownloadReason = getNoDownloadReason(chartData); const onExportClick = useCallback( - (type: 'image' | 'text') => { + (e: MouseEvent, type: 'image' | 'text') => { + e.preventDefault(); if (!chartData.data?.timeseries.length) { return; } @@ -90,7 +91,13 @@ export default function ChartCard(props: ChartCardProps) { if (type === 'image') { chartRef.current?.saveAsImage(filename); } else { - exportCsv(filename, data, data[startIndex].date, data[endIndex].date, activeMetrics); + exportCsv( + filename, + data, + data[startIndex].date, + data[endIndex].date, + activeMetrics + ); } }, [id, chartData.data, brushIndex, activeMetrics] @@ -147,7 +154,8 @@ export default function ChartCard(props: ChartCardProps) {
  • onExportClick('image')} + onClick={(e) => onExportClick(e, 'image')} + href='#' > Image (JPG) @@ -155,7 +163,8 @@ export default function ChartCard(props: ChartCardProps) {
  • onExportClick('text')} + onClick={(e) => onExportClick(e, 'text')} + href='#' > Text (CSV) From 517c7905c45e062ce434edc5010ef4ad1150a4a1 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Thu, 17 Nov 2022 14:10:33 +0100 Subject: [PATCH 4/4] Used DropMenuItemButton --- .../analysis/results/chart-card.tsx | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/app/scripts/components/analysis/results/chart-card.tsx b/app/scripts/components/analysis/results/chart-card.tsx index 08f45c6f5..a47520eb6 100644 --- a/app/scripts/components/analysis/results/chart-card.tsx +++ b/app/scripts/components/analysis/results/chart-card.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useRef, useMemo, useState, MouseEvent } 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'; @@ -11,12 +17,7 @@ import { CollecticonCircleInformation, CollecticonDownload2 } from '@devseed-ui/collecticons'; -import { - Dropdown, - DropMenu, - DropMenuItem, - DropTitle -} from '@devseed-ui/dropdown'; +import { Dropdown, DropMenu, DropTitle } from '@devseed-ui/dropdown'; import { TimeseriesData } from './timeseries-data'; import { @@ -39,6 +40,7 @@ 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; @@ -152,22 +154,16 @@ export default function ChartCard(props: ChartCardProps) { Select a file format
  • - onExportClick(e, 'image')} - href='#' > Image (JPG) - +
  • - onExportClick(e, 'text')} - href='#' - > + onExportClick(e, 'text')}> Text (CSV) - +