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(
@@ -34,7 +34,7 @@ const BatchPlotAdvancedSettings = connect(
@@ -49,7 +49,7 @@ const BatchPlotAdvancedSettings = connect(
@@ -59,7 +59,7 @@ const BatchPlotAdvancedSettings = connect(
diff --git a/src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx b/src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx
new file mode 100644
index 00000000..4d9597c6
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/BatchPlotConfiguration.jsx
@@ -0,0 +1,112 @@
+import React, { useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+
+import * as Modal from '../../../../app-components/modal';
+import ScatterLineDisplay from './components/_scatterLineDisplay';
+import ProfileDisplay from './components/_profileDisplay';
+import ContourDisplay from './components/_contourDisplay';
+import BullseyeDisplay from './components/_bullseyeDisplay';
+import { generatePayloadForType } from '../helper';
+
+const BatchPlotConfigurationModal = connect(
+ 'doSaveBatchPlotConfiguration',
+ 'selectProjectsIdByRoute',
+ ({
+ doSaveBatchPlotConfiguration,
+ projectsIdByRoute: project,
+ isEdit = false,
+ initConfig = {},
+ }) => {
+ const title = `${isEdit ? 'Edit' : 'New'} Batch Plot Configuration`;
+
+ const { id, name, plot_type, display = {} } = initConfig;
+
+ const [inputError, setInputError] = useState('');
+ const [newConfigName, setNewConfigName] = useState(name ?? '');
+ const [plotType, setPlotType] = useState(plot_type || null);
+ const [newDisplay, setNewDisplay] = useState(display || null);
+ const [isDisplayValid, setIsDisplayValid] = useState(false);
+
+ const handleChange = (display, isValid) => {
+ setNewDisplay(display);
+ setIsDisplayValid(isValid)
+ };
+
+ return (
+
+
+
+ {
+ if (inputError) setInputError('');
+ setNewConfigName(e.target.value);
+ }}
+ />
+ setPlotType(val.value)}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val : false}
+ options={[
+ { value: 'scatter-line', label: 'Scatter Line' },
+ { value: 'profile', label: 'Profile' },
+ { value: 'contour', label: 'Contour' },
+ { value: 'bullseye', label: 'Bullseye' },
+ ]}
+ renderInput={(params) => (
+
+ )}
+ />
+ {plotType === 'scatter-line' && (
+
+ )}
+ {plotType === 'profile' && (
+
+ )}
+ {plotType === 'contour' && (
+
+ )}
+ {plotType === 'bullseye' && (
+
+ )}
+
+ doSaveBatchPlotConfiguration(plotType, id, generatePayloadForType(plotType, newConfigName, newDisplay, project.projectId))}
+ />
+
+ );
+ },
+);
+
+export default BatchPlotConfigurationModal;
diff --git a/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx b/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx
index 22ee61aa..90cc2d5c 100644
--- a/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx
+++ b/src/app-pages/project/batch-plotting/modals/components/LegendOrder.jsx
@@ -5,8 +5,10 @@ import { Button, List, ListItem } from '@mui/material';
const LegendOrder = ({
chartData,
plotConfig,
- doBatchPlotConfigurationsSave,
+ doSaveBatchPlotConfiguration,
}) => {
+ const { display, plot_type, id } = plotConfig;
+
const [items, setItems] = useState(chartData.map(el => ({
name: el.name,
id: el.timeseriesId,
@@ -36,7 +38,7 @@ const LegendOrder = ({
};
const saveLegendOrder = () => {
- const traces = plotConfig.display.traces;
+ const traces = display.traces;
const newTraces = traces.map(trace => {
const newOrder = items.findIndex(el => el.id === trace.timeseries_id);
@@ -46,13 +48,17 @@ const LegendOrder = ({
};
})
- doBatchPlotConfigurationsSave({
- ...plotConfig,
- display: {
- ...plotConfig.display,
- traces: newTraces,
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ traces: newTraces,
+ }
}
- })
+ );
};
return (
diff --git a/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx b/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx
index b6d82d4f..bf4fb17d 100644
--- a/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx
+++ b/src/app-pages/project/batch-plotting/modals/components/PlotSymbology.jsx
@@ -157,8 +157,9 @@ const ExamplePlot = ({ settings }) => {
const PlotSymbology = ({
plotConfig,
chartData,
- doBatchPlotConfigurationsSave,
+ doSaveBatchPlotConfiguration,
}) => {
+ const { display, plot_type, id } = plotConfig;
const [settings, setSettings] = useState({});
const [selectedTs, setSelectedTs] = useState(undefined);
@@ -168,7 +169,7 @@ const PlotSymbology = ({
}));
const saveTraceSettings = () => {
- const traces = plotConfig?.display?.traces || [];
+ const traces = display?.traces || [];
const index = traces.findIndex(trace => trace.timeseries_id === selectedTs);
const newTrace = {
...index !== -1 ? traces[index] : {},
@@ -180,13 +181,17 @@ const PlotSymbology = ({
traces.splice(index, 1, newTrace)
- doBatchPlotConfigurationsSave({
- ...plotConfig,
- display: {
- ...plotConfig.display,
- traces,
- },
- });
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ traces,
+ },
+ }
+ );
};
return (
diff --git a/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx b/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx
index 520629b6..31733eb6 100644
--- a/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx
+++ b/src/app-pages/project/batch-plotting/modals/components/SecondaryAxis.jsx
@@ -23,12 +23,14 @@ const SecondaryAxis = connect(
instrumentTimeseriesItems: timeseries,
chartData,
plotConfig,
- doBatchPlotConfigurationsSave,
+ doSaveBatchPlotConfiguration,
}) => {
+ const { display, plot_type, id } = plotConfig || {};
+
const [axisSettings, setAxisSettings] = useState({
- primaryTitle: plotConfig?.display?.layout?.yaxis_title || 'Measurement',
- secondaryTitle: plotConfig?.display?.layout?.secondary_axis_title,
- timeseriesIds: plotConfig?.display?.traces?.filter(trace => trace.y_axis === 'y2').map(el => el.timeseries_id) || [],
+ primaryTitle: display?.layout?.y_axis_title || 'Measurement',
+ secondaryTitle: display?.layout?.y2_axis_title,
+ timeseriesIds: display?.traces?.filter(trace => trace.y_axis === 'y2').map(el => el.timeseries_id) || [],
});
const chartTimeseriesIds = chartData?.map(d => d.timeseriesId);
const timeseriesOptions = generateTimeseriesOptions(chartTimeseriesIds, timeseries);
@@ -36,7 +38,7 @@ const SecondaryAxis = connect(
const saveSecondaryAxisSettings = () => {
const { primaryTitle, secondaryTitle, timeseriesIds } = axisSettings;
- const newTraces = plotConfig.display.traces.map(trace => {
+ const newTraces = display.traces.map(trace => {
let axis = 'y1';
if (timeseriesIds.includes(trace.timeseries_id)) {
axis = 'y2';
@@ -47,18 +49,22 @@ const SecondaryAxis = connect(
};
});
- doBatchPlotConfigurationsSave({
- ...plotConfig,
- display: {
- ...plotConfig.display,
- layout: {
- ...plotConfig.display.layout,
- yaxis_title: primaryTitle,
- secondary_axis_title: secondaryTitle,
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ layout: {
+ ...display.layout,
+ y_axis_title: primaryTitle,
+ y2_axis_title: secondaryTitle,
+ },
+ traces: newTraces,
},
- traces: newTraces,
- },
- });
+ }
+ );
};
return (
diff --git a/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx b/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx
index 1b59e167..98e22275 100644
--- a/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx
+++ b/src/app-pages/project/batch-plotting/modals/components/Thresholds.jsx
@@ -12,14 +12,15 @@ const newThreshold = {
const Thresholds = ({
plotConfig,
- doBatchPlotConfigurationsSave,
+ doSaveBatchPlotConfiguration,
}) => {
- const currentShapes = plotConfig?.display?.layout?.custom_shapes || [];
+ const { display, plot_type, id } = plotConfig || {};
+ const currentShapes = display?.layout?.custom_shapes || [];
const [workingData, setWorkingData] = useState({});
const [key, setKey] = useState(0);
const saveThreshold = (threshold) => {
- const shapes = plotConfig?.display?.layout?.custom_shapes || [];
+ const shapes = display?.layout?.custom_shapes || [];
if (!threshold) shapes.push(newThreshold);
else {
@@ -30,16 +31,20 @@ const Thresholds = ({
})
}
- doBatchPlotConfigurationsSave({
- ...plotConfig,
- display: {
- ...plotConfig.display,
- layout: {
- ...plotConfig.display.layout,
- custom_shapes: shapes,
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...display,
+ layout: {
+ ...display.layout,
+ custom_shapes: shapes,
+ },
},
- },
- });
+ }
+ );
if (threshold) {
setWorkingData(prev => ({ ...prev, [threshold.index]: undefined }))
@@ -49,19 +54,23 @@ const Thresholds = ({
};
const deleteThreshold = index => {
- const shapes = plotConfig?.display?.layout?.custom_shapes || [];
+ const shapes = display?.layout?.custom_shapes || [];
shapes.splice(index, 1);
- doBatchPlotConfigurationsSave({
- ...plotConfig,
- display: {
- ...plotConfig.display,
- layout: {
- ...plotConfig.display.layout,
- custom_shapes: shapes,
+ doSaveBatchPlotConfiguration(
+ plot_type,
+ id,
+ {
+ ...plotConfig,
+ display: {
+ ...plotConfig.display,
+ layout: {
+ ...plotConfig.display.layout,
+ custom_shapes: shapes,
+ },
},
- },
- });
+ }
+ );
setKey(prev => prev + 1);
};
@@ -169,7 +178,7 @@ const Thresholds = ({
))}
>
) : (
-
+
No thresholds set. Click the Add Threshold button to create one.
)}
diff --git a/src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx
new file mode 100644
index 00000000..e9e1c495
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/_bullseyeDisplay.jsx
@@ -0,0 +1,76 @@
+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 = (xTimeseries, yTimeseries, display) => {
+ return {
+ ...display,
+ x_axis_timeseries_id: xTimeseries?.value,
+ y_axis_timeseries_id: yTimeseries?.value,
+ };
+};
+
+const BullseyeDisplay = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: timeseries,
+ display,
+ onChange,
+ }) => {
+ const { x_axis_timeseries_id, y_axis_timeseries_id } = display || {};
+ const options = useMemo(() => formatTimeseriesOptions(timeseries).sort((a, b) => -b.label.localeCompare(a.label)), [timeseries]);
+
+ const [xTimeseries, setXTimeseries] = useState(formatTimeseriesId(x_axis_timeseries_id, timeseries));
+ const [yTimeseries, setYTimeseries] = useState(formatTimeseriesId(y_axis_timeseries_id, timeseries));
+
+ useEffect(() => {
+ const newDisplay = formatDisplay(xTimeseries, yTimeseries, display);
+ onChange(newDisplay, (!!xTimeseries && !!yTimeseries));
+ }, [xTimeseries, yTimeseries, display, onChange]);
+
+ return (
+ <>
+ opt.instrumentName}
+ getOptionLabel={opt => opt.label}
+ options={options}
+ value={xTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val?.value : false}
+ onChange={(e, val) => setXTimeseries(val)}
+ renderInput={(params) => (
+
+ )}
+ />
+ opt.instrumentName}
+ getOptionLabel={opt => opt.label}
+ options={options}
+ value={yTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val?.value : false}
+ onChange={(e, val) => setYTimeseries(val.value)}
+ renderInput={(params) => (
+
+ )}
+ />
+ >
+ );
+ },
+);
+
+export default BullseyeDisplay;
diff --git a/src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx b/src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx
new file mode 100644
index 00000000..dcab3d5c
--- /dev/null
+++ b/src/app-pages/project/batch-plotting/modals/components/_contourDisplay.jsx
@@ -0,0 +1,201 @@
+import React, { useMemo, useState } from 'react';
+import ReactDatePicker from 'react-datepicker';
+import isEqual from 'lodash.isequal';
+import { addDays, subDays } from 'date-fns';
+import { Autocomplete, Checkbox, FormControl, FormControlLabel, FormGroup, InputLabel, MenuItem, Select, TextField } from '@mui/material';
+import { connect } from 'redux-bundler-react';
+import { DateTime } from 'luxon';
+import { useDeepCompareEffect } from 'react-use';
+
+import { formatMeasurementOptions, formatTimeseriesOptions, formatTimeseriesId, formatMeasurementTimestamp } from '../../helper';
+import { useGetMeasurementTimestamps } from '../../../../../app-services/collections/instrument-timeseries-measurements.ts';
+
+const formatDisplay = (oldDisplay, newOptions) => {
+ const {
+ selectedDuration,
+ isContourSmoothing,
+ isGradientSmoothing,
+ isShowLabels,
+ selectedTimeseries,
+ selectedMeasurement,
+ } = newOptions;
+
+ return {
+ ...oldDisplay,
+ time: selectedMeasurement._original,
+ timeseries_ids: selectedTimeseries?.map(t => t.value),
+ show_labels: isShowLabels,
+ contour_smoothing: isContourSmoothing,
+ gradient_smoothing: isGradientSmoothing,
+ locf_backfill: selectedDuration,
+ };
+};
+
+const ContourDisplay = connect(
+ 'selectInstrumentTimeseriesItems',
+ ({
+ instrumentTimeseriesItems: timeseries,
+ initConfig,
+ display,
+ onChange,
+ }) => {
+ const { id, project_id } = initConfig || {};
+ const { time, contour_smoothing, gradient_smoothing, show_labels, locf_backfill, timeseries_ids } = display || {};
+
+ const [selectedDuration, setSelectedDuration] = useState(locf_backfill || '');
+ const [isContourSmoothing, setIsContourSmoothing] = useState(contour_smoothing || false);
+ const [isGradientSmoothing, setIsGradientSmoothing] = useState(gradient_smoothing || false);
+ const [isShowLabels, setIsShowLabels] = useState(show_labels || false);
+ const [selectedTimeseries, setSelectedTimeseries] = useState(timeseries_ids ? timeseries_ids.map(id => formatTimeseriesId(id, timeseries)) : []);
+ const [selectedMeasurement, setSelectedMeasurement] = useState(time ? formatMeasurementTimestamp(time) : '');
+ const [dateRange, setDateRange] = useState([subDays(Date.now(), 7), new Date()]);
+
+ const { data: timestamps } = useGetMeasurementTimestamps({
+ projectId: project_id,
+ plotConfigId: id,
+ before: DateTime.fromJSDate(dateRange[1]).toISO(),
+ after: DateTime.fromJSDate(dateRange[0]).toISO(),
+ });
+
+ const timeseriesOptions = useMemo(() => formatTimeseriesOptions(timeseries).sort((a, b) => -b.label.localeCompare(a.label)), [timeseries]);
+ const measurementOptions = useMemo(() => formatMeasurementOptions(timestamps?.length ? timestamps : []), [timestamps]);
+
+ useDeepCompareEffect(() => {
+ const newDisplay = formatDisplay(display, {
+ selectedDuration,
+ isContourSmoothing,
+ isGradientSmoothing,
+ isShowLabels,
+ selectedTimeseries,
+ selectedMeasurement,
+ });
+
+ if (!isEqual(newDisplay, display)) {
+ onChange(newDisplay, (!!selectedMeasurement && !!selectedDuration && !!selectedTimeseries?.length));
+ }
+ }, [selectedMeasurement, selectedDuration, selectedTimeseries, isShowLabels, isContourSmoothing, isGradientSmoothing, onChange]);
+
+ return (
+ <>
+
+ {/* @TODO: Should probably save display on change, to make sure measurements reflect selected TS */}
+ opt.instrumentName}
+ getOptionLabel={option => option.label}
+ options={timeseriesOptions}
+ value={selectedTimeseries}
+ isOptionEqualToValue={(opt, val) => val ? opt?.value === val.value : false}
+ onChange={(_e, val) => setSelectedTimeseries(val)}
+ renderInput={(params) => (
+
+ )}
+ />
+
+ Duration
+
+
+
+ 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)',
},
},