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);
-}