diff --git a/frontend/src/_helpers/hydroCron.js b/frontend/src/_helpers/hydroCron.js index bcd725c..b6a433c 100644 --- a/frontend/src/_helpers/hydroCron.js +++ b/frontend/src/_helpers/hydroCron.js @@ -2,6 +2,7 @@ import { ENDPOINTS } from '@/constants' import { useFeaturesStore } from '@/stores/features' import { useAlertStore } from '@/stores/alerts' import { useHydrologicStore } from '@/stores/hydrologic' +import { useChartsStore } from '@/stores/charts' import { EARLIEST_HYDROCRON_DATETIME } from '../constants' String.prototype.hashCode = function () { @@ -242,7 +243,48 @@ async function downloadBlob(blob, filename) { URL.revokeObjectURL(url) } +const buildJsonFromDatasets = (datasets, options = {}) => { + const { includeLabel = true, labelKey = 'series_label' } = options + const rows = [] + datasets.forEach((dataset) => { + if (!dataset?.data?.length || dataset.hidden) { + return + } + dataset.data.forEach((point) => { + const row = {} + if (includeLabel && dataset.label) { + row[labelKey] = dataset.label + } + Object.entries(point).forEach(([key, value]) => { + if (key === 'datetime') { + return + } + row[key] = value + }) + rows.push(row) + }) + }) + return rows +} + async function downloadFeatureJson(feature = null) { + const chartStore = useChartsStore() + const filteredDatasets = + chartStore.chartData?.datasets?.filter((dataset) => { + return dataset.seriesType === 'swot_reach_series' + }) || [] + + if (filteredDatasets.length > 0) { + const rows = buildJsonFromDatasets(filteredDatasets) + const jsonData = JSON.stringify({ rows }) + const blob = new Blob([jsonData], { type: 'application/json' }) + const featuresStore = useFeaturesStore() + const activeFeature = featuresStore.activeFeature + const filename = getLongFilename(activeFeature) + '.json' + downloadBlob(blob, filename) + return + } + if (feature == null) { const featuresStore = useFeaturesStore() feature = featuresStore.activeFeature @@ -259,6 +301,26 @@ async function downloadFeatureJson(feature = null) { } async function downloadMultiNodesJson(nodes = []) { + const chartStore = useChartsStore() + const filteredDatasets = + chartStore.nodeChartData?.datasets?.filter((dataset) => { + return dataset.seriesType === 'swot_node_series' + }) || [] + + if (filteredDatasets.length > 0) { + const rows = buildJsonFromDatasets(filteredDatasets) + const jsonData = JSON.stringify({ rows }) + const blob = new Blob([jsonData], { type: 'application/json' }) + const featuresStore = useFeaturesStore() + const firstNode = featuresStore.nodes?.[0] + const filenameBase = firstNode ? getLongFilename(firstNode) : getLongFilename() + const nodeCount = featuresStore.nodes?.length || filteredDatasets.length + let filename = `${filenameBase}.json` + filename = `${nodeCount}_nodes_${filename}` + downloadBlob(blob, filename) + return + } + if (nodes.length === 0) { nodes = useFeaturesStore().nodes } @@ -281,7 +343,86 @@ async function modifyDateTimeStringForExcel(csvData) { return csvData } +const escapeCsvValue = (value) => { + if (value == null) { + return '' + } + let text = value instanceof Date ? value.toISOString() : String(value) + if (text.includes('"')) { + text = text.replace(/"/g, '""') + } + if (/[",\n]/.test(text)) { + return `"${text}"` + } + return text +} + +const buildCsvFromDatasets = (datasets, options = {}) => { + const { includeLabelColumn = true, labelKey = 'series_label' } = options + const rows = [] + const headerSet = new Set() + + datasets.forEach((dataset) => { + if (!dataset?.data?.length || dataset.hidden) { + return + } + dataset.data.forEach((point) => { + const row = {} + if (includeLabelColumn && dataset.label) { + row[labelKey] = dataset.label + } + Object.entries(point).forEach(([key, value]) => { + if (key === 'datetime') { + return + } + row[key] = value + }) + rows.push(row) + Object.keys(row).forEach((key) => headerSet.add(key)) + }) + }) + + const headers = [] + if (headerSet.has(labelKey)) { + headers.push(labelKey) + headerSet.delete(labelKey) + } + if (headerSet.has('time_str')) { + headers.push('time_str') + headerSet.delete('time_str') + } + const remaining = Array.from(headerSet).sort() + headers.push(...remaining) + + const lines = [headers.join(',')] + rows.forEach((row) => { + const line = headers.map((header) => escapeCsvValue(row[header])).join(',') + lines.push(line) + }) + return lines.join('\n') +} + async function downloadMultiNodesCsv(nodes = []) { + const chartStore = useChartsStore() + const filteredDatasets = + chartStore.nodeChartData?.datasets?.filter((dataset) => { + return dataset.seriesType === 'swot_node_series' + }) || [] + + if (filteredDatasets.length > 0) { + let csvData = buildCsvFromDatasets(filteredDatasets) + csvData = await modifyDateTimeStringForExcel(csvData) + const blob = new Blob([csvData], { type: 'text/csv' }) + const featuresStore = useFeaturesStore() + const firstNode = featuresStore.nodes?.[0] + const filenameBase = firstNode ? getLongFilename(firstNode) : getLongFilename() + const nodeCount = featuresStore.nodes?.length || filteredDatasets.length + let filename = `${filenameBase}.csv` + filename = `${nodeCount}_nodes_${filename}` + downloadBlob(blob, filename) + return + } + if (nodes.length === 0) { nodes = useFeaturesStore().nodes } @@ -303,6 +444,23 @@ async function downloadMultiNodesCsv(nodes = []) { } async function downloadCsv(feature = null) { + const chartStore = useChartsStore() + const filteredDatasets = + chartStore.chartData?.datasets?.filter((dataset) => { + return dataset.seriesType === 'swot_reach_series' + }) || [] + + if (filteredDatasets.length > 0) { + let csvData = buildCsvFromDatasets(filteredDatasets) + csvData = await modifyDateTimeStringForExcel(csvData) + const blob = new Blob([csvData], { type: 'text/csv' }) + const featuresStore = useFeaturesStore() + const activeFeature = featuresStore.activeFeature + const filename = `${getLongFilename(activeFeature)}.csv` + downloadBlob(blob, filename) + return + } + // if feature not defined, use featuresStore.activeFeature if (feature == null) { const featuresStore = useFeaturesStore() diff --git a/frontend/src/components/LineChart.vue b/frontend/src/components/LineChart.vue index 6c83855..4aba67d 100644 --- a/frontend/src/components/LineChart.vue +++ b/frontend/src/components/LineChart.vue @@ -15,8 +15,8 @@ size="small" style="position: absolute; top: 120px; right: 45px; z-index: 10" :icon="mdiMagnifyMinusOutline" - @click="resetZoom()" - /> + > + RESET ZOOM diff --git a/frontend/src/components/NodeChart.vue b/frontend/src/components/NodeChart.vue index d0684e1..642b88b 100644 --- a/frontend/src/components/NodeChart.vue +++ b/frontend/src/components/NodeChart.vue @@ -15,8 +15,8 @@ size="small" style="position: absolute; top: 80px; right: 45px; z-index: 10" :icon="mdiMagnifyMinusOutline" - @click="resetZoom()" - /> + > + RESET ZOOM diff --git a/frontend/src/components/PlotActions.vue b/frontend/src/components/PlotActions.vue index 1a8b435..a7ac145 100644 --- a/frontend/src/components/PlotActions.vue +++ b/frontend/src/components/PlotActions.vue @@ -15,8 +15,8 @@ Download JSON - - + + Reset Zoom