From 18b84636f3e87ac819a121e9df9b12809b4383e5 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 29 Sep 2026 11:47:34 -0400 Subject: [PATCH 1/3] Add missing useEffect dependency --- src/components/AladinViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AladinViewer.tsx b/src/components/AladinViewer.tsx index 29bf0ff..f35f973 100644 --- a/src/components/AladinViewer.tsx +++ b/src/components/AladinViewer.tsx @@ -84,7 +84,7 @@ export function AladinViewer({ source, nearbySources }: AladinViewerProps) { return () => { aladinInstanceRef.current = null; }; - }, [source.source_id, source.ra, source.dec, nearbySources]); + }, [source.source_id, source.name, source.ra, source.dec, nearbySources]); return
; } From b039ab115e40d93882f15590a9ee161b08fc295a Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 29 Sep 2026 11:48:04 -0400 Subject: [PATCH 2/3] Changes labels to mJy --- src/components/FluxFilterMenu.tsx | 2 +- src/components/SourceFluxFilter.tsx | 2 +- src/configs/stats.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/FluxFilterMenu.tsx b/src/components/FluxFilterMenu.tsx index 8756be1..311fbae 100644 --- a/src/components/FluxFilterMenu.tsx +++ b/src/components/FluxFilterMenu.tsx @@ -89,7 +89,7 @@ export default function FluxFilterMenu({ ))} - +

Filtered on {appliedBand} from {appliedRange[0]} to{' '} - {appliedRange[1]} Jy + {appliedRange[1]} mJy

