Skip to content

Transforms#673

Closed
TheJeran wants to merge 21 commits into
mainfrom
jp/new_transform
Closed

Transforms#673
TheJeran wants to merge 21 commits into
mainfrom
jp/new_transform

Conversation

@TheJeran

Copy link
Copy Markdown
Collaborator

This PR allows the user to rotate and mirror their plots. This is useful for custom datasets with no meta or coordinate data. Also useful if a user wants to visualize sphere or flat plots at different axis' angles.

image
Screencast.from.2026-06-26.15-13-46.webm

Weak-points

The primary difficulty in this was updating the axis' logic to update values with the new axis'. I prioritized the volume version of the axis. The new logic breaks the scaling logic for point-cloud and while the flat map version works, rotating along the X-axis breaks the logic of which axis are used for the values.

These issues with the axis lines stem from how complicated the component has become so I think the next overhaul I do will be to the axislines #445

"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
"border bg-background shadow-xs hover:bg-accent cursor-pointer hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't touch shadcn components. use the Button from button-enhanced instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also deleted a lot of stuff. Like all the icechunk stuff. I dunno why that happened. Lemme redo this at another date and figure out what just happened. Didn't want it to touch so many files

@TheJeran TheJeran closed this Jun 26, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces plot transformation capabilities (rotation and mirroring) across various plot types, updates state management with a new PlotTransformStore, and simplifies dataset loading by replacing the old catalogs with a curated dataset component. Feedback on these changes highlights a critical bug in WGSLShaders.ts where mean calculations fail to adjust the divisor when NaNs are present, and a misalignment of axes slicers in AxisLines.tsx. Additionally, the reviewer suggests verifying the removal of depthWrite: false in DataCube.tsx to prevent rendering artifacts, refactoring the complex transformation logic in Plot.tsx into helper functions, and moving the local mod function in AdjustPlot.tsx to a shared utility file.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +554 to +555
let xMean: f32 = xSum / f32(dimLength);
let yMean: f32 = ySum / f32(dimLength);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The calculation for xMean and yMean appears to be incorrect when NaN values are present in the data. The loop correctly skips NaN values when calculating xSum and ySum, but the divisor is dimLength, which doesn't account for the skipped NaNs. This will result in an incorrect (underestimated) mean.

This issue is also present in the covariance and linearRegression shaders.

To fix this, you should reintroduce a counter for valid (non-NaN) data points and use that for the division.

Suggested change
let xMean: f32 = xSum / f32(dimLength);
let yMean: f32 = ySum / f32(dimLength);
var nanCount: u32 = 0;
// ... in the loop ...
if (isNaN(xi) || isNaN(yi)){
nanCount++;
continue;
}
// ... after the loop ...
let validCount = dimLength - nanCount;
if (validCount == 0u) {
// Handle case with all NaNs
outputData[outputIndex] = ${precision}(0.0);
return;
}
let xMean: f32 = xSum / f32(validCount);
let yMean: f32 = ySum / f32(validCount);

