Creates a new contour trace.
The MAIDR layer carrying one curve per level
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 ReadonlyxProtected ReadonlyyProtected ReadonlyzProtected ReadonlynavigationProtected ReadonlylayerProtected ReadonlysupportsAbstract property that subclasses must implement to indicate extrema support
Protected ReadonlyrotorProtected ReadonlymovableProtected ReadonlypointsProtected ReadonlylineProtected ReadonlyhighlightOne entry per point, or -- where a series is drawn as several elements
with no way to tell which is which -- the whole set behind every point of
it, outlined together. ContourTrace is the case: a level with
islands has one <path> per island and no dependable order among them
(xability/maidr#1142).
ProtectedhighlightProtected ReadonlyminProtected ReadonlymaxProtectedisWhether 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
ProtectedoutThe state pushed to observers when navigation leaves the data.
Declared as TraceEmptyState rather than the whole TraceState union
because that is what every implementation actually returns — the accessor
exists so AbstractPlot.notifyOutOfBounds has an empty state to
push. Narrowing it here is what lets the braille and highlight callers
hand the value straight on: those states accept the empty trace shape but
not a populated one, so a wider declaration would force each of them to
cast, and a cast is exactly what stops the compiler from noticing if an
override ever starts returning a populated state.
The empty trace state, positioned for out-of-bounds audio panning.
ProtectedhighlightProtectedautoplayProtectedhasThe 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.
ProtectedgroupWhat this chart calls one of its series, when the layer names no z axis.
Overridable for the reason LineTrace.seriesLabels is: a subclass navigates the same grid but draws something else, and this label is announced literally beside the series' own name. A chart that named its own noun everywhere else and then said "Group is Honda Civic" -- or "Group is Ash" -- would use two words for one referent in a single sentence.
The fallback label
ProtectedseriesThe vocabulary the description dialog is rendered with.
Inherited, a contour's description opens "Number of lines: 3, Points per
line: 40, Line names: Line 1, Line 2, Line 3" and its data table has a
Line column of Line 1 -- the reading this class exists to replace,
printed in the part of the description a reader consults most.
The four labels the description uses
ProtectedtextGets the description state for the line trace.
The description state containing chart metadata and data table
ProtectedvaluesProtectedaudioProtectedbrailleProtecteddimensionGets the current state of the trace including audio, braille, text, and highlight information.
The current TraceState
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.
Utility function to compare point values for rotor functionality
boolean value
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).
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
ProtectedgetComputes the trace state at an arbitrary position without moving the user's cursor or notifying observers. Used by monitor mode to sonify and announce a newly appended point while the user stays put.
The state getters read this.row/this.col internally, so the cursor
is moved temporarily and always restored in a finally block — this
method is the single owner of that pattern.
Re-entrancy hazard: this is only safe because the entire call chain is synchronous (no await points), so timers (e.g. autoplay ticks) cannot interleave before the finally-restore, and state getters never notify observers. If a getter ever becomes async or triggers notifications, callers could observe the temporary cursor.
The row of the position to compute state for
The column of the position to compute state for
The trace state at the requested position
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, including z only when the layer explicitly provides a z-axis label. Subclasses should call this instead of constructing the axes object inline so charts without a real z dimension don't surface the placeholder default.
ProtectedfinalizeCommon post-navigation cleanup that should be called by subclasses after they update their internal state
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.
Notifies the trace that the rotor is entering or leaving INTERSECTION_MODE. Default is a no-op; line-style traces don't need to track mode state because their state output is unchanged by the rotor mode. Traces whose audio/text output differs in intersection mode (e.g. ScatterTrace, which normally plays the whole x-column as a chord and must instead focus a single point) override this to flip an internal flag.
True when entering intersection mode, false when leaving.
Trace-specific rotor filter units appended to the rotor cycle after the built-in data/compare/grid/intersection modes. Each unit restricts navigation to points matching a predicate (e.g. only bullish candles). Default: none. Override to opt in (e.g. Candlestick exposes bullish/bearish/neutral units). The returned list is treated as read-only by the rotor service.
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.
The RotorFilterUnit.key of the active unit
The direction to search
True if the cursor moved, false otherwise
Checks if this plot supports extrema navigation.
True if extrema navigation is supported
Base implementation for getting current X value Subclasses can override if they have different data structures
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
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:
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
ProtectedmapWhere a level's highlight lands.
A contour is the one line-shaped trace whose series is not reliably one
drawn element. Plotly writes one <path> per curve under one
g.contourlevel per level, and only the level is dependably
addressable. Measured for xability/py-maidr#643 across 33 fields and 207
levels -- random sums of gaussians, a saddle, a monkey saddle, ripples, a
staircase, noise -- against what contourpy traces from the same grid:
levels where the counts disagreed 0 / 207
levels where the order within one did 18 / 207
Five of the eighteen were ordinary two-peaked gaussian fields, so
g.contourlevel:nth-of-type(k) is dependable and ... path:nth-of-type(j)
is not. Inherited, that left a producer two options: a per-curve selector
that resolves to a real element and the wrong one, or no selector at all
and no highlight (xability/maidr#1142).
So a selector resolving to several elements is read as a level rather
than a curve, and every point of that series outlines all of them. That is
the reading matplotlib's contour has always given -- it draws one <path>
per level, so naming the level names an element -- and a reader on one
island of the 0.5 contour sees the 0.5 contour outlined.
Decided per level rather than per layer, because a field usually has islands at some levels and not others, and a reader on a single-curve level keeps the per-point highlight that walks with them.
Naming the g.contourlevel itself instead does not work, and is why the
selector resolves to the paths: Svg.createHighlightElement sets stroke
on the clone, and stroke is inherited only by a child that declares
none -- plotly stamps one on every contour path, so the clone comes out
colourless.
Optionalselectors: string[]One selector per level, as the layer declared them
The elements to outline, point by point
ProtectedgroupWhat a curve is called wherever the description names one.
The level, because that is what the curve is. Line 2 is an index into
the order the producer happened to emit the curves in, and a reader given
it in a column headed Level would reasonably take it for one.
Which curve
Its level, or the best name available for a curve declaring none
ProtectedauthoredName a line carries in the spec, or undefined when the data authors
none. Consumers that have nothing meaningful to say without a real name
(e.g. the position announcement) can then stay silent about groups.
Protected for the same reason LineTrace.groupNameAt is: a subclass
that names its series from its own data -- a contour from its level --
still has to answer for a series the layer named and nothing else did, and
calling groupNameAt for that would recurse.
Gets the line series with human-readable labels. Used by the candlestick delta feature to list reference-line candidates (e.g., moving averages).
One entry per series with its label and points
Cleans up trace resources including values and highlighted SVG elements.
Only removes elements MAIDR created (hidden clones, synthetic markers); traces that highlight the chart's original live elements in place (e.g. heatmap cells, line dots) merely drop their references so the visible chart geometry survives focusout and live-data rebuilds.
ProtectedpanningWhere a cell sits in the stereo field.
Split out because two places need it and they must agree: the tone for the cursor's own point, and the tones of every series a chord sounds at an intersection. A subclass that hears its columns somewhere other than a straight left-to-right sweep -- a radar's spokes go out and back around a circle -- overrides this one method and both follow, rather than one of them keeping the line's sweep and contradicting the other.
The series index
The column index
The panning for that cell
ProtectedreconcileReconciles the vertices parsed out of a rendered <path> with the number
of data points in a series, mutating coordinates in place so it ends up
one entry per data point in data order.
SVG renderers (e.g. Plotly) may simplify a path by dropping collinear vertices, leaving fewer coordinates than data points; those are recovered by interpolating along the surviving segments. Extra vertices are dropped from the end, which is right for a straight polyline whose vertices are its data points — subclasses whose rendered geometry has vertices that are not data points (see StepTrace) override this.
Vertices parsed from the path, mutated in place
Index of the series these coordinates belong to
ProtectedmapGet extrema targets for the current line plot Returns min, max values, and intersection points within the current group
Array of extrema targets for navigation
Navigate to a specific extrema target
The extrema target to navigate to
ProtectedupdateUpdate the visual position of the current point This method should be called when navigation changes
Move the line plot to the position that matches the given X value
The X value to move to
true if the position was found and set, false otherwise
Base implementation of navigation in HIGHER and LOWER modes of ROTOR, default is no-op Needs to be implemented in Line, Bar, Heatmap, Candlestick
Intersection navigation mode is available only when the plot has
multiple lines. points is a 2-D array whose outer dimension is the line
index, so length > 1 means at least two lines exist — a prerequisite for
any crossing between sampled series to be possible.
Move to the next point intersection (right arrow in intersection rotor mode). Default is a no-op returning false; subclasses that advertise supportsIntersectionMode must override to provide real behavior.
Move to the previous point intersection (left arrow in intersection rotor mode). Default is a no-op returning false; subclasses that advertise supportsIntersectionMode must override to provide real behavior.
Trace implementation for contour and filled contour plots.
A scalar field drawn as curves of constant value. Structurally that is a multi-line layer -- one curve per level -- so navigation, braille and highlighting all transfer from LineTrace, and walking a row is walking one iso-value curve.
The level is a first-class object, not a colour. Read as a plain line layer, the level is a series name at best: the curve announces where it runs and never what value it runs at, which is the first thing anyone asks of a contour plot. So it is announced on the
zaxis -- the field's own axis -- where the layer'szformat applies to it, because it is a value of the field rather than a label for the curve.Spacing is the gradient. How densely the curves are drawn is the visual cue for how steeply the field changes, and it is the one thing a reader walking a single curve can never assemble: the curve says nothing about its neighbours. So each point announces the distance to the nearest point on the adjacent level, which is exactly the distance an eye measures between two lines -- tight spacing is a cliff, wide spacing is a plateau.
The lattice underneath a contour is a heatmap and Heatmap reads it, which is the fallback the issue calls the cheap one. What is here is the reading that makes the chart legible.