= { min_flux: { label: 'Min Flux', precision: numSigFigs, - units: 'Jy', + units: 'mJy', }, max_flux: { label: 'Max Flux', precision: numSigFigs, - units: 'Jy', + units: 'mJy', }, mean_flux: { label: 'Mean Flux', precision: numSigFigs, - units: 'Jy', + units: 'mJy', }, stddev_flux: { label: 'Std. Dev. Flux', precision: numSigFigs, - units: 'Jy', + units: 'mJy', }, median_flux: { label: 'Median Flux', precision: numSigFigs, - units: 'Jy', + units: 'mJy', }, }; From 0445f5c979dcc6d248301d34c1e71322e182f327 Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 29 Sep 2026 11:48:31 -0400 Subject: [PATCH 3/3] Refactor unassignment lightcurve to use Lightcurve.tsx instance --- src/components/Lightcurve.tsx | 35 ++++- src/components/UnassignedLightcurvePlot.tsx | 134 ++++---------------- src/components/UnassignedSource.tsx | 5 +- src/utils/unassignedPlotHelpers.ts | 47 ++++++- 4 files changed, 107 insertions(+), 114 deletions(-) diff --git a/src/components/Lightcurve.tsx b/src/components/Lightcurve.tsx index cffe314..1909b8c 100644 --- a/src/components/Lightcurve.tsx +++ b/src/components/Lightcurve.tsx @@ -70,6 +70,12 @@ type LightcurveProps = { legendBottomRowYOffset?: number; hideStrategyToggle?: boolean; hideFlaggedObsToggle?: boolean; + /** Whether flagged observations start out hidden; note that hiding the flagged obs toggle + * would hold the value determined by this prop. Defaults to true. */ + defaultHideFlaggedData?: boolean; + /** Whether a flagged point gets the red marker outline. Defaults to true. A caller can turn it + * off when the styling isn't meaningful for its data, e.g. unassigned measurements */ + showFlaggedMarkerStyling?: boolean; title?: string; subtitle?: string; }; @@ -207,6 +213,8 @@ export function Lightcurve({ legendBottomRowYOffset = DEFAULT_INSTRUMENT_LEGEND_LAYOUT.legend2.y, hideStrategyToggle, hideFlaggedObsToggle, + defaultHideFlaggedData = true, + showFlaggedMarkerStyling = true, title, subtitle, }: LightcurveProps) { @@ -220,7 +228,9 @@ export function Lightcurve({ const plotElementId = `lightcurve-plot-${useId()}`; const [isDataReady, setIsDataReady] = useState(false); - const [hideFlaggedData, setHideFlaggedData] = useState(true); + const [hideFlaggedData, setHideFlaggedData] = useState( + defaultHideFlaggedData + ); const [viewMode, setViewMode] = useState<'binned' | 'unbinned'>('unbinned'); const [binnedStartTime, setBinnedStartTime] = useState(''); @@ -419,7 +429,13 @@ export function Lightcurve({ } data.module[idx] = module; - populatePoint(data, lightcurve, lightcurveKey, idx, isFlagged); + populatePoint( + data, + lightcurve, + lightcurveKey, + idx, + isFlagged && showFlaggedMarkerStyling + ); }); for (const data of tracesByModule.values()) { @@ -434,7 +450,7 @@ export function Lightcurve({ const data = { // Used for the marker click tooltip's header (see handleMarkerClick) - // showlegend is false below since the dual legend explains color/shape instead. - name: `${lightcurveKey}, f${lightcurve.frequency}`, + name: `${instrumentModule}, f${lightcurve.frequency}`, showlegend: false, visible: isGroupHidden(lightcurve.frequency, instrumentModule) ? 'legendonly' @@ -475,7 +491,13 @@ export function Lightcurve({ return; } - populatePoint(data, lightcurve, lightcurveKey, idx, isFlagged); + populatePoint( + data, + lightcurve, + lightcurveKey, + idx, + isFlagged && showFlaggedMarkerStyling + ); }); finalData.push(data); @@ -507,6 +529,7 @@ export function Lightcurve({ lightcurveData, hideFlaggedData, hiddenLegendGroups, + showFlaggedMarkerStyling, ]); /** @@ -519,7 +542,7 @@ export function Lightcurve({ margin: { t: legendMarginTop }, yaxis: { title: { - text: 'Flux Density (Jy)', + text: 'Flux Density (mJy)', }, }, xaxis: { @@ -946,7 +969,7 @@ export function Lightcurve({ Flux Density: {String(Number(clickedMarkerData.data.y).toFixed(3))} +/-{' '} {String(Number(clickedMarkerData.data.flux_err).toFixed(3))}{' '} - Jy + mJy

Flags: diff --git a/src/components/UnassignedLightcurvePlot.tsx b/src/components/UnassignedLightcurvePlot.tsx index fb799d9..41383db 100644 --- a/src/components/UnassignedLightcurvePlot.tsx +++ b/src/components/UnassignedLightcurvePlot.tsx @@ -1,125 +1,47 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import Plotly, { Config, Data, Layout } from 'plotly.js-dist-min'; +import { useMemo } from 'react'; import { UnassignedFluxMeasurement } from '../types'; -import { frequencyColor, moduleSymbol } from '../configs/socolors'; -import { - buildInstrumentLegendTraces, - groupMeasurementsByInstrument, - INSTRUMENT_LEGEND_LAYOUT, - INSTRUMENT_LEGEND_MARGIN, -} from '../utils/unassignedPlotHelpers'; +import { Lightcurve } from './Lightcurve'; +import { toInstrumentLightcurveData } from '../utils/unassignedPlotHelpers'; +import { DEFAULT_PLOT_LAYOUT } from '../configs/constants'; type UnassignedLightcurvePlotProps = { + sourceId: string; measurements: UnassignedFluxMeasurement[]; height?: number; }; -const PLOT_CONFIG: Partial = { - displaylogo: false, - responsive: true, -}; - -function buildTrace(key: string, group: UnassignedFluxMeasurement[]): Data { - const [frequency, module] = key.split(':'); - const color = frequencyColor(Number(frequency)); - return { - name: `${module} ยท f${frequency}`, - // The frequency/optics-tube legend is built separately from legend-only proxy traces (see - // buildInstrumentLegendTraces) so it can split color and shape into two legends. - showlegend: false, - x: group.map((m) => new Date(m.time)), - y: group.map((m) => m.flux), - error_y: { - type: 'data', - array: group.map((m) => m.flux_err), - visible: true, - color: color, - thickness: 1.5, - }, - customdata: group.map((m) => [m.frequency, m.module]), - mode: 'markers', - type: 'scatter', - marker: { - color, - symbol: moduleSymbol(module), - size: 6, - line: { width: 1, color }, - }, - hovertemplate: - 'Time: %{x|%Y-%m-%d %H:%M:%S UTC}
' + - 'Flux: %{y:.3f} mJy
' + - 'Frequency: %{customdata[0]} GHz
' + - 'Optic tube: %{customdata[1]}', - }; -} - -/** Interactive flux-versus-time plot for an unassigned source; one marker trace per - * (frequency, module), colored by frequency and shaped by optics tube. */ +/** Interactive flux-versus-time plot for an unassigned source, rendered through the same + * Lightcurve component an assigned source's plot uses. There are no cutout images for + * unassigned measurements yet, so a clicked marker's tooltip will show "Cutout Not Found" + * for now. */ export function UnassignedLightcurvePlot({ + sourceId, measurements, height = 400, }: UnassignedLightcurvePlotProps) { - const plotRef = useRef(null); - const [isDataReady, setIsDataReady] = useState(false); - - const plotData = useMemo(() => { - const grouped = groupMeasurementsByInstrument(measurements); - return [ - ...Array.from(grouped.entries()).map(([key, group]) => - buildTrace(key, group) - ), - ...buildInstrumentLegendTraces(measurements), - ]; - }, [measurements]); - - const plotLayout = useMemo>(() => { - const maxFlux = measurements.length - ? Math.max(...measurements.map((m) => m.flux)) - : 1; - return { - autosize: true, - margin: INSTRUMENT_LEGEND_MARGIN, - hovermode: 'closest', - showlegend: true, - ...INSTRUMENT_LEGEND_LAYOUT, - xaxis: { title: { text: 'Observation time (UTC)' } }, - yaxis: { - title: { text: 'Flux (mJy)' }, - range: [0, maxFlux * 1.05], - zeroline: false, - }, - }; - }, [measurements]); - - useEffect(() => { - const el = plotRef.current; - if (!el) return; - - setIsDataReady(false); - void Plotly.newPlot(el, plotData, plotLayout, PLOT_CONFIG).then(() => - setIsDataReady(true) - ); - - return () => { - Plotly.purge(el); - }; - }, [plotData, plotLayout]); + const lightcurveData = useMemo( + () => toInstrumentLightcurveData(sourceId, measurements), + [sourceId, measurements] + ); if (!measurements.length) { return

No detections to plot.

; } return ( -
-
- {!isDataReady && ( -
- Loading... -
- )} -
+ ); } diff --git a/src/components/UnassignedSource.tsx b/src/components/UnassignedSource.tsx index 66de72c..ce93795 100644 --- a/src/components/UnassignedSource.tsx +++ b/src/components/UnassignedSource.tsx @@ -310,7 +310,10 @@ export function UnassignedSource() { showCard={data?.source.status !== 'merged'} > {data ? ( - + ) : ( 'Loading...' )} diff --git a/src/utils/unassignedPlotHelpers.ts b/src/utils/unassignedPlotHelpers.ts index 49b08f7..94880ac 100644 --- a/src/utils/unassignedPlotHelpers.ts +++ b/src/utils/unassignedPlotHelpers.ts @@ -1,5 +1,9 @@ import { Data, Legend } from 'plotly.js-dist-min'; -import { UnassignedFluxMeasurement } from '../types'; +import { + InstrumentLightcurveData, + InstrumentLightcurveMeasurements, + UnassignedFluxMeasurement, +} from '../types'; import { buildInstrumentLegendTraces as buildInstrumentLegendTracesForItems, DEFAULT_INSTRUMENT_LEGEND_LAYOUT as BASE_INSTRUMENT_LEGEND_LAYOUT, @@ -23,6 +27,47 @@ export function groupMeasurementsByInstrument( return grouped; } +/** Converts a flat list of unassigned flux measurements into the same InstrumentLightcurveData + * shape Lightcurve.tsx expects for an assigned source's instrument-strategy lightcurve, grouped + * by (frequency, module) via groupMeasurementsByInstrument - each unassigned measurement is + * already a fixed (frequency, module) pair, exactly like an instrument-strategy point, just not + * yet grouped into a record keyed that way. Lets UnassignedLightcurvePlot.tsx render an + * unassigned source's flux history through the same Lightcurve component/interactions (flagging, + * marker click, cutouts once those exist for unassigned measurements too) as an assigned source, + * rather than maintaining a second plot implementation. */ +export function toInstrumentLightcurveData( + sourceId: string, + measurements: UnassignedFluxMeasurement[] +): InstrumentLightcurveData { + const lightcurves: Record = {}; + + for (const [key, group] of groupMeasurementsByInstrument(measurements)) { + lightcurves[key] = { + frequency: group[0].frequency, + source_id: sourceId, + module: group[0].module, + measurement_id: group.map((m) => m.measurement_id), + time: group.map((m) => m.time), + ra: group.map((m) => m.ra), + ra_uncertainty: group.map((m) => m.ra_uncertainty ?? 0), + dec: group.map((m) => m.dec), + dec_uncertainty: group.map((m) => m.dec_uncertainty ?? 0), + flux: group.map((m) => m.flux), + flux_err: group.map((m) => m.flux_err), + extra: group.map((m) => m.extra ?? null), + }; + } + + return { + source_id: sourceId, + // Unassigned measurements aren't fetched through the binned/unbinned endpoints - "none" is + // the same convention BaseLightcurveData uses for an unbinned assigned-source response. + binning_strategy: 'none', + selection_strategy: 'instrument', + lightcurves, + }; +} + /** Offsets a position from the source position onto the source's tangent plane, in arcminutes. * Equivalent to astropy's SkyCoord.spherical_offsets_to at the arcminute scales these plots * cover. */