Skip to content

Commit 8fd8e1f

Browse files
committed
with average line
1 parent d8ecad5 commit 8fd8e1f

File tree

13 files changed

+577
-185
lines changed

13 files changed

+577
-185
lines changed

package-lock.json

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"react-dom": "^19.0.0",
2929
"react-plotly.js": "^2.6.0",
3030
"react-rnd": "^10.5.2",
31-
"react-router-dom": "^7.6.2"
31+
"react-router-dom": "^7.6.2",
32+
"react-zoom-pan-pinch": "^3.7.0"
3233
},
3334
"devDependencies": {
3435
"@eslint/eslintrc": "^3",

public/data/slice50.bin

100 KB
Binary file not shown.

public/data/slice50.meta.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"width": 160,
3+
"height": 160,
4+
"dtype": "float32_le",
5+
"order": "row-major",
6+
"norm": {
7+
"kind": "linear",
8+
"source": "zscale",
9+
"vmin": -2.5081933540227115,
10+
"vmax": 3.165679448851614
11+
},
12+
"arcsec_per_pix": 0.06242396026470463,
13+
"center_px": [
14+
70.17383946424924,
15+
112.1276696930126
16+
]
17+
}

src/app/jwst-bin/page.tsx

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
'use client';
2+
3+
import React, { useEffect, useRef, useState } from 'react';
4+
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
5+
6+
type Meta = {
7+
width: number;
8+
height: number;
9+
dtype: 'float32_le';
10+
order: 'row-major';
11+
norm?: { kind: 'linear'; source: 'zscale'; vmin: number; vmax: number };
12+
arcsec_per_pix?: number | null;
13+
center_px?: [number, number] | null;
14+
};
15+
16+
/** simple viridis colormap (256 RGB tuples, 0–255 range) */
17+
const VIRIDIS = (() => {
18+
const hex = [
19+
'#440154','#482878','#3E4989','#31688E','#26828E','#1F9E89',
20+
'#35B779','#6CCE59','#B4DE2C','#FDE725'
21+
];
22+
const stops = hex.map(h => {
23+
const v = parseInt(h.slice(1), 16);
24+
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
25+
});
26+
// interpolate between 10 stops → 256
27+
const arr: [number, number, number][] = [];
28+
for (let i = 0; i < 256; i++) {
29+
const t = i / 255 * (stops.length - 1);
30+
const i0 = Math.floor(t);
31+
const i1 = Math.min(i0 + 1, stops.length - 1);
32+
const f = t - i0;
33+
const c0 = stops[i0], c1 = stops[i1];
34+
arr.push([
35+
Math.round(c0[0] + f * (c1[0] - c0[0])),
36+
Math.round(c0[1] + f * (c1[1] - c0[1])),
37+
Math.round(c0[2] + f * (c1[2] - c0[2]))
38+
]);
39+
}
40+
return arr;
41+
})();
42+
43+
export default function JWSTBinPage() {
44+
const binUrl = '/data/slice50.bin';
45+
const metaUrl = '/data/slice50.meta.json';
46+
47+
const [meta, setMeta] = useState<Meta | null>(null);
48+
const [buf, setBuf] = useState<Float32Array | null>(null);
49+
const [center, setCenter] = useState<[number, number] | null>(null);
50+
51+
const [useArcsec, setUseArcsec] = useState(false);
52+
const [rIn, setRIn] = useState(10);
53+
const [rOut, setROut] = useState(30);
54+
const [colorMap, setColorMap] = useState<'gray' | 'viridis'>('viridis');
55+
56+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
57+
58+
useEffect(() => {
59+
let alive = true;
60+
(async () => {
61+
const m: Meta = await (await fetch(metaUrl)).json();
62+
if (!alive) return;
63+
setMeta(m);
64+
setCenter(m.center_px ?? null);
65+
const ab = await (await fetch(binUrl)).arrayBuffer();
66+
if (!alive) return;
67+
setBuf(new Float32Array(ab));
68+
})();
69+
return () => { alive = false; };
70+
}, [binUrl, metaUrl]);
71+
72+
useEffect(() => {
73+
if (!meta || !buf || !canvasRef.current) return;
74+
const { width, height } = meta;
75+
const cv = canvasRef.current;
76+
cv.width = width;
77+
cv.height = height;
78+
79+
const ctx = cv.getContext('2d');
80+
if (!ctx) return;
81+
82+
let vmin = meta.norm?.vmin ?? Infinity;
83+
let vmax = meta.norm?.vmax ?? -Infinity;
84+
if (meta.norm == null) {
85+
for (const v of buf) {
86+
if (!Number.isFinite(v)) continue;
87+
if (v < vmin) vmin = v;
88+
if (v > vmax) vmax = v;
89+
}
90+
if (vmax <= vmin) vmax = vmin + 1e-6;
91+
}
92+
93+
const img = ctx.createImageData(width, height);
94+
const out = img.data;
95+
96+
for (let i = 0, j = 0; i < buf.length; i++, j += 4) {
97+
const v = buf[i];
98+
let t = (v - vmin) / (vmax - vmin);
99+
if (!Number.isFinite(t)) t = 0;
100+
if (t < 0) t = 0;
101+
if (t > 1) t = 1;
102+
103+
let r: number, g: number, b: number;
104+
if (colorMap === 'gray') {
105+
const x = Math.round(t * 255);
106+
r = g = b = x;
107+
} else {
108+
const c = VIRIDIS[Math.round(t * 255)];
109+
[r, g, b] = c;
110+
}
111+
out[j + 0] = r;
112+
out[j + 1] = g;
113+
out[j + 2] = b;
114+
out[j + 3] = 255;
115+
}
116+
ctx.putImageData(img, 0, 0);
117+
}, [meta, buf, colorMap]);
118+
119+
function handleClick(e: React.MouseEvent) {
120+
if (!meta || !canvasRef.current) return;
121+
const rect = canvasRef.current.getBoundingClientRect();
122+
const dispX = e.clientX - rect.left;
123+
const dispY = e.clientY - rect.top;
124+
const sx = meta.width / rect.width;
125+
const sy = meta.height / rect.height;
126+
setCenter([dispX * sx, dispY * sy]);
127+
}
128+
129+
function Overlay() {
130+
if (!meta || !center || !canvasRef.current) return null;
131+
const dispW = canvasRef.current.clientWidth;
132+
const dispH = canvasRef.current.clientHeight;
133+
const sx = dispW / meta.width;
134+
const sy = dispH / meta.height;
135+
136+
const rInPx =
137+
useArcsec && meta.arcsec_per_pix ? rIn / meta.arcsec_per_pix : rIn;
138+
const rOutPx =
139+
useArcsec && meta.arcsec_per_pix ? rOut / meta.arcsec_per_pix : rOut;
140+
141+
return (
142+
<svg
143+
className="absolute inset-0 pointer-events-none"
144+
viewBox={`0 0 ${dispW} ${dispH}`}
145+
preserveAspectRatio="none"
146+
>
147+
{rOutPx > 0 && (
148+
<circle
149+
cx={center[0] * sx}
150+
cy={center[1] * sy}
151+
r={rOutPx * sx}
152+
fill="none"
153+
stroke="white"
154+
strokeWidth={2}
155+
/>
156+
)}
157+
{rInPx > 0 && (
158+
<circle
159+
cx={center[0] * sx}
160+
cy={center[1] * sy}
161+
r={rInPx * sx}
162+
fill="none"
163+
stroke="white"
164+
strokeWidth={1.5}
165+
strokeDasharray="6 4"
166+
/>
167+
)}
168+
</svg>
169+
);
170+
}
171+
172+
return (
173+
<div className="mx-auto max-w-6xl p-6 space-y-6">
174+
<h1 className="text-xl font-semibold">JWST Flux Slice Viewer</h1>
175+
176+
<div className="flex flex-wrap items-center gap-4">
177+
<label className="flex items-center gap-2">
178+
<input
179+
type="checkbox"
180+
checked={useArcsec}
181+
onChange={(e) => setUseArcsec(e.target.checked)}
182+
/>
183+
<span className="text-sm">Use arcsec for radii</span>
184+
</label>
185+
186+
<label className="flex items-center gap-2">
187+
<span className="text-sm">r_in:</span>
188+
<input
189+
className="border rounded p-1 w-16 text-center"
190+
type="number"
191+
value={rIn}
192+
onChange={(e) => setRIn(Number(e.target.value))}
193+
/>
194+
</label>
195+
<label className="flex items-center gap-2">
196+
<span className="text-sm">r_out:</span>
197+
<input
198+
className="border rounded p-1 w-16 text-center"
199+
type="number"
200+
value={rOut}
201+
onChange={(e) => setROut(Number(e.target.value))}
202+
/>
203+
</label>
204+
205+
<button
206+
onClick={() => setColorMap((c) => (c === 'gray' ? 'viridis' : 'gray'))}
207+
className="px-3 py-1 rounded bg-slate-700 text-white hover:bg-slate-600 text-sm"
208+
>
209+
Toggle Colormap ({colorMap})
210+
</button>
211+
</div>
212+
213+
<div className="relative w-full border rounded bg-black flex justify-center items-center overflow-hidden">
214+
<TransformWrapper initialScale={2.5} minScale={0.5} maxScale={15} wheel={{ step: 0.15 }}>
215+
<TransformComponent wrapperClass="w-full h-full flex justify-center">
216+
<div className="relative" style={{ width: '800px', height: '800px' }} onClick={handleClick}>
217+
<canvas ref={canvasRef} className="w-full h-full block select-none" />
218+
<Overlay />
219+
</div>
220+
</TransformComponent>
221+
</TransformWrapper>
222+
</div>
223+
224+
<div className="text-sm text-gray-300">
225+
{center
226+
? <>Center: ({center[0].toFixed(2)}, {center[1].toFixed(2)}) px</>
227+
: <>Center: not set (click image)</>}
228+
</div>
229+
{meta?.arcsec_per_pix && (
230+
<div className="text-sm text-gray-400">
231+
Pixel scale ≈ {meta.arcsec_per_pix.toFixed(3)} arcsec/pixel
232+
</div>
233+
)}
234+
</div>
235+
);
236+
}

0 commit comments

Comments
 (0)