diff --git a/package.json b/package.json index 4701c3e..27e7bcf 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/react": "^18.3.5", "@types/react-dom": "18.3.0", "axios": "^1.7.4", + "echarts": "^5.5.1", "env-cmd": "^10.1.0", "lodash": "^4.17.21", "msal-react-tester": "^0.3.1", diff --git a/src/api/generated/index.ts b/src/api/generated/index.ts index 65b977b..b2517b1 100644 --- a/src/api/generated/index.ts +++ b/src/api/generated/index.ts @@ -111,6 +111,7 @@ export type { OutcropDto } from './models/OutcropDto'; export type { ParameterList } from './models/ParameterList'; export type { PatchAnalogueModelCommandResponse } from './models/PatchAnalogueModelCommandResponse'; export type { PatchAnalogueModelDto } from './models/PatchAnalogueModelDto'; +export type { PercentilesDto } from './models/PercentilesDto'; export type { PrepareChunkedUploadCommandResponse } from './models/PrepareChunkedUploadCommandResponse'; export type { PrepareChunkedUploadDto } from './models/PrepareChunkedUploadDto'; export type { ProblemDetails } from './models/ProblemDetails'; diff --git a/src/api/generated/models/GetVariogramResultsDto.ts b/src/api/generated/models/GetVariogramResultsDto.ts index 62d33e7..9504462 100644 --- a/src/api/generated/models/GetVariogramResultsDto.ts +++ b/src/api/generated/models/GetVariogramResultsDto.ts @@ -17,6 +17,9 @@ export type GetVariogramResultsDto = { rvertical: number; sigma: number; quality: number; + qualityX: number; + qualityY: number; + qualityZ: number; family: string; archelFilter?: string | null; indicator?: string | null; diff --git a/src/api/generated/models/ObjectEstimationResultDto.ts b/src/api/generated/models/ObjectEstimationResultDto.ts index 380cb37..fb3c7b9 100644 --- a/src/api/generated/models/ObjectEstimationResultDto.ts +++ b/src/api/generated/models/ObjectEstimationResultDto.ts @@ -3,9 +3,16 @@ /* tslint:disable */ /* eslint-disable */ +import type { PercentilesDto } from './PercentilesDto'; + export type ObjectEstimationResultDto = { mean: number; sd: number; count: number; + coefficentOfVariation: number; + meanEstimateStandardError: number; + min: number; + max: number; + percentiles: PercentilesDto; }; diff --git a/src/api/generated/models/ObjectHeightDto.ts b/src/api/generated/models/ObjectHeightDto.ts index 772f3a4..918b11e 100644 --- a/src/api/generated/models/ObjectHeightDto.ts +++ b/src/api/generated/models/ObjectHeightDto.ts @@ -3,10 +3,17 @@ /* tslint:disable */ /* eslint-disable */ +import type { PercentilesDto } from './PercentilesDto'; + export type ObjectHeightDto = { mean: number; sd: number; count: number; + coefficentOfVariation: number; + meanEstimateStandardError: number; + min: number; + max: number; + percentiles: PercentilesDto; modeSd: number; modeMean: number; }; diff --git a/src/api/generated/models/PercentilesDto.ts b/src/api/generated/models/PercentilesDto.ts new file mode 100644 index 0000000..dfdce7c --- /dev/null +++ b/src/api/generated/models/PercentilesDto.ts @@ -0,0 +1,17 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type PercentilesDto = { + p10: number; + p20: number; + p30: number; + p40: number; + p50: number; + p60: number; + p70: number; + p80: number; + p90: number; +}; + diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.styled.ts b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.styled.ts index 2ab8758..53d5ea0 100644 --- a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.styled.ts +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.styled.ts @@ -3,13 +3,8 @@ import { spacings } from '../../../../../tokens/spacings'; import { theme } from '../../../../../tokens/theme'; export const Wrapper = styled.div` - display: flex; - flex-direction: column; - border: solid 0.5px ${theme.light.ui.background.medium}; -`; - -export const InnerWrapper = styled.div` display: flex; flex-direction: row; column-gap: ${spacings.LARGE}; + border: solid 0.5px ${theme.light.ui.background.medium}; `; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.tsx b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.tsx index 9a1db11..1874b17 100644 --- a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.tsx +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ChannelResult.tsx @@ -28,17 +28,15 @@ export const ChannelResult = ({ return ( - - - - + + ); }; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/Echarts/ReactECharts.tsx b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/Echarts/ReactECharts.tsx new file mode 100644 index 0000000..9573fc7 --- /dev/null +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/Echarts/ReactECharts.tsx @@ -0,0 +1,81 @@ +/** + * Component 'Borrowed' from https://dev.to/manufac/using-apache-echarts-with-react-and-typescript-353k + * Echarts wrapper to make Echarts more usable with React lifecycle. + * + * This component only ensures that the chart is initialized with provided options, resizes the chart on container resize + * and cleans up the chart when the component is removed from DOM. It also interacts with echarts + * built in chart loading functionally such that this does not need to be done elsewhere. + * + * **Note: This base is configured such that the chart's width and height will be set to take the entire width and height + * of its parent element. This behaviour can be overridden using the {@see style} prop.** + */ + +import { + ECharts, + EChartsOption, + getInstanceByDom, + init, + SetOptionOpts, +} from 'echarts'; +import { CSSProperties, useEffect, useRef } from 'react'; + +export interface ReactEChartsProps { + option: EChartsOption; + style?: CSSProperties; + settings?: SetOptionOpts; + loading?: boolean; + theme?: 'light' | 'dark'; +} + +export function ReactECharts({ + option, + style, + settings, + loading, + theme, +}: ReactEChartsProps): JSX.Element { + const chartRef = useRef(null); + + useEffect(() => { + // Initialize chart + let chart: ECharts | undefined; + + if (chartRef.current !== null) { + chart = init(chartRef.current, theme); + } + + // Add chart resize listener + // ResizeObserver is leading to a bit janky UX + function resizeChart() { + chart?.resize(); + } + window.addEventListener('resize', resizeChart); + + // Return cleanup function + return () => { + chart?.dispose(); + window.removeEventListener('resize', resizeChart); + }; + }, [theme]); + + useEffect(() => { + // Update chart + if (chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current); + chart?.setOption(option, settings); + } + }, [option, settings, theme]); // Whenever theme changes we need to add option and setting due to it being deleted in cleanup function + + useEffect(() => { + // Update chart + if (chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + loading === true ? chart?.showLoading() : chart?.hideLoading(); + } + }, [loading, theme]); + + return ( +
+ ); +} diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/GraphPlot/GraphPlot.tsx b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/GraphPlot/GraphPlot.tsx new file mode 100644 index 0000000..5f7d982 --- /dev/null +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/GraphPlot/GraphPlot.tsx @@ -0,0 +1,59 @@ +import { PercentilesDto } from '../../../../../../api/generated'; +import { ReactECharts, ReactEChartsProps } from '../Echarts/ReactECharts'; + +export interface ExtendedPrecetile extends PercentilesDto { + min: number; + max: number; +} +export const GraphPlot = ({ + data, + mode, +}: { + data: ExtendedPrecetile | undefined; + mode: string; +}) => { + if (data === undefined) return <>Loading ... ; + + const option: ReactEChartsProps['option'] = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + grid: { + left: '10%', + right: '10%', + top: '10%', + bottom: '20%', + }, + xAxis: { + type: 'value', + name: mode + ' (m)', + nameLocation: 'middle', + nameGap: 30, + }, + yAxis: { + type: 'category', + data: Object.keys(data), + name: 'Precentile', + nameLocation: 'middle', + nameGap: 40, + }, + series: [ + { + type: 'line', + data: Object.values(data), + }, + ], + }; + + return ( +
+ +
+ ); +}; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.styled.ts b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.styled.ts index f8c8b19..5042de6 100644 --- a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.styled.ts +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.styled.ts @@ -3,32 +3,52 @@ import { spacings } from '../../../../../../tokens/spacings'; export const Wrapper = styled.div` display: flex; - flex-direction: row; - align-items: center; - padding-left: ${spacings.LARGE}; - min-width: 320px; + flex-direction: column; + align-items: flex-start; + padding: ${spacings.MEDIUM_SMALL} 0 ${spacings.MEDIUM_SMALL} ${spacings.LARGE}; + row-gap: ${spacings.MEDIUM_SMALL}; `; -export const Info = styled.div` +export const ResultHeader = styled.div` display: flex; - flex-direction: column; - row-gap: ${spacings.MEDIUM}; - - width: 150px; + flex-direction: row; + justify-content: space-between; + width: 100%; `; -export const Coordinates = styled.div` +export const MetadataWrapperDiv = styled.div` display: flex; - flex-direction: column; - row-gap: ${spacings.MEDIUM}; + flex-direction: row; + align-items: center; +`; + +export const MetadataDiv = styled.div` + align-items: start; + padding-right: ${spacings.MEDIUM}; + > label { + margin: 0; + } `; -export const CoordinateRow = styled.div` +export const CoordinateDiv = styled.div` display: flex; flex-direction: row; - column-gap: ${spacings.MEDIUM}; `; export const RowElement = styled.div` white-space: nowrap; + > label { + margin: 0; + } +`; + +export const Divider = styled.div` + width: 100%; +`; + +export const VerticalDivider = styled.div` + width: 0px; + height: 100%; + margin: 0 ${spacings.MEDIUM}; + border: 0.5px solid #e0e0e0; `; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.tsx b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.tsx index fe1e8e1..c4a839c 100644 --- a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.tsx +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultArea.tsx @@ -1,8 +1,18 @@ /* eslint-disable max-lines-per-function */ -import { Label, Typography } from '@equinor/eds-core-react'; -import * as Styled from './ResultArea.styled'; -import { GetObjectResultsDto } from '../../../../../../api/generated/models/GetObjectResultsDto'; +import { + Button, + Divider, + Icon, + Label, + Typography, +} from '@equinor/eds-core-react'; +import { bar_chart as barChart } from '@equinor/eds-icons'; +import { useState } from 'react'; +import { GetObjectResultsDto } from '../../../../../../api/generated/models/GetObjectResultsDto'; +import { ResultPlotDialog } from '../ResultPlotDialog/ResultPlotDialog'; +import * as Styled from './ResultArea.styled'; +import { ResultCaseMetadata } from './ResultCaseMetadata/ResultCaseMetadata'; export const ResultArea = ({ computeMethod, modelArea, @@ -12,6 +22,11 @@ export const ResultArea = ({ modelArea: string; data: GetObjectResultsDto; }) => { + const [open, setOpen] = useState(false); + + const toggleOpen = () => { + setOpen(!open); + }; const xCoordinate = data.box?.filter((b) => b.m === 0)[0]; const yCoordinate = data.box?.filter((b) => b.m === 1)[0]; @@ -34,47 +49,64 @@ export const ResultArea = ({ }; return ( - - -
- {computeMethod} - {modelArea} -
-
- - {area() ? area() : '-'} -
-
- - + <> + + + + + + + + + + + + + {area() ? area() : '-'} + + - + {modelArea === 'Whole model' ? '-' : xCoordinate?.x + ' m'} + - + {modelArea === 'Whole model' ? '-' : xLength() + ' m'} - - + - + {modelArea === 'Whole model' ? '-' : yCoordinate?.x + ' m'} + - + {modelArea === 'Whole model' ? '-' : yLength() + ' m'} - - -
+ + + + ); }; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultCaseMetadata/ResultCaseMetadata.styled.ts b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultCaseMetadata/ResultCaseMetadata.styled.ts new file mode 100644 index 0000000..0071b90 --- /dev/null +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultCaseMetadata/ResultCaseMetadata.styled.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; +import { spacings } from '../../../../../../../tokens/spacings'; + +export const MetadataWrapperDiv = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +export const MetadataDiv = styled.div` + align-items: start; + padding-right: ${spacings.MEDIUM}; + > label { + margin: 0; + } +`; + +export const VerticalDivider = styled.div` + width: 0px; + height: 100%; + margin: 0 ${spacings.MEDIUM}; + border: 0.5px solid #e0e0e0; +`; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultCaseMetadata/ResultCaseMetadata.tsx b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultCaseMetadata/ResultCaseMetadata.tsx new file mode 100644 index 0000000..8effc0b --- /dev/null +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultArea/ResultCaseMetadata/ResultCaseMetadata.tsx @@ -0,0 +1,24 @@ +import { Label, Typography } from '@equinor/eds-core-react'; +import * as Styled from './ResultCaseMetadata.styled'; + +export const ResultCaseMetadata = ({ + computeMethod, + modelArea, +}: { + computeMethod?: string; + modelArea: string; +}) => { + return ( + + + + {computeMethod} + + + + + {modelArea} + + + ); +}; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultPlotDialog/ResultPlotDialog.styled.ts b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultPlotDialog/ResultPlotDialog.styled.ts new file mode 100644 index 0000000..5c7dab2 --- /dev/null +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultPlotDialog/ResultPlotDialog.styled.ts @@ -0,0 +1,33 @@ +import { Dialog } from '@equinor/eds-core-react'; +import styled from 'styled-components'; +import { spacings } from '../../../../../../tokens/spacings'; + +export const Content = styled(Dialog.Content)` + display: flex; + flex-direction: column; + row-gap: ${spacings.MEDIUM}; +`; + +export const GraphDialog = styled(Dialog)` + min-height: 400px; + min-width: 700px; +`; + +export const RadioGroup = styled.div` + display: flex; + flex-direction: row; + align-items: center; + column-gap: ${spacings.X_LARGE}; + padding-left: ${spacings.SMALL}; +`; + +export const RadioButton = styled.div` + display: flex; + flex-direction: row; + align-items: center; + + > span { + padding-right: 0; + padding-left: 0; + } +`; diff --git a/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultPlotDialog/ResultPlotDialog.tsx b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultPlotDialog/ResultPlotDialog.tsx new file mode 100644 index 0000000..b8acccc --- /dev/null +++ b/src/features/Results/CaseResult/CaseResultView/ObjectCaseResult/ResultPlotDialog/ResultPlotDialog.tsx @@ -0,0 +1,131 @@ +/* eslint-disable max-lines-per-function */ +import { Button, Dialog, Label, Radio } from '@equinor/eds-core-react'; +import { ChangeEvent, useEffect, useState } from 'react'; +import { GetObjectResultsDto } from '../../../../../../api/generated'; +import { ExtendedPrecetile, GraphPlot } from '../GraphPlot/GraphPlot'; +import { ResultCaseMetadata } from '../ResultArea/ResultCaseMetadata/ResultCaseMetadata'; +import * as Styled from './ResultPlotDialog.styled'; + +export const ResultPlotDialog = ({ + open, + computeMethod, + modelArea, + data, + toggleOpen, +}: { + open: boolean; + computeMethod?: string; + modelArea: string; + data: GetObjectResultsDto; + toggleOpen: () => void; +}) => { + const [selectedValue, setSelectedValue] = useState('width'); + const onChange = (event: ChangeEvent) => { + setSelectedValue(event.target.value); + }; + const [precentilesMinMax, setPrecentilesMinMax] = + useState(); + + useEffect(() => { + if (selectedValue === 'height') { + setPrecentilesMinMax({ + min: data.height.min, + p10: data.height.percentiles.p10, + p20: data.height.percentiles.p20, + p30: data.height.percentiles.p30, + p40: data.height.percentiles.p40, + p50: data.height.percentiles.p50, + p60: data.height.percentiles.p60, + p70: data.height.percentiles.p70, + p80: data.height.percentiles.p80, + p90: data.height.percentiles.p90, + max: data.height.max, + }); + } else if (selectedValue === 'width') { + setPrecentilesMinMax({ + min: data.width.min, + p10: data.width.percentiles.p10, + p20: data.width.percentiles.p20, + p30: data.width.percentiles.p30, + p40: data.width.percentiles.p40, + p50: data.width.percentiles.p50, + p60: data.width.percentiles.p60, + p70: data.width.percentiles.p70, + p80: data.width.percentiles.p80, + p90: data.width.percentiles.p90, + max: data.width.max, + }); + } else if (selectedValue === 'length') { + setPrecentilesMinMax({ + min: data.length.min, + p10: data.length.percentiles.p10, + p20: data.length.percentiles.p20, + p30: data.length.percentiles.p30, + p40: data.length.percentiles.p40, + p50: data.length.percentiles.p50, + p60: data.length.percentiles.p60, + p70: data.length.percentiles.p70, + p80: data.length.percentiles.p80, + p90: data.length.percentiles.p90, + max: data.length.max, + }); + } + }, [selectedValue, data]); + + return ( + <> + + + + + + +
+ + + + + + + + + + + + + +
+
+ + + +
+ + ); +}; diff --git a/yarn.lock b/yarn.lock index 09261a4..7c47d0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5347,6 +5347,14 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +echarts@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c" + integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA== + dependencies: + tslib "2.3.0" + zrender "5.6.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -10688,6 +10696,11 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tslib@2.6.2, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" @@ -11531,3 +11544,10 @@ zod@^3.23.8: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + +zrender@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.0.tgz#01325b0bb38332dd5e87a8dbee7336cafc0f4a5b" + integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg== + dependencies: + tslib "2.3.0"