poetry-charts 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.controller_docs +1 -0
  3. data/.jsdoc_floor +1 -0
  4. data/.mcp.json +8 -0
  5. data/.yard-lint.yml +25 -0
  6. data/.yard_coverage_all +1 -0
  7. data/Archspec.rb +17 -0
  8. data/CHANGELOG.md +20 -0
  9. data/app/components/poetry/charts/area_chart/component.html.erb +1 -14
  10. data/app/components/poetry/charts/bar_chart/component.rb +5 -0
  11. data/app/components/poetry/charts/bar_math.rb +2 -0
  12. data/app/components/poetry/charts/cartesian_family.rb +25 -1
  13. data/app/components/poetry/charts/composed_chart/component.html.erb +1 -14
  14. data/app/components/poetry/charts/composed_chart/component.rb +1 -0
  15. data/app/components/poetry/charts/container/component.html.erb +1 -1
  16. data/app/components/poetry/charts/container/component.rb +3 -4
  17. data/app/components/poetry/charts/error_bars.rb +2 -0
  18. data/app/components/poetry/charts/legend_content/component.html.erb +1 -1
  19. data/app/components/poetry/charts/legend_content/component.rb +3 -5
  20. data/app/components/poetry/charts/line_chart/component.html.erb +1 -14
  21. data/app/components/poetry/charts/live.rb +1 -0
  22. data/app/components/poetry/charts/motion.rb +1 -0
  23. data/app/components/poetry/charts/polar_family.rb +1 -0
  24. data/app/components/poetry/charts/reference_marks.rb +3 -0
  25. data/app/components/poetry/charts/tooltip_content/component.html.erb +1 -1
  26. data/app/components/poetry/charts/tooltip_content/component.rb +7 -5
  27. data/app/components/poetry/charts/tooltip_layer/component.html.erb +1 -1
  28. data/app/components/poetry/charts/tooltip_layer/component.rb +3 -5
  29. data/app/javascript/poetry/charts/adapter_controller.js +12 -7
  30. data/app/javascript/poetry/charts/live_controller.js +20 -18
  31. data/app/javascript/poetry/charts/motion_controller.js +30 -29
  32. data/app/javascript/poetry/charts/tooltip_controller.js +19 -10
  33. data/app/javascript/poetry/charts/window_controller.js +13 -9
  34. data/config/component_registry.yml +10 -0
  35. data/config/controllers_manifest.json +56 -10
  36. data/eslint.config.mjs +34 -0
  37. data/lib/poetry/charts/cartesian.rb +3 -0
  38. data/lib/poetry/charts/config.rb +5 -0
  39. data/lib/poetry/charts/geometry/area.rb +13 -0
  40. data/lib/poetry/charts/geometry/curve.rb +27 -0
  41. data/lib/poetry/charts/geometry/line.rb +12 -0
  42. data/lib/poetry/charts/geometry/nice_ticks.rb +42 -0
  43. data/lib/poetry/charts/geometry/path.rb +21 -0
  44. data/lib/poetry/charts/geometry/scale/band.rb +24 -0
  45. data/lib/poetry/charts/geometry/scale/linear.rb +3 -0
  46. data/lib/poetry/charts/geometry/stack.rb +8 -0
  47. data/lib/poetry/charts/geometry/ticks.rb +13 -1
  48. data/lib/poetry/charts/geometry.rb +6 -0
  49. data/lib/poetry/charts/polar.rb +45 -2
  50. data/lib/poetry/charts/spec.rb +3 -0
  51. data/lib/poetry/charts/theme_style.rb +2 -0
  52. data/lib/poetry/charts/version.rb +1 -1
  53. data/lib/poetry/charts.rb +4 -1
  54. data/rubydex_linter/rules/registry_class.rb +44 -0
  55. data/rubydex_linter/rules/style_sidecar.rb +38 -0
  56. metadata +16 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73dfd392d17b87636eeea4e4566432c7cd99db60f86a298e0c8f0e4db0ab975a
