From 4e0063360e0ffb3178e81131e4be254dd6b620ca Mon Sep 17 00:00:00 2001 From: Kevin Jackson <30411845+KevinJJackson@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:57:54 -0400 Subject: [PATCH] feature/new-plot-types (#228) --- package-lock.json | 15 +- package.json | 4 +- .../batch-plot-configurations-bundle.js | 41 +++- .../time-series-measurements-bundle.js | 4 +- src/app-bundles/upload-bundle.js | 2 +- src/app-components/chart/minify-plotly.js | 2 + src/app-components/hero/hero.jsx | 9 + .../project/batch-plotting/batch-plotting.jsx | 120 +++++----- .../chart-content/bullseye-plot.jsx | 104 +++++++++ .../chart-content/contour-plot.jsx | 66 ++++++ .../chart-content/profile-plot.jsx | 216 +++++++++++++++++ .../scatter-line-plot.jsx} | 48 ++-- .../components/batch-plot-chart-settings.jsx | 46 ++-- .../components/configuration-panel.jsx | 137 ----------- .../components/data-configuration.jsx | 167 ++++++------- .../project/batch-plotting/helper.js | 87 +++++++ .../modals/BatchPlotAdvancedSettings.jsx | 12 +- .../modals/BatchPlotConfiguration.jsx | 112 +++++++++ .../modals/components/LegendOrder.jsx | 22 +- .../modals/components/PlotSymbology.jsx | 23 +- .../modals/components/SecondaryAxis.jsx | 38 +-- .../modals/components/Thresholds.jsx | 55 +++-- .../modals/components/_bullseyeDisplay.jsx | 76 ++++++ .../modals/components/_contourDisplay.jsx | 201 ++++++++++++++++ .../modals/components/_profileDisplay.jsx | 46 ++++ .../modals/components/_scatterLineDisplay.jsx | 66 ++++++ .../tab-content/depth-chart.jsx | 219 ------------------ .../project/dashboard/cards/batchPlotCard.jsx | 6 +- .../dashboard/cards/reportConfigsCard.jsx | 6 +- .../instrument-timeseries-measurements.ts | 67 ++++++ src/upload-parsers/timeseries_measurements.js | 8 +- 31 files changed, 1401 insertions(+), 624 deletions(-) create mode 100644 src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx create mode 100644 src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx create mode 100644 src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx rename src/app-pages/project/batch-plotting/{tab-content/batch-plot-chart.jsx => chart-content/scatter-line-plot.jsx} (74%) delete mode 100644 src/app-pages/project/batch-plotting/components/configuration-panel.jsx create mode 100644 src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx create mode 100644 src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx create mode 100644 src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx create mode 100644 src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx create mode 100644 src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx delete mode 100644 src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx create mode 100644 src/app-services/collections/instrument-timeseries-measurements.ts diff --git a/package-lock.json b/package-lock.json index 1c1e819c..0c4a8ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "date-fns": "^2.30.0", "graceful-fs": "^4.2.11", "internal-nav-helper": "^3.1.0", - "keycloak-js": "^25.0.0", + "keycloak-js": "^25.0.2", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "luxon": "^3.3.0", @@ -5543,9 +5543,10 @@ "license": "ISC" }, "node_modules/keycloak-js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.0.tgz", - "integrity": "sha512-7vNDYWbi9H2LqeNvkpADL/Y/25KgG+3Byc5epd1eNAXM32FNi1DRMsbdiIHpzrTZhYlxZRWeDGhbLYIwmoMonw==", + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.2.tgz", + "integrity": "sha512-ACLf5O5PqzfDJwGqvLpqM0kflYWmyl3+T7M2C23gztJYccDxdfNP54+B9OkXz2GnDpLUId0ceoA+lbHw9t4Wng==", + "license": "Apache-2.0", "dependencies": { "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0" @@ -12722,9 +12723,9 @@ "version": "3.0.0" }, "keycloak-js": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.0.tgz", - "integrity": "sha512-7vNDYWbi9H2LqeNvkpADL/Y/25KgG+3Byc5epd1eNAXM32FNi1DRMsbdiIHpzrTZhYlxZRWeDGhbLYIwmoMonw==", + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.2.tgz", + "integrity": "sha512-ACLf5O5PqzfDJwGqvLpqM0kflYWmyl3+T7M2C23gztJYccDxdfNP54+B9OkXz2GnDpLUId0ceoA+lbHw9t4Wng==", "requires": { "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0" diff --git a/package.json b/package.json index 40f01f01..056e8468 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hhd-ui", - "version": "0.15.7", + "version": "0.16.0", "private": true, "dependencies": { "@ag-grid-community/client-side-row-model": "^30.0.3", @@ -24,7 +24,7 @@ "date-fns": "^2.30.0", "graceful-fs": "^4.2.11", "internal-nav-helper": "^3.1.0", - "keycloak-js": "^25.0.0", + "keycloak-js": "^25.0.2", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "luxon": "^3.3.0", diff --git a/src/app-bundles/batch-plot-configurations-bundle.js b/src/app-bundles/batch-plot-configurations-bundle.js index 0011d5da..82314b55 100644 --- a/src/app-bundles/batch-plot-configurations-bundle.js +++ b/src/app-bundles/batch-plot-configurations-bundle.js @@ -5,10 +5,11 @@ export default createRestBundle({ name: 'batchPlotConfigurations', uid: 'id', persist: false, - getTemplate: '/projects/:projectId/plot_configurations', - putTemplate: '/projects/:projectId/plot_configurations/{:item.id}', + getTemplate: '/projects/:projectId/plot_configs', + putTemplate: '/projects/:projectId/plot_configs/{:item.id}', + // @TODO: Remove the `postTemplate` postTemplate: '/projects/:projectId/plot_configurations', - deleteTemplate: '/projects/:projectId/plot_configurations/{:item.id}', + deleteTemplate: '/projects/:projectId/plot_configs/{:item.id}', fetchActions: ['URL_UPDATED', 'PROJECTS_FETCH_FINISHED'], forceFetchActions: ['BATCHPLOTCONFIGURATIONS_SAVE_FINISHED'], urlParamSelectors: ['selectProjectsIdByRoute'], @@ -29,6 +30,40 @@ export default createRestBundle({ store.doBatchPlotMapAddData(); }, + /** + * Save plot plot config settings. note: formData payloads differ based on plotType. + * + * @param {string} plotType one of ['scatter-line', 'profile', 'contour', 'bullseye'] + * @param {string} id Batch Plot Config Id + * @param {object} formData api-defined trace structure for related plotType + * @returns + */ + doSaveBatchPlotConfiguration: (plotType, id = null, formData = {}) => ({ dispatch, store, apiPost, apiPut }) => { + const uriMap = { + 'scatter-line': 'scatter_line_plots', + 'profile': 'profile_plots', + 'contour': 'contour_plots', + 'bullseye': 'bullseye_plots', + }; + const method = !id ? apiPost : apiPut; + const projectId = store.selectProjectsIdByRoute()?.projectId; + const uri = `/projects/${projectId}/plot_configs/${uriMap[plotType]}${!id ? '' : `/${id}`}`; + + const finalFormData = { + ...formData, + date_range: '1 year', + } + + method(uri, finalFormData, (err, _body) => { + if (err) { + dispatch({ type: 'BATCH_PLOT_CONFIGURATION_SAVE_ERROR', payload: err }); + } else { + dispatch({ type: 'BATCH_PLOT_CONFIGURATION_SAVED' }); + store.doBatchPlotConfigurationsFetch(); + } + }); + }, + selectBatchPlotConfigurationsRaw: (state) => state.batchPlotConfigurations, selectBatchPlotConfigurationsActiveId: (state) => state.batchPlotConfigurations._activeBatchPlotConfigurationId, diff --git a/src/app-bundles/time-series-measurements-bundle.js b/src/app-bundles/time-series-measurements-bundle.js index 655a03dc..0ad16eef 100644 --- a/src/app-bundles/time-series-measurements-bundle.js +++ b/src/app-bundles/time-series-measurements-bundle.js @@ -44,8 +44,8 @@ export default createRestBundle({ dispatch({ type: 'TIMESERIES_FETCH_BY_ID_START', payload: {} }); const [after, before] = dateRange; - const isoAfter = after ? after?.toISOString() : afterDate; - const isoBefore = before ? before?.toISOString() : beforeDate; + const isoAfter = after ? new Date(after)?.toISOString() : afterDate; + const isoBefore = before ? new Date(before)?.toISOString() : beforeDate; const url = `/timeseries/${timeseriesId}/measurements?after=${isoAfter}&before=${isoBefore}&threshold=${threshold}`; const flags = store['selectTimeseriesMeasurementsFlags'](); diff --git a/src/app-bundles/upload-bundle.js b/src/app-bundles/upload-bundle.js index e50507f3..96291525 100644 --- a/src/app-bundles/upload-bundle.js +++ b/src/app-bundles/upload-bundle.js @@ -396,7 +396,7 @@ const uploadBundle = { : setAllTo[1]; } else { // If field not mapped, set to null; if required field, push error - const data = row[sourceKey] || fieldMap[key]; + const data = row[sourceKey]; if (!data) { parsedRow[key] = null; if (config.required) parsedRow.errors.push(key); diff --git a/src/app-components/chart/minify-plotly.js b/src/app-components/chart/minify-plotly.js index 4d88ab1e..436fe61a 100644 --- a/src/app-components/chart/minify-plotly.js +++ b/src/app-components/chart/minify-plotly.js @@ -3,6 +3,7 @@ import * as Bar from 'plotly.js/lib/bar'; import * as Pie from 'plotly.js/lib/pie'; import * as Surface from 'plotly.js/lib/surface'; import * as Scatter3D from 'plotly.js/lib/scatter3d'; +import * as Contour from 'plotly.js/lib/contour'; Plotly.register([ /* @@ -11,6 +12,7 @@ Plotly.register([ List of available imports can be found here `node_modules/plotly.js/lib/index.js` */ Bar, + Contour, Pie, Surface, Scatter3D, diff --git a/src/app-components/hero/hero.jsx b/src/app-components/hero/hero.jsx index e2c231c2..d57c7872 100644 --- a/src/app-components/hero/hero.jsx +++ b/src/app-components/hero/hero.jsx @@ -49,6 +49,15 @@ const Hero = () => ( > IPM SubCOP + + Contact Support + ); diff --git a/src/app-pages/project/batch-plotting/batch-plotting.jsx b/src/app-pages/project/batch-plotting/batch-plotting.jsx index 7fd8b26d..6a75ecbf 100644 --- a/src/app-pages/project/batch-plotting/batch-plotting.jsx +++ b/src/app-pages/project/batch-plotting/batch-plotting.jsx @@ -1,70 +1,81 @@ -import React from 'react'; +import React, { useState } from 'react'; import { connect } from 'redux-bundler-react'; import { Engineering } from '@mui/icons-material'; +import { toast } from 'react-toastify'; +import { Link } from '@mui/material'; -import BatchPlotChart from './tab-content/batch-plot-chart'; +import BullseyePlot from './chart-content/bullseye-plot.jsx'; import Card from '../../../app-components/card'; +import ContourPlot from './chart-content/contour-plot.jsx'; import DataConfiguration from './components/data-configuration'; -import DepthChart from './tab-content/depth-chart'; import Map from '../../../app-components/classMap'; -import TabContainer from '../../../app-components/tab/tabContainer'; +import ProfilePlot from './chart-content/profile-plot.jsx'; +import ScatterLinePlot from './chart-content/scatter-line-plot.jsx'; +import { downloadFinalReport, useGetReportStatus, useInitializeReportDownload } from '../../../app-services/collections/report-configuration-download.ts'; +import { tUpdateSuccess } from '../../../common/helpers/toast-helpers'; +import { titlize } from '../../../common/helpers/utils.js'; import './batch-plotting.scss'; -const batchPlotContainsInclinometers = (activeId, items, timeseries, instruments, domains) => { - const ret = { - containsInclinometers: false, - inclinometerTimeseriesIds: [], - }; - if (!Object.keys(items).length || !activeId) return ret; - const config = items[activeId]; - const { timeseries_id } = config?.display?.traces?.map(el => el.timeseries_id) || {}; - - if (!timeseries_id) return ret; - - const { id: inclinometerTypeId } = domains['instrument_type'].find(el => el.value === 'Inclinometer'); - - timeseries_id.forEach(id => { - const { instrument_id } = timeseries.find(ts => ts.id === id) || {}; - const { type_id } = instruments.find(i => i.id === instrument_id) || {}; - - if (type_id === inclinometerTypeId) { - ret.containsInclinometers = true; - ret.inclinometerTimeseriesIds = [...ret.inclinometerTimeseriesIds, id]; - } - }); - - return ret; -}; - const BatchPlotting = connect( 'doMapsInitialize', 'doMapsShutdown', 'selectMapsObject', 'selectHashQuery', + 'selectProjectsByRoute', 'selectBatchPlotConfigurationsActiveId', 'selectBatchPlotConfigurationsItemsObject', 'selectInstrumentTimeseriesItems', 'selectInstrumentsItems', 'selectDomainsItemsByGroup', + 'selectProjectReportConfigurations', ({ doMapsInitialize, doMapsShutdown, mapsObject, hashQuery, + projectsByRoute: project, batchPlotConfigurationsActiveId: batchPlotId, batchPlotConfigurationsItemsObject: batchPlotItems, - instrumentTimeseriesItems: timeseries, - instrumentsItems: instruments, - domainsItemsByGroup: domains, + projectReportConfigurations: reportConfigs, }) => { const crossSectionReady = import.meta.env.VITE_CROSS_SECTION === 'true'; const userConfigId = hashQuery ? hashQuery['c'] : ''; + const activeConfig = batchPlotItems[batchPlotId]; + const { plot_type } = activeConfig || {}; + + const [toastId, setToastId] = useState(undefined); - const { - containsInclinometers, - inclinometerTimeseriesIds, - } = batchPlotContainsInclinometers(batchPlotId, batchPlotItems, timeseries, instruments, domains); + const { id: projectId } = project; + const { data: jobDetails, mutate: initReportJobMutator } = useInitializeReportDownload(projectId); + const { report_config_id: reportConfigId, id: jobId } = jobDetails || {}; + + const { data: currentJob } = useGetReportStatus({ projectId, reportConfigId, jobId }, { + enabled: !!((projectId && reportConfigId && jobId) && (['INIT', 'IN_PROGRESS'].includes(jobDetails?.status))), + refetchInterval: 3000, + }); + const { file_key, progress } = currentJob || {}; + + if (file_key && progress === 100) { + tUpdateSuccess( + toastId, + 'Your file is ready, click this message to open!', + { + autoClose: false, + onClose: () => { + downloadFinalReport({ projectId, reportConfigId, jobId }); + } + } + ); + } + + const beginDownloadReportJob = id => { + const tId = toast.loading('Getting your report ready for download. This may take a minute...'); + setToastId(tId); + initReportJobMutator(id); + }; + + const containedInReports = reportConfigs?.filter(cfg => cfg.plot_configs.some(el => el.id === batchPlotId)) || []; return ( <> @@ -79,12 +90,20 @@ const BatchPlotting = connect( This Batch Plot Configuration is a part of the following reports. Click on the report name to download the report or click on the  Remove Button to remove the plot configuration from the correlated Report Configuration. +
+ {containedInReports.length ? containedInReports.map(cfg => ( + beginDownloadReportJob(cfg.id)} + > + {cfg.name} + + )) : No Reports}
-
+
)}
- - , - }, - containsInclinometers && { - title: 'Depth Based Plot', - content: - }, - ].filter(e => e)} - /> + + titlize(s)).join('-')} Plot`} /> + + {plot_type === 'scatter-line' && } + {plot_type === 'profile' && } + {plot_type === 'contour' && } + {plot_type === 'bullseye' && } + )} diff --git a/src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx new file mode 100644 index 00000000..fb5a8f89 --- /dev/null +++ b/src/app-pages/project/batch-plotting/chart-content/bullseye-plot.jsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Icon } from '@iconify/react'; + +import Chart from '../../../../app-components/chart/chart.jsx'; +import { useGetBullseyeMeasurements } from '../../../../app-services/collections/instrument-timeseries-measurements.ts'; + +const generateCircle = (maxValue, scalar, isLight) => { + const scaled = maxValue * scalar; + + return { + type: 'circle', + xref: 'x', + yref: 'y', + x0: -scaled, + y0: -scaled, + x1: scaled, + y1: scaled, + opacity: 0.6, + line: { + width: 2, + color: isLight ? 'gray' : 'black', + }, + }; +}; + +const generateBullseyeData = (display, measurements = []) => { + const x = measurements.map(el => el.x); + const y = measurements.map(el => el.y); + + const xMax = Math.max(...x); + const yMax = Math.max(...y); + const totalMax = Math.max(xMax, yMax); + + return { + data: [ + { + mode: 'lines+markers', + type: 'scatter', + x, + y, + }, + ], + totalMax, + }; +}; + +const BullseyePlot = ({ + plotConfig, +}) => { + const { project_id, display, id } = plotConfig || {}; + + const { data: measurements, isLoading } = useGetBullseyeMeasurements({ projectId: project_id, plotConfigId: id }); + const { data, totalMax } = generateBullseyeData(display, measurements); + + const config = { + repsonsive: true, + displaylogo: false, + displayModeBar: true, + scrollZoom: true, + }; + + const layout = { + showlegend: true, + height: 800, + width: 800, + yaxis: { + showgrid: true, + title: `<-- South | North -->`, + }, + xaxis: { + showgrid: true, + title: `<-- West | East -->`, + }, + shapes: [ + generateCircle(totalMax, 0.25, true), + generateCircle(totalMax, 0.5, false), + generateCircle(totalMax, 0.75, true), + generateCircle(totalMax, 1, false), + generateCircle(totalMax, 1.25, true), + ], + }; + + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( +
+ +
+ )} + + ); +}; + +export default BullseyePlot; diff --git a/src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx new file mode 100644 index 00000000..a32af3b9 --- /dev/null +++ b/src/app-pages/project/batch-plotting/chart-content/contour-plot.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Icon } from '@iconify/react'; + +import Chart from '../../../../app-components/chart/chart'; +import { useGetContourMeasurements } from '../../../../app-services/collections/instrument-timeseries-measurements.ts'; + +const generateContourData = (display, measurements) => { + const { time } = display || {}; + if (!time) return []; + + return [{ + ...measurements, + connectgaps: true, + type: 'contour', + colorscale: 'Jet', + }]; +}; + +const ContourPlot = ({ + plotConfig, +}) => { + const { project_id, display, id } = plotConfig || {}; + const { time } = display || {}; + + const { data: measurements, isLoading } = useGetContourMeasurements({ projectId: project_id, plotConfigId: id, time: time }); + + const config = { + repsonsive: true, + displaylogo: false, + displayModeBar: true, + scrollZoom: true, + }; + + const layout = { + showlegend: false, + autosize: true, + height: 600, + yaxis: { + title: `Latitude`, + }, + xaxis: { + title: `Longitude`, + }, + }; + + const data = generateContourData(display, measurements); + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + + )} + + ); +}; + +export default ContourPlot; diff --git a/src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx new file mode 100644 index 00000000..457f8db7 --- /dev/null +++ b/src/app-pages/project/batch-plotting/chart-content/profile-plot.jsx @@ -0,0 +1,216 @@ +import React, { useState } from 'react'; +import { Slider } from '@mui/material'; +import { DateTime } from 'luxon'; +import { Icon } from '@iconify/react'; + +import Chart from '../../../../app-components/chart/chart'; +import { useGetMeasurementsByInstrumentType } from '../../../../app-services/collections/instrument-timeseries-measurements.ts'; + +const colors = [ + '#800000', + '#000075', + '#e6194B', + '#3cb44b', + '#911eb4', + '#fabed4', +]; + +const formatData = (data = [], indexes = [], _isMetric = false) => { + if (!data.length) return {}; + + const depthIncrements = data.map((datum, i) => { + const { time, measurements = [] } = datum; + + const valueDisplacement = measurements.sort((a, b) => b.elevation - a.elevation); + + return { time, valueDisplacement, colorIndex: i }; + }).sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis()); + + const relevantData = depthIncrements.slice(indexes[0], indexes[1] + 1); + const dataArray = []; + + for (let i = 0; i < relevantData.length; i++) { + const { valueDisplacement, time, colorIndex } = relevantData[i]; + + dataArray.push( + valueDisplacement.reduce((accum, current, ind) => { + const { elevation, x_increment, y_increment, } = current; + + if (ind === 0) { + accum.nDepth.push(elevation); + accum.aIncrement.push(x_increment); + accum.bIncrement.push(y_increment); + accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss'); + accum.colorIndex = colorIndex; + } else { + accum.nDepth.push(elevation); + accum.aIncrement.push(accum.aIncrement[ind - 1] + x_increment); + accum.bIncrement.push(accum.bIncrement[ind - 1] + y_increment); + accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss'); + accum.colorIndex = colorIndex; + } + + return accum; + }, { + nDepth: [], + aIncrement: [], + bIncrement: [], + time: '', + colorIndex: '', + }) + ) + } + + return { depthIncrements, dataArray, relevantData }; +}; + +const build3dTraces = (dataArray, unit) => dataArray.map(data => ( + { + x: data.aIncrement, + y: data.bIncrement, + z: data.nDepth, + mode: 'markers+lines', + marker: { size: 3, color: colors[data.colorIndex % colors.length] }, + line: { width: 1 }, + type: 'scatter3d', + name: `${data.time} Cumulative Displacement (in ${unit})`, + } + + // If client wants A and B Displacement on the 3-D plot, add these back in and adjust function to a forEach using push logic. + // , { + // x: data.aIncrement, + // y: new Array(data.bIncrement.length).fill(0), + // z: data.nDepth, + // mode: 'markers+lines', + // marker: { size: 5, color: 'green' }, + // type: 'scatter3d', + // name: `A Displacement (in ${unit})`, + // }, { + // x: new Array(data.aIncrement.length).fill(0), + // y: data.bIncrement, + // z: data.nDepth, + // mode: 'markers+lines', + // marker: { size: 5, color: 'orange' }, + // type: 'scatter3d', + // name: `B Displacement (in ${unit})` + // } +)); + +const build2dTrace = (dataArray, key, unit) => dataArray.map(data => ( + { + x: data[key], + y: data.nDepth, + mode: 'markers+lines', + marker: { size: 5, color: colors[data.colorIndex % colors.length] }, + line: { width: 1 }, + type: 'scatter', + name: `${key} Displacement (in ${unit})`, + hovertemplate: ` + ${data.time}
+ Depth: %{y}
+ Displacement: %{x}
+ + `, + } +)); + +const ProfilePlot = ({ + plotConfig, +}) => { + const [sliderVal, setSliderVal] = useState([0, 0]); + const { display } = plotConfig || {}; + const { instrument_id, instrument_type = 'SAA' } = display || {}; + + const { data: measuremnts, isLoading } = useGetMeasurementsByInstrumentType({ instrumentId: instrument_id, instrumentType: instrument_type }); + + const isMetric = false; + const unit = isMetric ? 'mm' : 'inches'; + const { dataArray = [], depthIncrements = [] } = formatData(measuremnts, sliderVal, isMetric); + + const config = { + repsonsive: true, + displaylogo: false, + displayModeBar: true, + scrollZoom: true, + }; + + const layout3d = { + autosize: true, + height: 800, + scene: { + xaxis: { title: `A-Displacement (in ${unit})` }, + yaxis: { title: `B-Displacement (in ${unit})` }, + zaxis: { title: 'Depth', autorange: 'reversed' }, + }, + legend: { + 'orientation': 'h', + }, + }; + + const layoutTall = (key) => ({ + showlegend: false, + autosize: true, + height: 800, + yaxis: { + autorange: 'reversed', + title: `Depth in Feet`, + }, + xaxis: { + title: `${key}-Displacement in ${unit}`, + }, + }); + + const incrementData = build3dTraces(dataArray, unit); + + return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + <> +
+
+ +
+
+ +
+
+ +
+
+
+
+ {DateTime.fromISO(depthIncrements[val]?.time).toFormat('MMM dd, yyyy hh:mm:ss')}} + onChange={(_e, newVal) => setSliderVal(newVal)} + /> +
+
+ + )} + + ); +}; + +export default ProfilePlot; diff --git a/src/app-pages/project/batch-plotting/tab-content/batch-plot-chart.jsx b/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx similarity index 74% rename from src/app-pages/project/batch-plotting/tab-content/batch-plot-chart.jsx rename to src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx index 3dc9c3e3..112b542e 100644 --- a/src/app-pages/project/batch-plotting/tab-content/batch-plot-chart.jsx +++ b/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { connect } from 'redux-bundler-react'; import { subDays } from 'date-fns'; import { useDeepCompareEffect } from 'react-use'; @@ -6,12 +6,12 @@ import { useDeepCompareEffect } from 'react-use'; import BatchPlotChartSettings from '../components/batch-plot-chart-settings'; import Chart from '../../../../app-components/chart/chart'; import ChartErrors from '../components/batch-plot-errors'; -import { generateNewChartData } from '../helper'; +import { determineDateRange, generateNewChartData } from '../helper'; -const BatchPlotChart = connect( +const ScatterLinePlot = connect( // 'doPrintSetData', 'doTimeseriesMeasurementsFetchById', - 'doBatchPlotConfigurationsSave', + 'doSaveBatchPlotConfiguration', 'selectBatchPlotConfigurationsActiveId', 'selectBatchPlotConfigurationsItemsObject', 'selectTimeseriesMeasurementsItems', @@ -19,21 +19,24 @@ const BatchPlotChart = connect( ({ // doPrintSetData, doTimeseriesMeasurementsFetchById, - doBatchPlotConfigurationsSave, + doSaveBatchPlotConfiguration, batchPlotConfigurationsActiveId: activeId, batchPlotConfigurationsItemsObject: batchPlotConfigs, timeseriesMeasurementsItems: timeseriesMeasurements, instrumentTimeseriesItems: timeseries, + plotConfig, }) => { - const [dateRange, setDateRange] = useState([subDays(new Date(), 365), new Date()]); - const [threshold, setThreshold] = useState(3000); - const [chartSettings, setChartSettings] = useState({ auto_range: false }); + const { auto_range, date_range, display, show_masked, show_comments, show_nonvalidated, plot_type, id } = plotConfig || {}; - const plotConfig = batchPlotConfigs[activeId]; - const plotTimeseriesIds = plotConfig?.display?.traces?.map(el => el.timeseries_id) || []; + const plotTimeseriesIds = display?.traces?.map(el => el.timeseries_id) || []; const plotTimeseries = timeseries.filter(ts => plotTimeseriesIds.includes(ts.id)); const plotMeasurements = plotTimeseriesIds.map(id => timeseriesMeasurements.find(elem => elem.timeseries_id === id)); - const chartData = useMemo(() => generateNewChartData(plotMeasurements, plotTimeseries, chartSettings, batchPlotConfigs[activeId]), [plotMeasurements]); + + const [dateRange, setDateRange] = useState([subDays(new Date(), 365), new Date()]); + const [threshold, setThreshold] = useState(3000); + const [chartSettings, setChartSettings] = useState({ auto_range, display, show_masked, show_comments, show_nonvalidated, date_range }); + + const chartData = useMemo(() => generateNewChartData(plotMeasurements, plotTimeseries, chartSettings, plotConfig), [plotMeasurements, activeId]); const withPrecipitation = plotTimeseries.some(ts => ts.parameter === 'precipitation'); const layout = { xaxis: { @@ -44,14 +47,14 @@ const BatchPlotChart = connect( mirror: true, }, yaxis: { - title: plotConfig?.display?.layout?.yaxis_title || 'Measurement', + title: display?.layout?.y_axis_title || 'Measurement', showline: true, mirror: true, domain: [0, withPrecipitation ? 0.66 : 1], }, - ...(plotConfig?.display?.layout?.secondary_axis_title && { + ...(display?.layout?.y2_axis_title && { yaxis2: { - title: plotConfig?.display?.layout?.secondary_axis_title, + title: display?.layout?.y2_axis_title, showline: true, side: 'right', overlaying: 'y1', @@ -67,7 +70,7 @@ const BatchPlotChart = connect( domain: [0.66, 1], }, }), - shapes: plotConfig?.display?.layout?.custom_shapes?.map(shape => + shapes: display?.layout?.custom_shapes?.map(shape => shape.enabled ? { type: 'line', x0: dateRange[0], @@ -86,9 +89,20 @@ const BatchPlotChart = connect( const savePlotSettings = (params) => { plotTimeseriesIds.forEach(id => doTimeseriesMeasurementsFetchById({ timeseriesId: id, dateRange, threshold })); - doBatchPlotConfigurationsSave(...params); + doSaveBatchPlotConfiguration(plot_type, id, { ...params }); }; + useEffect(() => { + const currentConfig = batchPlotConfigs[activeId]; + const { threshold, date_range, auto_range, show_comments, show_nonvalidated, show_masked } = currentConfig; + + if (activeId) { + setThreshold(threshold); + setDateRange(determineDateRange(date_range)); + setChartSettings({ auto_range, date_range, show_comments, show_nonvalidated, show_masked }); + } + }, [activeId]); + /** Fetches All Timeseries Measurements used by the plot */ useDeepCompareEffect(() => { const cfg = plotConfig || {}; @@ -139,4 +153,4 @@ const BatchPlotChart = connect( } ); -export default BatchPlotChart; +export default ScatterLinePlot; diff --git a/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx b/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx index 6d770529..7c772b3d 100644 --- a/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx +++ b/src/app-pages/project/batch-plotting/components/batch-plot-chart-settings.jsx @@ -1,19 +1,17 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import DatePicker from 'react-datepicker'; import { connect } from 'redux-bundler-react'; import { CSVLink } from 'react-csv'; import { DateTime } from 'luxon'; import { Slider } from '@mui/material'; -import { subDays, startOfDay } from 'date-fns'; import { toast } from 'react-toastify'; import BatchPlotAdvancedSettings from '../modals/BatchPlotAdvancedSettings'; import Button from '../../../../app-components/button'; import HelperTooltip from '../../../../app-components/helper-tooltip'; +import { determineDateRange } from '../helper'; // import PrintButton from './print-button'; -const dateAgo = days => subDays(new Date(), days); - // TODO const customDateFormat = (fromTime, endTime) => { const fromISO = fromTime.toISOString(); @@ -66,18 +64,11 @@ const BatchPlotChartSettings = connect( chartData, }) => { const [fromTime, endTime] = dateRange; + const { date_range, auto_range, show_comments, show_masked, show_nonvalidated } = chartSettings; + const [currentThreshold, setCurrentThreshold] = useState(threshold); const [csvData, setCsvData] = useState([]); - const [activeButton, setActiveButton] = useState('1 year'); - const { auto_range, show_comments, show_masked, show_nonvalidated } = chartSettings; - - const alterRange = (daysAgo) => { - setDateRange([startOfDay(dateAgo(daysAgo)), new Date()]); - }; - - const calcLifetime = () => { - setDateRange([new Date(0), new Date()]); - }; + const [activeButton, setActiveButton] = useState(date_range); const isDisplayAllActive = () => show_comments && show_masked && show_nonvalidated; @@ -85,6 +76,10 @@ const BatchPlotChartSettings = connect( e.preventDefault(); }; + useEffect(() => { + setDateRange(determineDateRange(activeButton)); + }, [activeButton]); + return (
Plot Settings: @@ -94,43 +89,31 @@ const BatchPlotChartSettings = connect(
-
-
- setSelectedTimeseries(val)} - initialValues={selectedTimeseries} - /> -
-
- ); - } -); - -export default ConfigurationPanel; diff --git a/src/app-pages/project/batch-plotting/components/data-configuration.jsx b/src/app-pages/project/batch-plotting/components/data-configuration.jsx index 626332cf..e4fa2609 100644 --- a/src/app-pages/project/batch-plotting/components/data-configuration.jsx +++ b/src/app-pages/project/batch-plotting/components/data-configuration.jsx @@ -1,22 +1,51 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'redux-bundler-react'; +import { Autocomplete, TextField } from '@mui/material'; import { Delete, Edit } from '@mui/icons-material'; import Button from '../../../../app-components/button'; -import ConfigurationPanel from './configuration-panel'; +import BatchPlotConfigurationModal from '../modals/BatchPlotConfiguration'; import DeleteButton from '../../../../app-components/delete-confirm'; -import Select from '../../../../app-components/select'; import usePrevious from '../../../../customHooks/usePrevious'; import '../batch-plotting.scss'; +// const groupBatchPlotConfigurations = batchPlotConfigurationsItems => { +// return batchPlotConfigurationsItems.reduce((accum, current) => { +// const { name, id, plot_type = 'Scatter Line' } = current; + +// const groupIndex = accum.findIndex(el => el.groupName === plot_type); +// const elem = accum[groupIndex]; +// const newElem = { +// name, +// id, +// }; + +// if (groupIndex >= 0) { +// accum.splice(groupIndex, 1, { +// groupName: plot_type, +// plotConfigurations: [...elem.plotConfigurations, newElem], +// }) +// } else { +// accum.push({ +// groupName: plot_type, +// plotConfigurations: [newElem], +// }); +// } + +// return accum; +// }, []); +// }; + const DataConfiguration = connect( + 'doModalOpen', 'selectBatchPlotConfigurationsItems', 'selectBatchPlotConfigurationsItemsObject', 'selectBatchPlotConfigurationsActiveId', 'doBatchPlotConfigurationsDelete', 'doBatchPlotConfigurationsSetActiveId', ({ + doModalOpen, batchPlotConfigurationsItems, batchPlotConfigurationsItemsObject, batchPlotConfigurationsActiveId, @@ -24,24 +53,8 @@ const DataConfiguration = connect( doBatchPlotConfigurationsSetActiveId, initialConfigurationId, }) => { - const [isPanelOpen, setIsPanelOpen] = useState(false); - const [isEditMode, setIsEditMode] = useState(false); const previousConfigId = usePrevious(batchPlotConfigurationsActiveId); - const configurations = batchPlotConfigurationsItems.map(config => ({ - text: config.name, - value: config.id, - })).sort((a, b) => a.text.localeCompare(b.text)); - - const handleEditClick = () => { - const currentItem = batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId]; - - if (currentItem) { - setIsEditMode(true); - setIsPanelOpen(true); - } - }; - const handleDeleteClick = () => { const currentItem = batchPlotConfigurationsItemsObject[batchPlotConfigurationsActiveId]; @@ -51,79 +64,75 @@ const DataConfiguration = connect( } }; - const handleNewClick = () => { - setIsEditMode(false); - setIsPanelOpen(true); - }; - const handleSelectChange = val => { if (val && val !== previousConfigId) { - doBatchPlotConfigurationsSetActiveId(val); + doBatchPlotConfigurationsSetActiveId(val.id); + } else if (!val) { + doBatchPlotConfigurationsSetActiveId(''); } }; - // Set active id to empty before we start. makes sure certain actions aren't available to the user that shouldn't be - useEffect(() => { - doBatchPlotConfigurationsSetActiveId(''); - }, [doBatchPlotConfigurationsSetActiveId]); - useEffect(() => { - if (initialConfigurationId) { - doBatchPlotConfigurationsSetActiveId(initialConfigurationId); - } - }, [initialConfigurationId, doBatchPlotConfigurationsSetActiveId]); + const toSet = initialConfigurationId ? initialConfigurationId : ''; + doBatchPlotConfigurationsSetActiveId(toSet); + }, [doBatchPlotConfigurationsSetActiveId, initialConfigurationId]); return (
- setSelectedDuration(e.target.value)} + > + 15 Minutes + 30 Minutes + 1 Hour + 12 Hours + 1 Day + 1 Week + 1 Month + Any + + + + setIsContourSmoothing(checked)} + checked={isContourSmoothing} + /> + )} + /> + setIsGradientSmoothing(checked)} + checked={isGradientSmoothing} + /> + )} + /> + setIsShowLabels(checked)} + checked={isShowLabels} + /> + )} + /> + + +
+
+
+ Start Date + setDateRange([date, addDays(date, 7)])} + /> +
+
+ End Date + setDateRange([subDays(date, 7), date])} + /> +
+
+ setSelectedMeasurement(value)} + options={measurementOptions} + isOptionEqualToValue={(opt, val) => opt._original === val._original} + renderInput={(params) => ( + + )} + /> + + ); + }, +); + +export default ContourDisplay; diff --git a/src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx new file mode 100644 index 00000000..07c29da8 --- /dev/null +++ b/src/app-pages/project/batch-plotting/modals/components/_profileDisplay.jsx @@ -0,0 +1,46 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { connect } from 'redux-bundler-react'; +import { formatInstrument, formatInstrumentOptions } from '../../helper'; + +const ProfileDisplay = connect( + 'selectInstrumentsItems', + ({ + instrumentsItems: instruments, + display, + onChange, + }) => { + const { instrument_id } = display || {}; + const [newInstrument, setNewInstrument] = useState(formatInstrument(instrument_id, instruments)); + + const instrumentOptions = useMemo(() => formatInstrumentOptions(instruments), [instruments]); + + useEffect(() => { + onChange({ + instrument_id: newInstrument, + }, !!newInstrument); + }, [newInstrument, onChange]); + + return ( + opt.label} + options={instrumentOptions} + value={newInstrument} + isOptionEqualToValue={(opt, val) => val ? opt?.value === val?.value : false} + onChange={(e, val) => setNewInstrument(val)} + renderInput={(params) => ( + + )} + /> + ); + }, +); + +export default ProfileDisplay; diff --git a/src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx new file mode 100644 index 00000000..53b652b1 --- /dev/null +++ b/src/app-pages/project/batch-plotting/modals/components/_scatterLineDisplay.jsx @@ -0,0 +1,66 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { connect } from 'redux-bundler-react'; +import { formatTimeseriesOptions, formatTimeseriesId } from '../../helper'; + +const formatDisplay = (timeseries, display) => { + return { + ...display, + traces: timeseries.map((ts, index) => ({ + timeseries_id: ts.value, + color: '#ffffff', + trace_order: index, + y_axis: 'y1', + line_style: 'solid', + show_markers: true, + width: 3, + })) + }; +}; + +const ScatterLineDisplay = connect( + 'selectInstrumentTimeseriesItems', + ({ + instrumentTimeseriesItems: instrumentTimeseries, + display, + onChange = () => {}, + }) => { + const { traces = [] } = display || {}; + const timeseries = useMemo(() => formatTimeseriesOptions(instrumentTimeseries), [instrumentTimeseries]); + + const [timeseriesInput, setTimeseriesInput] = useState(''); + const [selectedTimeseries, setSelectedTimeseries] = useState(traces.length ? traces.map(trace => formatTimeseriesId(trace.timeseries_id, instrumentTimeseries)) : []); + + useEffect(() => { + const newDisplay = formatDisplay(selectedTimeseries, display); + onChange(newDisplay, !!selectedTimeseries.length); + }, [selectedTimeseries, formatDisplay, onChange]); + + return ( + opt.instrumentName} + getOptionLabel={option => option.label} + options={timeseries.sort((a, b) => -b.label.localeCompare(a.label))} + value={selectedTimeseries} + isOptionEqualToValue={(opt, val) => val ? opt?.value === val.value : false} + onChange={(e, val) => setSelectedTimeseries(val)} + renderInput={(params) => ( + setTimeseriesInput(e.target.value)} + variant='outlined' + label='Timeseries' + /> + )} + /> + ); + }, +); + +export default ScatterLineDisplay; diff --git a/src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx b/src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx deleted file mode 100644 index d6e21fda..00000000 --- a/src/app-pages/project/batch-plotting/tab-content/depth-chart.jsx +++ /dev/null @@ -1,219 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { connect } from 'redux-bundler-react'; -import { Slider } from '@mui/material'; -import { DateTime } from 'luxon'; - -import Chart from '../../../../app-components/chart/chart'; - -const colors = [ - '#800000', - '#000075', - '#e6194B', - '#3cb44b', - '#911eb4', - '#fabed4', -]; - -const formatData = (data = [], indexes = []) => { - const inclinometerIds = Object.keys(data); - if (!inclinometerIds.length) return {}; - - const workingData = data[inclinometerIds[0]].inclinometers; - - const depthIncrements = workingData.map((datum, i) => { - const { time, values } = datum; - - const valueDisplacement = values.sort((a, b) => b.nDepth - a.nDepth); - - return { time, valueDisplacement, colorIndex: i }; - }).sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis()); - - const relevantData = depthIncrements.slice(indexes[0], indexes[1] + 1); - const dataArray = []; - - for (let i = 0; i < relevantData.length; i++) { - const { valueDisplacement, time, colorIndex } = relevantData[i]; - - dataArray.push( - valueDisplacement.reduce((accum, current, ind) => { - const { depth, aIncrement, bIncrement } = current; - - if (ind === 0) { - accum.nDepth.push(depth); - accum.aIncrement.push(aIncrement); - accum.bIncrement.push(bIncrement); - accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss'); - accum.colorIndex = colorIndex; - } else { - accum.nDepth.push(depth); - accum.aIncrement.push(accum.aIncrement[ind - 1] + aIncrement); - accum.bIncrement.push(accum.bIncrement[ind - 1] + bIncrement); - accum.time = DateTime.fromISO(time).toFormat('MMM dd, yyyy hh:mm:ss'); - accum.colorIndex = colorIndex; - } - - return accum; - }, { - nDepth: [], - aIncrement: [], - bIncrement: [], - time: '', - colorIndex: '', - }) - ) - } - - return { depthIncrements, dataArray, relevantData }; -}; - -const build3dTraces = (dataArray, unit) => dataArray.map(data => ( - { - x: data.aIncrement, - y: data.bIncrement, - z: data.nDepth, - mode: 'markers+lines', - marker: { size: 3, color: colors[data.colorIndex % colors.length] }, - line: { width: 1 }, - type: 'scatter3d', - name: `${data.time} Cumulative Displacement (in ${unit})`, - } - - // If client wants A and B Displacement on the 3-D plot, add these back in and adjust function to a forEach using push logic. - // , { - // x: data.aIncrement, - // y: new Array(data.bIncrement.length).fill(0), - // z: data.nDepth, - // mode: 'markers+lines', - // marker: { size: 5, color: 'green' }, - // type: 'scatter3d', - // name: `A Displacement (in ${unit})`, - // }, { - // x: new Array(data.aIncrement.length).fill(0), - // y: data.bIncrement, - // z: data.nDepth, - // mode: 'markers+lines', - // marker: { size: 5, color: 'orange' }, - // type: 'scatter3d', - // name: `B Displacement (in ${unit})` - // } -)); - -const build2dTrace = (dataArray, key, unit) => dataArray.map(data => ( - { - x: data[key], - y: data.nDepth, - mode: 'markers+lines', - marker: { size: 5, color: colors[data.colorIndex % colors.length] }, - line: { width: 1 }, - type: 'scatter', - name: `${key} Displacement (in ${unit})`, - hovertemplate: ` - ${data.time}
- Depth: %{y}
- Displacement: %{x}
- - `, - } -)); - -const DepthChart = connect( - 'doFetchInclinometerMeasurementsByTimeseriesId', - 'selectCurrentInclinometerMeasurements', - ({ - doFetchInclinometerMeasurementsByTimeseriesId, - currentInclinometerMeasurements, - inclinometerTimeseriesIds, - }) => { - const [sliderVal, setSliderVal] = useState([0, 0]); - - useEffect(() => { - inclinometerTimeseriesIds.forEach(id => { - doFetchInclinometerMeasurementsByTimeseriesId(id); - }); - }, [inclinometerTimeseriesIds, doFetchInclinometerMeasurementsByTimeseriesId]); - - const inclinometerIds = Object.keys(currentInclinometerMeasurements || {}); - const isMetric = false; - const unit = isMetric ? 'mm' : 'inches'; - const { dataArray = [], depthIncrements = [] } = formatData(currentInclinometerMeasurements, sliderVal, isMetric); - - const config = { - repsonsive: true, - displaylogo: false, - displayModeBar: true, - scrollZoom: true, - }; - - const layout3d = { - autosize: true, - height: 800, - scene: { - xaxis: { title: `A-Displacement (in ${unit})` }, - yaxis: { title: `B-Displacement (in ${unit})` }, - zaxis: { title: 'Depth', autorange: 'reversed' }, - }, - legend: { - 'orientation': 'h', - }, - }; - - const layoutTall = (key) => ({ - showlegend: false, - autosize: true, - height: 800, - yaxis: { - autorange: 'reversed', - title: `Depth in Feet`, - }, - xaxis: { - title: `${key}-Displacement in ${unit}`, - }, - }); - - const incrementData = build3dTraces(dataArray, unit); - - return inclinometerIds.length ? ( - <> -
-
- -
-
- -
-
- -
-
-
-
- {DateTime.fromISO(depthIncrements[val]?.time).toFormat('MMM dd, yyyy hh:mm:ss')}} - onChange={(_e, newVal) => setSliderVal(newVal)} - /> -
-
- - ) : Loading Chart Data....; - }, -); - -export default DepthChart; diff --git a/src/app-pages/project/dashboard/cards/batchPlotCard.jsx b/src/app-pages/project/dashboard/cards/batchPlotCard.jsx index 2d1c4b96..ae8ec9ef 100644 --- a/src/app-pages/project/dashboard/cards/batchPlotCard.jsx +++ b/src/app-pages/project/dashboard/cards/batchPlotCard.jsx @@ -28,10 +28,8 @@ const BatchPlotCard = connect( {plots.length ? ( -
- {plots.map((plot, i) => { - if (i < 5) return createReportRow(project, plot); - })} +
+ {plots.map((plot, _i) => createReportRow(project, plot))}
) : (

diff --git a/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx b/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx index 98e531a3..37c92f4d 100644 --- a/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx +++ b/src/app-pages/project/dashboard/cards/reportConfigsCard.jsx @@ -53,8 +53,8 @@ const ReportConfigsCard = connect( const { report_config_id: reportConfigId, id: jobId } = jobDetails || {}; const { data: currentJob } = useGetReportStatus({ projectId, reportConfigId, jobId }, { - enabled: !!((projectId && reportConfigId && jobId) && (jobDetails?.status === 'INIT')), - refetchInterval: (data) => (!data || data.progress < 100) ? 3000 : undefined, + enabled: !!((projectId && reportConfigId && jobId) && (['INIT', 'IN_PROGRESS'].includes(jobDetails?.status))), + refetchInterval: 3000, }); const { file_key, progress } = currentJob || {}; @@ -75,7 +75,7 @@ const ReportConfigsCard = connect( const tId = toast.loading('Getting your report ready for download. This may take a minute...'); setToastId(tId); initReportJobMutator(id); - } + }; return ( diff --git a/src/app-services/collections/instrument-timeseries-measurements.ts b/src/app-services/collections/instrument-timeseries-measurements.ts new file mode 100644 index 00000000..27a38245 --- /dev/null +++ b/src/app-services/collections/instrument-timeseries-measurements.ts @@ -0,0 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; + +import { apiGet } from '../fetch-helpers'; + +interface InstrumentParams { + instrumentId: string, + instrumentType: string, +} + +interface BullseyeParams { + projectId: string, + plotConfigId: string, +} + +interface ContourParams extends BullseyeParams { + time: string, +} + +interface TimestampParams extends BullseyeParams { + before?: string, + after?: string, +} + +export const useGetContourMeasurements = ({ projectId, plotConfigId, time }: ContourParams, opts: ClientQueryOptions) => { + const uri = `/projects/${projectId}/plot_configs/contour_plots/${plotConfigId}/measurements?time=${time}`; + + return useQuery({ + queryKey: [`contourMeasurements`, projectId, plotConfigId, time], + queryFn: () => apiGet(uri), + ...opts, + }); +}; + +export const useGetBullseyeMeasurements = ({ projectId, plotConfigId }: BullseyeParams, opts: ClientQueryOptions) => { + const uri = `/projects/${projectId}/plot_configs/bullseye_plots/${plotConfigId}/measurements`; + + return useQuery({ + queryKey: [`contourMeasurements`, projectId, plotConfigId], + queryFn: () => apiGet(uri), + ...opts, + }); +}; + +export const useGetMeasurementsByInstrumentType = ({ instrumentId, instrumentType }: InstrumentParams, opts: ClientQueryOptions) => { + const typeMap = { + 'SAA': 'saa', + 'IPI': 'ipi', + } as Record; + + const uri = `/instruments/${typeMap[instrumentType]}/${instrumentId}/measurements`; + + return useQuery({ + queryKey: [`instrumentMeasurementsType`, instrumentId, instrumentType], + queryFn: () => apiGet(uri), + ...opts, + }); +}; + +export const useGetMeasurementTimestamps = ({ projectId, plotConfigId, before, after }: TimestampParams, opts: ClientQueryOptions) => { + const uri = `/projects/${projectId}/plot_configs/contour_plots/${plotConfigId}/times?before=${before}&after=${after}`; + + return useQuery({ + queryKey: [`measurementTimestamps`, projectId, plotConfigId, before, after], + queryFn: () => apiGet(uri), + ...opts, + }); +} diff --git a/src/upload-parsers/timeseries_measurements.js b/src/upload-parsers/timeseries_measurements.js index 82abcf4c..10893f45 100644 --- a/src/upload-parsers/timeseries_measurements.js +++ b/src/upload-parsers/timeseries_measurements.js @@ -8,7 +8,7 @@ const timeseriesMeasurementParser = { prePostFilter: (data) => ( /** this will work for single timeseries_id, needs to be updated to allow for multiple */ data.reduce((accum, current) => { - const { timeseries_id, time, value, masked, validated, annotation } = current; + const { timeseries_id, time, value, masked, validated, annotation = '' } = current; return ({ ...accum, @@ -61,7 +61,7 @@ const timeseriesMeasurementParser = { label: 'Masked', type: 'boolean', required: false, - parse: val => val === 'true' || val === 'T' || val ==='Y', + parse: val => val === 'true' || val === 'T' || val === 'Y', validate: val => !!val, helpText: 'Boolean value of whether the measurement should be masked (Optional, default to false)', }, @@ -69,7 +69,7 @@ const timeseriesMeasurementParser = { label: 'Validated', type: 'boolean', required: false, - parse: val => val === 'true' || val === 'T' || val ==='Y', + parse: val => val === 'true' || val === 'T' || val === 'Y', validate: val => !!val, helpText: 'Boolean value of whether the measurement is already validated (Optional, default to false)', }, @@ -78,7 +78,7 @@ const timeseriesMeasurementParser = { type: 'string', required: false, parse: val => val, - validate: val => !!val, + validate: val => !!val || val === '', helpText: 'String note to be associated with the measurement (Optional, can be empty)', }, },