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
194 changes: 194 additions & 0 deletions src/components/AllSkyMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { useEffect, useRef, useMemo } from 'react';
import Plotly, {
Layout,
Data,
PlotMouseEvent,
PlotlyHTMLElement,
} from 'plotly.js-dist-min';
import { useNavigate } from 'react-router';

export interface SkySource {
sourceId: string;
ra: number;
dec: number;
name: string;
/** Optional: drives marker color via a continuous colorscale */
value?: number;
}

interface AllSkyMapProps {
sources: SkySource[];
/** Plotly geo projection type. Aitoff and Mollweide are the most common for astronomy. */
projection?:
| 'mollweide'
| 'aitoff'
| 'orthographic'
| 'equirectangular'
| 'stereographic';
/** Axis system label shown on the plot – purely cosmetic, does NOT reproject the data */
coordinateSystem?: 'Equatorial (RA/Dec)' | 'Galactic (l/b)';
/** Color the markers by this field. "none" uses a flat color. */
colorBy?: 'value' | 'dec' | 'none';
title?: string;
height?: number;
}

/**
* Remap RA from [0, 360) → [-180, 180] so the map centers on RA = 0h.
* Plotly's geo treats longitude 0 as the center; this keeps the center at RA 0h.
*/
function raToLon(ra: number): number {
return ra > 180 ? ra - 360 : ra;
}

export default function AllSkyMap({
sources,
projection = 'mollweide',
coordinateSystem = 'Equatorial (RA/Dec)',
colorBy = 'none',
title,
height = 500,
}: AllSkyMapProps) {
const containerRef = useRef<PlotlyHTMLElement>(null);
const navigate = useNavigate();

const lon = useMemo(() => sources.map((s) => raToLon(s.ra)), [sources]);
const lat = useMemo(() => sources.map((s) => s.dec), [sources]);
const customdata = useMemo(() => sources.map((s) => s.sourceId), [sources]);

const markerColor: number[] | string = useMemo(() => {
if (colorBy === 'value') return sources.map((s) => s.value ?? 0);
if (colorBy === 'dec') return lat;
return '#1f77b4';
}, [sources, colorBy, lat]);

const hoverText = useMemo(
() =>
sources.map(
(s) =>
`${s.name ?? 'Source'}<br>RA: ${s.ra.toFixed(3)}°<br>Dec: ${s.dec.toFixed(3)}°${
s.value !== undefined ? `<br>Value: ${s.value.toFixed(3)}` : ''
}`
),
[sources]
);

const isColored = colorBy !== 'none';

useEffect(() => {
const el = containerRef.current;
if (!el) return;

const data: Data[] = [
{
type: 'scattergeo',
lon,
lat,
customdata,
mode: 'markers',
text: hoverText,
hoverinfo: 'text',
marker: {
size: 5,
opacity: 0.8,
color: markerColor as never,
...(isColored
? {
colorscale: 'Viridis',
showscale: true,
colorbar: {
title: colorBy === 'dec' ? 'Dec (°)' : 'Value',
thickness: 12,
len: 0.6,
},
}
: {}),
},
},
];

const layout: Partial<Layout> = {
title: title ? { text: title, font: { size: 15 } } : undefined,
height,
margin: { t: title ? 48 : 24, b: 8, l: 8, r: 8 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
geo: {
projection: { type: projection as never },
showland: false,
showocean: false,
showlakes: false,
showcoastlines: false,
showcountries: false,
showframe: true,
framewidth: 1,
lonaxis: {
showgrid: true,
gridcolor: '#2a3a4a',
gridwidth: 0.5,
dtick: 30,
tick0: 0,
},
lataxis: {
showgrid: true,
gridcolor: '#2a3a4a',
gridwidth: 0.5,
dtick: 30,
},
},
modebar: {
bgcolor: 'rgba(255,255,255,0.5)',
},
annotations: [
{
text: coordinateSystem,
xref: 'paper',
yref: 'paper',
x: 0.01,
y: 0.01,
showarrow: false,
font: { size: 11, color: '#888888' },
},
],
};

void Plotly.react(el, data, layout, {
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['select2d', 'lasso2d'],
responsive: true,
});

void el.on('plotly_click', (e: PlotMouseEvent) => {
e.event.preventDefault();
e.event.stopPropagation();
const sourceId = e.points[0].customdata as string;
const sourcePageUrl = '/source/' + sourceId;
void navigate(sourcePageUrl);
});

const resizeObserver = new ResizeObserver(() => Plotly.Plots.resize(el));
resizeObserver.observe(el);

return () => {
resizeObserver.disconnect();
Plotly.purge(el);
};
}, [
lon,
lat,
customdata,
navigate,
markerColor,
hoverText,
isColored,
colorBy,
projection,
coordinateSystem,
title,
height,
]);

/* @ts-expect-error plotlyRef is an extended version of an HTMLDivElement*/
return <div ref={containerRef} />;
}
80 changes: 0 additions & 80 deletions src/components/AllSourcesPlot.tsx

This file was deleted.

5 changes: 2 additions & 3 deletions src/components/Lightcurve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import Plotly, {
Datum,
PlotMouseEvent,
ScatterData,
ErrorBar,
PlotDatum,
PlotlyHTMLElement,
Layout,
Expand Down Expand Up @@ -147,7 +146,7 @@ export function Lightcurve({
color: undefined,
thickness: 1.0,
width: 1.0,
} as ErrorBar,
},
type: 'scatter',
mode: 'markers',
marker: {
Expand Down Expand Up @@ -175,7 +174,7 @@ export function Lightcurve({
color: undefined,
thickness: 1.0,
width: 1.0,
} as ErrorBar,
},
type: 'scatter',
mode: 'markers',
marker: {
Expand Down
31 changes: 19 additions & 12 deletions src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import home_content from '../configs/home_content.md?raw';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router';
import { useState } from 'react';
import { AllSourcesPlot } from './AllSourcesPlot';
import { lightcurveApi } from '../api/client';
import AllSkyMap from './AllSkyMap';

/** Renders the "home" page of the web app */
export function Main() {
Expand Down Expand Up @@ -52,6 +52,24 @@ export function Main() {
return (
<main>
<ReactMarkdown>{home_content}</ReactMarkdown>
<p>
Below is a plot of sources by their (RA,Dec) position. Click a source's
marker to view its light curve and data on its source page.
</p>
{initialLoadData?.sources ? (
<div className="sources-plot-container">
<AllSkyMap
sources={initialLoadData.sources.map((s) => ({
ra: s.ra,
dec: s.dec,
name: s.name,
sourceId: s.source_id,
}))}
/>
</div>
) : (
<div className="sources-plot-placeholder"></div>
)}
<p>
Below is an example light curve.{' '}
<Link to={sourceUrl}>View the source page</Link> to learn more about the
Expand All @@ -72,17 +90,6 @@ export function Main() {
) : (
<div className="home-lightcurve-placeholder" />
)}
<p>
Below is a plot of sources by their (RA,Dec) position. Click a source's
marker to view its light curve and data on its source page.
</p>
{initialLoadData?.sources ? (
<div className="sources-plot-container">
<AllSourcesPlot sources={initialLoadData.sources} />
</div>
) : (
<div className="sources-plot-placeholder"></div>
)}
</main>
);
}
Loading