Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and optimise creation of DataCurve geometries #1393

Merged
merged 3 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified cypress/snapshots/app.cy.ts/auxspectrum.snap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified cypress/snapshots/app.cy.ts/nxspectrum.snap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 108 additions & 11 deletions packages/lib/src/vis/line/DataCurve.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import type { NumArray } from '@h5web/shared';
import { extend, useThree } from '@react-three/fiber';
import type { Object3DNode, ThreeEvent } from '@react-three/fiber';
import { useCallback, useLayoutEffect, useState } from 'react';
import { extend, useThree } from '@react-three/fiber';
import { useCallback, useLayoutEffect, useMemo } from 'react';
import { BufferGeometry, Line } from 'three';

import { useVisCanvasContext } from '../shared/VisCanvasProvider';
import { createBufferAttr } from '../utils';
import ErrorBars from './ErrorBars';
import GlyphMaterial from './GlyphMaterial';
import { useCanvasPoints } from './hooks';
import { useValueToErrorPositions, useValueToPosition } from './hooks';
import { CurveType, GlyphType } from './models';

/* Render points with NaN/Infinity coordinates (i.e. values <= 0 in log)
* at origin to avoid Three warning, and outside of camera's field of view
* to hide them and any segments connecting them. */
const CAMERA_FAR = 1000; // R3F's default
axelboc marked this conversation as resolved.
Show resolved Hide resolved

extend({ Line_: Line });

// https://github.com/pmndrs/react-three-fiber/issues/1152
Expand Down Expand Up @@ -54,15 +61,106 @@ function DataCurve(props: Props) {
ignoreValue,
} = props;

const [dataGeometry] = useState(() => new BufferGeometry());
const points = useCanvasPoints(abscissas, ordinates, errors, ignoreValue);
const { length } = ordinates;
const hasErrors = !!errors;
const { abscissaScale, ordinateScale } = useVisCanvasContext();
const invalidate = useThree((state) => state.invalidate);

const valueToPosition = useValueToPosition(
abscissas,
abscissaScale,
ordinateScale,
ignoreValue
);

const valueToErrorPositions = useValueToErrorPositions(errors, ordinateScale);
axelboc marked this conversation as resolved.
Show resolved Hide resolved

const dataGeometry = useMemo(() => {
const geometry = new BufferGeometry();
geometry.setAttribute('position', createBufferAttr(length, 3));
return geometry;
}, [length]);

const errorGeometries = useMemo(() => {
if (!hasErrors) {
return undefined;
}

const geometries = {
caps: new BufferGeometry(),
bars: new BufferGeometry(),
};

geometries.caps.setAttribute('position', createBufferAttr(length * 2, 3));
geometries.bars.setAttribute('position', createBufferAttr(length * 2, 3));

return geometries;
}, [hasErrors, length]);

// eslint-disable-next-line sonarjs/cognitive-complexity
useLayoutEffect(() => {
dataGeometry.setFromPoints(points.data);
const { position: dataPosition } = dataGeometry.attributes;
const errorPositions = errorGeometries && {
caps: errorGeometries.caps.attributes.position,
bars: errorGeometries.bars.attributes.position,
};
axelboc marked this conversation as resolved.
Show resolved Hide resolved

ordinates.forEach((value, index) => {
const pos = valueToPosition(value, index);

if (pos) {
dataPosition.setXYZ(index, pos[0], pos[1], 0);
} else {
dataPosition.setXYZ(index, 0, 0, CAMERA_FAR);
}

if (!errorPositions) {
return;
}

const { topCap, bottomCap, bar } = valueToErrorPositions(
value,
index,
pos
);

if (topCap) {
errorPositions.caps.setXYZ(index * 2 + 1, topCap[0], topCap[1], 0);
} else {
errorPositions.caps.setXYZ(index * 2 + 1, 0, 0, CAMERA_FAR);
axelboc marked this conversation as resolved.
Show resolved Hide resolved
}

if (bottomCap) {
errorPositions.caps.setXYZ(index * 2, bottomCap[0], bottomCap[1], 0);
} else {
errorPositions.caps.setXYZ(index * 2, 0, 0, CAMERA_FAR);
}

if (bar) {
errorPositions.bars.setXYZ(index * 2, bar[0], bar[1], 0);
errorPositions.bars.setXYZ(index * 2 + 1, bar[2], bar[3], 0);
} else {
errorPositions.bars.setXYZ(index * 2, 0, 0, CAMERA_FAR);
errorPositions.bars.setXYZ(index * 2 + 1, 0, 0, CAMERA_FAR);
}
});

dataGeometry.computeBoundingSphere();
dataPosition.needsUpdate = true;
if (errorPositions) {
errorPositions.caps.needsUpdate = true;
errorPositions.bars.needsUpdate = true;
}

invalidate();
}, [dataGeometry, invalidate, points.data]);
}, [
ordinates,
dataGeometry,
errorGeometries,
valueToErrorPositions,
valueToPosition,
invalidate,
]);

