diff --git a/cda-gui/package-lock.json b/cda-gui/package-lock.json index cc71620c0..fe02ed8e0 100644 --- a/cda-gui/package-lock.json +++ b/cda-gui/package-lock.json @@ -11,7 +11,7 @@ "@headlessui/react": "^2.2.10", "@tanstack/react-query": "^5.64.1", "@tanstack/react-virtual": "^3.14.3", - "@usace-watermanagement/groundwork-water": "^3.9.0", + "@usace-watermanagement/groundwork-water": "github:USACE-WaterManagement/groundwork-water#agent/cwms-plot-cda-parity", "@usace/groundwork": "^3.14.18", "cwmsjs": "^2.3.2-2025.3.19", "dayjs": "^1.11.13", @@ -1200,9 +1200,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1217,9 +1214,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1234,9 +1228,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1251,9 +1242,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1268,9 +1256,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1285,9 +1270,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1302,9 +1284,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1319,9 +1298,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1336,9 +1312,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1353,9 +1326,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1370,9 +1340,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1387,9 +1354,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1404,9 +1368,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1552,12 +1513,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.14.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.3.tgz", - "integrity": "sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ==", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.6.tgz", + "integrity": "sha512-4+Uq8m0/gzO4kMCHUEpTtGX1RnONK0C+g88b2ltwPMWUBiaVarBuWKoPJaz7gj1cKCVRAdyu+U8GcKhwCc2beA==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.17.1" + "@tanstack/virtual-core": "3.17.4" }, "funding": { "type": "github", @@ -1569,9 +1530,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.1.tgz", - "integrity": "sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==", + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.4.tgz", + "integrity": "sha512-nGm5KteqxasUdThLc2izl6dHUqLv0LQj7Nuyo5gYalTPf/U8a9ermvsl7reT+6ioBW1l8WfpP/mcU338nLXpqw==", "license": "MIT", "funding": { "type": "github", @@ -1672,11 +1633,11 @@ "license": "ISC" }, "node_modules/@usace-watermanagement/groundwork-water": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@usace-watermanagement/groundwork-water/-/groundwork-water-3.9.0.tgz", - "integrity": "sha512-pUrTg3voG8aiKqYJiihZ2kJDaH51qYrj2VSyrsVMSpeGACm0ZoV2N73z3mKCS/ujhHehWK7/63fUGjrEyutEWA==", + "version": "3.11.0", + "resolved": "git+ssh://git@github.com/USACE-WaterManagement/groundwork-water.git#a8baa910adb212692d66ed1642867b72a26d570d", "license": "MIT", "dependencies": { + "@tanstack/react-virtual": "^3.14.6", "cwmsjs": "^2.3.0-2024.12.10", "dayjs": "^1.11.11", "deepmerge": "^4.3.1", @@ -1688,7 +1649,7 @@ }, "peerDependencies": { "@tanstack/react-query": "^5.51.23", - "@usace/groundwork": "^3.14.19", + "@usace/groundwork": "^3.14.19 || ^4.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" } diff --git a/cda-gui/package.json b/cda-gui/package.json index 97d0854bd..04d6e715b 100644 --- a/cda-gui/package.json +++ b/cda-gui/package.json @@ -25,7 +25,7 @@ "@headlessui/react": "^2.2.10", "@tanstack/react-query": "^5.64.1", "@tanstack/react-virtual": "^3.14.3", - "@usace-watermanagement/groundwork-water": "^3.9.0", + "@usace-watermanagement/groundwork-water": "github:USACE-WaterManagement/groundwork-water#agent/cwms-plot-cda-parity", "@usace/groundwork": "^3.14.18", "cwmsjs": "^2.3.2-2025.3.19", "dayjs": "^1.11.13", diff --git a/cda-gui/src/pages/data-query/components/CWMSPlot.jsx b/cda-gui/src/pages/data-query/components/CWMSPlot.jsx deleted file mode 100644 index f7e86a203..000000000 --- a/cda-gui/src/pages/data-query/components/CWMSPlot.jsx +++ /dev/null @@ -1,369 +0,0 @@ -import { useEffect, useMemo, useState, useRef } from "react"; -import { Configuration, LevelsApi, TimeSeriesApi } from "cwmsjs"; -import Plotly from "plotly.js-basic-dist"; -import { gwMerge, Skeleton } from "@usace/groundwork"; -import deepmerge from "deepmerge"; -import PropTypes from "prop-types"; - -/** - * Normalize a data prop to an array of objects - * - * This function provides support for data props entered as a single element - * or as an array as well as support for data points entered as strings or as - * objects. Data is adjusted as necessary to return a normalized prop formatted - * as an array of data objects in the format {id, ...additionalOptions}. - * @param prop The data prop to be normalized. - * @returns An array of data objects in the format {id, ...additionalOptions} - */ -const normalizeDataProp = (prop) => { - if (!prop) return []; - if (Array.isArray(prop)) - return prop.map((datum) => { - if (Array.isArray(datum)) - console.error("Nested array not valid in plot data props"); - else if (typeof datum === "string") return { id: datum }; - else if (typeof datum === "object") return datum; - else console.error(`Unrecognized data format: ${datum}`); - }); - else if (typeof prop === "string") return [{ id: prop }]; - else if (typeof prop === "object") return [prop]; - else console.error(`Unrecognized data format: ${prop}`); -}; - -const getYAxisId = (timeseriesParam) => { - const yaxis = timeseriesParam?.traceOptions?.yaxis; - if (!yaxis) return undefined; - if ((yaxis == "y") | (yaxis == "y1")) return "yaxis"; - const re = /^y(\d+)$/; - const match = yaxis.match(re); - if (match) { - const axisInt = match[1]; - return `yaxis${axisInt}`; - } else { - return undefined; - } -}; - -function CWMSPlot({ - begin, - end, - unit, - office, - timeSeries, - pageSize, - locationLevels, - inputTSValues, - layoutOptions = {}, - className = "", - responsive = true, - datum, - timezone, - trim, - staticTraces, - cdaUrl, -}) { - const [isLoading, setIsLoading] = useState(true); - const [tsData, setTsData] = useState(null); - const plotElement = useRef(null); - const [error, setError] = useState(null); - - const timeSeriesArray = useMemo(() => normalizeDataProp(timeSeries), [timeSeries]); - - const locationLevelsArray = useMemo( - () => normalizeDataProp(locationLevels), - [locationLevels], - ); - - const ts_api = useMemo(() => { - const config_v2 = new Configuration({ - basePath: cdaUrl, - headers: { - accept: "application/json;version=2", - }, - }); - return new TimeSeriesApi(config_v2); - }, [cdaUrl]); - - const level_api = useMemo(() => { - const config_level = new Configuration({ - basePath: cdaUrl, - headers: { - accept: "*/*", - }, - }); - return new LevelsApi(config_level); - }, [cdaUrl]); - - const defaultLayout = { - height: 750, - grid: { - columns: 1, - }, - xaxis: { - showgrid: true, - showline: true, - mirror: "all", - linecolor: "black", - linewidth: 1, - }, - }; - - timeSeriesArray.forEach((item, index) => { - const yaxis_id = getYAxisId(item) ?? (index == 0 ? "yaxis" : "yaxis" + index); - if (!(yaxis_id in defaultLayout)) { - defaultLayout[yaxis_id] = { - title: { - text: - item.id.split(".")[1] + - (item?.traceOptions?.units ? " (" + item.traceOptions.units + ")" : ""), - font: { - family: "Arial, sans-serif", - size: 14, - }, - }, - }; - } - }); - - defaultLayout.grid.rows = Object.keys(defaultLayout).filter((key) => - key.includes("yaxis"), - ).length; - - const layout = deepmerge(defaultLayout, layoutOptions); - - useEffect(() => { - const tsids = timeSeriesArray.map((ts) => ts.id); - const levels = locationLevelsArray.map((level) => level.id); - - if (!tsids?.length) { - setError("You must specify one or more Timeseries IDs to plot."); - return; - } - - if (!office) setError("You must specify a 3 letter ID for the office"); - - const fetchData = async () => { - let values = []; - let _data = { ts: {} }; - - if (!inputTSValues) { - let ts_promises = tsids.map(async (name) => { - try { - // Currently, large page size calls are blocked, so the default of 500 is used - // let pageSize = 500; - // const delta = dayjs(end.value).diff(dayjs(start.value), "day", true) - // let interval = tsid.split(".")[3] - // if (interval.includes("Minute")) { - // if (interval.includes("15")) { pageSize = delta * 100 } - // if (interval.includes("1Minute")) { pageSize = delta * 1500 } - // } - // if (interval.includes("Hour")) { pageSize = delta * 10 } - // if (interval.includes("Day")) { pageSize = delta * 1 } - return await ts_api.getTimeSeries({ - name, - office, - unit, - datum, - begin, - end, - pageSize, - timezone, - trim, - }); - } catch (error) { - console.error("Error fetching timeseries data:", error); - } - }); - - values = await Promise.all(ts_promises); - } else { - values = inputTSValues; - } - let lev_promises = levels?.map(async (item) => { - let level; - // The Level API doesn't accept the same date format - try { - level = await level_api.getLevels({ - levelIdMask: item, - begin: begin?.slice(0, 10), - end: end?.slice(0, 10), - office: office, - format: "json", - unit: unit, - }); - } catch (error) { - console.error("Error fetching location level data:", error); - } - return level; - }); - values.forEach((result) => { - if (result && result.name) { - if (!_data.ts[result.name]) { - _data.ts[result.name] = []; - } - _data.ts[result.name].push(result); - } else if (result === null) { - console.warn(`Skipping as no data was found.`); - } else { - console.warn(`No timeseries data found for ${result?.name}`); - } - }); - - let lev_values; - if (lev_promises) { - lev_values = await Promise.all(lev_promises); - } - - lev_values?.forEach((result) => { - const name = result["location-levels"]["location-levels"][0]?.name; - const levels = result["location-levels"]["location-levels"][0]?.values; - if (result && name) { - if (!_data.ts[name]) { - _data.ts[name] = []; - } - _data.ts[name].push(levels); - } else if (result === null) { - console.warn(`Skipping as no data was found.`); - } else { - console.warn(`No location level data found for ${result?.name}`); - } - }); - - setTsData(_data); - }; - - fetchData(); - }, [ - begin, - datum, - end, - inputTSValues, - level_api, - locationLevelsArray, - office, - timeSeriesArray, - timezone, - trim, - unit, - pageSize, - ts_api, - ]); - - useEffect(() => { - const tsids = timeSeriesArray.map((ts) => ts.id); - const levels = locationLevelsArray.map((level) => level.id); - - if (!plotElement.current || !tsData) { - return; - } - - setIsLoading(true); - - let ts_keys = Object.keys(tsData.ts); - - // Create traces keyed to the ts data - let traces = []; - let ts; - - // Add Static Data first to the list of traces so they show up behind - if (staticTraces) { - staticTraces.map((trace) => traces.push(trace)); - } - - // Loop thru TS Data for timeseries and location levels - for (let k_idx = 0; k_idx < ts_keys.length; k_idx++) { - const key = ts_keys[k_idx]; - - // Add Timeseries to list of traces - if (tsids.includes(key)) { - for (let ts_idx = 0; ts_idx < tsData.ts[key].length; ts_idx++) { - ts = tsData.ts[key][ts_idx]; - // Defaults for trace - const trace = { - x: ts.values.map((value) => new Date(value[0])), - y: ts.values.map((value) => value[1]), - showlegend: true, - legend: { x: 1, xanchor: "right", y: 1 }, - }; - // Add all other parameters - const tsObj = timeSeriesArray?.filter((item) => item.id === ts.name)[0]; - const fullTrace = tsObj?.traceOptions - ? deepmerge(trace, tsObj?.traceOptions) - : trace; - traces.push(fullTrace); - } - } - - // Add Location Levels to list of traces - if (levels?.includes(key)) { - for (let ts_idx = 0; ts_idx < tsData.ts[key].length; ts_idx++) { - ts = tsData.ts[key][ts_idx]; - // Defaults for trace - const trace = { - x: ts.segments[0].values.map((value) => new Date(value[0])), - y: ts.segments[0].values.map((value) => value[1]), - showlegend: true, - legend: { x: 1, xanchor: "right", y: 1 }, - }; - // Add all other parameters - const levelObj = locationLevelsArray?.filter((item) => item.id == key)[0]; - const fullTrace = levelObj?.traceOptions - ? deepmerge(trace, levelObj?.traceOptions) - : trace; - traces.push(fullTrace); - } - } - } - - setIsLoading(false); - - Plotly.newPlot(plotElement.current, traces, layout, { - responsive: responsive, - }); - }, [layout, locationLevelsArray, responsive, staticTraces, timeSeriesArray, tsData]); - - return ( -
-
- {error ? ( -
Error: {error}
- ) : isLoading ? ( -
- -
- ) : ( - <> - )} -
-
- ); -} -CWMSPlot.propTypes = { - begin: PropTypes.any, - end: PropTypes.any, - unit: PropTypes.string, - office: PropTypes.string, - timeSeries: PropTypes.any, - pageSize: PropTypes.number, - locationLevels: PropTypes.any, - inputTSValues: PropTypes.any, - layoutOptions: PropTypes.object, - className: PropTypes.string, - responsive: PropTypes.bool, - datum: PropTypes.any, - timezone: PropTypes.string, - trim: PropTypes.any, - staticTraces: PropTypes.any, - cdaUrl: PropTypes.string, -}; - -export default CWMSPlot; -export { CWMSPlot }; diff --git a/cda-gui/src/pages/data-query/components/CWMSTable.jsx b/cda-gui/src/pages/data-query/components/CWMSTable.jsx deleted file mode 100644 index 30702a6cc..000000000 --- a/cda-gui/src/pages/data-query/components/CWMSTable.jsx +++ /dev/null @@ -1,244 +0,0 @@ -import { useEffect, useMemo, useState, useRef } from "react"; -import { Configuration, TimeSeriesApi } from "cwmsjs"; -import dayjs from "dayjs"; -import PropTypes from "prop-types"; -import { - Table, - TableBody, - TableRow, - TableHead, - TableHeader, - TableCell, - Badge, -} from "@usace/groundwork"; - -function CWMSTable({ - timeseriesParams, - office, - unit = "EN", - datum, - begin, - end, - timezone, - inputTSValues, - trim = true, - pageSize, - interval = 1, - sortAscending = true, - missingString = "", - dateFormat = "ddd MMM DD HH:mm", - cdaUrl, -}) { - const tableElement = useRef([]); - const [tableData, setTableData] = useState(null); - const [tsData, setTsData] = useState(null); - const tsids = useMemo( - () => timeseriesParams.map((item) => item.tsid), - [timeseriesParams], - ); - const ts_api = useMemo(() => { - const config = new Configuration({ - basePath: cdaUrl, - headers: { - accept: "application/json;version=2", - }, - }); - return new TimeSeriesApi(config); - }, [cdaUrl]); - - useEffect(() => { - // Need support for page size, either defined or set with time delta - // And then code to page thru each page and append into ts data - - const fetchData = async () => { - let values = []; - if (!inputTSValues) { - let promises = tsids.map(async (name) => { - try { - return await ts_api.getTimeSeries({ - name, - office, - unit, - datum, - begin, - end, - timezone, - trim, - pageSize, - }); - } catch (error) { - if (error.response?.status === 404) { - console.warn(`Data for ${name} not found: 404`); - return null; - } else { - throw error; - } - } - }); - - values = await Promise.all(promises); - } else { - values = inputTSValues; - } - let data = { ts: {}, dates: [] }; - let dates = []; - values.forEach((result) => { - if (result && result.values) { - if (!data.ts[result.values]) { - data.ts[result.name] = []; - } - - let precision = 2; - timeseriesParams.forEach((entry) => { - if (entry.tsid == result.name && entry.precision != null) { - precision = entry.precision; - } - }); - - result.values.forEach((item) => { - const dt = item[0]; - const val = item[1]; - - if (val === null) { - data.ts[result.name][dt] = missingString; - } else { - data.ts[result.name][dt] = val.toFixed(precision); - } - - if (!dates.includes(dt)) { - if (dt % (interval * 1000 * 60) == 0) { - dates.push(dt); - } - } - }); - } else if (result === null) { - console.warn(`Skipping as no data was found.`); - data.ts[null] = []; - } else { - console.warn(`No unit found for ${result?.name}`); - } - }); - - dates.sort(); - - if (!sortAscending) { - dates.reverse(); - } - - data.dates = dates; - - setTsData(data); - }; - - fetchData(); - }, [ - timeseriesParams, - office, - unit, - datum, - begin, - end, - timezone, - trim, - pageSize, - sortAscending, - missingString, - interval, - inputTSValues, - ts_api, - tsids, - ]); - - useEffect(() => { - if (!tableElement.current || !tsData) { - return; - } - - const table = []; - - tsData.dates.forEach((dt) => { - const row = []; - - Object.keys(tsData.ts).forEach((item) => { - row.push(tsData.ts[item][dt]); - }); - table.push([new Date(dt), row]); - }); - setTableData(table); - }, [tsData, timeseriesParams]); - if (!tsids.length) { - return ( -
- - No Data Found - - Try A Larger Time Window? -
- ); - } - - if (!office) - return ( -
- - No Office Selected - -
- ); - - return ( - - - - Date & Time (Local) - {timeseriesParams.map((item, index) => ( - {item.header} - ))} - - - - {/* Each Row */} - {tableData?.map((item, index) => ( - - {/* Date-Time */} - - {dayjs(item[0]).format(dateFormat)} - - - {/* Loop over columns */} - {item[1].map((val, idx) => ( - {val} - ))} - - ))} - -
- ); -} - -export default CWMSTable; -export { CWMSTable }; - -CWMSTable.propTypes = { - begin: PropTypes.any, - cdaUrl: PropTypes.string, - dateFormat: PropTypes.string, - datum: PropTypes.string, - end: PropTypes.any, - inputTSValues: PropTypes.array, - interval: PropTypes.number, - missingString: PropTypes.string, - office: PropTypes.string, - pageSize: PropTypes.number, - sortAscending: PropTypes.bool, - timezone: PropTypes.string, - timeseriesParams: PropTypes.arrayOf( - PropTypes.shape({ - header: PropTypes.string, - precision: PropTypes.number, - tsid: PropTypes.string.isRequired, - }), - ).isRequired, - trim: PropTypes.bool, - unit: PropTypes.string, -}; diff --git a/cda-gui/src/pages/data-query/components/DataQueryTable.jsx b/cda-gui/src/pages/data-query/components/DataQueryTable.jsx deleted file mode 100644 index 177ecf32a..000000000 --- a/cda-gui/src/pages/data-query/components/DataQueryTable.jsx +++ /dev/null @@ -1,271 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import { useVirtualizer } from "@tanstack/react-virtual"; -import dayjs from "dayjs"; -import PropTypes from "prop-types"; -import { buildTableIndex, buildTableRowValues } from "../utils/tableData"; - -function getDefaultMobileColumns(timeseriesParams) { - return timeseriesParams.slice(0, 2).map((param) => param.tsid); -} - -function normalizeMobileColumns(columns, timeseriesParams) { - const validTsids = new Set(timeseriesParams.map((param) => param.tsid)); - const fallback = getDefaultMobileColumns(timeseriesParams); - - return [ - ...new Set([...columns.filter((tsid) => validTsids.has(tsid)), ...fallback]), - ].slice(0, Math.min(2, timeseriesParams.length)); -} - -export default function DataQueryTable({ - dateFormat = "YYYY-MM-DD HH:mm:ss", - missingString = "---", - rawSeries, - sortAscending, - timeseriesParams, -}) { - const parentRef = useRef(null); - const [mobileColumns, setMobileColumns] = useState(() => - getDefaultMobileColumns(timeseriesParams), - ); - - useEffect(() => { - setMobileColumns((current) => normalizeMobileColumns(current, timeseriesParams)); - }, [timeseriesParams]); - - const tableIndex = useMemo( - () => - buildTableIndex({ - rawSeries, - sortAscending, - timeseriesParams, - }), - [rawSeries, sortAscending, timeseriesParams], - ); - const precisionByTsid = useMemo( - () => new Map(timeseriesParams.map((param) => [param.tsid, param.rounding ?? 2])), - [timeseriesParams], - ); - - const visibleMobileParams = useMemo( - () => - mobileColumns - .map((tsid) => timeseriesParams.find((param) => param.tsid === tsid)) - .filter(Boolean), - [mobileColumns, timeseriesParams], - ); - - const visibleMobileIndexes = useMemo( - () => - visibleMobileParams.map((param) => - timeseriesParams.findIndex((item) => item.tsid === param.tsid), - ), - [timeseriesParams, visibleMobileParams], - ); - - const rowVirtualizer = useVirtualizer({ - count: tableIndex.dates.length, - estimateSize: () => 36, - getScrollElement: () => parentRef.current, - overscan: 12, - }); - - const virtualRows = rowVirtualizer.getVirtualItems(); - const totalSize = rowVirtualizer.getTotalSize(); - const mobileColumnSlots = Array.from({ - length: Math.min(2, timeseriesParams.length), - }); - const desktopGridStyle = { - gridTemplateColumns: `12rem repeat(${timeseriesParams.length}, minmax(8rem, 1fr))`, - minWidth: `${12 + timeseriesParams.length * 8}rem`, - }; - const mobileGridStyle = { - gridTemplateColumns: `12rem repeat(${visibleMobileParams.length}, minmax(8rem, 1fr))`, - minWidth: `${12 + visibleMobileParams.length * 8}rem`, - }; - - if (!timeseriesParams.length) { - return null; - } - - if (!tableIndex.dates.length) { - return ( -
- No table rows found for the selected time series. -
- ); - } - - return ( -
-
- {mobileColumnSlots.map((_, slot) => ( - - ))} -
- -
-
-
-
- Date & Time (Local) -
- {timeseriesParams.map((param) => ( -
- {param.header} -
- ))} -
-
- {virtualRows.map((virtualRow) => { - const date = tableIndex.dates[virtualRow.index]; - const values = buildTableRowValues({ - date, - missingString, - precisionByTsid, - seriesLookup: tableIndex.seriesLookup, - visibleTsids: tableIndex.visibleTsids, - }); - return ( -
-
- {dayjs(date).format(dateFormat)} -
- {values.map((value, index) => ( -
- {value} -
- ))} -
- ); - })} -
-
- -
-
-
- Date & Time (Local) -
- {visibleMobileParams.map((param) => ( -
- {param.header} -
- ))} -
-
- {virtualRows.map((virtualRow) => { - const date = tableIndex.dates[virtualRow.index]; - const values = buildTableRowValues({ - date, - missingString, - precisionByTsid, - seriesLookup: tableIndex.seriesLookup, - visibleTsids: tableIndex.visibleTsids, - }); - return ( -
-
- {dayjs(date).format(dateFormat)} -
- {visibleMobileIndexes.map((index) => ( -
- {values[index]} -
- ))} -
- ); - })} -
-
-
-
- ); -} - -DataQueryTable.propTypes = { - dateFormat: PropTypes.string, - missingString: PropTypes.string, - rawSeries: PropTypes.array, - sortAscending: PropTypes.bool.isRequired, - timeseriesParams: PropTypes.arrayOf( - PropTypes.shape({ - header: PropTypes.string.isRequired, - rounding: PropTypes.number, - tsid: PropTypes.string.isRequired, - }), - ).isRequired, -}; diff --git a/cda-gui/src/pages/data-query/components/DataTabs.jsx b/cda-gui/src/pages/data-query/components/DataTabs.jsx index 8cd1eff6c..6e1f3d9b3 100644 --- a/cda-gui/src/pages/data-query/components/DataTabs.jsx +++ b/cda-gui/src/pages/data-query/components/DataTabs.jsx @@ -1,6 +1,5 @@ import { Tabs, Skeleton } from "@usace/groundwork"; -import CWMSPlot from "./CWMSPlot"; -import DataQueryTable from "./DataQueryTable"; +import { CWMSTable, CWMSPlot } from "@usace-watermanagement/groundwork-water"; import MetaDataTab from "./MetaDataTab"; import PropTypes from "prop-types"; @@ -32,12 +31,14 @@ export default function DataTabs({ className="relative z-10 bg-white" > {timeseriesParams.length > 0 && ( - )} diff --git a/cda-gui/src/pages/data-query/index.jsx b/cda-gui/src/pages/data-query/index.jsx index f4aa8b453..b27c389be 100644 --- a/cda-gui/src/pages/data-query/index.jsx +++ b/cda-gui/src/pages/data-query/index.jsx @@ -13,7 +13,11 @@ import DataTabs from "./components/DataTabs"; import TimeSeriesBuilder from "./components/TimeSeriesBuilder"; import TimeSeriesManager from "./components/TimeSeriesManager"; import SettingsMenu from "./components/SettingsMenu"; -import { buildCsvContent, buildTableRows, downloadBlob } from "./utils/tableData"; +import { + buildCsvContent, + buildTableRows, + downloadBlob, +} from "@usace-watermanagement/groundwork-water"; const CDA_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ssZ"; const v2_config = new Configuration({ diff --git a/cda-gui/src/pages/data-query/utils/tableData.js b/cda-gui/src/pages/data-query/utils/tableData.js deleted file mode 100644 index c932a2b3e..000000000 --- a/cda-gui/src/pages/data-query/utils/tableData.js +++ /dev/null @@ -1,108 +0,0 @@ -import dayjs from "dayjs"; - -export function buildSeriesLookup(rawSeries = []) { - return new Map( - rawSeries.map((series) => [ - series.name, - { - ...series, - valuesByDate: new Map((series.values || []).map((value) => [value[0], value])), - }, - ]), - ); -} - -export function buildTableIndex({ - rawSeries = [], - sortAscending = true, - timeseriesParams = [], -}) { - const seriesLookup = buildSeriesLookup(rawSeries); - const visibleTsids = timeseriesParams.map((param) => param.tsid); - const dateSet = new Set(); - - visibleTsids.forEach((tsid) => { - const series = seriesLookup.get(tsid); - series?.values?.forEach((value) => dateSet.add(value[0])); - }); - - return { - dates: [...dateSet].sort((a, b) => (sortAscending ? a - b : b - a)), - seriesLookup, - visibleTsids, - }; -} - -export function buildTableRowValues({ - date, - missingString = "", - precisionByTsid = new Map(), - seriesLookup, - visibleTsids = [], -}) { - return visibleTsids.map((tsid) => { - const rawValue = seriesLookup.get(tsid)?.valuesByDate.get(date)?.[1]; - const precision = precisionByTsid.get(tsid) ?? 2; - - if (rawValue === null || rawValue === undefined) return missingString; - const numericValue = Number(rawValue); - return Number.isFinite(numericValue) - ? numericValue.toFixed(precision) - : String(rawValue); - }); -} - -export function buildTableRows({ - dateFormat = "YYYY-MM-DD HH:mm:ss", - missingString = "", - rawSeries = [], - sortAscending = true, - timeseriesParams = [], -}) { - const { dates, seriesLookup, visibleTsids } = buildTableIndex({ - rawSeries, - sortAscending, - timeseriesParams, - }); - const precisionByTsid = new Map( - timeseriesParams.map((param) => [param.tsid, param.rounding ?? 2]), - ); - - return dates.map((date) => ({ - date, - formattedDate: dayjs(date).format(dateFormat), - values: buildTableRowValues({ - date, - missingString, - precisionByTsid, - seriesLookup, - visibleTsids, - }), - })); -} - -function escapeCsvCell(value) { - const text = value === null || value === undefined ? "" : String(value); - return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; -} - -export function buildCsvContent({ rows, timeseriesParams }) { - const header = ["Date", ...timeseriesParams.map((param) => param.header)]; - return [header, ...rows.map((row) => [row.formattedDate, ...row.values])] - .map((row) => row.map(escapeCsvCell).join(",")) - .join("\n"); -} - -export function downloadBlob({ content, fileName, type }) { - const blob = new Blob([content], { type }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - - link.href = url; - link.download = fileName; - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(url); -}