# poetry-charts API

The charts gem's Ruby surface: the chart helpers, the shared chart chassis, and the geometry layer (scales, curves, ticks, paths).

## Poetry::Charts::ComponentsHelper

The poetry_chart_* view helpers - the agent-facing chart surface,
from the chart-root dispatcher down to the per-family helpers.

### #poetry_area_chart(**, &)

Renders an area chart - filled trends over an ordered axis,
optionally stacked.

### #poetry_bar_chart(**, &)

Renders a bar chart - grouped or stacked rectangles per category.

### #poetry_chart(type, engine: nil, **, &block)

Renders a chart of the given type through one dispatcher; the block
receives the chart component for slot composition. Unknown types
raise with the list of known ones.

### #poetry_chart_container(**, &)

Renders the chart container - the sized, theme-scoped wrapper that
emits var(--color-<key>) for every configured series and hosts the
chart plus its tooltip and legend.

### #poetry_chart_legend_content(**, &)

Renders a standalone legend for the configured series - a swatch
plus label per entry.

### #poetry_chart_tooltip_content(**, &)

Renders the tooltip panel a chart's hover layer positions and fills -
place it inside the container alongside the chart.

### #poetry_line_chart(**, &)

Renders a line chart - one stroked curve per series.

## Poetry::Charts::Config

The chart config contract:
series key -> { label:, icon:, color: } or { label:, icon:, theme:
{ light:, dark: } }. The config is the SINGLE place series get their
human labels and colors; the container's <style> emission, the tooltip
chrome, and the legend chrome all resolve through it.

Values land inside a <style> element and inline style attributes, so
every color (and key) is validated against a conservative character
set at wrap time - a config can never smuggle CSS out of its block.

### .wrap(source)

Accepts a Config (pass-through) or a Hash keyed by series name.

### #[](key)

The entry for a series key.

### #color_entries

The entries that carry a color (flat or themed) - the set the
container's <style> emission covers, in config order.

### #entries

Returns the value of attribute entries.

### #initialize(hash)

Builds and validates entries from a Hash keyed by series name;
a CSS-unsafe key or color raises here, at wrap time.

### #keys

Every series key, in config order.

### #label_for(key, fallback = nil)

The label for a series key, falling back to the key itself - the
tooltip/legend resolution rule.

### #to_h

The config back as a plain Hash (label/color/theme per key),
compacted.

## Poetry::Charts::Spec

The chart-spec: the CLOSED, VERSIONED description every poetry chart
compiles to. The server engine consumes it Ruby-side; swappable
adapters consume the same spec JSON-side through the duck-typed
protocol (render(el, spec) / update / destroy). No engine-specific key
ever enters this schema - engine styling lives in the adapter,
declared; a pass-through options bag would tie call sites to one
engine and the spec could never close.

Keys ride the wire camelCased (dataKey, stackId) - one vocabulary
on both sides of the seam.

### #axes

Returns the value of attribute axes.

### #config

Returns the value of attribute config.

### #data

Returns the value of attribute data.

### #initialize(type:, data:, series:, axes: {}, config: nil)

### #series

Returns the value of attribute series.

### #to_h

The wire form: string keys, camelCased entry keys, the version
stamped in.

### #to_json(...)

The wire form serialized - what the spec <script> embeds.

### #type

Returns the value of attribute type.

## Poetry

The poetry component family's shared root namespace.

## Poetry::Charts

poetry's chart tier: charts as server-rendered SVG. Ruby runs the
whole geometry pipeline - data -> domains -> scales -> ticks ->
points -> paths, with decimal-exact nice ticks - and the finished
chart ships in the initial HTML: no-JS/print/email valid, themed by
CSS variables (--chart-1..5 + per-chart --color-<key>), dark mode
with zero re-render. Stimulus chrome adds tooltip/legend/active
interactivity by reading SERVER-EMBEDDED coordinates - no chart math
in the browser.

