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