4
- data.tar.gz: 4cffa7098a6f2244c9369584f492f8aaf005ef5738dfc608ac534419c6f7b274
3
+ metadata.gz: 4a52637ce30111cd3f0b53f0362ae2f943b7c9184f04d4e8ff39ff0a5701694a
4
+ data.tar.gz: 1dd2c325e2d16de0bbc0883332cad6cf1d46c2feea560d28b022dc793b266ddd
5
5
  SHA512:
6
- metadata.gz: 0ffc568f9e74a322576b7f2003d821349cdd7cf4849073bd3771d709f637ad33325f11bc3c28d9526ddb17fcef06cd727664d9c45cec463728a46e42b3c17331
7
- data.tar.gz: fcdc5263f1d4c8792bd881f68341630b5ec231c3761df8b7d719ef2bebbbe0a780eac62dfb5ba5df0f60b436eb7feff9987bcde338dd0fdb9dc20af1c60fe1ae
6
+ metadata.gz: db9401f68bf56fb3be0c65f725366d89fcf921f2f915acad97dcdb63c37f034d3115ce115744b866b0b57b30d582ad7b8e4dad6e467704e9748f2bf580bbf603
7
+ data.tar.gz: 9493dc5dc4243deebd4be0169938c72229032d279716f5f0a7244e97ced2654346cdbe316f082fb889030061fbd65868d595f90b76f10c7b0fcf9a9ba78ad6c2
data/.controller_docs ADDED
@@ -0,0 +1 @@
1
+ 0
data/.jsdoc_floor ADDED
@@ -0,0 +1 @@
1
+ 53
data/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "rubydex": {
4
+ "command": "bundle",
5
+ "args": ["exec", "rdx", "mcp"]
6
+ }
7
+ }
8
+ }
data/.yard-lint.yml ADDED
@@ -0,0 +1,25 @@
1
+ # yard-lint: the documentation tier YARD's own gates miss - a documented
2
+ # method missing a @param, an @example that does not parse, option tags,
3
+ # tag order, invalid types. Runs as rake yard:lint (in the default task).
4
+ # UndocumentedObjects stays off: yard:coverage gates public objects at its
5
+ # recorded floor, and this validator counts the @api private internals the
6
+ # gems hide on purpose (--hide-api is not honoured here). MissingReturn is
7
+ # off until the @param backlog is cleared; revisit then.
8
+ AllValidators:
9
+ YardOptions:
10
+ - --no-private
11
+ Documentation/UndocumentedObjects:
12
+ Enabled: false
13
+ Documentation/MissingReturn:
14
+ Enabled: false
15
+ Tags/InvalidTypes:
16
+ Enabled: true
17
+ # A **options splat passed through to a control is documented as @param
18
+ # options [Hash], not one @option per key; and @example blocks here are
19
+ # ERB as often as Ruby, which this validator cannot parse.
20
+ Documentation/UndocumentedOptions:
21
+ Enabled: false
22
+ Tags/OptionTags:
23
+ Enabled: false
24
+ Tags/ExampleSyntax:
25
+ Enabled: false
@@ -0,0 +1 @@
1
+ 0
data/Archspec.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The architecture the family enforces by review, as checks (rake arch:check).
4
+ # charts depends on core, and on ui only through the registry: no chart
5
+ # names a ui component class.
6
+ root "."
7
+ source "app/**/*.rb", "lib/**/*.rb"
8
+
9
+ component :components, in: "app/components/**/*.rb"
10
+ component :lib, in: "lib/**/*.rb"
11
+
12
+ lib.cannot_use :components, because: "lib reaches the components through the registry"
13
+ lib.cannot_reference_constants "Poetry::Ui", "Poetry::Agent", "Poetry::Extract", "ApplicationController",
14
+ because: "charts depends on core, on ui through the registry only, never the host"
15
+ components.cannot_reference_constants "Poetry::Ui", "Poetry::Agent", "Poetry::Extract", "ApplicationController",
16
+ because: "charts depends on core, on ui through the registry only, never the host"
17
+ no_cycles among: %i[lib components]
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.6] - 2026-09-20
4
+
5
+ ### Changed
6
+
7
+ - The gem is developed in the Poetry monorepo, roboruby/poetry, under `gems/poetry-charts`; the gemspec's source, changelog and issue links point there, and its file list no longer needs git on the machine (a path gem inside a slim image lists the same files).
8
+
9
+ ## [0.1.5] - 2026-09-18
10
+
11
+ ### Added
12
+
13
+ - The controllers manifest carries the prose beside each controller: its purpose (a JSDoc block above the class), the meaning of every value, and a summary of every action method, harvested when the manifest is generated; `rake stimulus:docs` holds the count of gaps at a committed floor, now zero.
14
+
15
+ ### Changed
16
+
17
+ - The registry carries the gem's internal namespaces (`internals`), so `poetry check` can warn a host that names one.
18
+ - 9 methods the reference already hid with `@api private` are Ruby-private now: each was called only by its own class or template, so the runtime enforces what the tag only stated. A host that reached one gets a NoMethodError instead of an internal that may change without notice. The tag remains on the internals the family shares between its gems and on whole internal classes.
19
+ - Every class, module and method carries a one-sentence description, private helpers included: `rake yard:coverage:all` measures the whole tree (a tag-only docstring counts as blank) and the committed floor now stands at zero.
20
+ - The area, line and composed charts draw their gridlines through one `grid_svg` builder instead of three identical template blocks; every visual baseline still matches.
21
+ - The container, legend, tooltip and tooltip layer roots ride the core default `root_attributes`, passing only their own markup up; the rendered attributes are unchanged apart from their order on the element.
22
+
3
23
  ## [0.1.4] - 2026-09-15
