Skip to content

Commit 422b065

Browse files
committed
snr
1 parent f601c55 commit 422b065

7 files changed

Lines changed: 170 additions & 62 deletions

File tree

README.md

Lines changed: 92 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,105 @@
1-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1+
# Interactive Light-Curve Visualization and Analysis Tool
22

3-
## Getting Started
3+
## Overview
44

5-
First, run the development server:
5+
This program is an interactive, web-based visualization system designed for exploratory analysis of **time-resolved astronomical light-curve data**, with a particular focus on **ZTF J1539 PSF surface-brightness measurements** used in **JWST precision timing studies**. The application enables users to seamlessly switch between **raw observational data** and **statistically averaged representations**, while preserving a direct link between aggregated points and their underlying measurements.
66

7-
```bash
8-
npm run dev
9-
# or
10-
yarn dev
11-
# or
12-
pnpm dev
13-
# or
14-
bun dev
15-
```
7+
## Core Functionality
8+
9+
The system renders **dual-panel light curves** using Plotly, displaying **short-wavelength (SW)** and **long-wavelength (LW)** surface brightness measurements simultaneously. Users can:
10+
11+
- Select different data representations (**raw**, **averaged**, or **combined**)
12+
- Configure binning parameters
13+
- Choose among multiple x-axis domains:
14+
- Phase
15+
- MJD
16+
- Absolute time
17+
- Derived temporal units (days, hours, minutes, seconds)
18+
19+
A central design goal of the tool is **traceability**: every averaged data point retains a mapping to the raw points that contributed to it. This mapping allows users to drill down from a statistical summary to the exact measurements used to compute it.
20+
21+
## Interactive Exploration
22+
23+
The program supports rich, event-driven interaction:
24+
25+
- Clicking an **averaged point** dynamically opens a floating panel displaying all associated raw data points.
26+
- The raw-data panel is **draggable and resizable**, allowing side-by-side comparison with the main light curve.
27+
- The raw view overlays the **mean value and uncertainty bands**, enabling visual assessment of scatter and outliers.
28+
- Users can interactively switch the **raw-data x-axis** (e.g., phase → time → seconds) without recomputing the data.
29+
30+
Hover interactions provide **context-aware tooltips**, showing metadata such as epoch, radial bounds, phase, MJD, and measurement uncertainty, ensuring that scientific context is always available.
31+
32+
## Raw-Data Filtering and Quality Control
33+
34+
To support data-quality assessment, the program includes configurable **raw-point filtering mechanisms**:
35+
36+
- **Percent-based filtering**, which keeps points within a user-defined percentage of the average
37+
- **Sigma-band filtering**, which selects points within a specified standard-deviation range
38+
39+
Filters can be applied **inside or outside** the selected band, and the accepted range is visually highlighted directly on the plot. Filtered results update both the displayed raw points and the corresponding averaged annotations, enabling rapid sensitivity testing of binning and filtering assumptions.
40+
41+
## Image-Linked Annotations
42+
43+
For datasets with associated observational images, the tool allows users to:
44+
45+
- Click individual raw or averaged points to **attach annotated thumbnails**
46+
- Maintain a synchronized collection of selected images below the plot
47+
- Open high-resolution images in a modal view alongside contextual numerical values extracted from the plotted data
48+
49+
This tightly integrates **numerical trends and image-level evidence**, supporting visual validation of astrophysical features or anomalies.
1650

17-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
51+
## Architecture and Design
1852

19-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
53+
The system is implemented as a **client-side React / Next.js application**, leveraging:
2054

21-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
55+
- **React Plotly.js** for high-performance interactive plotting
56+
- A centralized **PlotSettings context** for consistent global state management
57+
- Modular utilities for trace construction, filtering, and annotation generation
58+
- Dynamic imports to ensure compatibility with server-side rendering constraints
2259

23-
## Learn More
60+
The architecture cleanly separates **data processing**, **visual styling**, and **interaction logic**, making the tool extensible to additional targets, wavelength bands, or survey datasets.
2461

25-
To learn more about Next.js, take a look at the following resources:
62+
## Intended Use
2663

27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
64+
This program is intended for astronomers and data analysts who require:
2965

30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
66+
- Transparent linkage between averaged results and raw observations
67+
- Interactive validation of binning, filtering, and uncertainty modeling
68+
- High-precision timing and variability studies using JWST-related datasets
3169

32-
## Deploy on Vercel
70+
By combining statistical aggregation with direct access to raw measurements and imagery, the tool bridges the gap between **quantitative analysis** and **visual inspection**, enabling more robust scientific interpretation.
3371

34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
3572

