From fb5be81fb2cf8f8c3b1ea6fd2b5dc087b8785624 Mon Sep 17 00:00:00 2001 From: MSDrao Date: Wed, 11 Feb 2026 15:28:41 -0600 Subject: [PATCH 1/3] Added filter for download --- frontend/src/_helpers/hydroCron.js | 194 ++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 6 deletions(-) diff --git a/frontend/src/_helpers/hydroCron.js b/frontend/src/_helpers/hydroCron.js index a4bff1bb..b6a433cc 100644 --- a/frontend/src/_helpers/hydroCron.js +++ b/frontend/src/_helpers/hydroCron.js @@ -1,7 +1,8 @@ -import { HYDROCRON_URL } from '@/constants' +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 () { @@ -87,6 +88,12 @@ const queryHydroCron = async (swordFeature = null, output = 'geojson') => { const start_time = EARLIEST_HYDROCRON_DATETIME const end_time = new Date(Date.now() + MS_TO_KEEP_CACHE).toISOString().split('.')[0] + 'Z' + // determine which collection name to use based on feature type ('Reach' or 'PriorLake') + let collection_name = 'SWOT_L2_HR_RiverSP_D' + if (feature_type === 'PriorLake') { + collection_name = 'SWOT_L2_HR_LakeSP_D' + } + params = { feature: feature_type, feature_id, @@ -95,9 +102,15 @@ const queryHydroCron = async (swordFeature = null, output = 'geojson') => { output, fields, // https://podaac.github.io/hydrocron/timeseries.html#compact-string-required-no - compact: 'true' + compact: 'true', + // https://podaac.github.io/hydrocron/timeseries.html#collection-name-string-required-no + collection_name } - let response = await fetchHydroCronData(HYDROCRON_URL, params, swordFeature) + + // Use our API proxy URL instead of the direct HydroCron URL + // This is due to CORS issues with the HydroCron server + // https://github.com/podaac/hydrocron/issues/306 + let response = await fetchHydroCronData(ENDPOINTS.hydrocron, params, swordFeature) if (response == null) { return } @@ -134,19 +147,31 @@ const fetchHydroCronData = async (url, params, swordFeature) => { }) if (response.status < 500) { if (response.status == 400) { + let text = 'No data found for: ' + if (params.feature && params.feature_id) { + text += `${params.feature} ${params.feature_id}` + } else { + text += JSON.stringify(params) + } alertStore.displayAlert({ title: 'No data found', - text: `No data found for ${JSON.stringify(params)}`, + text, type: 'warning', closable: true, - duration: 6 + duration: 3 }) return null } } else { + let text = 'Error while fetching SWOT data: ' + if (response.statusText) { + text += response.statusText + } else { + text += 'Unknown error' + } alertStore.displayAlert({ title: 'Error fetching SWOT data', - text: `Error while fetching SWOT data: ${response.statusText}`, + text, type: 'error', closable: true, duration: 3 @@ -218,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 @@ -235,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 } @@ -257,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 } @@ -279,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() From fb2ee2ed562175e5d291a3c104b655a4f9c797fb Mon Sep 17 00:00:00 2001 From: MSDrao Date: Wed, 11 Feb 2026 15:33:58 -0600 Subject: [PATCH 2/3] zoom icon changes --- frontend/src/components/LineChart.vue | 4 ++-- frontend/src/components/NodeChart.vue | 4 ++-- frontend/src/components/PlotActions.vue | 10 ++-------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/LineChart.vue b/frontend/src/components/LineChart.vue index a230a7e2..26ba9918 100644 --- a/frontend/src/components/LineChart.vue +++ b/frontend/src/components/LineChart.vue @@ -15,7 +15,7 @@ size="small" @click="resetZoom()" style="position: absolute; top: 120px; right: 45px; z-index: 10" - :icon="mdiMagnifyMinusOutline" + :icon="mdiRestore" > @@ -73,7 +73,7 @@ import { ref, computed } from 'vue' import { storeToRefs } from 'pinia' import { useDisplay } from 'vuetify' import { onMounted, nextTick } from 'vue' -import { mdiChartBellCurveCumulative, mdiCloseBox, mdiMagnifyMinusOutline } from '@mdi/js' +import { mdiChartBellCurveCumulative, mdiCloseBox, mdiRestore } from '@mdi/js' import { convertDateStringToSeconds } from '@/_helpers/time' import { useStatsStore } from '../stores/stats' diff --git a/frontend/src/components/NodeChart.vue b/frontend/src/components/NodeChart.vue index 2935e7ce..426a0238 100644 --- a/frontend/src/components/NodeChart.vue +++ b/frontend/src/components/NodeChart.vue @@ -15,7 +15,7 @@ size="small" @click="resetZoom()" style="position: absolute; top: 80px; right: 45px; z-index: 10" - :icon="mdiMagnifyMinusOutline" + :icon="mdiRestore" > @@ -35,7 +35,7 @@ import { nextTick, onMounted } from 'vue' import { useDisplay } from 'vuetify' import { useChartsStore } from '@/stores/charts' import { storeToRefs } from 'pinia' -import { mdiMagnifyMinusOutline } from '@mdi/js' +import { mdiRestore } from '@mdi/js' const { lgAndUp } = useDisplay() diff --git a/frontend/src/components/PlotActions.vue b/frontend/src/components/PlotActions.vue index 8c2e230f..599f050f 100644 --- a/frontend/src/components/PlotActions.vue +++ b/frontend/src/components/PlotActions.vue @@ -16,7 +16,7 @@ Download JSON - + Reset Zoom @@ -34,13 +34,7 @@ import { useChartsStore } from '@/stores/charts' import { useFeaturesStore } from '@/stores/features' import { ref, computed, defineEmits, watch } from 'vue' import { storeToRefs } from 'pinia' -import { - mdiDownloadBox, - mdiFileDelimited, - mdiCodeJson, - mdiMagnifyMinusOutline, - mdiEraser -} from '@mdi/js' +import { mdiDownloadBox, mdiFileDelimited, mdiCodeJson, mdiRestore, mdiEraser } from '@mdi/js' import { downloadCsv, downloadMultiNodesCsv, From d84661d23f20035261889dd88f32e12dded38bcc Mon Sep 17 00:00:00 2001 From: MSDrao Date: Sat, 14 Feb 2026 21:48:17 -0600 Subject: [PATCH 3/3] Revert "zoom icon changes" This reverts commit fb2ee2ed562175e5d291a3c104b655a4f9c797fb. --- frontend/src/components/LineChart.vue | 4 ++-- frontend/src/components/NodeChart.vue | 4 ++-- frontend/src/components/PlotActions.vue | 10 ++++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/LineChart.vue b/frontend/src/components/LineChart.vue index 8725f261..4aba67db 100644 --- a/frontend/src/components/LineChart.vue +++ b/frontend/src/components/LineChart.vue @@ -14,7 +14,7 @@ color="input" size="small" style="position: absolute; top: 120px; right: 45px; z-index: 10" - :icon="mdiRestore" + :icon="mdiMagnifyMinusOutline" > @@ -72,7 +72,7 @@ import { ref, computed } from 'vue' import { storeToRefs } from 'pinia' import { useDisplay } from 'vuetify' import { onMounted, nextTick } from 'vue' -import { mdiChartBellCurveCumulative, mdiCloseBox, mdiRestore } from '@mdi/js' +import { mdiChartBellCurveCumulative, mdiCloseBox, mdiMagnifyMinusOutline } from '@mdi/js' import { convertDateStringToSeconds } from '@/_helpers/time' import { useStatsStore } from '../stores/stats' diff --git a/frontend/src/components/NodeChart.vue b/frontend/src/components/NodeChart.vue index 7b320fef..642b88bf 100644 --- a/frontend/src/components/NodeChart.vue +++ b/frontend/src/components/NodeChart.vue @@ -14,7 +14,7 @@ color="input" size="small" style="position: absolute; top: 80px; right: 45px; z-index: 10" - :icon="mdiRestore" + :icon="mdiMagnifyMinusOutline" > @@ -34,7 +34,7 @@ import { nextTick, onMounted } from 'vue' import { useDisplay } from 'vuetify' import { useChartsStore } from '@/stores/charts' import { storeToRefs } from 'pinia' -import { mdiRestore } from '@mdi/js' +import { mdiMagnifyMinusOutline } from '@mdi/js' const { lgAndUp } = useDisplay() diff --git a/frontend/src/components/PlotActions.vue b/frontend/src/components/PlotActions.vue index 63bea3da..a7ac1454 100644 --- a/frontend/src/components/PlotActions.vue +++ b/frontend/src/components/PlotActions.vue @@ -16,7 +16,7 @@ Download JSON - + Reset Zoom @@ -34,7 +34,13 @@ import { useChartsStore } from '@/stores/charts' import { useFeaturesStore } from '@/stores/features' import { ref, computed, defineEmits, watch } from 'vue' import { storeToRefs } from 'pinia' -import { mdiDownloadBox, mdiFileDelimited, mdiCodeJson, mdiRestore, mdiEraser } from '@mdi/js' +import { + mdiDownloadBox, + mdiFileDelimited, + mdiCodeJson, + mdiMagnifyMinusOutline, + mdiEraser +} from '@mdi/js' import { downloadCsv, downloadMultiNodesCsv,