const shapeLength = axisDimArrays.length
const dimSlices = useMemo(()=> {
const slicers = permuteArr([zSlice, ySlice, xSlice], permute)
const [xSlicer, ySlicer, zSlicer] = slicers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The destructuring of the slicers array is confusing and likely incorrect. The permuteArr function is applied to [zSlice, ySlice, xSlice], but the result is destructured into [xSlicer, ySlicer, zSlicer]. This seems to misalign the slicers with their corresponding axes.

For clarity and correctness, the destructuring should match the order of the original array to avoid confusion. For example, if permute is [0, 2, 1], slicers becomes [zSlice, xSlice, ySlice]. Your current code would assign xSlicer = zSlice, ySlicer = xSlice, and zSlicer = ySlice. Then zSlicer (ySlice) is used for the Z-axis, which is probably not what you intend.

I recommend changing the destructuring to maintain the z, y, x order.

Suggested change
const [xSlicer, ySlicer, zSlicer] = slicers
const [zSlicer, ySlicer, xSlicer] = slicers

fragmentShader: useFragOpt ? fragOpt : fragmentShader,
transparent: true,
blending: THREE.NormalBlending,
depthWrite: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The depthWrite: false property has been removed from the ShaderMaterial. This property is often crucial for correctly rendering multiple transparent objects, as it prevents objects from occluding others behind them in the depth buffer.

Was its removal intentional? If so, a comment explaining the reasoning would be helpful for future maintenance. If not, I recommend adding it back to prevent potential rendering artifacts with the transparent volume.

Comment on lines +196 to +258
useEffect(()=>{
let axisMapping = [0, 1, 2];
let axisReversed = [false, false, false];
const origSlices = [zSlice, ySlice, xSlice]

if (rotateZ === 1){
// 90: X=-Y, Y=X
axisMapping = [axisMapping[0], axisMapping[2], axisMapping[1]];
axisReversed = [axisReversed[0], axisReversed[2], !axisReversed[1]];
} else if ( rotateZ === 2){
// 180: X=-X, Y=-Y
axisReversed = [axisReversed[0], !axisReversed[1], !axisReversed[2]];
} else if (rotateZ === 3) {
// 270: X=Y, Y=-X
axisMapping = [axisMapping[0], axisMapping[2], axisMapping[1]];
axisReversed = [axisReversed[0], !axisReversed[2], axisReversed[1]];
}

if (rotateX === 1){
// 90: Y=-Z, Z=Y
axisMapping = [axisMapping[1], axisMapping[0], axisMapping[2]];
axisReversed = [axisReversed[1], !axisReversed[0], axisReversed[2]];
}
else if (rotateX === 2){
// 180: Y=-Y, Z=-Z
axisReversed = [!axisReversed[1], !axisReversed[0], axisReversed[2]];
} else if (rotateX === 3){
// 270: Y=Z, Z=-Y
axisMapping = [axisMapping[1], axisMapping[0], axisMapping[2]];
axisReversed = [!axisReversed[1], axisReversed[0], axisReversed[2]];
}

if (mirrorHorizontal) {
axisReversed[2] = !axisReversed[2];
}
if (mirrorVertical) {
axisReversed[1] = !axisReversed[1];
}

const transformedDimArrays = axisMapping.map((origIdx, newIdx) =>{
const arr = dimArrays[origIdx].slice();
if (axisReversed[newIdx]) arr.reverse()
return arr
})

const transformedDimNames = axisMapping.map(origIdx => dimNames[origIdx])
const transformedDimUnits = axisMapping.map(origIdx => dimUnits[origIdx])
const axisShape = axisMapping.map(origIdx => dataShape[origIdx]/dataShape[dataShape.length-1])
const axisSlices = axisMapping.map(origIdx => origSlices[origIdx])
setAxisDimArrays(transformedDimArrays)
setAxisDimNames(transformedDimNames)
setAxisDimUnits(transformedDimUnits)
setAxisShape(axisShape)
setAxisOrder(axisMapping)
setAxisFlipped(axisReversed)
usePlotStore.setState({
zSlice:axisSlices[0],
ySlice:axisSlices[1],
xSlice:axisSlices[2],
permute:axisMapping
})

},[rotateX, rotateZ, mirrorHorizontal, mirrorVertical, dimArrays, dimNames, dimUnits])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This useEffect hook contains the core transformation logic and is quite complex. The logic for axisMapping and axisReversed is dense and hard to follow without careful inspection.

To improve maintainability and readability, consider refactoring this logic into smaller, pure helper functions with descriptive names and JSDoc comments. For example, you could create a function like calculateAxisPermutation(rotateX, rotateZ, mirrorHorizontal, mirrorVertical) that returns the axisMapping and axisReversed arrays. This would make the effect itself easier to understand and allow the transformation logic to be tested in isolation.

setMirrorVertical(false)
return null;
}
const mod = (a: number, b: number) => ((a % b) + b) % b; // JS doesn't actually have a module operator. So need our own

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mod function is a useful utility for correct modulo operations with negative numbers. Since it's a general-purpose function, it would be better to define it in a shared utility file, such as src/utils/HelperFuncs.ts, rather than locally within the GlobalOptions component. This promotes reusability and makes the codebase cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants