Transforms#673
Conversation
| "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", |
There was a problem hiding this comment.
we don't touch shadcn components. use the Button from button-enhanced instead.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| let xMean: f32 = xSum / f32(dimLength); | ||
| let yMean: f32 = ySum / f32(dimLength); |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| const [xSlicer, ySlicer, zSlicer] = slicers | |
| const [zSlicer, ySlicer, xSlicer] = slicers |
| fragmentShader: useFragOpt ? fragOpt : fragmentShader, | ||
| transparent: true, | ||
| blending: THREE.NormalBlending, | ||
| depthWrite: false, |
There was a problem hiding this comment.
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.
| 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]) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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.
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