-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
enhancement/instrument-charts (#253)
* 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
1 parent
4f33685
commit 42a0f48
Showing
15 changed files
with
668 additions
and
371 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
121
src/app-pages/instrument/chart/instrument-timeseries-plot.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.