Embedding Map

Pro

A 2D projection of an embedding space with clusters and lasso selection, drawn without inviting the false reading every t-SNE plot invites. 3 variants: scatter, clusters and selection.

</>sv-embedding-map aiembeddingumaptsnecluster
import { SvEmbeddingMapComponent } from '@styloviz-pro/embedding-map';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Cluster report & lasso

Pro

When to use

  • Exploring an embedding space that has already been projected to two dimensions by UMAP, t-SNE or PCA.
  • Checking whether a clustering actually separated anything, rather than whether it looks like it did.
  • Lassoing a region of interest out of a corpus for labelling or review.

Async states

  • loading
  • error
  • retry

Item identity & owned state

Restorable state
  • selectedIds

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the projected points are being fetched; shows a chart skeleton in place of the plot.
errorSvDataError | string | nullnullLoad failure, shown as an error panel in place of the content. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
variantEmbeddingVariant'scatter'Presentation: the scatter, the cluster report, or lasso selection.
pointsreadonly EmbeddingPoint[][]The projected points.
clustersreadonly EmbeddingCluster[][]Cluster metadata — labels and optional colours.
widthnumber640Plot box, in px.
heightnumber400Plot box height in px — the SVG viewBox height the points are projected into.
neighboursnumberSV_KNN_KNeighbours used for the separation statistics.
selectedIdsreadonly string[] (two-way)[]Ids of the currently selected points.
lowPuritynumberSV_LOW_PURITYShare of same-cluster neighbours below which separation is called weak. A JUDGEMENT ABOUT ONE KIND OF DATA. Sixty per cent is weak separation for four well-defined topics and rather good for forty fine-grained intents that genuinely overlap — the same figure calls one projection broken and flatters the other. A share, so `0`–`1`.
knnMaxPointsnumberSV_KNN_MAX_POINTSPoints above which the neighbourhood statistics use a deterministic sample. A PERFORMANCE BUDGET, NOT A STATISTICAL ONE. The k-NN reads a grid index, so it is roughly linear — but it runs twice (purity and cluster tangle), synchronously, whenever `points` or `neighbours` change: about 65 ms at 2,000 points and 300 ms at 10,000 on a laptop. Set it to `SV_CANVAS_MIN` (2,000) to measure every SVG-drawn map exactly, or higher if your machines can take the longer freeze for an exact figure. The result is still LABELLED as sampled whatever the cap is.
rendererSvEmbeddingRenderer'auto'How the plot is drawn. `'auto'` BECAUSE NEITHER ANSWER IS RIGHT AT BOTH ENDS. Below a couple of thousand points the SVG is better and not merely adequate — real nodes mean real hover, a `<title>` per dot and a selection the browser paints for you. Above it the DOM is the ceiling: 100,000 elements is a layout the browser does not finish. `'svg'` and `'canvas'` force it, for a host that knows something this component does not.
devicePixelRationumber | nullnullDevice pixel ratio for the canvas backing store. An input so a test and a screenshot can pin it; unset reads the browser's, clamped — see `SV_MAX_DPR`.
maxKeyboardPointsnumberSV_KEYBOARD_POINT_CEILINGPoints a keyboard reader can walk inside one cluster. A BUDGET FOR A PERSON, NOT A RENDERER — see `SV_KEYBOARD_POINT_CEILING`. Whatever it is set to, the list says what it left out.
navClusterstring | null (two-way)nullThe cluster whose points the keyboard walk is inside; `null` at the top. A `model()` RATHER THAN PRIVATE STATE. A reader four hundred rows into `billing` who follows a link and comes back lands at the top of the cluster list with no way to say where they were — unless the host can read this back and restore it, which it cannot do with a private signal.

Outputs