4
24
 
5
25
  ### Changed
@@ -15,20 +15,7 @@
15
15
  <% end %>
16
16
  </defs>
17
17
  <% end %>
18
- <% if grid? %>
19
- <g data-slot="chart-grid" aria-hidden="true">
20
- <% if grid_config.horizontal %>
21
- <% cartesian.y_ticks.each do |tick| %>
22
- <line class="<%= css(:grid_line) %>" x1="<%= fnum(cartesian.plot_left) %>" x2="<%= fnum(cartesian.plot_right) %>" y1="<%= fnum(cartesian.y_scale.call(tick)) %>" y2="<%= fnum(cartesian.y_scale.call(tick)) %>" />
23
- <% end %>
24
- <% end %>
25
- <% if grid_config.vertical %>
26
- <% cartesian.x_centers.each do |x| %>
27
- <line class="<%= css(:grid_line) %>" x1="<%= fnum(x) %>" x2="<%= fnum(x) %>" y1="<%= fnum(cartesian.plot_top) %>" y2="<%= fnum(cartesian.plot_bottom) %>" />
28
- <% end %>
29
- <% end %>
30
- </g>
31
- <% end %>
18
+ <%= grid_svg %>
32
19
  <%= cursor_svg(:line) %>
33
20
  <g data-slot="chart-areas"<% if animate? %> clip-path="url(#<%= chart_id %>-reveal)"<% end %>>
34
21
  <% series_entries.each do |entry| %>
@@ -264,6 +264,7 @@ module Poetry
264
264
  # The widest the auto-sized strip may grow, as a share of the width.
265
265
  CATEGORY_STRIP_SHARE = 0.4
266
266
 
267
+ # The axis margin, widened on the left by the category strip allowance when the strip auto-sizes.
267
268
  # @api private
268
269
  def axis_margin
269
270
  base = super
@@ -271,6 +272,7 @@ module Poetry
271
272
  allowance.positive? ? base.merge(left: Cartesian::DEFAULT_MARGIN[:left] + allowance) : base
272
273
  end
273
274
 
275
+ # Whether the category strip sizes itself: a horizontal chart with a y axis and no explicit left margin.
274
276
  # @api private
275
277
  def auto_category_strip?
