Skip to content

Commit d0b611f

Browse files
authored
Merge pull request #414 from EarthyScience/jp/flat-ts
Timeseries Select in Flat Mode
2 parents 8c585e7 + d3b7876 commit d0b611f

File tree

6 files changed

+167
-62
lines changed

6 files changed

+167
-62
lines changed

src/app/globals.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
--muted: oklch(0.97 0 0);
3333
--muted-foreground: oklch(0.556 0 0);
3434
--accent: oklch(0.97 0 0);
35-
--accent-foreground: oklch(20.463% 0.00002 271.152 / 0.151);
35+
--accent-foreground: oklch(0.205 0 0);
3636
--destructive: oklch(0.577 0.245 27.325);
3737
--border: oklch(0.922 0 0);
3838
--input: oklch(0.922 0 0);
@@ -82,6 +82,7 @@
8282
--glass-border: rgba(255, 255, 255, 0.25);
8383
--glass-shadow: rgba(0, 0, 0, 0.15);
8484
--play-background: rgba(200, 200, 200, 0.8);
85+
--notice-shadow: rgb(230, 230, 230);
8586
}
8687

8788
/* Dark Theme */
@@ -116,7 +117,7 @@
116117
--muted: oklch(0.269 0 0);
117118
--muted-foreground: oklch(0.708 0 0);
118119
--accent: oklch(0.269 0 0);
119-
--accent-foreground: oklch(98.511% 0.00011 271.152 / 0.226);
120+
--accent-foreground: oklch(0.985 0 0);
120121
--destructive: oklch(0.704 0.191 22.216);
121122
--border: oklch(1 0 0 / 10%);
122123
--input: oklch(1 0 0 / 15%);
@@ -139,6 +140,7 @@
139140
--glass-border: rgba(255, 255, 255, 0.1);
140141
--glass-shadow: rgba(0, 0, 0, 0.3);
141142
--play-background: rgba(200, 200, 200, 0.8);
143+
--notice-shadow: rgb(70, 70, 70);
142144
}
143145

