Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"plotly.js-dist-min": "^3.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-range": "^1.10.0",
"react-router": "^8.3.0"
},
"devDependencies": {
Expand Down
127 changes: 112 additions & 15 deletions src/components/AllSkyMap.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { CSSProperties, useEffect, useRef, useState } from 'react';
import {
CSSProperties,
useCallback,
useEffect,
useRef,
useState,
useMemo,
} from 'react';
import SourceFluxFilter from './SourceFluxFilter';
import { MIN_MAX_FLUX_VALUES } from '../configs/constants';
import { FREQUENCY_COLORS, SO_FALLBACK_COLOR } from '../configs/socolors';

export interface SkySource {
sourceId: string;
ra: number;
dec: number;
name: string;
properties?: {
median_flux: Record<string, number>;
};
}

interface AllSkyMapProps {
sources: SkySource[];
bands: Set<string>;
title?: string;
subtitle?: string;
height?: CSSProperties['height'];
Expand All @@ -19,24 +33,49 @@ interface HoveredSource {
name: string;
ra: number;
dec: number;
x: number;
y: number;
/** Which side of the marker the tooltip is anchored to, and how far from it;
* lets the tooltip flip to the marker's left near the right edge instead of
* overflowing the (overflow: hidden) all-sky-wrapper. */
horizontal: { side: 'left' | 'right'; offset: number };
}

// Rough upper bound on the tooltip's rendered width (name + RA/Dec lines), used to decide
// whether anchoring it to the marker's right edge would run it past the container's edge.
const TOOLTIP_WIDTH_ESTIMATE = 180;

// Creates a shape function for Aladin's catalogs used to update the marker color
const getShapeFunction =
(appliedBand: string) =>
(source: { x: number; y: number }, canvasCtx: CanvasRenderingContext2D) => {
canvasCtx.beginPath();
canvasCtx.arc(source.x, source.y, 4, 0, 2 * Math.PI, false);
canvasCtx.closePath();
// Sets AllSkyMap marker colors to the filter's applied freq band, if selected
// and defined in FREQUENCY_COLORS
canvasCtx.fillStyle = FREQUENCY_COLORS[appliedBand] ?? SO_FALLBACK_COLOR;
canvasCtx.globalAlpha = 0.8;
canvasCtx.fill();
};

/**
* Renders every source's (RA, Dec) position on an all-sky Mollweide projection using
* Aladin Lite (loaded globally as window.A via the script tag in index.html - see
* AladinViewer.tsx for the same pattern used on the Source page).
*/
export default function AllSkyMap({
sources,
bands,
title = 'Sources by position',
subtitle = "Click a source's marker to preview its light curve",
height = 600,
setClickedSourceId,
}: AllSkyMapProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const aladinInstanceRef = useRef<Aladin | null>(null);
const catalogRef = useRef<Catalog | null>(null);
const [appliedBand, setAppliedBand] = useState('');
const [appliedRange, setAppliedRange] = useState(MIN_MAX_FLUX_VALUES);

// setClickedSourceId isn't guaranteed to be a stable reference across every render (its
// caller may recreate it), so keep it in a ref for the init effect below to read. Putting it
Expand Down Expand Up @@ -91,12 +130,17 @@ export default function AllSkyMap({
typeof object.ra === 'number' &&
typeof object.dec === 'number'
) {
const containerWidth = containerRef.current?.clientWidth ?? 0;
const wouldOverflowRight =
xyMouseCoords.x + TOOLTIP_WIDTH_ESTIMATE + 12 > containerWidth;
setHoveredSource({
name,
ra: object.ra,
dec: object.dec,
x: xyMouseCoords.x,
y: xyMouseCoords.y,
horizontal: wouldOverflowRight
? { side: 'right', offset: containerWidth - xyMouseCoords.x }
: { side: 'left', offset: xyMouseCoords.x },
});
}
});
Expand Down Expand Up @@ -128,14 +172,7 @@ export default function AllSkyMap({

const catalog = window.A.catalog({
name: 'All sources',
shape: (source, canvasCtx) => {
canvasCtx.beginPath();
canvasCtx.arc(source.x, source.y, 4, 0, 2 * Math.PI, false);
canvasCtx.closePath();
canvasCtx.fillStyle = '#1f77b4';
canvasCtx.globalAlpha = 0.8;
canvasCtx.fill();
},
shape: getShapeFunction(''),
});
aladin.addCatalog(catalog);
catalog.addSources(
Expand All @@ -146,13 +183,69 @@ export default function AllSkyMap({
})
)
);
catalogRef.current = catalog;
}, [sources, isDataReady]);

// The set of sources currently shown on the map. Derived (rather than copied into its own
// state on "Apply") so that it automatically recomputes if `sources` itself changes (e.g. a
// refetch) while a filter is active - otherwise a stale filter snapshot would keep hiding
// markers from the old source list after the catalog below has already been rebuilt with new
// ones.
const visibleSources = useMemo(() => {
if (appliedBand === '') return sources;
return sources.filter((s) => {
const flux = s.properties?.median_flux[appliedBand];
return flux != null && flux >= appliedRange[0] && flux <= appliedRange[1];
});
}, [sources, appliedBand, appliedRange]);

// Applies the derived visible set to the Aladin catalog. Re-runs whenever `visibleSources`
// changes, which includes right after the catalog-rebuild effect above runs (since that
// effect shares the `sources` dependency), so a newly rebuilt catalog picks the active filter
// back up instead of momentarily showing every source.
useEffect(() => {
const catalog = catalogRef.current;
if (!catalog) return;
const visibleIds = new Set(visibleSources.map((s) => s.sourceId));
catalog.setShape(getShapeFunction(appliedBand));
catalog.getSources().forEach((s) => {
const isVisible = visibleIds.has(s.data?.sourceId);
if (isVisible) {
s.show();
} else {
s.hide();
}
});
}, [visibleSources, appliedBand]);

// Stable identities so the memoized SourceFluxFilter doesn't re-render just because AllSkyMap
// re-rendered for an unrelated reason (e.g. hoveredSource changing on every mouse move).
const handleApplyFilter = useCallback((band: string, range: number[]) => {
setAppliedBand(band);
setAppliedRange(range);
}, []);

const handleClearFilter = useCallback(() => {
setAppliedBand('');
setAppliedRange(MIN_MAX_FLUX_VALUES);
}, []);

return (
<div className="all-sky-wrapper">
<div className="title-container">
<p className="title-text">{title}</p>
<p className="subtitle-text">{subtitle}</p>
<div className="all-sky-header">
<div className="title-container">
<p className="title-text">{title}</p>
<p className="subtitle-text">{subtitle}</p>
</div>
<SourceFluxFilter
sources={sources}
bands={bands}
visibleCount={visibleSources.length}
appliedBand={appliedBand}
appliedRange={appliedRange}
onApply={handleApplyFilter}
onClear={handleClearFilter}
/>
</div>
<div
ref={containerRef}
Expand All @@ -170,7 +263,11 @@ export default function AllSkyMap({
{hoveredSource && (
<div
className="all-sky-tooltip"
style={{ left: hoveredSource.x + 12, top: hoveredSource.y + 12 }}
style={{
[hoveredSource.horizontal.side]:
hoveredSource.horizontal.offset + 12,
top: hoveredSource.y + 12,
}}
>
<div className="all-sky-tooltip-name">{hoveredSource.name}</div>
<div>RA: {hoveredSource.ra.toFixed(3)}°</div>
Expand Down
Loading
Loading