276
278
  horizontal? && y_axis? && !(margin || {}).to_h.symbolize_keys.key?(:left)
@@ -285,6 +287,7 @@ module Poetry
285
287
  [[needed, category_strip_cap].min - Cartesian::Y_AXIS_WIDTH, 0].max.ceil
286
288
  end
287
289
 
290
+ # The most pixels the category strip may take, a share of the chart width.
288
291
  # @api private
289
292
  def category_strip_cap
290
293
  (width * CATEGORY_STRIP_SHARE).floor
@@ -299,12 +302,14 @@ module Poetry
299
302
  [((category_strip_cap - y_axis_config.tick_margin.to_f) / CATEGORY_LABEL_CHAR_WIDTH).floor, 2].max
300
303
  end
301
304
 
305
+ # The longest formatted category label, in characters.
302
306
  # @api private
303
307
  def longest_category_label
304
308
  key = y_axis_config.data_key
305
309
  data.map { |row| formatted_category_label(row.to_h[key] || row.to_h[key.to_sym]).length }.max || 0
306
310
  end
307
311
 
312
+ # A category through the y axis's tick formatter when one is given, else as text.
308
313
  # @api private
309
314
  def formatted_category_label(category)
310
315
  formatter = y_axis_config&.tick_formatter
@@ -90,6 +90,8 @@ module Poetry
90
90
  text = value.to_s
91
91
  text.end_with?("%") ? total * (text.to_f / 100.0) : text.to_f
92
92
  end
93
+
94
+ private :bar_slots_for, :bar_cells, :motion_origin, :bar_path_for, :bar_percent_value
93
95
  end
94
96
  end
95
97
  end
@@ -25,6 +25,7 @@ module Poetry
25
25
  # include Poetry::Charts::CartesianFamily
26
26
  # end
27
27
  module CartesianFamily
28
+ # Declares the x axis, the value axis, the grid and the class-level helpers on the including chart.
28
29
  def self.included(base)
29
30
  base.extend(ClassMethods)
30
31
 
@@ -99,7 +100,30 @@ module Poetry
99
100
  @grid_config
100
101
  end
101
102
 
102
- private :x_axis_config, :y_axis_config, :grid_config
103
+ # The gridlines group, per the grid slot: horizontal rules at the value
104
+ # ticks, vertical rules at the category centers; nil without the slot.
105
+ def grid_svg
106
+ return unless grid?
107
+
108
+ lines = []
109
+ if grid_config.horizontal
110
+ cartesian.y_ticks.each do |tick|
111
+ y = cartesian.y_scale.call(tick)
112
+ lines << grid_line(cartesian.plot_left, cartesian.plot_right, y, y)
113
+ end
114
+ end
115
+ if grid_config.vertical
116
+ cartesian.x_centers.each { |x| lines << grid_line(x, x, cartesian.plot_top, cartesian.plot_bottom) }
117
+ end
118
+ tag.g(safe_join(lines), "data-slot": "chart-grid", "aria-hidden": true)
119
+ end
120
+
121
+ # One gridline from one point to another.
122
+ def grid_line(from_x, to_x, from_y, to_y)
123
+ tag.line(class: css(:grid_line), x1: fnum(from_x), x2: fnum(to_x), y1: fnum(from_y), y2: fnum(to_y))
124
+ end
125
+
126
+ private :x_axis_config, :y_axis_config, :grid_config, :grid_svg, :grid_line
103
127
  end
104
128
  end
105
129
  end
@@ -15,20 +15,7 @@
15
15
  <% end %>
16
16
  </defs>
17
17
  <% end %>
