From 420f0f581c270dd80b4a4a3cf9344ede588d9f7b Mon Sep 17 00:00:00 2001 From: liang Date: Sun, 18 Jan 2026 15:32:43 -0500 Subject: [PATCH 1/3] feat(viewer): add highlight prop for programmatic point highlighting Add a new `highlight` prop to EmbeddingAtlasProps that allows external code to programmatically highlight specific data points on the map. When set to an array of row IDs: - The first point animates into view with tooltip - Orange circles are drawn at all highlighted points (same as search) - Table view scrolls to show highlighted row Implementation: - Sets highlightStore to trigger animation to first point - Sets searchResultStore to render orange overlay circles for all IDs Usage: ```tsx ``` Pass null or empty array to clear the highlight. --- packages/viewer/src/EmbeddingAtlas.svelte | 31 ++++++++++++++++++++++- packages/viewer/src/api.ts | 8 ++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/viewer/src/EmbeddingAtlas.svelte b/packages/viewer/src/EmbeddingAtlas.svelte index 376434f3..0e8c5898 100644 --- a/packages/viewer/src/EmbeddingAtlas.svelte +++ b/packages/viewer/src/EmbeddingAtlas.svelte @@ -58,6 +58,7 @@ onExportSelection, onStateChange, cache, + highlight: highlightProp = null, }: EmbeddingAtlasProps = $props(); const { colorScheme, userColorScheme } = makeColorSchemeStore(); @@ -285,6 +286,34 @@ chartThemeStore.set(chartTheme ?? undefined); }); + // Highlight store for programmatic highlighting (single point animation + tooltip) + let highlightStore = writable(null); + + // Sync external highlight prop to internal stores + $effect.pre(() => { + if (highlightProp != null && highlightProp.length > 0) { + // Set highlight store to animate to first point + highlightStore.set(highlightProp[0]); + // Set search result store to show orange overlay circles for all points + searchResultStore.set({ + query: "highlight", + mode: "highlight", + ids: highlightProp, + label: `${highlightProp.length} highlighted`, + highlight: "", + items: [], + }); + } else { + highlightStore.set(null); + // Only clear if we previously set it via highlight prop + let current = null; + searchResultStore.subscribe((v) => (current = v))(); + if (current?.mode === "highlight") { + searchResultStore.set(null); + } + } + }); + let chartContext: ChartContext = { coordinator: coordinator, filter: crossFilter, @@ -299,7 +328,7 @@ searchModes: searchModes, search: doSearch, searchResult: searchResultStore, - highlight: writable(null), + highlight: highlightStore, embeddingViewConfig: embeddingViewConfig, embeddingViewLabels: embeddingViewLabels, tableCellRenderers: tableCellRenderers, diff --git a/packages/viewer/src/api.ts b/packages/viewer/src/api.ts index 71cc9aef..6f3fe597 100644 --- a/packages/viewer/src/api.ts +++ b/packages/viewer/src/api.ts @@ -87,6 +87,14 @@ export interface EmbeddingAtlasProps { /** A cache to speed up initialization of the viewer. */ cache?: Cache | null; + + /** + * An array of row IDs to highlight on the embedding view. + * When set, orange circles will be drawn at the specified points. + * The view will animate to show the first point in the array. + * Pass null or an empty array to clear the highlight. + */ + highlight?: any[] | null; } export interface EmbeddingAtlasState { From 5aa9c2694e934aa6e8db97c27dbb63f25421df2d Mon Sep 17 00:00:00 2001 From: liang Date: Sun, 18 Jan 2026 21:49:36 -0500 Subject: [PATCH 2/3] Simplify: only set highlightStore, remove searchResultStore usage --- packages/viewer/src/EmbeddingAtlas.svelte | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/viewer/src/EmbeddingAtlas.svelte b/packages/viewer/src/EmbeddingAtlas.svelte index 0e8c5898..3f8fd9d9 100644 --- a/packages/viewer/src/EmbeddingAtlas.svelte +++ b/packages/viewer/src/EmbeddingAtlas.svelte @@ -286,31 +286,15 @@ chartThemeStore.set(chartTheme ?? undefined); }); - // Highlight store for programmatic highlighting (single point animation + tooltip) + // Highlight store for programmatic highlighting let highlightStore = writable(null); - // Sync external highlight prop to internal stores + // Sync external highlight prop to internal store $effect.pre(() => { if (highlightProp != null && highlightProp.length > 0) { - // Set highlight store to animate to first point highlightStore.set(highlightProp[0]); - // Set search result store to show orange overlay circles for all points - searchResultStore.set({ - query: "highlight", - mode: "highlight", - ids: highlightProp, - label: `${highlightProp.length} highlighted`, - highlight: "", - items: [], - }); } else { highlightStore.set(null); - // Only clear if we previously set it via highlight prop - let current = null; - searchResultStore.subscribe((v) => (current = v))(); - if (current?.mode === "highlight") { - searchResultStore.set(null); - } } }); From ff45f430d571a27aeca78aa653840c06662c8cbf Mon Sep 17 00:00:00 2001 From: liang Date: Tue, 20 Jan 2026 22:21:20 -0500 Subject: [PATCH 3/3] Add highlightIds store for programmatic multi-point highlighting Implement a clean approach using a dedicated highlightIdsStore instead of reusing searchResultStore. This provides: - highlightStore: single point (animation + tooltip) - first ID from prop - highlightIdsStore: multiple points (overlay circles) - all IDs from prop Changes: - chart.ts: Add highlightIds to ChartContext interface - EmbeddingAtlas.svelte: Create highlightIdsStore and sync with highlight prop - Embedding.svelte: Subscribe to highlightIds and render overlay circles --- packages/viewer/src/EmbeddingAtlas.svelte | 10 +++++-- packages/viewer/src/charts/chart.ts | 3 +++ .../src/charts/embedding/Embedding.svelte | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/viewer/src/EmbeddingAtlas.svelte b/packages/viewer/src/EmbeddingAtlas.svelte index 3f8fd9d9..530f0233 100644 --- a/packages/viewer/src/EmbeddingAtlas.svelte +++ b/packages/viewer/src/EmbeddingAtlas.svelte @@ -286,15 +286,20 @@ chartThemeStore.set(chartTheme ?? undefined); }); - // Highlight store for programmatic highlighting + // Highlight store for single point (animation + tooltip) let highlightStore = writable(null); - // Sync external highlight prop to internal store + // Highlight IDs store for multiple points (overlay circles) + let highlightIdsStore = writable(null); + + // Sync external highlight prop to internal stores $effect.pre(() => { if (highlightProp != null && highlightProp.length > 0) { highlightStore.set(highlightProp[0]); + highlightIdsStore.set(highlightProp); } else { highlightStore.set(null); + highlightIdsStore.set(null); } }); @@ -313,6 +318,7 @@ search: doSearch, searchResult: searchResultStore, highlight: highlightStore, + highlightIds: highlightIdsStore, embeddingViewConfig: embeddingViewConfig, embeddingViewLabels: embeddingViewLabels, tableCellRenderers: tableCellRenderers, diff --git a/packages/viewer/src/charts/chart.ts b/packages/viewer/src/charts/chart.ts index 8a6752d8..e04edfdd 100644 --- a/packages/viewer/src/charts/chart.ts +++ b/packages/viewer/src/charts/chart.ts @@ -89,6 +89,9 @@ export interface ChartContext { /** The current highlight point. When this changes, supported views will highlight the given point. */ highlight: Writable; + /** Multiple highlight points. When set, supported views will show overlay circles at these points. */ + highlightIds?: Readable; + /** Configuration for the embedding view. See docs for the EmbeddingView. */ embeddingViewConfig?: EmbeddingViewConfig | null; diff --git a/packages/viewer/src/charts/embedding/Embedding.svelte b/packages/viewer/src/charts/embedding/Embedding.svelte index ef2e53f8..e5b28c8f 100644 --- a/packages/viewer/src/charts/embedding/Embedding.svelte +++ b/packages/viewer/src/charts/embedding/Embedding.svelte @@ -150,6 +150,33 @@ }), ); + // Subscribe to highlightIds for programmatic multi-point highlighting + $effect.pre(() => { + if (context.highlightIds == null) return; + return context.highlightIds.subscribe(async (ids) => { + if (ids == null || ids.length == 0) { + overlayProps = null; + return; + } + let r = Array.from( + await context.coordinator.query( + SQL.Query.from(context.table) + .select({ identifier: SQL.column(context.id), x: SQL.column(spec.data.x), y: SQL.column(spec.data.y) }) + .where( + SQL.isIn( + context.id, + ids.map((x) => SQL.literal(x)), + ), + ), + ), + ) as DataPoint[]; + overlayProps = { + center: null, + points: r, + }; + }); + }); + async function animateToPoint(identifier: RowID): Promise { let defaultScale = await context.cache.value(`embedding/default-viewport-scale/${spec.data.x},${spec.data.y}`, () => defaultViewportScale(context.coordinator, context.table, spec.data.x, spec.data.y),