diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts index ada046c29..db5e2b519 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveDataTransformation.ts @@ -1,5 +1,6 @@ import { ExportableContentTableColumn } from "components/ContentViews/table"; import { CurveSpecification } from "models/logData"; +import { CustomCurveRange } from "./CurveValuesPlot"; const calculateMean = (arr: number[]): number => arr.reduce((a, b) => a + b, 0) / arr.length; @@ -113,7 +114,9 @@ export const transformCurveData = ( data: any[], columns: ExportableContentTableColumn[], thresholdLevel: ThresholdLevel, - removeOutliers: boolean + removeOutliers: boolean, + ranges: CustomCurveRange[], + applyCustomRanges: boolean ) => { let transformedData = data; @@ -122,5 +125,20 @@ export const transformCurveData = ( } // Other potential transformations should be added here. + if (applyCustomRanges === true) { + const dataWithRange = transformedData.map((dataRow) => ({ ...dataRow })); + ranges.forEach((range) => { + for (let i = 0; i < dataWithRange.length; i++) { + if ( + dataWithRange[i][range.curve] < range.minValue || + dataWithRange[i][range.curve] > range.maxValue + ) { + delete dataWithRange[i][range.curve]; + } + } + }); + return dataWithRange; + } + return transformedData; }; diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx index b10212626..5f9bc4db8 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesPlot.tsx @@ -1,4 +1,9 @@ -import { EdsProvider, Switch, Typography } from "@equinor/eds-core-react"; +import { + Button, + EdsProvider, + Switch, + Typography +} from "@equinor/eds-core-react"; import { ThresholdLevel, transformCurveData @@ -32,6 +37,8 @@ import { useParams } from "react-router-dom"; import { RouterLogType } from "routes/routerConstants"; import { Colors } from "styles/Colors"; import { normaliseThemeForEds } from "../../tools/themeHelpers.ts"; +import { SettingCustomRanges } from "./SettingCustomRanges.tsx"; +import { Box } from "@mui/material"; const COLUMN_WIDTH = 135; const MNEMONIC_LABEL_WIDTH = COLUMN_WIDTH - 10; @@ -44,6 +51,11 @@ interface ControlledTooltipProps { content: string; } +export interface CustomCurveRange { + curve: string; + minValue: number; + maxValue: number; +} interface CurveValuesPlotProps { data: any[]; columns: ExportableContentTableColumn[]; @@ -69,17 +81,24 @@ export const CurveValuesPlot = React.memo( } = useOperationState(); const [enableScatter, setEnableScatter] = useState(false); const [removeOutliers, setRemoveOutliers] = useState(false); + const [useCustomRanges, setUseCustomRanges] = useState(false); + const [refreshGraph, setRefreshGraph] = useState(false); const [outliersThresholdLevel, setOutliersThresholdLevel] = useState(ThresholdLevel.Medium); const chart = useRef(null); const selectedLabels = useRef>(null); const scrollIndex = useRef(0); const horizontalZoom = useRef<[number, number]>([0, 100]); + const verticalZoom = useRef<[number, number]>([0, 100]); const [maxColumns, setMaxColumns] = useState(15); + const { width: contentViewWidth } = useContext( ContentViewDimensionsContext ); + + const [defineCustomRanges, setDefineCustomRanges] = + useState(false); const { logType } = useParams(); const isTimeLog = logType === RouterLogType.TIME; const extraWidth = getExtraWidth(data, columns, dateTimeFormat, isTimeLog); @@ -89,15 +108,53 @@ export const CurveValuesPlot = React.memo( useState({ visible: false } as ControlledTooltipProps); + + const minMaxValuesCalculation = ( + myColumns: ExportableContentTableColumn[] + ) => + myColumns + .map((col) => col.columnOf.mnemonic) + .map((curve) => { + const curveData = props.data + .map((obj) => obj[curve]) + .filter(Number.isFinite); + return { + curve: curve, + minValue: + curveData.length == 0 + ? null + : curveData.reduce((min, v) => (min <= v ? min : v), Infinity), + maxValue: + curveData.length == 0 + ? null + : curveData.reduce((max, v) => (max >= v ? max : v), -Infinity) + }; + }) + .slice(1); + + const [ranges, setRanges] = useState( + minMaxValuesCalculation(columns) + ); + const transformedData = useMemo( () => transformCurveData( data, columns, outliersThresholdLevel, - !autoRefresh && removeOutliers + !autoRefresh && removeOutliers, + ranges, + useCustomRanges ), - [data, columns, outliersThresholdLevel, removeOutliers, autoRefresh] + [ + data, + columns, + outliersThresholdLevel, + removeOutliers, + autoRefresh, + ranges, + useCustomRanges + ] ); useEffect(() => { @@ -125,7 +182,8 @@ export const CurveValuesPlot = React.memo( horizontalZoom.current, verticalZoom.current, isTimeLog, - enableScatter + enableScatter, + refreshGraph ); const onMouseOver = (e: any) => { @@ -190,6 +248,10 @@ export const CurveValuesPlot = React.memo( }; }; + const openCustomRanges = () => { + setDefineCustomRanges(true); + }; + const onLegendScroll = (params: { scrollDataIndex: number }) => { scrollIndex.current = params.scrollDataIndex; }; @@ -217,6 +279,15 @@ export const CurveValuesPlot = React.memo( mouseout: onMouseOut }; + const onChange = (curveRanges: CustomCurveRange[]) => { + setRanges(curveRanges); + setRefreshGraph(!refreshGraph); + }; + + const onClose = () => { + setDefineCustomRanges(false); + }; + return (
@@ -268,6 +339,39 @@ export const CurveValuesPlot = React.memo( )} + setUseCustomRanges(!useCustomRanges)} + size={theme === UserTheme.Compact ? "small" : "default"} + /> + + Show Custom Ranges + + {useCustomRanges && ( + + )} + {defineCustomRanges ? ( + + + + ) : null} )} @@ -332,13 +436,15 @@ const getChartOption = ( horizontalZoom: [number, number], verticalZoom: [number, number], isTimeLog: boolean, - enableScatter: boolean + enableScatter: boolean, + _refreshGraph: boolean ) => { + _refreshGraph = true; const VALUE_OFFSET_FROM_COLUMN = 0.01; const AUTO_REFRESH_SIZE = 300; const LABEL_MAXIMUM_LENGHT = 13; const LABEL_NUMBER_MAX_LENGTH = 9; - if (autoRefresh) data = data.slice(-AUTO_REFRESH_SIZE); // Slice to avoid lag while streaming + if (autoRefresh && _refreshGraph) data = data.slice(-AUTO_REFRESH_SIZE); // Slice to avoid lag while streaming const indexCurve = columns[0].columnOf.mnemonic; const indexUnit = columns[0].columnOf.unit; const dataColumns = columns.filter((col) => col.property != indexCurve); diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/SettingCustomRanges.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/SettingCustomRanges.tsx new file mode 100644 index 000000000..27c1ea570 --- /dev/null +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/SettingCustomRanges.tsx @@ -0,0 +1,143 @@ +import { + Button, + CellProps, + Divider, + EdsProvider, + Table, + TextField +} from "@equinor/eds-core-react"; +import { useOperationState } from "hooks/useOperationState"; +import { ChangeEvent, useState } from "react"; +import styled from "styled-components"; +import { Colors } from "styles/Colors"; +import { CustomCurveRange } from "./CurveValuesPlot"; + +export const SettingCustomRanges = (props: { + minMaxValuesCalculation: CustomCurveRange[]; + onChange: (curveRanges: CustomCurveRange[]) => void; + onClose: () => void; +}): React.ReactElement => { + const { + operationState: { colors } + } = useOperationState(); + + const [ranges, setRanges] = useState( + props.minMaxValuesCalculation + ); + + const close = () => { + props.onClose(); + }; + + return ( + + + + + Close custom ranges definition + + + + + + + + Curve + + Min. value + + + Max. value + + + + + {(ranges ?? []).map((customRange: CustomCurveRange) => ( + + + {customRange.curve} + + + ) => { + const range = ranges.find( + (x) => x.curve === customRange.curve + ); + range.minValue = Number(e.target.value); + setRanges(ranges); + props.onChange(ranges); + }} + /> + + + ) => { + const range = ranges.find( + (x) => x.curve === customRange.curve + ); + range.maxValue = Number(e.target.value); + setRanges(ranges); + props.onChange(ranges); + }} + /> + + + ))} + +
+
+
+
+ ); +}; + +export const StyledTableCell = styled(Table.Cell)<{ colors: Colors }>` + background-color: ${(props) => + props.colors.interactive.tableHeaderFillResting}; + color: ${(props) => props.colors.text.staticIconsDefault}; +`; + +const Container = styled.div<{ colors: Colors }>` + display: flex; + flex-direction: column; + gap: 0.5em; + padding: 0.5em; + user-select: none; + box-shadow: 1px 4px 5px 0px rgba(0, 0, 0, 0.3); + background: ${(props) => props.colors.ui.backgroundLight}; +`; + +const InnerContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledTextField = styled(TextField)<{ colors: Colors }>` + label { + color: red; + } + + div { + background-color: ${(props) => props.colors.ui.backgroundLight}; + } +`; + +const CloseButton = styled(Button)` + width: 300px; +`; + +const StyledTableHeadCell = styled(Table.Cell)<{ colors: Colors } & CellProps>` + { + background-color: ${(props) => + props.colors.interactive.tableHeaderFillResting}; + color: ${(props) => props.colors.text.staticIconsDefault}; + } +`;