diff --git a/package.json b/package.json index a855ffa2..f8de9e60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hhd-ui", - "version": "0.18.9", + "version": "0.19.1", "private": true, "dependencies": { "@ag-grid-community/client-side-row-model": "^30.0.3", diff --git a/src/app-bundles/instrument-sensors-bundle.js b/src/app-bundles/instrument-sensors-bundle.js index 7e397648..ca75a6f2 100644 --- a/src/app-bundles/instrument-sensors-bundle.js +++ b/src/app-bundles/instrument-sensors-bundle.js @@ -66,7 +66,7 @@ export default { // eslint-disable-next-line no-console console.log('todo', err); } else { - store.doFetchInstrumentSensorsById(); + store.doFetchInstrumentSensorsById(type, uriId); } }); }, diff --git a/src/app-pages/instrument/chart/chart.jsx b/src/app-pages/instrument/chart/chart.jsx index a6844c8e..bf2b2eca 100644 --- a/src/app-pages/instrument/chart/chart.jsx +++ b/src/app-pages/instrument/chart/chart.jsx @@ -1,11 +1,19 @@ import React from 'react'; -import { connect } from 'redux-bundler-react'; -export default connect(() => ( +import InstrumentTimeseriesPlot from './instrument-timeseries-plot'; +import IpiDepthBasedPlots from './ipi-depth-based-plots'; +import SaaDepthBasedPlots from './saa-depth-based-plots'; +import InclinometerPlots from './inclinometer-plots'; + +const InstrumentChartTab = ({ isIPI, isShapeArray, isInclinometer }) => (
-

- The chart reflecting the result of the data passed through the - instrument formula will be displayed here. -