36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
73+
## Installation and Getting Started
74+
75+
### Prerequisites
76+
77+
Before installing the project, ensure you have the following installed:
78+
79+
- **Node.js** (version 18 or later recommended)
80+
- **npm**, **yarn**, or **pnpm** (npm is used in the examples below)
81+
- A modern web browser (Chrome, Firefox, Edge)
82+
83+
### Installation
84+
85+
1. **Clone the repository**
86+
87+
```bash
88+
git clone https://github.com/your-org/your-repo-name.git
89+
cd your-repo-name
90+
```
91+
92+
2. **Install dependencies**
93+
```bash
94+
npm install
95+
```
96+
97+
3. **Build**
98+
```bash
99+
npm run build
100+
```
101+
102+
4. **Start**
103+
```bash
104+
npm run start
105+
```

src/app/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
'use client';
12
import LightCurvePlot from '@/components/LightCurvePlot';
3+
import { usePlotSettings } from '@/context/PlotSettingsContext';
4+
import { useEffect } from 'react';
25

36
export default function HomePage() {
7+
8+
const {
9+
setSettings
10+
} = usePlotSettings();
11+
useEffect(() => {
12+
setSettings({ dataType: [] })
13+
}
14+
, []);
415
return (
516
<main className="min-h-full bg-gray-50 overflow-auto">
617
<LightCurvePlot />

src/app/snr/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use client';
2+
import LightCurvePlot from '@/components/LightCurvePlot';
3+
import { usePlotSettings } from '@/context/PlotSettingsContext';
4+
import { useEffect } from 'react';
5+
6+
export default function SNR() {
7+
const {
8+
setSettings
9+
} = usePlotSettings();
10+
useEffect(() => {
11+
setSettings({ dataType: ['snr'] })
12+
}
13+
, []);
14+
15+
16+
return (
17+
<main className="min-h-full bg-gray-50 overflow-auto">
18+
<LightCurvePlot />
19+
</main>
20+
);
21+
}

src/components/LightCurvePlot.tsx

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ function applyRawFilter(
498498
});
499499

500500
}
501-
}, [avgPointRawMap, dataSelection, rawFigure, filterPercent, sigmaK, rawFilterMode, xAxis]);
501+
}, [avgPointRawMap, dataSelection, rawFigure, filterPercent, sigmaK, rawFilterMode, xAxis, isRawFilterInside]);
502502

503503

504504
useEffect(() => {
@@ -822,35 +822,6 @@ function applyRawFilter(
822822
handleOriginalPointClick(figure, pt);
823823
}
824824
}, [dataType, avgPointRawMap, figure, selectedImages]);
825-
// const handlePointClick = useCallback((e: Plotly.PlotMouseEvent) => {
826-
// const pt = e.points[0];
827-
// if (!figure) return;
828-
829-
// const hasAverage = dataType.includes('average');
830-
// const hasRaw = dataType.includes('raw');
831-
// const hasBoth = hasAverage && hasRaw;
832-
833-
// const dataMode = ((pt.customdata as unknown) as AveragePointCustomData)?.dataMode;
834-
835-
// // BLOCK average clicks when both raw+average selected
836-
// if (hasBoth && dataMode === 'average') {
837-
// console.log("Average click blocked because raw+avg selected");
838-
// return;
839-
// }
840-
841-
// // normal logic
842-
// if (dataMode === '') {
843-
// if (dataType.includes('average')) {
844-
// handleAveragePointClick(figure, pt);
845-
// } else {
846-
// handleOriginalPointClick(figure, pt);
847-
// }
848-
// } else if (dataMode === 'average') {
849-
// handleAveragePointClick(figure, pt);
850-
// } else if (dataMode === 'raw') {
851-
// handleOriginalPointClick(figure, pt);
852-
// }
853-
// }, [dataType, avgPointRawMap, figure, selectedImages]);
854825

855826

