Skip to content

Commit

Permalink
enhancement/instrument-charts (#253)
Browse files Browse the repository at this point in the history
* ipi-saa charts moved, scatter plots created
* minor changes, default time period to 1 year
* fix for weird disappeance when changing tabs occasional
* bugfix/plotting-lifetime-issue | bugfix/sorting-plot-configs
* incl charts working
  • Loading branch information
KevinJJackson authored Jan 21, 2025
1 parent 4f33685 commit 42a0f48
Show file tree
Hide file tree
Showing 15 changed files with 668 additions and 371 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/app-bundles/instrument-sensors-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default {
// eslint-disable-next-line no-console
console.log('todo', err);
} else {
store.doFetchInstrumentSensorsById();
store.doFetchInstrumentSensorsById(type, uriId);
}
});
},
Expand Down
22 changes: 15 additions & 7 deletions src/app-pages/instrument/chart/chart.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div>
<p>
The chart reflecting the result of the data passed through the
instrument formula will be displayed here.
</p>
<section className='container-fluid'>
{isShapeArray && <SaaDepthBasedPlots />}
{isIPI && <IpiDepthBasedPlots />}
{isInclinometer && <InclinometerPlots />}
{(!isShapeArray && !isIPI && !isInclinometer) && <InstrumentTimeseriesPlot />}
</section>
</div>
));
);

export default InstrumentChartTab;
213 changes: 213 additions & 0 deletions src/app-pages/instrument/chart/inclinometer-plots.jsx
Original file line number Diff line number Diff line change
@@ -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: `
<b>${DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_SHORT)}</b><br>
Depth: %{y}<br>
Displacement: %{x}<br>
<extra></extra>
`,
...['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:
<div className='row mt-2'>
<div className='col-2'>
<i>Start Date</i>
<ReactDatePicker
placeholderText='mm/dd/yyyy'
className='form-control'
maxDate={Date.now()}
selected={dateRange[0]}
onChange={(date) => setDateRange([date, addDays(date, 7)])}
/>
</div>
<div className='col-2'>
<i>End Date</i>
<ReactDatePicker
placeholderText='mm/dd/yyyy'
className='form-control'
maxDate={Date.now()}
selected={dateRange[1]}
onChange={(date) => setDateRange([subDays(date, 7), date])}
/>
</div>
<div className='col-2 pt-3 mt-1'>
<Stack direction='row' spacing={1} alignItems='center'>
Cumulative
<Switch defaultChecked onChange={_e => setIsIncrement(prev => !prev)} />
Incremental
</Stack>
</div>
<div className='col-6 float-right'>
<Button
isOutline
className='float-right'
variant='info'
size='small'
text='Set Initial Time'
handleClick={() => doModalOpen(SetInitialTimeModal, { type: 'incl' }, 'lg')}
/>
</div>
</div>
<div className='row'>
<div className='col-12'>
<Chart
config={config}
layout={multiPlotLayout(isIncrement, initialMeasurement)}
data={[
build2dTrace(instrumentSensorsMeasurements, isIncrement ? 'a_increment' : 'a_cum_dev'),
build2dTrace(instrumentSensorsMeasurements, isIncrement ? 'b_increment' : 'b_cum_dev'),
...buildLongTraces(instrumentSensorsMeasurements, initialMeasurement),
]}
/>
</div>
</div>
</>
) : <i>No Sensors for this instrument.</i>}
</>
);
},
);

export default InclinometerPlots;
121 changes: 121 additions & 0 deletions src/app-pages/instrument/chart/instrument-timeseries-plot.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className='row my-2'>
<div className='col-3'>
<i>Start Date</i>
<ReactDatePicker
placeholderText='mm/dd/yyyy'
className='form-control'
maxDate={Date.now() > dateRange[1] ? dateRange[1] : Date.now()}
selected={dateRange[0]}
onChange={(date) => setDateRange(prev => [date, prev[1]])}
/>
</div>
<div className='col-3'>
<i>End Date</i>
<ReactDatePicker
placeholderText='mm/dd/yyyy'
className='form-control'
minDate={dateRange[0]}
maxDate={Date.now()}
selected={dateRange[1]}
onChange={(date) => setDateRange(prev => [prev[0], date])}
/>
</div>
</div>
<Chart
withOverlay
data={chartData(plotMeasurements, timeseries)}
layout={layout}
config={{
displaylogo: false,
displayModeBar: true,
scrollZoom: true,
}}
/>
</>
);
},
);

export default InstrumentTimeseriesPlot;
Loading

0 comments on commit 42a0f48

Please # to comment.