EventPayload typeDescription
retryvoidEmitted when the user presses the error panel's "Try again" button (retryable errors only); the host should re-run the failed load.
selectionChangereadonly EmbeddingPoint[]The reader finished a lasso.
pointSelectEmbeddingPointThe reader activated a single point.
selectedIdsChangereadonly string[]Emits the new `selectedIds` whenever it changes — the output half of `[(selectedIds)]`.
navClusterChangestring | nullEmits the new `navCluster` whenever it changes — the output half of `[(navCluster)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Pro

Bound to the names this component actually exposes.

Host class

Pro

Drive the three async inputs from your own fetch.

  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `points`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • `points` must already carry projected `x` / `y`; the component does no dimensionality reduction. `clusters` supplies labels and optional colours.
  • Three layouts via `variant`: `scatter`, `clusters` (per-cluster separation plus the entangled pairs) and `selection` (lasso).
  • Both axes share ONE scale. Stretching them independently to fill the box destroys local shape, which is the only property these layouts preserve.
  • Separation is measured as k-NN label agreement: for each point, the share of its `neighbours` (default 10) nearest neighbours in the projection that carry the same cluster.
  • `[knnMaxPoints]` (1200, from the still-exported `SV_KNN_MAX_POINTS`; set it to `SV_CANVAS_MIN`, 2000, to measure every map drawn as SVG exactly) is the point past which the statistics are computed on a deterministic even sample — labelled as sampled, never silently. It is a performance budget, not a statistical one: the grid-indexed k-NN is roughly linear but synchronous (about 65 ms at 2,000 points and 300 ms at 10,000 for both statistics), so it belongs to whoever knows what machine this runs on, and the purity figure and the tangle table always use the SAME cap so two statistics about one picture cannot disagree.
  • `[lowPurity]` (0.6, from `SV_LOW_PURITY`) is the share of same-cluster neighbours below which separation is called weak. Sixty per cent is weak for four well-defined topics and rather good for forty fine-grained intents that genuinely overlap. It is a share, so 0–1; a figure above 1 would call every projection weak — a permanent warning, which is the same as none — and is refused with a dev warning.
  • The lasso is point-in-polygon on screen coordinates, with the half-open vertex rule, so a point level with a corner is not randomly missed. It closes on release, not on every move.
  • Emits `selectionChange` with the caught points and `pointSelect` for a single dot; `selectedIds` is two-way.
  • `[maxKeyboardPoints]` (200, from `SV_KEYBOARD_POINT_CEILING`) is how many points one cluster offers to a keyboard walk. It is a budget for a person, not a renderer: two hundred rows is a long walk and two thousand is a list nobody finishes — so raise it for forty points per cluster and lower it for a million. Whatever it is, the list says what it left out.
  • `navCluster` is two-way: the cluster the keyboard walk is inside, `null` at the top level. A reader four hundred rows into one cluster who follows a link and comes back can be put back where they were, which a private signal could not do.
  • `[renderer]` (`'auto'`) picks the drawing surface: SVG below `SV_CANVAS_MIN` (2,000 points) and a canvas above it. Neither answer is right at both ends — real nodes mean real hover and a `<title>` per dot, and 100,000 of them is a layout the browser does not finish. `'svg'` and `'canvas'` force it.
  • The canvas has no per-point nodes, so the dots are not focusable and carry no `<title>`. That is stated on the chart and the keyboard route is the browse list, which renders under both renderers — the list is not a nicety here, it is the only reading a keyboard gets.
  • `[devicePixelRatio]` pins the backing store for a screenshot or a test; unset reads the browser's, clamped to 3. A canvas sized in CSS pixels on a 2× display is blurry, and a blurry scatter is one whose dots have a different apparent size — the exact property the caveat tells the reader not to interpret.
  • The neighbourhood statistics run on a uniform grid rather than a full scan: 40,000 points take about 0.6s where the scan takes about 10 minutes, and a full k=10 pass over 100,000 points takes under 3s. The grid is exact, not approximate — it returns the same neighbours the scan does, including the tie-breaks, which the specs assert point by point on a uniform cloud, a cloud flat in one axis and a lattice where every distance is a tie. `[knnMaxPoints]` is still yours to set; what changed is what it costs you.
  • Hit-testing and the lasso go through the same index. A click resolves to the NEAREST point rather than the first one in range, and the lasso tests only the cells its bounding box touches — 100,000 ray casts on pointer-up is a freeze at the moment the reader expects their selection.
  • Still not offered: zoom and pan (`AI-21-M2`). The statistics are computed once per data change rather than per frame, so the figures above are a load-time cost and not a per-interaction one.

Best practices

  • Do: Read the separation score before the picture. Two colours drawn over each other look grouped because they are coloured, not because they are apart.
  • Don't: Do not read distance off the plot. These layouts preserve which points are near each other and discard how far apart the groups are — the gap between two blobs is an artefact of the optimiser.
  • Don't: Do not read blob size as diversity. Apparent spread is set by perplexity or n_neighbors, not by the data.
  • Tip: Watch the overplotting note. Once dozens of points share a dot the darkness saturates, and a region holding 40 looks identical to one holding 400.

Accessibility

  • The plot is `role="img"` with an accessible name carrying the point count and the separation finding, so the conclusion does not depend on seeing the picture.
  • A dot carrying a `<title>` is not an accessibility story: an SVG `<circle>` is not focusable, so nothing in the plot answers an arrow key. The route in is a two-level list in a `<details>` beside the chart — clusters, then the points inside one — with one tab stop per level, arrow keys inside it, Enter to drill in and Escape to come back.
  • `tabindex` is deliberately NOT on the dots. It satisfies the same audit rule and produces one tab stop per point, in whatever order the host's array happened to be, with no way out but Tab — which is worse than the bug, because it looks solved.
  • A point row reads its name, its cluster, its position in the list and what is AROUND it — the share of its nearest points that carry the same label. Not its coordinates: those are layout units, and comparing them is the false reading the caveat refuses.
  • The point list is capped at `[maxKeyboardPoints]` and says how many of the cluster it did not reach, naming the cluster figures as what covers the rest. A list that claims to be the alternative to the plot and silently holds 200 of 5,000 is worse than no list.
  • Points with no cluster label get a row of their own, or they would be reachable by pointer and by nothing else.
  • In the selection variant, activating a row toggles that point — a lasso is a pointer gesture and cannot be anything else, so without it the variant has no keyboard story at all.
  • The caveat about distances is rendered on the chart as text, not as a tooltip.
  • Switching to the canvas renderer takes the per-point nodes away and nothing else: the plot keeps its `role="img"` summary label, the caveat and the separation finding stay, the browse list still walks the clusters and points, and a line on the chart says the dots are not individually focusable and where to go instead.

Feedback on Embedding Map

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.