18
- <% if grid? %>
19
- <g data-slot="chart-grid" aria-hidden="true">
20
- <% if grid_config.horizontal %>
21
- <% cartesian.y_ticks.each do |tick| %>
22
- <line class="<%= css(:grid_line) %>" x1="<%= fnum(cartesian.plot_left) %>" x2="<%= fnum(cartesian.plot_right) %>" y1="<%= fnum(cartesian.y_scale.call(tick)) %>" y2="<%= fnum(cartesian.y_scale.call(tick)) %>" />
23
- <% end %>
24
- <% end %>
25
- <% if grid_config.vertical %>
26
- <% cartesian.x_centers.each do |x| %>
27
- <line class="<%= css(:grid_line) %>" x1="<%= fnum(x) %>" x2="<%= fnum(x) %>" y1="<%= fnum(cartesian.plot_top) %>" y2="<%= fnum(cartesian.plot_bottom) %>" />
28
- <% end %>
29
- <% end %>
30
- </g>
31
- <% end %>
18
+ <%= grid_svg %>
32
19
  <%= cursor_svg(:line) %>
33
20
  <%# Declaration order IS paint order. %>
34
21
  <% series_entries.each do |entry| %>
@@ -290,6 +290,7 @@ module Poetry
290
290
 
291
291
  private
292
292
 
293
+ # Records one series of the given mark with the defaults filled in, validating its curve.
293
294
  def push_series(mark, key:, **attrs)
294
295
  attrs = SERIES_DEFAULTS.merge(attrs)
295
296
  if attrs[:curve] && !CURVES.include?(attrs[:curve])
@@ -1,4 +1,4 @@
1
- <%= content_tag(:div, **root_attributes.to_attributes) do %>
1
+ <%= content_tag(:div, **root_attributes) do %>
2
2
  <% if (css_text = theme_css) %><style><%= css_text.html_safe %></style><% end %>
3
3
  <%= content %>
4
4
  <% end %>
@@ -63,17 +63,16 @@ module Poetry
63
63
 
64
64
  # The frame div's attributes: part self-identification plus the
65
65
  # data-chart scope.
66
- # @api private
67
66
  def root_attributes
68
- html_attributes.merge_if_not_set(
67
+ super(
69
68
  {
70
69
  "data-slot" => "chart",
71
70
  "data-chart" => chart_id
72
- }.merge(component_data_attributes)
71
+ }
73
72
  )
74
73
  end
75
74
 
76
- private :chart_config, :chart_id, :theme_css, :root_attributes
75
+ private :chart_config, :chart_id, :theme_css
77
76
  end
78
77
  end
79
78
  end
@@ -30,6 +30,8 @@ module Poetry
30
30
  "M#{f.call(center_x)},#{f.call(y_low)}V#{f.call(y_high)}" \
31
31
  "M#{f.call(center_x - half)},#{f.call(y_high)}H#{f.call(center_x + half)}"
32
32
  end
33
+
34
+ private :error_range, :error_bar_path
33
35
  end
34
36
  end
35
37
  end
@@ -1,4 +1,4 @@
1
- <%= content_tag(:div, **root_attributes.to_attributes) do %>
1
+ <%= content_tag(:div, **root_attributes) do %>
2
2
  <% rows.each do |row| %>
3
3
  <%= content_tag(toggle ? :button : :div, **(toggle ? toggle_attributes(row) : { data: { slot: "chart-legend-item" }, class: css(:item) })) do %>
4
4
  <% unless hide_icon || row[:color].nil? %>
@@ -80,11 +80,8 @@ module Poetry
80
80
  end
81
81
 
82
82
  # The legend row's attributes with the part self-identification.
83
- # @api private
84
83
  def root_attributes
85
- html_attributes.merge_if_not_set(
86
- { "data-slot" => "chart-legend-content" }.merge(component_data_attributes)
87
- )
84
+ super({ "data-slot" => "chart-legend-content" })
88
85
  end
89
86
 
90
87
  # A swatch's guarded inline background (colors reaching a style
@@ -105,6 +102,7 @@ module Poetry
105
102
  entry.theme ? "var(--color-#{entry.key})" : entry.color
106
103
  end
107
104
 
105
+ # One legend item's key, label and color, the config filling what the item omits.
108
106
  def resolve(item)
109
107
  key = (item[:key] || item[:name]).to_s
110
108
  entry = chart_config[key]
@@ -115,7 +113,7 @@ module Poetry
115
113
  }
