Skip to content
Open
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
69 changes: 52 additions & 17 deletions src/components/AllSkyMap.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import {
CSSProperties,
useCallback,
useEffect,
useRef,
useState,
useMemo,
} from 'react';
import { 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';
import {
FREQUENCY_COLORS,
SO_FALLBACK_COLOR,
frequencyKey,
} from '../configs/socolors';

export interface SkySource {
sourceId: string;
Expand All @@ -25,25 +22,33 @@ interface AllSkyMapProps {
bands: Set<string>;
title?: string;
subtitle?: string;
height?: CSSProperties['height'];
height?: number;
setClickedSourceId: (id: string) => void;
}

interface HoveredSource {
name: string;
ra: number;
dec: 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 };
/** Same idea as horizontal, but for the bottom edge: flips the tooltip to sit above the marker
* instead of below it near the bottom edge. There's no analogous top-edge check because the
* tooltip's default ('top') anchoring already starts below the cursor, so it can't overflow
* upward regardless of how close to the top edge the marker is. */
vertical: { side: 'top' | 'bottom'; 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;

// Rough upper bound on the tooltip's rendered height (name + RA/Dec lines), used to decide
// whether anchoring it below the marker would run it past the container's bottom edge.
const TOOLTIP_HEIGHT_ESTIMATE = 70;

// Creates a shape function for Aladin's catalogs used to update the marker color
const getShapeFunction =
(appliedBand: string) =>
Expand All @@ -53,7 +58,9 @@ const getShapeFunction =
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;
const freq = Number(appliedBand.split('_')[1]);
canvasCtx.fillStyle =
FREQUENCY_COLORS[frequencyKey(freq)] ?? SO_FALLBACK_COLOR;
canvasCtx.globalAlpha = 0.8;
canvasCtx.fill();
};
Expand Down Expand Up @@ -90,6 +97,14 @@ export default function AllSkyMap({
const [hoveredSource, setHoveredSource] = useState<HoveredSource | null>(
null
);
// Aladin's fullscreen mode (with the default realFullscreen: false) doesn't use the browser's
// real Fullscreen API; instead, it makes the container div position:fixed and covers the whole
// viewport via a CSS class. That means xyMouseCoords (already relative to the container's own
// top-left) become viewport-relative too, but our tooltip's own position:absolute is anchored
// to all-sky-wrapper - a box that no longer corresponds to where the map is actually rendered
// once the container escapes it via position:fixed. Tracking this lets the tooltip switch to
// position:fixed itself (see the render below) so it keeps tracking the cursor in both modes.
const [isFullscreen, setIsFullscreen] = useState(false);

// Initialize the Aladin viewer once; it's never torn down for the lifetime of this
// component (see App.tsx, which keeps Main mounted across navigation).
Expand All @@ -107,13 +122,18 @@ export default function AllSkyMap({
window.A.init
.then(() => {
if (cancelled || !window.A) return;
// calculate a FOV based on user's viewport width to enforce full [360,180] on load
const fov =
window.innerWidth < height
? 360 / (window.innerWidth / height)
: 360 * (window.innerWidth / height);
const aladin = window.A.aladin(el, {
fov: 360,
fov,
cooFrame: 'equatorial',
projection: 'MOL',
});
// fov as an init option is unreliable on this build; set it explicitly.
aladin.setFov(360);
// for good measure, in case aladin ignores the init structure
aladin.setFov(fov);
aladinInstanceRef.current = aladin;

aladin.on('objectClicked', (object) => {
Expand All @@ -131,21 +151,30 @@ export default function AllSkyMap({
typeof object.dec === 'number'
) {
const containerWidth = containerRef.current?.clientWidth ?? 0;
const containerHeight = containerRef.current?.clientHeight ?? 0;
const wouldOverflowRight =
xyMouseCoords.x + TOOLTIP_WIDTH_ESTIMATE + 12 > containerWidth;
const wouldOverflowBottom =
xyMouseCoords.y + TOOLTIP_HEIGHT_ESTIMATE + 12 > containerHeight;
setHoveredSource({
name,
ra: object.ra,
dec: object.dec,
y: xyMouseCoords.y,
horizontal: wouldOverflowRight
? { side: 'right', offset: containerWidth - xyMouseCoords.x }
: { side: 'left', offset: xyMouseCoords.x },
vertical: wouldOverflowBottom
? { side: 'bottom', offset: containerHeight - xyMouseCoords.y }
: { side: 'top', offset: xyMouseCoords.y },
});
}
});
aladin.on('objectHoveredStop', () => setHoveredSource(null));

aladin.on('fullScreenToggled', (isInFullscreen) => {
setIsFullscreen(isInFullscreen);
});

setIsDataReady(true);
})
.catch(() => {
Expand Down Expand Up @@ -264,9 +293,15 @@ export default function AllSkyMap({
<div
className="all-sky-tooltip"
style={{
// In fullscreen, the Aladin container (and so xyMouseCoords, which these values are
// derived from) is viewport-relative rather than relative to all-sky-wrapper - see
// isFullscreen's comment above - so the tooltip has to switch to matching
// viewport-relative positioning too, or it ends up anchored to a box that no longer
// lines up with where the map is actually rendered.
position: isFullscreen ? 'fixed' : 'absolute',
[hoveredSource.horizontal.side]:
hoveredSource.horizontal.offset + 12,
top: hoveredSource.y + 12,
[hoveredSource.vertical.side]: hoveredSource.vertical.offset + 12,
}}
>
<div className="all-sky-tooltip-name">{hoveredSource.name}</div>
Expand Down
25 changes: 16 additions & 9 deletions src/components/Lightcurve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function makeLegendProxyTrace(
thickness: 1.0,
width: 1.0,
},
type: 'scatter',
type: 'scattergl',
mode: 'markers',
marker: {
size: 5,
Expand Down Expand Up @@ -276,7 +276,7 @@ export function Lightcurve({
thickness: 1.0,
width: 1.0,
},
type: 'scatter',
type: 'scattergl',
mode: 'markers',
marker: {
size: 5,
Expand Down Expand Up @@ -328,7 +328,7 @@ export function Lightcurve({
thickness: 1.0,
width: 1.0,
},
type: 'scatter',
type: 'scattergl',
mode: 'markers',
marker: {
size: 5,
Expand Down Expand Up @@ -554,9 +554,17 @@ export function Lightcurve({
/** Creates the Plotly plot and attaches our handlers to the plot */
useEffect(() => {
const stablePlotlyReference = plotlyRef.current;
if (stablePlotlyReference) {
setIsDataReady(false);
if (!stablePlotlyReference) return;

setIsDataReady(false);

// Plotly.newPlot is a synchronous, potentially expensive call for a lightcurve with many
// traces/points; since React flushes every effect for a commit in one synchronous
// pass, running it directly here would block any other component's effects in that same
// commit (e.g. Main.tsx's dialog-open transition-kickoff effect) until it finishes. Deferring
// it to the next animation frame lets those effects run first, so this Lightcurve mounting
// with already-cached (near-instant) data doesn't stall an in-progress UI transition.
const raf = requestAnimationFrame(() => {
void Plotly.newPlot(
stablePlotlyReference,
plotData,
Expand All @@ -574,12 +582,11 @@ export function Lightcurve({
);

void stablePlotlyReference.on('plotly_click', handleMarkerClick);
}
});

return () => {
if (stablePlotlyReference) {
Plotly.purge(stablePlotlyReference);
}
cancelAnimationFrame(raf);
Plotly.purge(stablePlotlyReference);
};
}, [
plotData,
Expand Down
Loading
Loading