Creates a new volcano trace.
The MAIDR layer carrying the points
Protected ReadonlyobserversProtectedisProtectedisTrue while AbstractTrace.getStateAt computes state at a temporarily moved cursor. Enforces (structurally, not just by documentation) that state getters never notify observers.
Protected ReadonlyidProtected ReadonlytypeProtected ReadonlytitleProtected ReadonlynameWhat this layer is, when the producer named it. See MaidrLayer.name.
Protected ReadonlylayerProtected ReadonlymovableProtected ReadonlysupportsAbstract property that subclasses must implement to indicate extrema support
ProtectedhighlightProtected ReadonlyflatProtected so a subclass can read the points themselves. A volcano plot needs each point's identity and whether it clears a threshold, neither of which is a coordinate.
Protected ReadonlyreadingProtected ReadonlyreadingProtected alongside readingOrder: it is the precomputed inverse of it,
and a subclass filtering the reading order needs O(1) position lookups
rather than a linear scan per keystroke.
ProtectedisProtectedpointProtectedxThe x axis's name, falling back to a generic one the layer authored none.
A getter rather than a field set in the constructor: the fallback is a translated word, and a trace outlives a language change — a name resolved once at construction would keep the language the chart was built in.
ProtectedyThe y axis's name, or a generic one. See xAxis.
ProtectedzThe z axis's name, or a generic one. See xAxis.
ProtectedisWhether the cursor has no point to report.
Read from dimension rather than from any one trace's data,
because that is the one shape every trace already answers in. It is also
evaluated at the cursor — LineTrace returns the current series'
length as cols — so this covers a ragged layer, where one series has
points and another has none, and not only a layer that is empty
throughout.
True when there is nothing at (row, col) to describe
Gets the current state of the trace including audio, braille, text, and highlight information.
The current TraceState
ProtectedautoplayHow this layer names itself in the description dialog's layer tabs.
The producer's name when there is one, and the chart-type label
otherwise — the same fallback order TextService.layerIdentity uses for
the spoken layer-switch announcement, so a tab and the announcement single
out the same layer by the same name. The two differ only in register: the
announcement says "point plot" where the dialog says "Scatter Plot",
because that written form is what the dialog's own Chart Type line has
always shown and a tab sitting above it should agree with it.
A plain-language introduction to this layer's chart type, for the description dialog. See chartGuide.
Which way this chart is drawn, or undefined for a type that has no orientation to speak of.
Entering the chart already announces it -- "This is a maidr plot of type: horizontal bar" -- but the description dialog said only "Chart Type: Bar Chart", and for the box, violin and boxen families the orientation also silently reverses the order of the rows in the table underneath, because their constructors reverse the groups when the chart is horizontal. A reader comparing the table with what they walked had no way to know why the two disagreed.
Exposed here, and read once by the description service, rather than pushed
as a stat by each of the thirty-odd traces that has one. Named apart from
the bar and distribution families' own orientation fields, which hold the
declared* value and default it to vertical for every type -- including the
ones that have no orientation at all.
horizontal or vertical, or undefined when the type has no
orientation to report.
The trace's chart type. Lightweight alternative to reading
state.traceType, which eagerly computes the full audio/braille/text/
highlight state just to expose this one string.
ProtectedhighlightReturns the appropriate highlight elements based on current navigation mode.
SVG elements for X-based or Y-based highlighting depending on mode
ProtectedvaluesProtectedbrailleProtectedaudioProtecteddimensionProtectedhighlightThe points the highlight currently covers, as indices into this layer's
data array.
This is highlight answered in a renderer-neutral currency. highlight
resolves the same five navigation modes into SVGElements, which a canvas
chart has none of — so a canvas adapter is handed back an index into the
data array it supplied, and inverts it against its own extraction walk.
That keeps the binning here, in the model that owns it: the adapter never
learns what a grid cell or an x-bucket is.
Deliberately not gated on SVG availability, where highlight falls back to
out-of-bounds when the binder supplied no elements. A canvas chart has no
elements by definition, and that is exactly the case this exists to serve.
Returns an empty array when nothing is addressed, which a consumer reads as "clear the overlay".
Indices into layer.data, in no particular order.
ProtectedhasProtectedoutReturns out-of-bounds state with the position the active navigation mode is actually on, so the boundary chime pans from where the user is.
Grid, point and intersection mode each keep their own cursor; the base implementation pans by row/col, which in those modes is the stale cursor from before the mode was entered.
ProtectedtextGets the description state for the scatter trace.
The description state containing chart metadata and data table
Runs action with this element's observers muted.
A cursor move normally is the announcement -- moveToIndex notifies, and
everything downstream of it speaks, brailles and highlights. That is wrong
for a move the reader did not make and is not waiting to hear: the
description dialog's layer tabs relocate the reader in the chart while the
modal is open, and the trace's announcement would land in the same live
region the dialog is using, so one of the two is dropped. The caller
announces afterwards, once, when the reader is back on the chart.
Nesting is safe and the flag is restored on every path, including a throw, because a flag left raised would silence the chart for good.
The moves to make in silence
ProtectedgetGets safe row and column indices to prevent accessing undefined values
Object with safe row and column indices
Registers an observer to receive state updates.
The observer to add
Removes an observer from receiving state updates.
The observer to remove
Notifies all registered observers with the current state.
Notifies observers that an out-of-bounds condition occurred.
Base implementation of navigation in HIGHER and LOWER modes of ROTOR, default is no-op Needs to be implemented in Line, Bar, Heatmap, Candlestick
Utility function to compare point values for rotor functionality
boolean value
ProtectedcompareRotor compare search along one row of numeric values.
Steps from the current column in the given direction and moves to the first value that satisfies the comparison; reports the rotor boundary when nothing further qualifies. For the traces whose values are a plain numeric grid indexed [row][col]; a trace with a richer layout (the bar's orientation-normalised rows, the candlestick's segments) keeps its own.
The values of the row being searched
Which way to search
Whether a lower or a higher value is sought
True when a matching value was found and moved to
Returns the rotor's compare-mode labels and boundary-message nouns. Override to rename the two compare units for a trace-specific semantic (e.g., the candlestick delta layer uses "above line" / "below line").
Get all highlight SVG elements for this trace Used by HighlightService for high contrast mode
Array of all SVG elements, or empty array if none
Get all original (visible) SVG elements for this trace. These are the actual rendered elements, not the hidden clones used for highlighting. Used by HighlightService for high contrast mode color changes.
Array of all original SVG elements, or empty array if none
Resets the trace to initial entry state
ProtectedgetReturns a human-readable label for this trace's chart type (e.g., 'Bar Chart', 'Scatter Plot') for display in the description modal. Falls back to the raw layer type if no mapping is registered.
ProtectedgetBuilds the axes object for the description state, carrying only the axes the layer actually authored a label for. Subclasses should call this instead of constructing the axes object inline so charts without a real z dimension don't surface the placeholder default.
Every axis is gated, not just z. xAxis and yAxis fall back to the
literal 'X' and 'Y' when the JSON authors no label -- legal, and
common in adapter-generated specs -- and the dialog rendered that as
"X: X", a line that says nothing twice. The figure-level branch of the
description already showed only authored labels
(DescriptionService.getFigureAxes), so gating here is what makes
the two levels of the same dialog follow one rule.
Blankness is tested with trim(), the same rule named applies
when it decides whether to substitute the fallback in the first place. A
whitespace-only z label is truthy but blank, so the untrimmed guard used
to pass it through and then print the 'Level' placeholder the guard
existed to keep out.
Get available extrema targets for the current navigation context
Array of extrema targets that can be navigated to Default implementation returns empty array (no extrema support)
Base implementation for navigateToExtrema Subclasses must override to provide actual implementation
The extrema target to navigate to
ProtectedfinalizeCommon post-navigation cleanup that should be called by subclasses after they update their internal state
ProtectedupdateDefault implementation for updating visual point position Subclasses can override if they need custom positioning logic
Checks if this plot supports extrema navigation.
True if extrema navigation is supported
Whether a mark of this trace sits at exactly this X, as moveToXValue finds one before it falls back to the nearest.
A layer switch asks this to choose between the two X values a position
can offer (see Trace.getAlternateXValue). Reads the x of each point;
a trace that keeps its X some other way answers false unless it says
otherwise, and the switch then carries what it always carried.
The X a layer switch would carry
True when some point's x is exactly xValue
Gets the unique identifier for this trace.
The trace ID
Moves the active point to the pointer location and returns directional guidance toward the nearest data geometry in a single call.
Combining both operations avoids running findNearestPoint twice per
pointermove event — important on dense plots where the scan is the
hot path.
Screen-space x position of the pointer/finger
Screen-space y position of the pointer/finger
Guidance state relative to nearest point, or null when unavailable
Checks if the specified coordinates are within bounds of the element.
The x-coordinate
The y-coordinate
Object containing the SVG element and its position
True if the point is in bounds, false otherwise
Cleans up resources and removes all highlight elements from the DOM.
ProtectedgetReturns an empty object to avoid grouping scatter points by audio tone.
Empty object without groupIndex to maintain consistent audio feedback
Where one of this layer's data points sits in the trace's own
coordinates — the inverse of ScatterTrace.highlightedPointIndices.
A scatter does not navigate its points in the order they arrived: the
constructor sorts by x and groups the duplicates, so a column is one
unique x. A raw data index read as a column therefore names a different
point, or none at all once duplicates have collapsed several into one.
That is what a streamed point needs translating out of before it can be
announced (LiveDataManager.appendData reports the new point by data
index).
Mode-aware, because the coordinate the state getters read is: COL
announces the column at col and pans by the point's place within it,
ROW announces the row at row.
An index into this layer's data array
The position to read that point at, or null when the index is not one this trace has
ProtectedhandleInitializes scatter plot navigation at the origin in column mode.
The x the reader is at, in terms of the axis they are walking.
COL mode walks the x values, so it is the column's x. ROW mode walks the
y values, so the x reported is the one the reader would land on when
switching back to columns (the same middle-of-the-row rule
toggleNavigation applies), which keeps a layer switch near the points
they were hearing. The inherited reading of values[row][col] over
[xValues, yValues] answered with a y value at row 1 and with nothing
at all above it.
The current x, or null when the cursor is off the data
Moves to the column at an x value, entering COL mode to do so.
An exact x wins; a numeric x with no exact column falls back to the nearest one, as the shared helper does for other traces, and a categorical x is matched against the column labels.
The x to move to
True when a column was found and the cursor moved
Checks if movement in the specified direction is possible from current position.
Direction or coordinate to check
True if movement is possible, false otherwise
Returns true if this trace supports compare (lower/higher value) navigation. Override to false for trace types that don't use compare modes (e.g., scatter, which is all we currently have).
Returns the display name for the default data navigation mode. Override to provide a trace-specific name (e.g., "ROW AND COLUMN NAVIGATION" for scatter).
Checks if currently in grid cell mode (navigating points within a cell).
Enters grid cell mode to navigate points within the current cell. Groups cell points by X coordinate (like COL mode) for navigation.
true if entered successfully, false if no points in cell
Exits grid cell mode and returns to grid navigation.
Moves to the previous point within the current grid cell.
true if moved, false if at boundary
Moves to the next X-grouped point within the current grid cell.
true if moved, false if at boundary
Gets the current point index within the cell (0-indexed).
Gets the total number of X-grouped points in the current grid cell.
Gets the current X-grouped point within the grid cell.
Mirrors LineTrace's intersection rule: the mode is offered only when navigating across the data is meaningful. For scatter, that means a stack exists on either axis — either an x with multiple ys (column stack) or a y with multiple xs (row stack). Either base mode (COL / ROW) can enter the rotor and cycle through its corresponding stack, so the capability check is the OR of both.
Called by the rotor service when entering / leaving INTERSECTION_MODE. Preserve the active NavMode: entering from COL cycles through the y-stack at the current x; entering from ROW cycles through the x-stack at the current y. Force-switching modes here would silently re-anchor the user on a different point set — the bug fixed by this commit.
No notifyStateUpdate(): mirrors setPointMode and setGridMode; the rotor announces the mode name and the next arrow press emits the focused state.
Walks one step forward through the current stack — y values at the current x in COL mode, x values at the current y in ROW mode. Returns false at the top of the stack so the rotor can announce the boundary; no chime (consistent with LineTrace's intersection bounds).
Walks one step backward through the current stack.
Returns true if this trace supports point-by-point navigation mode. Opt-in per trace type: override to return true for traces that expose individual data points navigable in reading order (left/right) and column-major order (up/down). Currently only ScatterTrace.
Enters or exits point-by-point navigation mode.
On entry, the current point is seeded from whatever ROW_COL position the user was on so they keep their place in the data. When the previous mode highlighted a group of points (e.g. a column of points sharing an x in COL mode), we pick the first point in reading order from that group — highest y for COL mode, lowest x for ROW mode.
On exit, no state needs to be unwound; the existing row/col indices are untouched while in point mode, so ROW_COL navigation resumes where it left off.
ProtectedmapConverts SVG elements to center coordinates for proximity-based navigation.
Array of center points with coordinates and indices, or null if unavailable
Finds the nearest scatter point to the given screen coordinates.
The x-coordinate in screen space
The y-coordinate in screen space
The nearest point with its element and indices, or null if unavailable
Reads state at an explicit cursor, with the trace-local navigation modes suspended for the duration.
getStateAt moves row/col and reads the state getters, but point,
intersection and grid mode short-circuit those getters onto their own
cursor — so a live-appended point would be announced as whichever point
or cell the user happens to be focused on. Suspending the flags makes the
read positional again, which is what every caller of this method asks for.
Row index to read at
Column index to read at
The trace state at that position
ProtectedmoveMoves the trace to the nearest point when the pointer is within its bounds and the trace is not already focused on that point.
onCurve is intentionally non-optional: the caller has already paid
the cost of isPointInBounds to assemble guidance state, and
forcing subclasses to accept the value makes the contract explicit so a
future override cannot silently recompute (or worse, ignore) it.
Subclasses override this to customise hover-driven navigation:
Offers the significant points when the chart declares a threshold.
Withheld when no threshold is declared -- there is nothing to filter on -- and withheld when one is declared but nothing clears it, since a mode whose only possible answer is "none found" is worse than not offering it. That second case is a real reading of the chart, and the description says so in words.
The significant unit alongside whatever the scatter offers
Moves within an active rotor filter unit along the filtered axis.
Called by RotorNavigationService when the current rotor mode is
one of this trace's getRotorFilterUnits. Filter units navigate a
single axis, so only left/right are dispatched here — the service
announces up/down as unavailable without calling the model (matching
intersection mode). Implementations should move to the nearest point
matching the unit identified by key and notify observers, returning
true; when no such point exists they should call notifyRotorBounds
and return false.
Default is a no-op that reports bounds, so a trace advertising a filter unit but forgetting to implement movement fails safe (announces "no point found") rather than moving unexpectedly.
True if the cursor moved, false otherwise
Trace implementation for volcano and Manhattan plots.
A volcano puts effect size against significance; a Manhattan puts genomic position against it. Both are scatters, so the navigation, the pitch and the braille all transfer -- and both are read almost entirely through a threshold, which is what a scatter has no notion of.
Point-by-point navigation is not a viable path here. These charts routinely carry tens of thousands of points of which a few dozen matter. A reader who has to walk them has not been given access to the chart; they have been given a very long list. So two things carry the weight:
The summary on entry. "43 of 12,000 points above the significance threshold" is the first thing a sighted reader takes from the shape of the cloud, and the last thing a per-point reading would ever assemble.
A rotor filter over the points that clear it, so the few dozen that matter are reachable in a few dozen keystrokes rather than twelve thousand.
Identity is the payload. A reader told "x is 2.3, y is 14.1" has been given the two numbers the axes already describe and withheld the one thing they came for, which is which gene that is. The label travels as an aside, so it survives every one of the scatter's navigation modes and is never run through an axis formatter -- a gene name is not a value on an axis.
The threshold is declared, never guessed. These charts sit on transformed axes whose conventions differ by field and by tool: -log10(p) at 1.3 for p < 0.05, at 7.3 for genome-wide significance, and a raw p axis runs the other way entirely. A guessed line would sort every point on the figure onto the wrong side of it, silently. A layer that declares none gets the scatter's reading and no claim about significance.