116
114
  end
117
115
 
118
- private :chart_config, :rows, :toggle_attributes, :root_attributes, :swatch_style
116
+ private :chart_config, :rows, :toggle_attributes, :swatch_style
119
117
  end
120
118
  end
121
119
  end
@@ -1,20 +1,7 @@
1
1
  <%= render Poetry::Charts::Container::Component.new(config: chart_config, id: chart_id.delete_prefix("chart-")) do %>
2
2
  <%= content_tag(:div, frame_attributes.merge("class" => css(:frame))) do %>
3
3
  <svg viewBox="0 0 <%= width %> <%= height %>" class="<%= css(:svg) %>" data-slot="chart-svg" aria-label="<%= svg_label %>" preserveAspectRatio="xMidYMid meet" <%= tag.attributes(svg_interaction_attributes) %>>
4
- <% if grid? %>
5
- <g data-slot="chart-grid" aria-hidden="true">
6
- <% if grid_config.horizontal %>
7
- <% cartesian.y_ticks.each do |tick| %>
8
- <line class="<%= css(:grid_line) %>" x1="<%= fnum(cartesian.plot_left) %>" x2="<%= fnum(cartesian.plot_right) %>" y1="<%= fnum(cartesian.y_scale.call(tick)) %>" y2="<%= fnum(cartesian.y_scale.call(tick)) %>" />
9
- <% end %>
10
- <% end %>
11
- <% if grid_config.vertical %>
12
- <% cartesian.x_centers.each do |x| %>
13
- <line class="<%= css(:grid_line) %>" x1="<%= fnum(x) %>" x2="<%= fnum(x) %>" y1="<%= fnum(cartesian.plot_top) %>" y2="<%= fnum(cartesian.plot_bottom) %>" />
14
- <% end %>
15
- <% end %>
16
- </g>
17
- <% end %>
4
+ <%= grid_svg %>
18
5
  <%= cursor_svg(:line) %>
19
6
  <g data-slot="chart-lines">
20
7
  <% series_entries.each do |entry| %>
@@ -17,6 +17,7 @@ module Poetry
17
17
  # The live controller's full-string identifier.
18
18
  CONTROLLER = "poetry--charts--live"
19
19
 
20
+ # Declares the live update wiring on the including chart: registration, the tooltip re-read and the zoom drag.
20
21
  def self.included(base)
21
22
  base.extend(ClassMethods)
22
23
 
@@ -15,6 +15,7 @@ module Poetry
15
15
  # The motion controller's full-string identifier.
16
16
  CONTROLLER = "poetry--charts--motion"
17
17
 
18
+ # Declares the motion controller on the including chart's frame.
18
19
  def self.included(base)
19
20
  base.extend(ClassMethods)
20
21
 
@@ -20,6 +20,7 @@ module Poetry
20
20
  # The default polar margin - a slim, even inset on all sides.
21
21
  MARGIN = { top: 5, right: 5, bottom: 5, left: 5 }.freeze
22
22
 
23
+ # Declares the tooltip wiring on the including chart's svg.
23
24
  def self.included(base)
24
25
  base.use_stimulus do
25
26
  on :svg do
@@ -14,6 +14,7 @@ module Poetry
14
14
  # and fnum. Vertical layouts only (a horizontal bar raises - a
15
15
  # declared limit).
16
16
  module ReferenceMarks
17
+ # Declares the reference line, area and dot slots on the including chart.
17
18
  def self.included(base)
18
19
  # A dashed rule across the plot: x: (a category) draws it
19
20
  # vertical, y: (a value) horizontal; label: annotates it.
@@ -89,6 +90,7 @@ module Poetry
89
90
 
90
91
  private
91
92
 
93
+ # One reference mark's SVG, by its kind.
92
94
  def reference_mark_svg(mark)
93
95
  case mark[:kind]
94
96
  when :line then reference_line_svg(mark)
@@ -139,6 +141,7 @@ module Poetry
139
141
  safe_join([rect, label])
140
142
  end