Engines stay swappable (three doors): the container contract is
engine-agnostic; every chart also compiles to a closed, VERSIONED
chart-spec consumed by duck-typed adapters (render/update/destroy -
a canvas adapter ships as the reference); client-rendered chart
libraries remain reachable through a Stimulus-mounted island.

### .display_value(value)

The tooltip display string shared by every chart family (matches
TooltipContent's Row: delimited numerics from RAW values so
integers stay integers, verbatim strings, nil for missing).

### .registry

The registry builder this gem commits from (the poetry-ui shared-
builder rule: rake registry:generate/verify and the sync test share
ONE construction). helper_args carries each poetry_* helper's max
positional arity from its real signature - poetry_chart(type, ...)
legitimately takes one, which is exactly why arity is an emitted
per-helper fact and never a convention.

### .registry_descriptions

The editorial per-chart descriptions merged into the registry
(component_path => one-liner, from config/component_descriptions.yml).
Absent file -> nil, so the registry stays lint-identical without it.

### .registry_items

The installable-item projection, boot-free from the COMMITTED
registry - the docs site aggregates this with poetry-ui's for
/r/*.json.

### .root

Gem root (the directory containing lib/, app/, config/).

## Poetry::Charts::AdapterChart

The bring-your-own-engine chart mount.

## Poetry::Charts::AreaChart

The area chart family.

## Poetry::Charts::AxisConfig

The axis capture shape the cartesian family slots accumulate into
(scatter carries its own - both axes numeric, different fields).

### #data_key

Returns the value of attribute data_key

### #tick_count

Returns the value of attribute tick_count

### #tick_formatter

Returns the value of attribute tick_formatter

### #tick_margin

Returns the value of attribute tick_margin

## Poetry::Charts::BarChart

The bar chart family.

## Poetry::Charts::Cartesian

The server-side cartesian layout pipeline: data + series ->
plot rectangle, scales, ticks, and per-series pixel points -
computed top-down in one pass, everything a renderer needs before
writing a path.

Layout conventions: default margin 5 on every side, x-axis strip
height 30 at the bottom when shown, category x positions from a
zero-padding point scale (first/last categories AT the plot
edges - add left/right margin 12 when edge labels need room),
numeric y domain [0, auto] niced decimal-exactly (implicit tick
count 5), stacking per stack id with :none/:expand offsets.

### #band_width

One category band's width (0 on a point scale).

### #baseline

The area baseline: the y pixel of 0, clamped into the domain so
an all-positive domain pins the baseline at its bottom edge.

### #categories

The category values: the x_key column, else bare row indexes.

### #category_range

Point for line/area (categories AT the edges); band for bars
(a zero-padding band - the bar gaps come from the gap math inside
the band, not scale padding). Horizontal layout runs the category
scale down the Y side.

### #coordinates

Compact per-series pixel coordinates the tooltip controller reads -
no chart math in the browser.

### #height

Returns the value of attribute height.

### #horizontal?

Whether categories run down the Y side (bars growing rightward).

### #initialize(data:, series:, width:, height:, x_key: nil, margin: {}, category_axis: true, value_axis: false, y_tick_count: 5, offset: :none, x_scale_type: :point, layout: :vertical)

### #layout

Returns the value of attribute layout.

### #margin

Returns the value of attribute margin.

### #offset

Returns the value of attribute offset.

### #plot_bottom

The plot rect's bottom edge, inset for the category axis strip.

### #plot_left

The plot rect's left edge, inset for a reserved left strip.

### #plot_right

The plot rect's right edge.

### #plot_top

The plot rect's top edge.

### #points(entry)

[{x:, y0:, y1:, value:}] for one series entry - stacked entries ride
their stack group's offsets; independent entries base on the
baseline. NaN values (missing data) flow through as NaN, which the
generators' defined-gap machinery turns into path gaps.

### #width

Returns the value of attribute width.

### #x_centers

The per-category CENTER - where ticks, vertical grid lines, and the
tooltip's hit columns sit (band centers; point positions verbatim).

### #x_positions

Each category's scale position (a band's leading edge).

### #x_scale

The category scale over the category range (point or band).

### #y_domain

The niced value domain - the first and last tick.

### #y_scale

The value scale: y in the vertical layout (inverted - SVG y grows
down), x in the horizontal one.

### #y_tick_count

Returns the value of attribute y_tick_count.

### #y_ticks

The niced value ticks ([0, 1] when 100%-stacked).

## Poetry::Charts::CartesianFamily

The shared cartesian slot grammar (area/line/bar/composed): the
axis/grid/legend/tooltip slots plus the readers that force their
lazy evaluation. Lambda slots accumulate config into ivars and
return nil (the breadcrumb pattern - slot wrappers do not delegate
to lambda return values); readers force evaluation via the slot
predicate (slots evaluate lazily).

Slots register at the include site because declaration order IS the
registry's slot order - include this AFTER the family's mark slots.
The value axis is a class-method hook: a family whose Y side
diverges (bar - the horizontal orientation moves the category axis
there) defines its own value_axis_slot before including.

### .included(base)

## Poetry::Charts::CartesianFamily::ClassMethods

The class-level hooks the include site drives.

### #value_axis_slot

Declares the default numeric Y axis slot (tick count 3, no data
key). Families whose value axis diverges override this before
including the concern.

## Poetry::Charts::ChartFamily

The identity chassis every chart family shares: the data-chart id
scope, the wrapped config, the accessible SVG name, and the SVG
number formatter. Families supply svg_label_prefix (the accessible
name's chart-type lead-in); a family whose default name reads from a
different surface (scatter: series keys through the config) overrides
svg_label itself.

### #chart_config

The config: option wrapped as a {Poetry::Charts::Config} - series
entries with labels and colors.

### #chart_id

The data-chart scope: explicit id when given (stable for tests /
multiple charts), else unique per render.

### #svg_label

The accessible name for the role=img SVG: explicit label: or a
sensible default from the configured series.

## Poetry::Charts::ComposedChart

The composed chart family.

## Poetry::Charts::Config::Entry

One validated config entry: a series key with its label, icon,
and flat or themed color.

### #color

Returns the value of attribute color

### #color_for(theme_name)

The per-theme color: the flat color, or the theme map's value.

### #colored?

Whether the entry carries any color (flat or themed).

### #icon

Returns the value of attribute icon

### #key

Returns the value of attribute key

### #label

Returns the value of attribute label

### #theme

Returns the value of attribute theme

## Poetry::Charts::Container

The chart container.

## Poetry::Charts::Engine

The Rails engine: wires poetry-charts into the host app - component
autoload paths, the Stimulus controllers manifest, the view helper,
preview and asset paths, and the importmap pins. Loading the gem is
the only integration step.

## Poetry::Charts::Geometry

The geometry core: the tick, scale, and shape math the charts stand
on - array ticks, linear/band/point scales, line/area/curve
generators, stack layout, and decimal-exact nice-ticks. Every piece
is oracle-tested: committed fixtures
(test/support/generate_geometry_fixtures.mjs) pin the expected
outputs byte-for-byte, alongside translated spec cases.

JS number semantics are part of the contract - path strings must
match the fixture output byte-for-byte - so rounding and
stringification go through js_round / js_number below, never
through Ruby defaults (Ruby rounds half away from zero and prints
"80.0"; JS floors x+0.5 and prints "80").

### .js_number(value)

JS Number#toString for the values that appear in SVG path data:
integral doubles print bare ("80", not "80.0"), -0 prints "0", and
everything else uses shortest round-trip decimal (Ruby and V8 agree
on shortest-repr in the post-rounding magnitude range; the exponent
guard covers the sub-1e-4 corner where Ruby switches early).

### .js_round(value)

JS Math.round: floor(x + 0.5) - differs from Float#round at negative
halves (JS rounds -2.5 to -2; Ruby to -3).

### .js_truthy?(value)

JS truthiness for the curve state machines (the line state flag
runs nil | 0 | 1 | NaN): nil, 0, and NaN are falsy.

## Poetry::Charts::Geometry::Area

The area generator: the filled band between a top line (x/x1, y1)
and a baseline (x0, y0), walked forward along the top and
BACKWARD along the buffered baseline per defined-segment (the
x0z/y0z buffers). Stacked areas feed y0/y1 from Stack series;
simple areas use a constant y0 (the axis line).

### #initialize(x: nil, x1: nil, y0: nil, y1: nil, curve: :linear, defined: nil, digits: 3)

### #path(data)

The SVG path for the data (nil when nothing was defined).

## Poetry::Charts::Geometry::Curve

The curve state machines - including the `_line`
undefined/0/1/NaN dance that decides where subpaths close (JS
truthiness via Geometry.js_truthy?; `1 - undefined` becomes NaN
via js_flip). The set matches the families' curve: whitelist:
linear, step (+before/after), natural, and monotone_x.

### .build(name, context)

A curve state machine writing into the given path context.

### .js_flip(line)

JS `1 - line` where line may be undefined (nil) or NaN.

## Poetry::Charts::Geometry::Line

The line generator: data -> SVG path string through a curve state
machine, with defined-gaps starting new subpaths (a single toggle
loop). Accessors are lambdas (d, i), symbols/strings (hash key
lookup), or numeric constants; x/y default to the [x, y] pair
convention.

### #initialize(x: nil, y: nil, curve: :linear, defined: nil, digits: 3)

### #path(data)

The SVG path for the data (nil when nothing was defined).

## Poetry::Charts::Geometry::Line::Accessor

Accessor coercion shared by the generators.

### .wrap(value, &default)

A (d, i) lambda from a lambda, key, constant, or the default.

## Poetry::Charts::Geometry::NiceTicks

Adapted from an MIT-licensed source (source and license in
THIRD_PARTY_NOTICES.md).

The nice-ticks algorithm on BigDecimal, so the tick values stay
decimal-exact. Every operation is decimal, not binary: values
construct from the double's shortest decimal string, remainders
truncate with the dividend's sign (#remainder), division carries
20 significant digits (PRECISION), and digit counts come from
BigDecimal#exponent (exactly floor(log10) + 1).

Oracle: translated spec cases pin the expected tick values.

### .adaptive_step(rough_step, allow_decimals, correction_factor)

The default step function: amend the rough step to a value that
reads well at its order of magnitude.

### .calculate_step(min, max, tick_count, allow_decimals, correction_factor = 0, step_fn: method(:adaptive_step))

The step + tick bounds for an interval (recursive: a correction
factor grows the step until tickCount ticks cover the interval).

### .dec(value)

Coerce to BigDecimal via the double's shortest decimal string.

### .digit_count(value)

Digit count: 1 for [1,10), 0 for [0.1,1), -1 for [0.01,0.1)...
BigDecimal#exponent IS floor(log10(|v|)) + 1, exactly.

### .fixed_domain_ticks(domain, tick_count, allow_decimals: true, mode: :auto)

Nice-stepped ticks CONSTRAINED to [min, max] - the domain
boundary always closes the list.

### .nice_ticks(domain, tick_count = 6, allow_decimals: true, mode: :auto)

Nice ticks for [min, max] - ticks may run OUTSIDE the interval
to stay round.

### .range_step(start, stop, step)

[start, end) with a fixed decimal step.

### .snap125_step(rough_step, allow_decimals, correction_factor)

The opt-in snap125 step: snap to 1 / 2 / 2.5 / 5 at each order of
magnitude.

### .step_function(mode)

The step function a mode selects.

### .ticks_of_single_value(value, tick_count, allow_decimals)

Ticks when min == max: center a window of tickCount steps on the value.

### .valid_interval(min, max)

The interval sorted ascending.

## Poetry::Charts::Geometry::Path

The path buffer the shape generators write into: move_to /
line_to / bezier_curve_to / quadratic_curve_to / close_path.
Numbers are rounded to `digits` decimals with JS Math.round
semantics and stringified as JS does (digits defaults to 3) - the
contract that keeps poetry's path strings byte-equal to the
geometry fixtures. Internal cursor state keeps FULL precision
(rounding is output-formatting only).

The arc/sector verbs are deliberately absent - polar sector paths
are built by Polar, not through this class.

### #bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y)

A cubic curve to (x, y) with two control points.

### #close_path

Closes the current subpath back to its start (a no-op before
any move).

### #empty?

Whether nothing has been written yet.

### #initialize(digits: 3)

### #line_to(x, y)

A straight segment to (x, y).

### #move_to(x, y)

Starts a new subpath at (x, y).

### #quadratic_curve_to(cpx, cpy, x, y)

A quadratic curve to (x, y) with one control point.

### #to_s

The accumulated SVG path data.

## Poetry::Charts::Geometry::Scale

The scale namespace: Linear, Band, and Point.

## Poetry::Charts::Geometry::Scale::Band

A band scale: categorical domain -> evenly stepped positions
with inner/outer padding and alignment. Point is band with
padding_inner = 1 (bandwidth 0), where padding: drives the
outer padding - the axis shape line/area charts position
categories with.

### .padded(domain:, range:, padding: 0.0, align: 0.5, round: false)

Convenience for the common single padding: knob - one value
sets inner AND outer padding.

### #call(value)

The band's leading-edge position for a category (nil when the
category is unknown).

### #align

Returns the value of attribute align.

### #bandwidth

Returns the value of attribute bandwidth.

### #call(value)

The band's leading-edge position for a category (nil when the
category is unknown).

### #domain

Returns the value of attribute domain.

### #initialize(domain:, range:, padding_inner: 0.0, padding_outer: 0.0, align: 0.5, round: false)

### #padding_inner

Returns the value of attribute padding_inner.

### #padding_outer

Returns the value of attribute padding_outer.

### #positions

Returns the value of attribute positions.

### #range

Returns the value of attribute range.

### #step

Returns the value of attribute step.

## Poetry::Charts::Geometry::Scale::Linear

A linear scale reduced to the closed poetry surface: a
two-point numeric domain/range with bimap normalization
(descending domains supported), ticks via Geometry::Ticks, and
the nice() domain extension. Degenerate domains map every
input to the range midpoint.

### #call(value)

The range value for a domain value.

### #call(value)

The range value for a domain value.

### #domain

Returns the value of attribute domain.

### #initialize(domain: [0.0, 1.0], range: [0.0, 1.0])

### #invert(value)

The domain value for a range value - the inverse map.

### #nice(count = 10)

Extends the domain to tick-increment boundaries, iterating
until the increment is stable. Returns a NEW scale (poetry
immutability).

### #range

Returns the value of attribute range.

### #ticks(count = 10)

Float-exact ticks across the domain.

## Poetry::Charts::Geometry::Scale::Point

A point scale: a band with padding_inner pinned to 1 - every
category is a zero-width position, padding: is the outer
padding.

### #initialize(domain:, range:, padding: 0.0, align: 0.5, round: false)

## Poetry::Charts::Geometry::Stack

The stack layout (declaration order): rows x series keys ->
per-series [base, top] pairs. Missing values behave as
JS +undefined = NaN (the offsets carry the exact NaN fallbacks).
:expand normalizes each row to sum 1; :diverging routes negatives
below the axis.

### #initialize(keys:, value: nil, offset: :none)

### #series(data)

The stacked series for the rows, offsets applied.

## Poetry::Charts::Geometry::Stack::Series

One stacked series: its key, order index, and [base, top]
points.

### #[](index)

The [base, top] pair at one row index.

### #index

Returns the value of attribute index

### #index=(value)

Sets the attribute index

### #key

Returns the value of attribute key

### #key=(value)

Sets the attribute key

### #length

The number of rows stacked.

### #points

Returns the value of attribute points

### #points=(value)

Sets the attribute points

## Poetry::Charts::Geometry::Ticks

Float-exact tick generation: ticks / tick_increment / tick_step.
The 1-2-5-10 step selection runs against square-root thresholds,
and the inverted-increment encoding (negative inc = divisor)
keeps tick values float-exact - a sub-1 step divides by an
integer instead of multiplying by a fraction.

### .tick_increment(start, stop, count)

The raw increment for the run - negative encodes a divisor.

### .tick_step(start, stop, count)

The absolute step size for the run (the increment decoded,
signed by direction).

### .ticks(start, stop, count)

About `count` evenly stepped, float-exact values covering
[start, stop].

## Poetry::Charts::GridConfig

The grid capture shape - which rule directions render (radar
carries its own, with polygon/circle rings).

### #horizontal

Returns the value of attribute horizontal

### #vertical

Returns the value of attribute vertical

## Poetry::Charts::LegendContent

The chart legend chrome.

## Poetry::Charts::LineChart

The line chart family.

## Poetry::Charts::Live

Live mode: a chart that opts in with `live: true` embeds a
{spec, frame} payload the client renderer recomputes geometry from
when data changes too often to round-trip to the server. `spec` is
the FROZEN spec v1 built by the same Poetry::Charts::Spec the
adapter seam uses (the spec stays closed); `frame` is a PRIVATE engine
envelope carrying the geometry-affecting knobs the spec deliberately
omits. Everything else about the chart stays server-rendered.

Live charts cannot carry Ruby lambdas to the browser: tick
formatters and label slots raise a teaching error - format data
host-side (pre-formatted category strings) instead.

### .included(base)

## Poetry::Charts::Live::ClassMethods

The class-level macro families call to opt into live mode.

### #live_option

Declares the live-mode surface: the live: and zoom: options plus
the with_brush slot.

## Poetry::Charts::Motion

Shared animation surface for the chart families: chart-level
options (animate / animation_duration / animation_easing /
animation_begin, defaults set per family), emitted as data-animate plus
--poetry-motion-* custom properties on the SVG. The animations
themselves are CSS (the motion stylesheet) and the motion controller -
the server computes all geometry; the client only interpolates
between server-computed states.

### .included(base)

### #motion_style_extras

Hook: families append extra custom properties (radar ships the
polar center so CSS can scale from it).

## Poetry::Charts::Motion::ClassMethods

The class-level macro families call to declare their animation
options.

### #motion_options(duration: 1500, delay: 0, easing: :ease)

Declares the animation options with the family's defaults
(bar 400ms, pie delayed 400ms, scatter 400ms linear,
everything else 1500ms ease with no delay).

## Poetry::Charts::PieChart

The pie chart family.

## Poetry::Charts::Polar

The polar geometry: pie-sector accumulation, tangent-circle corner
rounding, and wedge/ring paths. Angles are degrees COUNTERCLOCKWISE
from 3 o'clock, negated into SVG's y-down plane by
polar_to_cartesian; pies start at angle 0 and sweep to 360.

### .max_radius(width, height)

The largest radius fitting the plot: half the shorter side.

### .percent_value(value, total, default = 0)

"80%" of the max radius, or a plain number.

### .pie_sectors(values, start_angle: 0, end_angle: 360, padding_angle: 0, min_angle: 0)

The pie accumulation: values -> per-slice angles. Zero values
collapse (and skip padding); paddings live BETWEEN non-zero
slices (full circles pad after the last slice too, closing the
ring).

### .polar_to_cartesian(cx, cy, radius, angle)

The [x, y] point at (radius, angle) from the center, in SVG's
y-down plane.

### .sector_path(cx:, cy:, inner_radius:, outer_radius:, start_angle:, end_angle:, fmt: nil)

The wedge/ring path. The delta clamps at 359.999 so a full
circle's endpoints never coincide.

### .sector_path_with_corners(cx:, cy:, inner_radius:, outer_radius:, start_angle:, end_angle:, corner_radius:, fmt: nil)

The ring segment with all four corners rounded by tangent
circles. Falls back to the plain path when the sweep is too small
to fit the corners.

### .sign(value)

-1, 0, or 1 by the value's sign.

### .tangent_circle(cx:, cy:, radius:, angle:, sign:, corner_radius:, external: false)

The corner circle tangent to an arc (at `radius`) and a radial
edge (at `angle`) - the rounded-corner primitive for the radial
bar's corner_radius.

## Poetry::Charts::PolarFamily

The polar family chassis (pie/radar/radial): the shared margin +
plot/center geometry, and the per-sector pointer hit - polar marks
are hit by pointerover on the marked sector/wedge, not bisect, so
the svg gains the enter action after TooltipWiring's pointer/
keyboard set (include order is emission order - include this after
TooltipWiring).

### .included(base)

## Poetry::Charts::PolarFamily::SingleSeriesTooltip

The single-series polar tooltip (pie/radial): the FIRST series
drives the chrome, and per-index names/colors retint the one row.
Families supply polar_items (the sector geometry), polar_anchor
(where the tooltip anchors on one item), and polar_value_rows (the
rows the values read from). Radar keeps TooltipWiring's
multi-series chrome and its own payload, so it includes
PolarFamily alone.

## Poetry::Charts::RadarChart

The radar chart family.

## Poetry::Charts::RadialBarChart

The radial bar chart family.

## Poetry::Charts::ReferenceMarks

Reference marks - annotation lines, areas, and dots - for every
cartesian family. Values speak the chart's own axes - categories on
the category axis (the band/point center), numbers on the value
axis (scatter overrides both to numeric) - and render as a single
group painted ABOVE the series so annotations stay readable over
the marks. Labels are strings (the live rule: no lambdas).

Hosts provide ref_x_pixel/ref_y_pixel (the concern's defaults speak
cartesian), plot edges via cartesian, css(:reference_line/:reference_area/:tick),
and fnum. Vertical layouts only (a horizontal bar raises - a
declared limit).

### .included(base)

## Poetry::Charts::ScatterChart

The scatter chart family.

## Poetry::Charts::ThemeStyle

Turns a chart's Config into the scoped per-series custom
properties, one block per theme -

  [data-chart=chart-revenue] { --color-desktop: var(--chart-1); }
  .dark [data-chart=chart-revenue] { --color-desktop: oklch(...); }

so series markup (SVG fills, tooltip indicators, legend swatches) can
reference var(--color-<key>) and follow theme flips with ZERO
re-render. Emission is safe by construction: Config validated every
key and color at wrap time.

### #css

The stylesheet text, or nil when no entry carries a color - a
colorless config renders no <style> element at all.

### #initialize(id:, config:)

## Poetry::Charts::TooltipContent

The chart tooltip chrome.

## Poetry::Charts::TooltipLayer

The chart families' internal hover-tooltip mount.

## Poetry::Charts::TooltipWiring

Shared tooltip wiring for the chart families: the frame
wrapper carries the controller, the SVG carries targets/actions plus
the accessibilityLayer floor (focusable, role=application, arrows walk
categories), and the hidden chrome pre-renders per-series rows the
controller text-swaps - zero chart math in the browser.

### .included(base)

The wiring is declared, not hand-built: the frame carries the
controller + sync value, the SVG carries the target and the
pointer/keyboard actions (the accessibilityLayer floor), and the
coordinates <script> is the data target the controller reads.