856827
function clearAll() {

src/components/Navbar/Navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function Navbar() {
2323
const mainLinks = [
2424
{ name: '2D Plot', href: `${process.env.BASE_PATH}/`, id: '2d-plot-link', disabled: false },
2525
{ name: 'Matrix', href: `${process.env.BASE_PATH}/matrix/`, id: 'matrix-link', disabled: false },
26-
{ name: 'Signal-to-noise', href: `${process.env.BASE_PATH}/noise`, id: 'noise-link', disabled: true },
26+
{ name: 'Signal-to-Noise Ratio', href: `${process.env.BASE_PATH}/snr`, id: 'noise-link', disabled: false },
2727
]
2828
// const settingsLink = { name: 'Settings', href: '#', id: 'settings-link' }
2929

src/components/Sidebar/Sidebar.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ const visibilityByPath: Record<string, Partial<Record<string, boolean>>> = {
1515
xAxis: true,
1616
errorBars: true,
1717
},
18+
'/snr/': {
19+
dataType: false,
20+
selectedSeries: true,
21+
dataSelection: true,
22+
averageConfig: true,
23+
xAxis: true,
24+
errorBars: true,
25+
},
1826
'/matrix/': {
1927
dataType: false,
2028
selectedSeries: true,
@@ -38,7 +46,7 @@ type RawFilterMode = "none" | "percent" | "sigma";
3846

3947
export default function Sidebar() {
4048
const {
41-
dataType, dataSelection, xAxis, errorBars, noOfBins, noOfDataPoint, colorBy, filterPercent, isRawFilterInside, selectedSeries,
49+
dataType, dataSelection, xAxis, errorBars, noOfBins, noOfDataPoint, filterPercent, isRawFilterInside, selectedSeries,
4250
sigmaK,
4351
rawFilterMode,
4452
setSettings,
@@ -74,6 +82,7 @@ export default function Sidebar() {
7482
const dataTypeOptions = [
7583
{ value: 'average', label: 'Average' },
7684
{ value: 'raw', label: 'Raw' },
85+
// { value: 'snr', label: 'Signal-to-Noise Ratio' },
7786
];
7887

7988
const xAxisOptions = [

src/utils/traces.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ function toHomogeneousX(x: Array<number | string | Date>): number[] | string[] |
1414
return x.map(v => v instanceof Date ? v.toISOString() : String(v));
1515
}
1616

17+
export function computeSNR(flux: number[], fluxErr: number[]) {
18+
const y = flux.map((v, i) =>
19+
fluxErr[i] > 0 ? v / fluxErr[i] : NaN
20+
);
21+
22+
// Optional: SNR has no natural "error bar"
23+
// If you want one, you could set err = 1 or 0
24+
const err = fluxErr.map(() => 0);
25+
26+
return { y, err };
27+
}
28+
1729
export function processForTrace({
1830
wave,
1931
json,
@@ -34,6 +46,7 @@ export function processForTrace({
3446
let err: number[] = [];
3547
let customdata: AveragePointCustomData[] = [];
3648

49+
const isSNR = mode === 'snr';
3750
if (mode === 'average') {
3851
if (axis === 'phase') {
3952
const flux: number[] = Array.isArray(json.psf_flux_phase) ? json.psf_flux_phase : [];
@@ -78,11 +91,13 @@ export function processForTrace({
7891
for (let i = 0; i < noOfBins; i++) {
7992
if (!bins[i].length) continue;
8093
const [avg, avgErr] = weightedAvg(bins[i], berr[i]);
94+
const yVal = isSNR && avgErr > 0 ? avg / avgErr : avg;
95+
const errVal = isSNR ? 0 : avgErr;
8196
const center = centers[i];
8297

8398
x.push(center);
84-
y.push(avg);
85-
err.push(avgErr);
99+
y.push(yVal);
100+
err.push(errVal);
86101
customdata.push({ type, epoch, r_in, r_out, phase: center.toFixed(5), avgErr });
87102

88103
const rawPoints = bins[i].map((yVal, j) => ({
@@ -130,11 +145,14 @@ export function processForTrace({
130145
if (!tc.length) return;
131146

132147
const [avg, avgErr] = weightedAvg(fc, ec);
148+
149+
const yVal = isSNR && avgErr > 0 ? avg / avgErr : avg;
150+
const errVal = isSNR ? 0 : avgErr;
133151
const center = tc.reduce((a, b) => a + b, 0) / tc.length;
134152

135153
x.push(center);
136-
y.push(avg);
137-
err.push(avgErr);
154+
y.push(yVal);
155+
err.push(errVal);
138156
customdata.push({ type, epoch, r_in, r_out, phase: center.toFixed(5), avgErr });
139157

140158
const rawPoints = fc.map((yVal, j) => ({
@@ -158,15 +176,16 @@ export function processForTrace({
158176
for (let i = 0; i < n; i++) doRange(i * chunkSize, (i + 1) * chunkSize);
159177
if (n * chunkSize < flux.length) doRange(n * chunkSize, flux.length);
160178
}
161-
} else {
179+
}
180+
else {
162181
// RAW
163182
const isPhase = axis === 'phase';
164183
const rawX = json[isPhase ? 'phase_values_phase' : timeKey];
165184
x = Array.isArray(rawX) ? (rawX as number[]) : [];
166-
y = isPhase
185+
const flux = isPhase
167186
? (Array.isArray(json.psf_flux_phase) ? (json.psf_flux_phase as number[]) : [])
168187
: (Array.isArray(json.psf_flux_time) ? (json.psf_flux_time as number[]) : []);
169-
err = isPhase
188+
const fluxErr = isPhase
170189
? (Array.isArray(json.psf_flux_unc_phase) ? (json.psf_flux_unc_phase as number[]) : [])
171190
: (Array.isArray(json.psf_flux_unc_time) ? (json.psf_flux_unc_time as number[]) : []);
172191
customdata = isPhase
@@ -175,6 +194,14 @@ export function processForTrace({
175194
// as AveragePointCustomData[];
176195
// console.log(json)
177196
// console.log(customdata)
197+
if (isSNR) {
198+
const snr = computeSNR(flux, fluxErr);
199+
y = snr.y;
200+
err = snr.err;
201+
} else {
202+
y = flux;
203+
err = fluxErr;
204+
}
178205

179206
if (isPhase) {
180207
x = x.concat((x as number[]).map(v => v + 1));

0 commit comments

Comments
 (0)