141
143
 
144
+ # A reference dot's SVG: the circle, and its label above it when given.
142
145
  def reference_dot_svg(mark)
143
146
  cx = ref_x_pixel(mark[:x])
144
147
  cy = ref_y_pixel(mark[:y])
@@ -1,4 +1,4 @@
1
- <%= content_tag(:div, **root_attributes.to_attributes) do %>
1
+ <%= content_tag(:div, **root_attributes) do %>
2
2
  <% if label_text && !nest_label? %><div data-slot="chart-tooltip-label" class="<%= css(:label) %>"><%= label_text %></div><% end %>
3
3
  <div class="<%= css(:inner) %>">
4
4
  <% rows.each do |row| %>
@@ -110,37 +110,40 @@ module Poetry
110
110
  end
111
111
 
112
112
  # The box's attributes with the part self-identification.
113
- # @api private
114
113
  def root_attributes
115
- html_attributes.merge_if_not_set(
116
- { "data-slot" => "chart-tooltip-content" }.merge(component_data_attributes)
117
- )
114
+ super("data-slot" => "chart-tooltip-content")
118
115
  end
119
116
 
120
117
  # One resolved tooltip row: name via config, color via item-else-config.
121
118
  # @api private
122
119
  class Row
120
+ # Wraps one tooltip item with the chart config it resolves against.
123
121
  def initialize(item, config)
124
122
  @item = item
125
123
  @config = config
126
124
  end
127
125
 
126
+ # The row's series key: the item's key, else its name, else value.
128
127
  def key
129
128
  (@item[:key] || @item[:name] || "value").to_s
130
129
  end
131
130
 
131
+ # The row's label, through the config.
132
132
  def name
133
133
  @config.label_for(key, @item[:name])
134
134
  end
135
135
 
136
+ # The row's swatch color: the item's own, else the config entry's.
136
137
  def color
137
138
  @item[:color] || @config[key]&.color
138
139
  end
139
140
 
141
+ # The row's raw value.
140
142
  def value
141
143
  @item[:value]
142
144
  end
143
145
 
146
+ # The value as text, a number with thousands separators; nil stays nil.
144
147
  def formatted_value
145
148
  return nil if value.nil?
146
149
  return ActiveSupport::NumberHelper.number_to_delimited(value) if value.is_a?(Numeric)
@@ -161,7 +164,6 @@ module Poetry
161
164
  end
162
165
 
163
166
  private :chart_config, :rows, :nest_label?, :label_text, :row_classes, :indicator_classes, :value_wrap_classes
164
- private :root_attributes
165
167
  end
166
168
  end
167
169
  end