const handleClick = useCallback(
(evt: ThreeEvent<MouseEvent>) => {
Expand Down Expand Up @@ -114,12 +212,11 @@ function DataCurve(props: Props) {
>
<GlyphMaterial glyphType={glyphType} color={color} size={glyphSize} />
</points>
{showErrors && errors && (
{errorGeometries && (
<ErrorBars
barsSegments={points.bars}
capsPoints={points.caps}
geometries={errorGeometries}
color={color}
visible={visible}
visible={visible && showErrors}
/>
)}
</>
Expand Down
35 changes: 11 additions & 24 deletions packages/lib/src/vis/line/ErrorBars.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
import { useThree } from '@react-three/fiber';
import { useLayoutEffect, useRef } from 'react';
import type { BufferGeometry, Vector3 } from 'three';
import type { BufferGeometry } from 'three';

import GlyphMaterial from './GlyphMaterial';
import { GlyphType } from './models';

export interface ErrorGeometries {
bars: BufferGeometry;
caps: BufferGeometry;
}

interface Props {
capsPoints: Vector3[];
barsSegments: Vector3[];
geometries: ErrorGeometries;
color: string;
visible?: boolean;
}

function ErrorBars(props: Props) {
const { barsSegments, capsPoints, color, visible } = props;
const invalidate = useThree((state) => state.invalidate);

const barsGeometry = useRef<BufferGeometry>(null);
useLayoutEffect(() => {
barsGeometry.current?.setFromPoints(barsSegments);
invalidate();
}, [barsGeometry, barsSegments, invalidate]);

const capsGeometry = useRef<BufferGeometry>(null);
useLayoutEffect(() => {
capsGeometry.current?.setFromPoints(capsPoints);
invalidate();
}, [capsGeometry, capsPoints, invalidate]);
const { geometries, color, visible } = props;

return (
<>
<lineSegments visible={visible}>
<lineBasicMaterial color={color} linewidth={2} />
<bufferGeometry ref={barsGeometry} />
<lineSegments geometry={geometries.bars} visible={visible}>
<lineBasicMaterial color={color} />
</lineSegments>
<points visible={visible}>
<points geometry={geometries.caps} visible={visible}>
<GlyphMaterial glyphType={GlyphType.Cap} color={color} size={9} />
<bufferGeometry ref={capsGeometry} />
</points>
</>
);
Expand Down
62 changes: 4 additions & 58 deletions packages/lib/src/vis/line/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,6 @@
import type { NumArray } from '@h5web/shared';
import { useMemo } from 'react';
import { Vector3 } from 'three';
import { createMemo } from '@h5web/shared';

import { useVisCanvasContext } from '../shared/VisCanvasProvider';
import { getValueToErrorPositions, getValueToPosition } from './utils';

const CAMERA_FAR = 1000; // R3F's default

export function useCanvasPoints(
abscissas: NumArray,
ordinates: NumArray,
errors?: NumArray,
ignoreValue?: (val: number) => boolean
) {
const { abscissaScale, ordinateScale } = useVisCanvasContext();

return useMemo(() => {
const dataPoints: Vector3[] = [];
const errorBarSegments: Vector3[] = [];
const errorCapPoints: Vector3[] = [];

ordinates.forEach((val, index) => {
const x = abscissaScale(abscissas[index]);
const y = ordinateScale(val);

const hasFiniteCoords =
!ignoreValue?.(val) && Number.isFinite(x) && Number.isFinite(y);
const dataVector = hasFiniteCoords
? new Vector3(x, y, 0)
: /* Render points with NaN/Infinity coordinates (i.e. values <= 0 in log)
* at origin to avoid Three warning, and outside of camera's field of view
* to hide them and any segments connecting them. */
new Vector3(0, 0, CAMERA_FAR);

dataPoints.push(dataVector);

const error = errors?.[index];
if (!error || !hasFiniteCoords) {
axelboc marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const yErrBottom = ordinateScale(val - error);
const yErrTop = ordinateScale(val + error);

if (Number.isFinite(yErrBottom)) {
const errBottomVector = new Vector3(x, yErrBottom, 0);
errorBarSegments.push(errBottomVector, dataVector);
errorCapPoints.push(errBottomVector);
axelboc marked this conversation as resolved.
Show resolved Hide resolved
}

if (Number.isFinite(yErrTop)) {
const errTopVector = new Vector3(x, yErrTop, 0);
errorBarSegments.push(dataVector, errTopVector);
errorCapPoints.push(errTopVector);
}
});

return { data: dataPoints, bars: errorBarSegments, caps: errorCapPoints };
}, [abscissaScale, abscissas, errors, ordinateScale, ordinates, ignoreValue]);
}
export const useValueToPosition = createMemo(getValueToPosition);
export const useValueToErrorPositions = createMemo(getValueToErrorPositions);
72 changes: 72 additions & 0 deletions packages/lib/src/vis/line/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { NumArray } from '@h5web/shared';

import type { AxisScale } from '../models';

const NO_ERROR_POSITIONS = {
topCap: undefined,
bottomCap: undefined,
bar: undefined,
};

export function getValueToPosition(
abscissas: NumArray,
abscissaScale: AxisScale,
ordinateScale: AxisScale,
ignoreValue?: (val: number) => boolean
): (value: number, index: number) => [number, number] | undefined {
return (value: number, index: number) => {
const x = abscissaScale(abscissas[index]);
const y = ordinateScale(value);

const isIgnored = ignoreValue ? ignoreValue(value) : false;
const hasFiniteCoords = Number.isFinite(x) && Number.isFinite(y);

return !isIgnored && hasFiniteCoords ? [x, y] : undefined;
};
}

// eslint-disable-next-line sonarjs/cognitive-complexity
export function getValueToErrorPositions(
errors: NumArray | undefined,
ordinateScale: AxisScale
): (
value: number,
index: number,
position: [number, number] | undefined
) => {
topCap: [number, number] | undefined;
bottomCap: [number, number] | undefined;
bar: [number, number, number, number] | undefined;
axelboc marked this conversation as resolved.
Show resolved Hide resolved
} {
if (!errors) {
return () => NO_ERROR_POSITIONS;
}

return (value, index, position) => {
if (!position) {
return NO_ERROR_POSITIONS;
}

const [x, y] = position;
const error = errors[index];
if (error < 0) {
return NO_ERROR_POSITIONS;
}

const yCapTop = ordinateScale(value + error);
const yCapBottom = ordinateScale(value - error);

const showTopCap = Number.isFinite(yCapTop);
const showBottomCap = Number.isFinite(yCapBottom);

const yBarTop = showTopCap ? yCapTop : y;
const yBarBottom = showBottomCap ? yCapBottom : y;
const showBar = showTopCap || showBottomCap;
loichuder marked this conversation as resolved.
Show resolved Hide resolved

return {
topCap: showTopCap ? [x, yCapTop] : undefined,
bottomCap: showBottomCap ? [x, yCapBottom] : undefined,
bar: showBar ? [x, yBarTop, x, yBarBottom] : undefined,
axelboc marked this conversation as resolved.
Show resolved Hide resolved
};
};
}