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
; } 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({ ))} - + ('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/SourceFluxFilter.tsx b/src/components/SourceFluxFilter.tsx index 7a195c5..ed9e011 100644 --- a/src/components/SourceFluxFilter.tsx +++ b/src/components/SourceFluxFilter.tsx @@ -42,7 +42,7 @@ function SourceFluxFilter({

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

= { - 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/configs/stats.ts b/src/configs/stats.ts index 39fb792..eb3705a 100644 --- a/src/configs/stats.ts +++ b/src/configs/stats.ts @@ -15,26 +15,26 @@ export const statsKeysToDisplaySpecs: Record = { 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', }, }; 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. */