144146
@layer utilities {

src/components/plots/FlatMap.tsx

Lines changed: 132 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"use client";
22

3-
import React, {useMemo, useEffect, useRef, useCallback} from 'react'
3+
import React, {useMemo, useEffect, useRef, useState} from 'react'
44
import * as THREE from 'three'
55
import { useAnalysisStore, useGlobalStore, usePlotStore } from '@/utils/GlobalStates'
66
import { vertShader } from '@/components/computation/shaders'
77
import { flatFrag3D, fragmentFlat } from '../textures/shaders';
88
import { useShallow } from 'zustand/shallow'
99
import { ThreeEvent } from '@react-three/fiber';
10-
import { GetCurrentArray } from '@/utils/HelperFuncs';
10+
import { GetCurrentArray, GetTimeSeries, parseUVCoords } from '@/utils/HelperFuncs';
11+
import { ZarrDataset } from '../zarr/ZarrLoaderLRU';
12+
import { evaluate_cmap } from 'js-colormaps-es';
1113

1214
interface InfoSettersProps{
1315
setLoc: React.Dispatch<React.SetStateAction<number[]>>;
@@ -21,29 +23,33 @@ function Rescale(value: number, scales: {minVal: number, maxVal: number}){
2123
return value * range + scales.minVal
2224
}
2325

24-
25-
const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture | THREE.Data3DTexture[], infoSetters : InfoSettersProps}) => {
26+
const FlatMap = ({textures, infoSetters, ZarrDS} : {textures : THREE.DataTexture | THREE.Data3DTexture[], infoSetters : InfoSettersProps, ZarrDS: ZarrDataset}) => {
2627
const {setLoc, setShowInfo, val, coords} = infoSetters;
27-
const {flipY, colormap, valueScales, dimArrays, isFlat, dataShape, textureArrayDepths} = useGlobalStore(useShallow(state => ({
28-
flipY: state.flipY,
29-
colormap: state.colormap,
30-
valueScales: state.valueScales,
31-
dimArrays: state.dimArrays,
32-
isFlat: state.isFlat,
33-
dataShape: state.dataShape,
34-
textureArrayDepths: state.textureArrayDepths
28+
const {flipY, colormap, valueScales, dimArrays, dimNames, dimUnits,
29+
isFlat, dataShape, textureArrayDepths, strides, timeSeries,
30+
setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(state => ({
31+
flipY: state.flipY, colormap: state.colormap,
32+
valueScales: state.valueScales, dimArrays: state.dimArrays,
33+
dimNames:state.dimNames, dimUnits: state.dimUnits,
34+
isFlat: state.isFlat, dataShape: state.dataShape,
35+
textureArrayDepths: state.textureArrayDepths,
36+
strides: state.strides, timeSeries: state.timeSeries,
37+
setPlotDim:state.setPlotDim,
38+
updateDimCoords:state.updateDimCoords,
39+
updateTimeSeries: state.updateTimeSeries
3540
})))
36-
const {cScale, cOffset, animProg, nanTransparency, nanColor, zSlice, ySlice, xSlice} = usePlotStore(useShallow(state => ({
37-
cOffset: state.cOffset,
38-
cScale: state.cScale,
39-
resetAnim: state.resetAnim,
40-
animate: state.animate,
41-
animProg: state.animProg,
42-
nanTransparency: state.nanTransparency,
43-
nanColor: state.nanColor,
44-
zSlice: state.zSlice,
45-
ySlice: state.ySlice,
46-
xSlice: state.xSlice
41+
42+
const {cScale, cOffset, animProg, nanTransparency, nanColor,
43+
zSlice, ySlice, xSlice, selectTS,
44+
getColorIdx, incrementColorIdx} = usePlotStore(useShallow(state => ({
45+
cOffset: state.cOffset, cScale: state.cScale,
46+
resetAnim: state.resetAnim, animate: state.animate,
47+
animProg: state.animProg, nanTransparency: state.nanTransparency,
48+
nanColor: state.nanColor, zSlice: state.zSlice,
49+
ySlice: state.ySlice, xSlice: state.xSlice,
50+
selectTS: state.selectTS,
51+
getColorIdx: state.getColorIdx,
52+
incrementColorIdx: state.incrementColorIdx
4753
})))
4854
const {axis, analysisMode, analysisArray} = useAnalysisStore(useShallow(state=> ({
4955
axis: state.axis,
@@ -71,19 +77,114 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture | THREE
7177
const infoRef = useRef<boolean>(false)
7278
const lastUV = useRef<THREE.Vector2>(new THREE.Vector2(0,0))
7379
const rotateMap = analysisMode && axis == 2;
74-
const sampleArray = useMemo(()=> analysisMode ? analysisArray : GetCurrentArray(),[analysisMode, analysisArray])
80+
const sampleArray = useMemo(()=> analysisMode ? analysisArray : GetCurrentArray(),[analysisMode, analysisArray, textures])
7581
const analysisDims = useMemo(()=>dimArrays.length > 2 ? dimSlices.filter((_e,idx)=> idx != axis) : dimSlices,[dimSlices,axis])
82+
useEffect(()=>{
83+
geometry.dispose()
84+
},[geometry])
85+
86+
// ----- MOUSE MOVE ----- //
87+
const eventRef = useRef<ThreeEvent<PointerEvent> | null>(null);
88+
const handleMove = (e: ThreeEvent<PointerEvent>) => {
89+
if (infoRef.current && e.uv) {
90+
eventRef.current = e;
91+
setLoc([e.clientX, e.clientY]);
92+
lastUV.current = e.uv;
93+
const { x, y } = e.uv;
94+
const xSize = isFlat ? (analysisMode ? analysisDims[1].length : dimSlices[1].length) : dimSlices[2].length;
95+
const ySize = isFlat ? (analysisMode ? analysisDims[0].length : dimSlices[0].length) : dimSlices[1].length;
96+
const xIdx = Math.round(x*xSize-.5)
97+
const yIdx = Math.round(y*ySize-.5)
98+
let dataIdx = xSize * yIdx + xIdx;
99+
dataIdx += isFlat ? 0 : Math.floor((dimSlices[0].length-1) * animProg) * xSize*ySize
100+
const dataVal = sampleArray ? sampleArray[dataIdx] : 0;
101+
val.current = dataVal;
102+
coords.current = isFlat ? analysisMode ? [analysisDims[0][yIdx], analysisDims[1][xIdx]] : [dimSlices[0][yIdx], dimSlices[1][xIdx]] : [dimSlices[1][yIdx], dimSlices[2][xIdx]]
103+
}
104+
}
105+
106+
107+
// ----- TIMESERIES ----- //
108+
const [boundsObj, setBoundsObj] = useState<Record<string, THREE.Vector4>>({})
109+
const [bounds, setBounds] = useState<THREE.Vector4[]>(new Array(10).fill(new THREE.Vector4(-1 , -1, -1, -1)))
110+
const [height, width] = [dataShape[dataShape.length-2], dataShape[dataShape.length-1]]
111+
112+
useEffect(()=>{ //This goes through the list of highlighted squares and removes those that aren't included in the timeseries object.
113+
let boundIDs = Object.keys(boundsObj)
114+
const tsIDs = Object.keys(timeSeries)
115+
boundIDs = boundIDs.filter((val) => tsIDs.includes(val))
116+
const pointValues = boundIDs.map(id => boundsObj[id]);
117+
const paddedArray = [
118+
...pointValues,
119+
...Array(Math.max(0, 10 - pointValues.length)).fill(new THREE.Vector4(-1 , -1, -1, -1))
120+
];
121+
setBounds(paddedArray)
122+
},[boundsObj, timeSeries])
123+
124+
function addBounds(uv : THREE.Vector2, tsID: string){ //This adds the bounds in UV space of a selected square on the sphere.
125+
const widthID = Math.floor(uv.x*(width))+.5;
126+
const heightID = Math.ceil(uv.y*height)-.5 ;
127+
const delX = 1/width;
128+
const delY = 1/height;
129+
const xBounds = [widthID/width-delX/2,widthID/width+delX/2]
130+
const yBounds = [heightID/height-delY/2,heightID/height+delY/2]
131+
const bounds = new THREE.Vector4(...xBounds, ...yBounds)
132+
const newBoundObj = {[tsID] : bounds}
133+
setBoundsObj(prev=>{ return {...newBoundObj, ...prev}})
134+
}
135+
function HandleTimeSeries(event: THREE.Intersection){
136+
const uv = event.uv;
137+
const normal = new THREE.Vector3(0,0,1)
138+
if(ZarrDS && uv){
139+
const tsUV = flipY ? new THREE.Vector2(uv.x, 1-uv.y) : uv
140+
const tempTS = GetTimeSeries({data:analysisMode ? analysisArray : GetCurrentArray(), shape:dataShape, stride:strides},{uv:tsUV,normal})
141+
setPlotDim(0) //I think this 2 is only if there are 3-dims. Need to rework the logic
142+
143+
const coordUV = parseUVCoords({normal:normal,uv:uv})
144+
let dimCoords = coordUV.map((val,idx)=>val ? dimSlices[idx][Math.round(val*dimSlices[idx].length)] : null)
145+
const thisDimNames = dimNames.filter((_,idx)=> dimCoords[idx] !== null)
146+
const thisDimUnits = dimUnits.filter((_,idx)=> dimCoords[idx] !== null)
147+
dimCoords = dimCoords.filter(val => val !== null)
148+
const tsID = `${dimCoords[0]}_${dimCoords[1]}`
149+
const tsObj = {
150+
color:evaluate_cmap(getColorIdx()/10,"Paired"),
151+
data:tempTS
152+
}
153+
incrementColorIdx();
154+
updateTimeSeries({ [tsID] : tsObj})
155+
const dimObj = {
156+
first:{
157+
name:thisDimNames[0],
158+
loc:dimCoords[0] ?? 0,
159+
units:thisDimUnits[0]
160+
},
161+
second:{
162+
name:thisDimNames[1],
163+
loc:dimCoords[1] ?? 0,
164+
units:thisDimUnits[1]
165+
},
166+
plot:{
167+
units:dimUnits[0]
168+
}
169+
}
170+
updateDimCoords({[tsID] : dimObj})
171+
addBounds(uv, tsID);
172+
}
173+
}
174+
// ----- SHADER MATERIAL ----- //
76175
const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({
77176
glslVersion: THREE.GLSL3,
78177
uniforms:{
79178
cScale: {value: cScale},
80179
cOffset: {value: cOffset},
180+
selectTS: {value: selectTS},
181+
selectBounds: {value: bounds},
81182
map : {value: textures},
82183
textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])},
83184
cmap : { value : colormap},
84185
animateProg: {value:animProg},
85186
nanColor: {value : new THREE.Color(nanColor)},
86-
nanAlpha: {value: 1 - nanTransparency}
187+
nanAlpha: {value: 1 - nanTransparency},
87188
},
88189
vertexShader: vertShader,
89190
fragmentShader: isFlat ? fragmentFlat : flatFrag3D,
@@ -100,29 +201,12 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture | THREE
100201
uniforms.nanColor.value = new THREE.Color(nanColor);
101202
uniforms.nanAlpha.value = 1 - nanTransparency;
102203
uniforms.cScale.value = cScale;
204+
uniforms.selectBounds.value = bounds;
205+
uniforms.selectTS.value = selectTS
103206
}
104-
},[cScale, cOffset, textures, colormap, animProg, nanColor, nanTransparency])
105-
useEffect(()=>{
106-
geometry.dispose()
107-
},[geometry])
108-
const eventRef = useRef<ThreeEvent<PointerEvent> | null>(null);
109-
const handleMove = useCallback((e: ThreeEvent<PointerEvent>) => {
110-
if (infoRef.current && e.uv) {
111-
eventRef.current = e;
112-
setLoc([e.clientX, e.clientY]);
113-
lastUV.current = e.uv;
114-
const { x, y } = e.uv;
115-
const xSize = isFlat ? (analysisMode ? analysisDims[1].length : dimSlices[1].length) : dimSlices[2].length;
116-
const ySize = isFlat ? (analysisMode ? analysisDims[0].length : dimSlices[0].length) : dimSlices[1].length;
117-
const xIdx = Math.round(x*xSize-.5)
118-
const yIdx = Math.round(y*ySize-.5)
119-
let dataIdx = xSize * yIdx + xIdx;
120-
dataIdx += isFlat ? 0 : Math.floor((dimSlices[0].length-1) * animProg) * xSize*ySize
121-
const dataVal = sampleArray ? sampleArray[dataIdx] : 0;
122-
val.current = isFlat && !analysisMode ? Rescale(dataVal, valueScales) : dataVal;
123-
coords.current = isFlat ? analysisMode ? [analysisDims[0][yIdx], analysisDims[1][xIdx]] : [dimSlices[0][yIdx], dimSlices[1][xIdx]] : [dimSlices[1][yIdx], dimSlices[2][xIdx]]
124-
}
125-
}, [sampleArray, dimSlices, animProg]);
207+
},[cScale, cOffset, textures, colormap, animProg, nanColor, nanTransparency, bounds, selectTS])
208+
209+
126210
return (
127211
<>
128212
<mesh
@@ -133,6 +217,7 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture | THREE
133217
onPointerEnter={()=>{setShowInfo(true); infoRef.current = true }}
134218
onPointerLeave={()=>{setShowInfo(false); infoRef.current = false }}
135219
onPointerMove={handleMove}
220+
onClick={selectTS && HandleTimeSeries}
136221
/>
137222
</>
138223
)

src/components/plots/Plot.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{
4848
const duration = 1000;
4949
const startTime = performance.now();
5050
const startPos = controls.object.position.clone();
51-
const endPos = controls.position0.clone()
51+
const endPos = isFlat ? new THREE.Vector3(0, 0, 5) : controls.position0.clone()
5252

5353
const startTarget = controls.target.clone();
5454
const endTarget = controls.target0.clone()
@@ -62,8 +62,8 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{
6262
controls.object.position.lerpVectors(startPos, endPos, t);
6363
controls.target.lerpVectors(startTarget,endTarget,t)
6464

65-
if (isFlat) {
66-
controls.object.zoom = THREE.MathUtils.lerp(startZoom, 1000, t);
65+
if (isFlat && useOrtho) {
66+
controls.object.zoom = THREE.MathUtils.lerp(startZoom, 50, t);
6767
controls.object.updateProjectionMatrix();
6868
controls.update()
6969
}
@@ -117,11 +117,11 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{
117117
return (
118118
<OrbitControls
119119
ref={orbitRef}
120-
enableRotate={!isFlat}
120+
enableRotate={!isFlat || !useOrtho}
121121
enablePan={true}
122122
maxDistance={50}
123-
// minZoom={1}
124-
// maxZoom={3000}
123+
minZoom={1}
124+
maxZoom={3000}
125125
/>
126126
);
127127
}
@@ -208,7 +208,6 @@ const Plot = ({ZarrDS}:{ZarrDS: ZarrDataset}) => {
208208
const shapeLength = result.shape.length
209209
if (shapeLength == 2){
210210
setIsFlat(true)
211-
setPlotType("sphere")
212211
}
213212
else{
214213
setIsFlat(false)
@@ -348,7 +347,7 @@ const Plot = ({ZarrDS}:{ZarrDS: ZarrDataset}) => {
348347
<ExportCanvas show={show}/>
349348
<CountryBorders/>
350349
{show && <AxisLines />}
351-
<FlatMap textures={textures as THREE.DataTexture | THREE.Data3DTexture[]} infoSetters={infoSetters} />
350+
<FlatMap textures={textures as THREE.DataTexture | THREE.Data3DTexture[]} infoSetters={infoSetters} ZarrDS={ZarrDS}/>
352351
<Orbiter isFlat={true}/>
353352
</Canvas>
354353
</>}

src/components/textures/shaders/flatFrag3D.glsl

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ uniform sampler3D map[14];
44
uniform sampler2D cmap;
55
uniform vec3 textureDepths;
66

7+
uniform bool selectTS;
78
uniform float cOffset;
89
uniform float cScale;
910
uniform float animateProg;
1011
uniform float nanAlpha;
1112
uniform vec3 nanColor;
13+
uniform vec4[10] selectBounds;
1214

1315
varying vec2 vUv;
1416
out vec4 Color;
@@ -32,6 +34,20 @@ float sample1(vec3 p, int index) { // Shader doesn't support dynamic indexing so
3234
else return 0.0;
3335
}
3436

37+
bool isValid(vec2 sampleCoord){
38+
for (int i = 0; i < 10; i++){
39+
vec4 thisBound = selectBounds[i];
40+
if (thisBound.x == -1.){
41+
return false;
42+
}
43+
bool cond = (sampleCoord.x < thisBound.r || sampleCoord.x > thisBound.g || sampleCoord.y < thisBound.b || sampleCoord.y > thisBound.a);
44+
if (!cond){
45+
return true;
46+
}
47+
}
48+
return false;
49+
}
50+
3551
void main() {
3652
int zStepSize = int(textureDepths.y) * int(textureDepths.x);
3753
int yStepSize = int(textureDepths.x);
@@ -47,5 +63,8 @@ void main() {
4763
float sampLoc = isNaN ? strength: (strength)*cScale;
4864
sampLoc = isNaN ? strength : min(sampLoc+cOffset,0.995);
4965
Color = isNaN ? vec4(nanColor, nanAlpha) : vec4(texture2D(cmap, vec2(sampLoc, 0.5)).rgb, 1.);
50-
66+
if (selectTS){
67+
bool cond = isValid(vUv);
68+
Color.rgb *= cond ? 1. : 0.65;
69+
}
5170
}

src/components/textures/shaders/sphereFrag.glsl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ void main(){
8787
if (!isNaN){
8888
color.a = 1.;
8989
}
90-
bool cond = isValid(sampleCoord);
91-
if (!cond && selectTS){
92-
color.rgb *= 0.65;
90+
if (selectTS){
91+
bool cond = isValid(vUv);
92+
color.rgb *= cond ? 1. : 0.65;
9393
}
9494
} else {
9595
color = vec4(nanColor, 1.); // Black

src/components/ui/css/PlotLineButton.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
font-size: 32px;
1313
background: var(--background-plot);
1414
transform: translateX(-50%);
15-
filter: drop-shadow(0px 0px 5px var(--accent-foreground));
15+
filter: drop-shadow(0px 0px 5px var(--notice-shadow));
1616
z-index: 4;
1717
padding-inline: 1rem;
1818
}

0 commit comments

Comments
 (0)