@@ -1,4 +1,4 @@
1
- <%= content_tag(:div, **root_attributes.to_attributes) do %>
1
+ <%= content_tag(:div, **root_attributes) do %>
2
2
  <%= render Poetry::Charts::TooltipContent::Component.new(
3
3
  config: chart_config, items: items, label: hide_label ? nil : "…",
4
4
  indicator: indicator, hide_label: hide_label, hide_indicator: hide_indicator
@@ -43,18 +43,16 @@ module Poetry
43
43
 
44
44
  # The hidden box's attributes: part self-identification plus the
45
45
  # parent frame controller's tooltip target.
46
- # @api private
47
46
  def root_attributes
48
- html_attributes.merge_if_not_set(
47
+ super(
49
48
  {
50
49
  "data-slot" => "chart-tooltip",
51
50
  "hidden" => ""
52
- }.merge(stimulus_attributes_for(:root))
53
- .merge(component_data_attributes)
51
+ }
54
52
  )
55
53
  end
56
54
 
57
- private :chart_config, :items, :root_attributes
55
+ private :chart_config, :items
58
56
  end
59
57
  end
60
58
  end
@@ -1,19 +1,24 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { chartAdapter } from "@poetry/charts/adapter_registry"
3
3
 
4
- // The adapter mount: reads the FROZEN chart-spec v1 the
5
- // server embedded, hands it to the registered engine adapter, and owns
6
- // the lifecycle - render on connect, destroy on disconnect (Turbo-safe),
7
- // themeChanged on dark-mode flips. The helpers close the canvas gap:
8
- // resolveColor turns var(--color-key)/var(--chart-N) into concrete
9
- // values at paint time (CSS variables cannot reach a canvas).
4
+ /**
5
+ * The adapter mount: reads the FROZEN chart-spec v1 the
6
+ * server embedded, hands it to the registered engine adapter, and owns
7
+ * the lifecycle - render on connect, destroy on disconnect (Turbo-safe),
8
+ * themeChanged on dark-mode flips. The helpers close the canvas gap:
9
+ * resolveColor turns var(--color-key)/var(--chart-N) into concrete
10
+ * values at paint time (CSS variables cannot reach a canvas).
11
+ */
10
12
  export default class ChartAdapterController extends Controller {
11
13
  // The events this controller dispatches (manifest surface;
12
14
  // events_declaration.test.js enforces the list stays honest).
13
15
  static events = ["poetry--charts--adapter:rendered"]
14
16
 
15
17
  static targets = ["mount", "spec"]
16
- static values = { engine: String }
18
+ static values = {
19
+ // The registered engine adapter that renders the embedded spec.
20
+ engine: String
21
+ }
17
22
 
18
23
  /**
19
24
  * Mounts the declared engine: parses the embedded spec, validates the
@@ -3,24 +3,26 @@ import { applyCartesian } from "@poetry/charts/live/renderer"
3
3
  import { captureGeometry, matchJobs, applyJobs, finishJobs } from "@poetry/charts/motion/flip"
4
4
  import { tween } from "@poetry/charts/motion/tween"
5
5
 
6
- // The live channel: a chart rendered with live: true carries
7
- // its {spec, frame} payload in the DOM; this controller re-renders
8
- // client-side (the kernel renderer, in-place attribute updates) whenever
9
- // new data arrives - no server round trip. Two channels feed one path:
10
- //
11
- // 1. Replace the payload script's JSON - exactly what a
12
- // turbo_stream.replace/update does. A MutationObserver picks it up.
13
- // 2. The JS/event API: dispatch `poetry-chart:update` with
14
- // { detail: { data } } on the frame, or call update(data) on the
15
- // controller instance.
16
- //
17
- // Updates animate through the shared FLIP machinery (old geometry ->
18
- // new, the same cross-render morph); a structure change (window growth, series
19
- // change) snaps instead of morphing. The tooltip's
20
- // embedded coordinates are regenerated by the renderer and a
21
- // live:updated dispatch tells the tooltip controller to re-read them,
22
- // so it serves fresh values mid-stream. prefers-reduced-motion snaps
23
- // every update.
6
+ /**
7
+ * The live channel: a chart rendered with live: true carries
8
+ * its {spec, frame} payload in the DOM; this controller re-renders
9
+ * client-side (the kernel renderer, in-place attribute updates) whenever
10
+ * new data arrives - no server round trip. Two channels feed one path:
11
+ *
12
+ * 1. Replace the payload script's JSON - exactly what a
13
+ * turbo_stream.replace/update does. A MutationObserver picks it up.
14
+ * 2. The JS/event API: dispatch `poetry-chart:update` with
15
+ * { detail: { data } } on the frame, or call update(data) on the
16
+ * controller instance.
17
+ *
18
+ * Updates animate through the shared FLIP machinery (old geometry ->
19
+ * new, the same cross-render morph); a structure change (window growth, series
20
+ * change) snaps instead of morphing. The tooltip's
21
+ * embedded coordinates are regenerated by the renderer and a
22
+ * live:updated dispatch tells the tooltip controller to re-read them,
23
+ * so it serves fresh values mid-stream. prefers-reduced-motion snaps
24
+ * every update.
25
+ */
24
26
  export default class ChartLiveController extends Controller {
25
27
  // The events this controller dispatches (manifest surface;
26
28
  // events_declaration.test.js enforces the list stays honest).