+
+ {isShapeArray && } + {isIPI && } + {isInclinometer && } + {(!isShapeArray && !isIPI && !isInclinometer) && } +
-)); +); + +export default InstrumentChartTab; diff --git a/src/app-pages/instrument/chart/inclinometer-plots.jsx b/src/app-pages/instrument/chart/inclinometer-plots.jsx new file mode 100644 index 00000000..61a888f2 --- /dev/null +++ b/src/app-pages/instrument/chart/inclinometer-plots.jsx @@ -0,0 +1,213 @@ +import React, { useEffect, useState } from 'react'; +import ReactDatePicker from 'react-datepicker'; +import { addDays, subDays } from 'date-fns'; +import { connect } from 'redux-bundler-react'; +import { DateTime } from 'luxon'; +import { Stack, Switch } from '@mui/material'; +import { useDeepCompareEffect } from 'react-use'; + +import Button from '../../../app-components/button'; +import Chart from '../../../app-components/chart/chart'; +import SetInitialTimeModal from '../setInitialTimeModal'; + +const colors = { + x: '#800000', + y: '#000075', +}; + +const config = { + repsonsive: true, + displaylogo: false, + displayModeBar: true, + scrollZoom: true, +}; + +const multiPlotLayout = (isIncrement, initialMeasurement) => ({ + showlegend: true, + autosize: true, + height: 1000, + xaxis: { + title: `A-${isIncrement ? 'Increment' : 'Cumulative Displacement'}`, + domain: [0, 0.45], + anchor: 'y1', + }, + yaxis: { + title: 'Elevation (ft)', + domain: [0.3, 1], + anchor: 'x1', + }, + xaxis2: { + title: `B-${isIncrement ? 'Increment' : 'Cumulative Displacement'}`, + domain: [0.55, 1], + anchor: 'y2', + }, + yaxis2: { + domain: [0.3, 1], + anchor: 'x2', + }, + xaxis3: { + type: 'date', + domain: [0, 1], + anchor: 'y3', + title: 'Time', + }, + yaxis3: { + domain: [0, 0.2], + anchor: 'x3', + title: `${initialMeasurement.elevation} ∆ mm`, + }, +}); + +const build2dTrace = (dataArray, key) => { + if (!dataArray.length) return {}; + + const { time, measurements } = dataArray[dataArray.length - 1]; + + const keyMeasurements = measurements.map(m => m[key]); + const elevation = measurements.map((m) => m.depth); + + return { + x: keyMeasurements, + y: elevation, + mode: 'markers+lines', + marker: { size: 5, color: colors[key] }, + line: { width: 1 }, + type: 'scatter', + name: key, + hovertemplate: ` + ${DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_SHORT)}
+ Depth: %{y}
+ Displacement: %{x}
+ + `, + ...['b_increment', 'b_cum_dev'].includes(key) && { + xaxis: 'x2', + yaxis: 'y2', + }, + }; +}; + +const buildLongTraces = (dataArray, initialMeasurement) => { + if (!dataArray.length) return []; + + const { elevation: initElevation } = initialMeasurement; + const elevationY = []; + const temperatureY = []; + const timeX = []; + + dataArray.forEach((el, index) => { + if (!index) return; + const { time, measurements } = el; + timeX.push(time); + + const lastMeasurement = measurements[measurements.length - 1]; + const { elevation, temp } = lastMeasurement; + + elevationY.push(elevation - initElevation); + temperatureY.push(temp); + }); + + return [{ + x: timeX, + y: elevationY, + mode: 'markers+lines', + marker: { size: 5 }, + line: { width: 1 }, + type: 'scatter', + name: '∆ Top Elevation', + xaxis: 'x3', + yaxis: 'y3', + }]; +}; + +const InclinometerPlots = connect( + 'doModalOpen', + 'doFetchInstrumentSensorsById', + 'doFetchInstrumentSensorMeasurements', + 'selectInstrumentSensors', + 'selectInstrumentSensorsMeasurements', + ({ + doModalOpen, + doFetchInstrumentSensorsById, + doFetchInstrumentSensorMeasurements, + instrumentSensors, + instrumentSensorsMeasurements, + }) => { + const [isIncrement, setIsIncrement] = useState(true); + const [dateRange, setDateRange] = useState([subDays(new Date(), 7), new Date()]); + + const initialMeasurement = instrumentSensorsMeasurements.length ? instrumentSensorsMeasurements[0]?.measurements?.findLast(e => e) : {}; + + useEffect(() => { + doFetchInstrumentSensorsById('incl'); + }, [doFetchInstrumentSensorsById]); + + useDeepCompareEffect(() => { + doFetchInstrumentSensorMeasurements('incl', dateRange[1].toISOString(), dateRange[0].toISOString()); + }, [dateRange]); + + return ( + <> + {instrumentSensors.length ? ( + <> + Select a timeframe to view plots for the associated timeseries: +
+
+ Start Date + setDateRange([date, addDays(date, 7)])} + /> +
+
+ End Date + setDateRange([subDays(date, 7), date])} + /> +
+
+ + Cumulative + setIsIncrement(prev => !prev)} /> + Incremental + +
+
+
+
+
+
+ +
+
+ + ) : No Sensors for this instrument.} + + ); + }, +); + +export default InclinometerPlots; diff --git a/src/app-pages/instrument/chart/instrument-timeseries-plot.jsx b/src/app-pages/instrument/chart/instrument-timeseries-plot.jsx new file mode 100644 index 00000000..4fb2a8f9 --- /dev/null +++ b/src/app-pages/instrument/chart/instrument-timeseries-plot.jsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import ReactDatePicker from 'react-datepicker'; +import { connect } from 'redux-bundler-react'; +import { useDeepCompareEffect } from 'react-use'; +import { subDays } from 'date-fns'; + +import Chart from '../../../app-components/chart/chart'; + +const chartData = (plotMeasurements, timeseries) => { + const data = plotMeasurements.map(elem => { + if (elem && timeseries.length) { + const { items, timeseries_id } = elem; + + const { + name, + unit, + parameter, + } = timeseries.find(t => t.id === timeseries_id) || {}; + + const traceData = items.reduce( + (accum, item) => ({ + x: [...accum.x, item.time], + y: [...accum.y, item.value], + }), + { x: [], y: [] }, + ); + + return { + x: traceData.x, + y: traceData.y, + name: `${name} - ${parameter} (${unit})` || '', + showlegend: true, + hoverinfo: 'x+y+text', + timeseriesId: timeseries_id, + }; + } + }); + + return data; +}; + +const InstrumentTimeseriesPlot = connect( + 'doTimeseriesMeasurementsFetchById', + 'selectInstrumentTimeseriesItems', + 'selectInstrumentsIdByRoute', + 'selectTimeseriesMeasurementsItems', + ({ + doTimeseriesMeasurementsFetchById, + instrumentTimeseriesItems: timeseries, + instrumentsIdByRoute, + timeseriesMeasurementsItems: measurements, + }) => { + const [dateRange, setDateRange] = useState([subDays(new Date(), 365), new Date()]); + + const { instrumentId } = instrumentsIdByRoute || {}; + const plotTimeseries = (timeseries || []).filter(el => el.instrument_id === instrumentId); + const plotMeasurements = plotTimeseries.map(ts => measurements.find(elem => elem.timeseries_id === ts.id)); + + const layout = { + xaxis: { + range: dateRange, + title: 'Date', + showline: true, + mirror: true, + }, + yaxis: { + title: 'Measurement', + showline: true, + mirror: true, + }, + }; + + useDeepCompareEffect(() => { + if (plotTimeseries?.length) { + plotTimeseries.forEach(ts => { + doTimeseriesMeasurementsFetchById({ timeseriesId: ts.id, dateRange }); + }) + } + }, [plotTimeseries, dateRange]); + + return ( + <> +
+
+ Start Date + dateRange[1] ? dateRange[1] : Date.now()} + selected={dateRange[0]} + onChange={(date) => setDateRange(prev => [date, prev[1]])} + /> +
+
+ End Date + setDateRange(prev => [prev[0], date])} + /> +
+
+ + + ); + }, +); + +export default InstrumentTimeseriesPlot; diff --git a/src/app-pages/instrument/chart/ipi-depth-based-plots.jsx b/src/app-pages/instrument/chart/ipi-depth-based-plots.jsx new file mode 100644 index 00000000..790ac736 --- /dev/null +++ b/src/app-pages/instrument/chart/ipi-depth-based-plots.jsx @@ -0,0 +1,231 @@ +import React, { useEffect, useState } from 'react'; +import ReactDatePicker from 'react-datepicker'; +import { Checkbox, FormControlLabel, Slider, Stack, Switch } from '@mui/material'; +import { addDays, subDays } from 'date-fns'; +import { connect } from 'redux-bundler-react'; +import { DateTime } from 'luxon'; +import { useDeepCompareEffect } from 'react-use'; + +import Button from '../../../app-components/button'; +import Chart from '../../../app-components/chart/chart'; +import SetInitialTimeModal from '../setInitialTimeModal'; + +const colors = { + init: '#000000', +}; + +const config = { + repsonsive: true, + displaylogo: false, + displayModeBar: true, + scrollZoom: true, +}; + +const layout = (showTemperature, showIncremental) => ({ + showlegend: true, + autosize: true, + height: 800, + rows: 1, + columns: showTemperature ? 2 : 1, + yaxis: { + domain: [0, 1], + anchor: 'x1', + autorange: 'reversed', + title: `Depth in Feet`, + }, + xaxis: { + domain: [0, showTemperature ? 0.4 : 1], + anchor: 'y1', + title: `${showIncremental ? 'Incremental' : 'Cumulative'} Displacement`, + }, + ...showTemperature && { + xaxis2: { + title: 'Temperature', + domain: [0.6, 1], + anchor: 'y2', + }, + yaxis2: { + domain: [0, 1], + anchor: 'x2', + autorange: 'reversed', + } + }, +}); + +const formatData = (measurements, indexes, showInitial, showTemperature, showIncremental) => { + if (!measurements.length) return {}; + + const timeIncrements = measurements.sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis()) + const relevantData = timeIncrements.slice(indexes[0], indexes[1] + 1); + + const dataArray = [ + ...(showInitial ? build2dTrace(timeIncrements[0], true, showTemperature, showIncremental).flat() : []), + ...relevantData.map(m => build2dTrace(m, false, showTemperature, showIncremental)).flat(), + ].filter(e => e); + + return { dataArray, timeIncrements, relevantData }; +}; + +const build2dTrace = (data, isInit, showTemperature, showIncremental) => { + if (!Object.keys(data).length) return {}; + + const { time, measurements } = data; + + const x = [], xTemp = [], y = []; + + measurements?.forEach(element => { + x.push(showIncremental ? (element?.inc_dev || 0) : (element?.cum_dev || 0)); + xTemp.push(element?.temp); + y.push(element?.elevation || 0); + }); + + const localDateString = DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_SHORT); + const common = { + y, + mode: 'markers+lines', + marker: { size: 5, color: isInit ? colors['init'] : undefined }, + line: { width: 1 }, + type: 'scatter', + }; + + return [{ + ...common, + x, + name: isInit ? `Initial Displacement (${localDateString})` : `Displacement at ${localDateString}`, + hovertemplate: ` + ${localDateString}
+ Elevation: %{y}
+ ${showIncremental ? 'Incremental' : 'Cumulative'} Displacement: %{x}
+ + `, + }, showTemperature ? { + ...common, + xTemp, + xaxis: 'x2', + yaxis: 'y2', + name: isInit ? `Initial Temperature (${localDateString})` : `Temperature at ${localDateString}`, + hovertemplate: ` + ${localDateString}
+ Elevation: %{y}
+ Temperature: %{x}
+ + `, + } : {}]; +}; + +const IpiDepthBasedPlots = connect( + 'doModalOpen', + 'doFetchInstrumentSensorsById', + 'doFetchInstrumentSensorMeasurements', + 'selectInstrumentSensors', + 'selectInstrumentSensorsMeasurements', + ({ + doModalOpen, + doFetchInstrumentSensorsById, + doFetchInstrumentSensorMeasurements, + instrumentSensors, + instrumentSensorsMeasurements, + }) => { + const [showTemperature, setShowTemperature] = useState(true); + const [showInitial, setShowInitial] = useState(false); + const [showIncremental, setShowIncremental] = useState(false); + const [sliderVal, setSliderVal] = useState([0, 0]); + const [dateRange, setDateRange] = useState([subDays(new Date(), 7), new Date()]); + + const { dataArray = [], timeIncrements = [] } = formatData(instrumentSensorsMeasurements, sliderVal, showInitial, showTemperature, showIncremental); + + useEffect(() => { + doFetchInstrumentSensorsById('ipi'); + }, [doFetchInstrumentSensorsById]); + + useDeepCompareEffect(() => { + doFetchInstrumentSensorMeasurements('ipi', dateRange[1].toISOString(), dateRange[0].toISOString()); + }, [dateRange]); + + return ( + <> + {instrumentSensors.length ? ( + <> + Select a timeframe to view plots for the associated timeseries: +
+
+ Start Date + setDateRange([date, addDays(date, 7)])} + /> +
+
+ End Date + setDateRange([subDays(date, 7), date])} + /> +
+
+ setShowInitial(prev => !prev)} />} + label='Show Initial Displacement' + /> +
+
+ setShowTemperature(prev => !prev)} />} + label='Show Temperature' + /> +
+
+ + Cumulative + setShowIncremental(prev => !prev)} /> + Incremental + +
+
+
+
+
+
+ +
+
+
+
+ {DateTime.fromISO(instrumentSensorsMeasurements[val]?.time).toFormat('MMM dd, yyyy hh:mm:ss')}} + onChange={(_e, newVal) => setSliderVal(newVal)} + /> +
+
+ + ) : No Sensors for this instrument.} + + ); + }, +); + +export default IpiDepthBasedPlots; diff --git a/src/app-pages/instrument/saa-depth-based-plots.jsx b/src/app-pages/instrument/chart/saa-depth-based-plots.jsx similarity index 52% rename from src/app-pages/instrument/saa-depth-based-plots.jsx rename to src/app-pages/instrument/chart/saa-depth-based-plots.jsx index 6c290d86..8ffb1640 100644 --- a/src/app-pages/instrument/saa-depth-based-plots.jsx +++ b/src/app-pages/instrument/chart/saa-depth-based-plots.jsx @@ -1,16 +1,14 @@ import React, { useEffect, useState } from 'react'; import ReactDatePicker from 'react-datepicker'; -import { Add, Remove } from '@mui/icons-material'; import { addDays, subDays } from 'date-fns'; import { connect } from 'redux-bundler-react'; import { DateTime } from 'luxon'; import { Stack, Switch } from '@mui/material'; import { useDeepCompareEffect } from 'react-use'; -import Button from '../../app-components/button'; -import Card from '../../app-components/card'; -import Chart from '../../app-components/chart/chart'; -import SetInitialTimeModal from './setInitialTimeModal'; +import Button from '../../../app-components/button'; +import Chart from '../../../app-components/chart/chart'; +import SetInitialTimeModal from '../setInitialTimeModal'; const colors = { x: '#800000', @@ -166,7 +164,6 @@ const SaaDepthBasedPlots = connect( instrumentSensors, instrumentSensorsMeasurements, }) => { - const [isOpen, setIsOpen] = useState(true); const [isIncrement, setIsIncrement] = useState(true); const [dateRange, setDateRange] = useState([subDays(new Date(), 7), new Date()]); @@ -177,88 +174,70 @@ const SaaDepthBasedPlots = connect( }, [doFetchInstrumentSensorsById]); useDeepCompareEffect(() => { - if (isOpen) { - doFetchInstrumentSensorMeasurements('saa', dateRange[1].toISOString(), dateRange[0].toISOString()); - } - }, [isOpen, dateRange]); + doFetchInstrumentSensorMeasurements('saa', dateRange[1].toISOString(), dateRange[0].toISOString()); + }, [dateRange]); return ( - - -