Skip to content

Commit

Permalink
Refactor and optimise creation of DataCurve geometries
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Mar 24, 2023
1 parent 398fa5e commit 4c8fa47
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 93 deletions.
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

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);

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 dataPosition = dataGeometry.attributes.position;
const errorPositions = errorGeometries && {
caps: errorGeometries.caps.attributes.position,
bars: errorGeometries.bars.attributes.position,
};

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);
}

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) {
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);
}

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;
} {
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;

return {
topCap: showTopCap ? [x, yCapTop] : undefined,
bottomCap: showBottomCap ? [x, yCapBottom] : undefined,
bar: showBar ? [x, yBarTop, x, yBarBottom] : undefined,
};
};
}

0 comments on commit 4c8fa47

Please sign in to comment.