poetry-charts 0.0.2

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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +6 -0
  3. data/CHANGELOG.md +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +94 -0
  6. data/THIRD_PARTY_NOTICES.md +39 -0
  7. data/app/assets/stylesheets/poetry-charts.css +117 -0
  8. data/app/components/poetry/charts/adapter_chart/component.html.erb +6 -0
  9. data/app/components/poetry/charts/adapter_chart/component.rb +99 -0
  10. data/app/components/poetry/charts/adapter_chart/preview.rb +37 -0
  11. data/app/components/poetry/charts/adapter_chart/style.rb +16 -0
  12. data/app/components/poetry/charts/area_chart/component.html.erb +75 -0
  13. data/app/components/poetry/charts/area_chart/component.rb +251 -0
  14. data/app/components/poetry/charts/area_chart/preview.rb +95 -0
  15. data/app/components/poetry/charts/area_chart/style.rb +29 -0
  16. data/app/components/poetry/charts/bar_chart/component.html.erb +95 -0
  17. data/app/components/poetry/charts/bar_chart/component.rb +318 -0
  18. data/app/components/poetry/charts/bar_chart/preview.rb +115 -0
  19. data/app/components/poetry/charts/bar_chart/style.rb +28 -0
  20. data/app/components/poetry/charts/bar_math.rb +95 -0
  21. data/app/components/poetry/charts/cartesian_family.rb +105 -0
  22. data/app/components/poetry/charts/chart_family.rb +58 -0
  23. data/app/components/poetry/charts/composed_chart/component.html.erb +95 -0
  24. data/app/components/poetry/charts/composed_chart/component.rb +309 -0
  25. data/app/components/poetry/charts/composed_chart/preview.rb +62 -0
  26. data/app/components/poetry/charts/composed_chart/style.rb +24 -0
  27. data/app/components/poetry/charts/container/component.html.erb +4 -0
  28. data/app/components/poetry/charts/container/component.rb +80 -0
  29. data/app/components/poetry/charts/container/preview.rb +45 -0
  30. data/app/components/poetry/charts/container/style.rb +22 -0
  31. data/app/components/poetry/charts/error_bars.rb +35 -0
  32. data/app/components/poetry/charts/legend_content/component.html.erb +10 -0
  33. data/app/components/poetry/charts/legend_content/component.rb +122 -0
  34. data/app/components/poetry/charts/legend_content/preview.rb +23 -0
  35. data/app/components/poetry/charts/legend_content/style.rb +26 -0
  36. data/app/components/poetry/charts/line_chart/component.html.erb +85 -0
  37. data/app/components/poetry/charts/line_chart/component.rb +252 -0
  38. data/app/components/poetry/charts/line_chart/preview.rb +115 -0
  39. data/app/components/poetry/charts/line_chart/style.rb +28 -0
  40. data/app/components/poetry/charts/live.rb +296 -0
  41. data/app/components/poetry/charts/motion.rb +98 -0
  42. data/app/components/poetry/charts/pie_chart/component.html.erb +36 -0
  43. data/app/components/poetry/charts/pie_chart/component.rb +256 -0
  44. data/app/components/poetry/charts/pie_chart/preview.rb +100 -0
  45. data/app/components/poetry/charts/pie_chart/style.rb +19 -0
  46. data/app/components/poetry/charts/polar_family.rb +95 -0
  47. data/app/components/poetry/charts/radar_chart/component.html.erb +73 -0
  48. data/app/components/poetry/charts/radar_chart/component.rb +329 -0
  49. data/app/components/poetry/charts/radar_chart/preview.rb +97 -0
  50. data/app/components/poetry/charts/radar_chart/style.rb +21 -0
  51. data/app/components/poetry/charts/radial_bar_chart/component.html.erb +65 -0
  52. data/app/components/poetry/charts/radial_bar_chart/component.rb +424 -0
  53. data/app/components/poetry/charts/radial_bar_chart/preview.rb +99 -0
  54. data/app/components/poetry/charts/radial_bar_chart/style.rb +27 -0
  55. data/app/components/poetry/charts/reference_marks.rb +156 -0
  56. data/app/components/poetry/charts/scatter_chart/component.html.erb +55 -0
  57. data/app/components/poetry/charts/scatter_chart/component.rb +396 -0
  58. data/app/components/poetry/charts/scatter_chart/preview.rb +93 -0
  59. data/app/components/poetry/charts/scatter_chart/style.rb +22 -0
  60. data/app/components/poetry/charts/tooltip_content/component.html.erb +19 -0
  61. data/app/components/poetry/charts/tooltip_content/component.rb +168 -0
  62. data/app/components/poetry/charts/tooltip_content/preview.rb +39 -0
  63. data/app/components/poetry/charts/tooltip_content/style.rb +31 -0
  64. data/app/components/poetry/charts/tooltip_layer/component.html.erb +6 -0
  65. data/app/components/poetry/charts/tooltip_layer/component.rb +61 -0
  66. data/app/components/poetry/charts/tooltip_layer/style.rb +13 -0
  67. data/app/components/poetry/charts/tooltip_wiring.rb +167 -0
  68. data/app/helpers/poetry/charts/components_helper.rb +125 -0
  69. data/app/javascript/poetry/charts/adapter_controller.js +75 -0
  70. data/app/javascript/poetry/charts/adapter_registry.js +52 -0
  71. data/app/javascript/poetry/charts/adapters/chartjs.js +86 -0
  72. data/app/javascript/poetry/charts/d3.js +14 -0
  73. data/app/javascript/poetry/charts/index.js +67 -0
  74. data/app/javascript/poetry/charts/live/renderer.js +456 -0
  75. data/app/javascript/poetry/charts/live_controller.js +195 -0
  76. data/app/javascript/poetry/charts/motion/flip.js +169 -0
  77. data/app/javascript/poetry/charts/motion/sector.js +62 -0
  78. data/app/javascript/poetry/charts/motion/tween.js +105 -0
  79. data/app/javascript/poetry/charts/motion_controller.js +230 -0
  80. data/app/javascript/poetry/charts/tooltip_controller.js +331 -0
  81. data/app/javascript/poetry/charts/window_controller.js +195 -0
  82. data/config/component_descriptions.yml +14 -0
  83. data/config/component_registry.yml +3055 -0
  84. data/config/controllers_manifest.json +100 -0
  85. data/config/importmap.rb +10 -0
  86. data/lib/poetry/charts/cartesian.rb +277 -0
  87. data/lib/poetry/charts/config.rb +149 -0
  88. data/lib/poetry/charts/engine.rb +76 -0
  89. data/lib/poetry/charts/geometry/area.rb +68 -0
  90. data/lib/poetry/charts/geometry/curve.rb +327 -0
  91. data/lib/poetry/charts/geometry/line.rb +64 -0
  92. data/lib/poetry/charts/geometry/nice_ticks.rb +220 -0
  93. data/lib/poetry/charts/geometry/path.rb +96 -0
  94. data/lib/poetry/charts/geometry/scale/band.rb +81 -0
  95. data/lib/poetry/charts/geometry/scale/linear.rb +127 -0
  96. data/lib/poetry/charts/geometry/stack.rb +129 -0
  97. data/lib/poetry/charts/geometry/ticks.rb +103 -0
  98. data/lib/poetry/charts/geometry.rb +66 -0
  99. data/lib/poetry/charts/polar.rb +179 -0
  100. data/lib/poetry/charts/spec.rb +108 -0
  101. data/lib/poetry/charts/theme_style.rb +53 -0
  102. data/lib/poetry/charts/version.rb +8 -0
  103. data/lib/poetry/charts.rb +114 -0
  104. data/lib/poetry-charts.rb +4 -0
  105. data/themes/default.css +146 -0
  106. data/themes/luma.css +139 -0
  107. data/themes/lyra.css +140 -0
  108. data/themes/maia.css +139 -0
  109. data/themes/mira.css +139 -0
  110. data/themes/nova.css +139 -0
  111. data/themes/rhea.css +139 -0
  112. data/themes/sera.css +140 -0
  113. data/themes/vega.css +144 -0
  114. data/vendor/d3-kernel/LICENSE-d3.txt +17 -0
  115. data/vendor/d3-kernel/LICENSE-decimal.js-light.txt +25 -0
  116. data/vendor/d3-kernel/LICENSE-recharts.txt +26 -0
  117. data/vendor/d3-kernel/README.md +30 -0
  118. data/vendor/d3-kernel/build.mjs +66 -0
  119. data/vendor/d3-kernel/entry.js +27 -0
  120. data/vendor/d3-kernel/recharts/state/cartesianAxisSlice.ts +6 -0
  121. data/vendor/d3-kernel/recharts/util/scale/getNiceTickValues.ts +310 -0
  122. data/vendor/d3-kernel/recharts/util/scale/util/arithmetic.ts +54 -0
  123. data/vendor/d3-kernel/recharts/util/types.ts +5 -0
  124. metadata +186 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 99231ee56a021b648de82294be7e444f38195bef2046f2df58bd24f0e3a8f46d
4
+ data.tar.gz: 4561421c7a07d32b09212358b80fb7753e1022b401552d47d3da42d0787fbb05
5
+ SHA512:
6
+ metadata.gz: 0b4dfbc1ca25a2478d3696cd7b5f83e370b50dffd544c89185e1fec3843326920c263073a3cd42f464d2e97bf1d190cccc3bf35d973c0a047ff8dcaf6f6fb8ed
7
+ data.tar.gz: 4e9e085244c9fc7ac1fcdf23546465854d5f1e6a921bbe6d9b8a9b6a84b1cecd294564b1571c9b1a636f31ad299f19e521254c2b7e470eac4bf6cd3f67577833
data/.gitattributes ADDED
@@ -0,0 +1,6 @@
1
+ # GitHub language stats (Linguist): the d3 kernel sources and its built
2
+ # bundle are vendored third-party code, and the JS test suite is not this
3
+ # gem's source.
4
+ vendor/d3-kernel/** linguist-vendored
5
+ app/javascript/poetry/charts/d3.js linguist-vendored
6
+ test/javascript/** linguist-vendored
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Matt Solt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # poetry-charts
2
+
3
+ poetry's chart tier: the shadcn chart surface as **server-rendered SVG**.
4
+
5
+ Ruby computes the geometry — data → domains → scales → ticks → points → paths
6
+ (d3-scale/d3-shape semantics, recharts' decimal-exact nice ticks) — and the
7
+ finished chart ships in the initial HTML: valid with JavaScript disabled, in
8
+ print, PDF, and email; themed by CSS variables (`--chart-1..5` + per-chart
9
+ `--color-<key>`); dark mode flips with zero re-render. A small Stimulus layer
10
+ adds tooltip/legend/active interactivity by reading **server-embedded
11
+ coordinates** — no chart math in the browser.
12
+
13
+ Entrance animation is on by default (recharts' defaults per family: bars
14
+ 400ms, everything else 1500ms ease) and stays doctrine-pure: the CSS motion
15
+ tier animates between server-computed states (line dash draw-in, area clip
16
+ reveal, bars growing from the value baseline, radar rising from the polar
17
+ center). `animate: false` opts a chart out; `prefers-reduced-motion` users
18
+ always get the finished chart in the initial paint.
19
+
20
+ Engines are swappable (three doors — full recipes in the Charts adapter guide on the poetry docs site):
21
+
22
+ 1. **The frame** (container/config/style + tooltip/legend chrome) is
23
+ engine-agnostic — the contract all three official shadcn ports share.
24
+ 2. **Adapters**: every chart also compiles to a closed, versioned chart-spec;
25
+ a duck-typed adapter (`render(el, spec)` / `update` / `destroy`) routes it
26
+ to another engine per chart via `engine:`. Chart.js ships as the reference
27
+ adapter (canvas — the big-data escape valve).
28
+ 3. **Islands**: React chart libraries (Recharts included) run inside the frame
29
+ via poetry's bounded island escape-hatch.
30
+
31
+ Scope: shadcn's chart surface — nine families (area, bar, line, pie, radar,
32
+ radial bar, scatter and composed, plus the adapter frame) with their tooltip
33
+ and legend variants.
34
+
35
+ ## Install
36
+
37
+ ```ruby
38
+ # Gemfile
39
+ gem "poetry-charts"
40
+ ```
41
+
42
+ Hosts running poetry-ui can wire everything below in one shot —
43
+ `bin/rails g poetry:install --charts` — and skip the rest of this
44
+ section.
45
+
46
+ The engine merges its importmap pins automatically (`@poetry/charts`) —
47
+ no build step. The tooltip chrome boots with one registration in your
48
+ Stimulus entrypoint:
49
+
50
+ ```js
51
+ import { registerPoetryChartsControllers } from "@poetry/charts"
52
+ registerPoetryChartsControllers(application)
53
+ ```
54
+
55
+ The motion tier is one `@import` in your CSS build (the engine also exposes
56
+ it to Propshaft as `poetry-charts.css`); skip it and charts render static:
57
+
58
+ ```css
59
+ @import "poetry-charts/app/assets/stylesheets/poetry-charts.css";
60
+ ```
61
+
62
+ Interactive charts are **real forms**: submit a filter, the chart
63
+ re-renders on the server (Turbo makes it smooth; the mechanics need no
64
+ JS). Give a chart a stable `id:` and it **morphs** between those server
65
+ renders — under any same-context swap (Turbo Drive/Frames/Streams), the
66
+ new render starts from the old geometry and tweens to its own; a shape
67
+ change (points added/removed) replays the entrance instead, recharts'
68
+ own update behavior. See the Charts adapter guide on the poetry docs site for BYO engines.
69
+
70
+ For data that can't round-trip (streaming metrics, tickers), the
71
+ cartesian trio takes `live: true`: the chart embeds its `{spec, frame}`
72
+ payload and a client renderer — running on the vendored
73
+ `@poetry/charts/d3` kernel, proven byte-equal to the Ruby engine —
74
+ redraws it in place. Feed it either channel:
75
+
76
+ ```js
77
+ // 1. Replace the payload script's JSON (what a turbo_stream.update does)
78
+ script.textContent = JSON.stringify({ ...payload, spec: { ...payload.spec, data } })
79
+ // 2. Or dispatch on the chart frame
80
+ frame.dispatchEvent(new CustomEvent("poetry-chart:update", { detail: { data } }))
81
+ ```
82
+
83
+ Updates tween through the same FLIP machinery, the tooltip keeps serving
84
+ fresh values mid-stream, and `prefers-reduced-motion` snaps. Live data
85
+ carries pre-formatted category strings (lambdas can't ride JSON — the
86
+ server raises a teaching error on `tick_formatter`/`labels`).
87
+
88
+ Part of the [poetry](../poetry) gem family. Status:
89
+ nine families, 46 gallery examples, the frozen spec-v1 adapter seam.
90
+
91
+ ## License
92
+
93
+ Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
94
+ See `THIRD_PARTY_NOTICES.md` for adapted code.
@@ -0,0 +1,39 @@
1
+ # Third-party notices - poetry-charts
2
+
3
+ All third-party code in this gem lives in the committed
4
+ `app/javascript/poetry/charts/d3.js` kernel (built from
5
+ `vendor/d3-kernel/`, attribution banner included) and the Ruby
6
+ transcription of recharts' nice-tick algorithm. License texts:
7
+
8
+ - d3 modules (ISC, Mike Bostock) - `vendor/d3-kernel/LICENSE-d3.txt`
9
+ - decimal.js-light (MIT, Michael Mclaughlin) - `vendor/d3-kernel/LICENSE-decimal.js-light.txt`
10
+ - recharts (MIT, Recharts Group) - `vendor/d3-kernel/LICENSE-recharts.txt`
11
+
12
+ ## shadcn/ui
13
+
14
+ - Source: https://github.com/shadcn-ui/ui (MIT)
15
+ - The chart component anatomy, class dictionaries, and the per-theme chart rules in themes/*.css are derived from shadcn/ui's chart surface and its style stylesheets (the theme fidelity ledger in poetry-ui records the pin and every deviation)
16
+
17
+ ```
18
+ MIT License
19
+
20
+ Copyright (c) 2023 shadcn
21
+
22
+ Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ of this software and associated documentation files (the "Software"), to deal
24
+ in the Software without restriction, including without limitation the rights
25
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ copies of the Software, and to permit persons to whom the Software is
27
+ furnished to do so, subject to the following conditions:
28
+
29
+ The above copyright notice and this permission notice shall be included in all
30
+ copies or substantial portions of the Software.
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ SOFTWARE.
39
+ ```
@@ -0,0 +1,117 @@
1
+ /*
2
+ * poetry-charts motion tier: entrance animation for the
3
+ * server-rendered charts, driven by the data-animate flag and the
4
+ * --poetry-motion-* custom properties the components emit on the SVG.
5
+ *
6
+ * Two invariants:
7
+ * - every keyframe ends at the natural attribute state, so a chart at
8
+ * rest is pixel-identical to animation never having run;
9
+ * - everything sits behind prefers-reduced-motion: no-preference, so
10
+ * reduced-motion users get the finished chart in the initial paint.
11
+ *
12
+ * Hosts add this file to their CSS build (see the README install story).
13
+ */
14
+
15
+ @media (prefers-reduced-motion: no-preference) {
16
+ /* Line: pathLength="1" normalizes the path to unit length, so the
17
+ classic dash draw-in needs no runtime path measurement. */
18
+ [data-animate] path[data-slot="chart-line"] {
19
+ stroke-dasharray: 1 1;
20
+ animation: poetry-chart-draw var(--poetry-motion-duration, 1500ms) var(--poetry-motion-easing, ease) var(--poetry-motion-delay, 0ms) both;
21
+ }
22
+
23
+ /* Area: the clipPath rect reveals the series left-to-right. The
24
+ rect covers the full frame at rest. */
25
+ [data-animate] rect[data-slot="chart-motion-reveal"] {
26
+ transform-origin: 0 0;
27
+ animation: poetry-chart-reveal var(--poetry-motion-duration, 1500ms) var(--poetry-motion-easing, ease) var(--poetry-motion-delay, 0ms) both;
28
+ }
29
+
30
+ /* Bars grow from the value baseline; data-motion-origin carries the
31
+ zero edge (sign x orientation). Corner radii stretch mid-flight -
32
+ the declared simplification vs rebuilding the path per frame. */
33
+ [data-animate] path[data-slot="chart-bar"] {
34
+ transform-box: fill-box;
35
+ transform-origin: center bottom;
36
+ animation: poetry-chart-grow-y var(--poetry-motion-duration, 400ms) var(--poetry-motion-easing, ease) var(--poetry-motion-delay, 0ms) both;
37
+ }
38
+ [data-animate] path[data-slot="chart-bar"][data-motion-origin="top"] {
39
+ transform-origin: center top;
40
+ }
41
+ [data-animate] path[data-slot="chart-bar"][data-motion-origin="left"],
42
+ [data-animate] path[data-slot="chart-bar"][data-motion-origin="right"] {
43
+ animation-name: poetry-chart-grow-x;
44
+ transform-origin: left center;
45
+ }
46
+ [data-animate] path[data-slot="chart-bar"][data-motion-origin="right"] {
47
+ transform-origin: right center;
48
+ }
49
+
50
+ /* Scatter points pop from their own centers (a 400ms linear
51
+ scale-in). */
52
+ [data-animate] circle[data-slot="chart-scatter-point"] {
53
+ transform-box: fill-box;
54
+ transform-origin: center;
55
+ animation: poetry-chart-rise var(--poetry-motion-duration, 400ms) var(--poetry-motion-easing, linear) var(--poetry-motion-delay, 0ms) both;
56
+ }
57
+
58
+ /* Radar rises from the polar center (the component ships it in
59
+ --poetry-motion-center). Linear interpolation of every vertex
60
+ toward one point IS a uniform scale, so this matches per-vertex
61
+ interpolation frame for frame. Dots ride the same group. */
62
+ [data-animate] g[data-slot="chart-radars"] {
63
+ transform-box: view-box;
64
+ transform-origin: var(--poetry-motion-center, center);
65
+ animation: poetry-chart-rise var(--poetry-motion-duration, 1500ms) var(--poetry-motion-easing, ease) var(--poetry-motion-delay, 0ms) both;
66
+ }
67
+
68
+ /* Labels wait for the series animation to finish, then take a
69
+ short fade. Line dots do the same; radar dots are excluded - they
70
+ ride the rising group above. */
71
+ [data-animate] g[data-slot="chart-labels"],
72
+ [data-animate] g[data-slot="chart-lines"] g[data-slot="chart-dots"] {
73
+ animation: poetry-chart-fade 200ms ease calc(var(--poetry-motion-delay, 0ms) + var(--poetry-motion-duration, 1500ms)) both;
74
+ }
75
+
76
+ /* A cross-render morph replaces the entrance: the controller
77
+ stamps data-motion="morph" before the new render's first paint, and
78
+ this kill switch stops the fresh elements' entrance animations so the
79
+ FLIP tween owns the frame. It stays on at "settled" - otherwise the
80
+ morph's end would re-arm the entrance keyframes and replay them.
81
+ Killing a COMPLETED entrance is pixel-identical by the invariant
82
+ above: every keyframe ends at the natural attribute state. */
83
+ [data-animate][data-motion="morph"] :is(path, g, rect, circle, text),
84
+ [data-animate][data-motion="settled"] :is(path, g, rect, circle, text) {
85
+ animation: none !important;
86
+ }
87
+
88
+ @keyframes poetry-chart-draw {
89
+ from { stroke-dashoffset: 1; }
90
+ to { stroke-dashoffset: 0; }
91
+ }
92
+
93
+ @keyframes poetry-chart-reveal {
94
+ from { transform: scaleX(0); }
95
+ to { transform: scaleX(1); }
96
+ }
97
+
98
+ @keyframes poetry-chart-grow-y {
99
+ from { transform: scaleY(0); }
100
+ to { transform: scaleY(1); }
101
+ }
102
+
103
+ @keyframes poetry-chart-grow-x {
104
+ from { transform: scaleX(0); }
105
+ to { transform: scaleX(1); }
106
+ }
107
+
108
+ @keyframes poetry-chart-rise {
109
+ from { transform: scale(0); }
110
+ to { transform: scale(1); }
111
+ }
112
+
113
+ @keyframes poetry-chart-fade {
114
+ from { opacity: 0; }
115
+ to { opacity: 1; }
116
+ }
117
+ }
@@ -0,0 +1,6 @@
1
+ <%= render Poetry::Charts::Container::Component.new(config: chart_config, id: chart_id.delete_prefix("chart-")) do %>
2
+ <div class="<%= css(:frame) %>" data-component="<%= self.class.component_title %>" <%= tag.attributes(stimulus_attributes_for(:frame)) %>>
3
+ <div class="<%= css(:mount) %>" data-slot="chart-adapter-mount" role="img" aria-label="<%= mount_label %>" <%= tag.attributes(stimulus_attributes_for(:mount)) %>></div>
4
+ <script type="application/json" data-slot="chart-spec" <%= tag.attributes(stimulus_attributes_for(:spec)) %>><%= script_json(spec) %></script>
5
+ </div>
6
+ <% end %>
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Charts
5
+ # The bring-your-own-engine chart mount.
6
+ module AdapterChart
7
+ # The BYO-engine mount: `poetry_chart :bar, engine:
8
+ # "chartjs", ...` renders the Container frame (theming intact), a
9
+ # mount element, and the FROZEN chart-spec v1 - the registered
10
+ # adapter draws whatever it likes inside. The seam is deliberately
11
+ # whole-chart coarse: the compositional slot grammar belongs to the
12
+ # default engine; adapters consume the closed spec via series:/axes:
13
+ # arguments.
14
+ #
15
+ # @example
16
+ # <%= poetry_chart :bar, engine: "chartjs", data: data, config: config,
17
+ # series: [{ data_key: :desktop }], axes: { x: { data_key: :month } } %>
18
+ class Component < Poetry::Core::Component
19
+ # Projected into the registry and agent surface.
20
+ AGENT_RULES = [
21
+ "The adapter path takes series:/axes: ARGUMENTS (the closed spec), not slots.",
22
+ "The host must register the engine first: registerChartAdapter(name, adapter) - " \
23
+ "poetry ships createChartJsAdapter(Chart) as the reference.",
24
+ "The Container contract still applies: config colors, --chart tokens, dark mode.",
25
+ "Prefer the default engine; adapters are for engine-specific needs (e.g. >10k points on canvas)."
26
+ ].freeze
27
+
28
+ # The adapter seam's whole JS surface: the frame registers the
29
+ # controller with the engine name, the mount div and the spec
30
+ # <script> are its two targets.
31
+ use_stimulus do
32
+ on :frame do
33
+ controller "poetry--charts--adapter" do
34
+ register
35
+ value :engine
36
+ end
37
+ end
38
+ on :mount do
39
+ controller("poetry--charts--adapter") { target :mount }
40
+ end
41
+ on :spec do
42
+ controller("poetry--charts--adapter") { target :spec }
43
+ end
44
+ end
45
+
46
+ option :type, :symbol, required: true, doc: "The chart type carried in the spec (see Spec::TYPES)."
47
+ option :engine, :string, required: true,
48
+ doc: "The registered adapter's name; the controller hands it the mount and the spec."
49
+ option :data, ActiveModel::Type::Value.new, required: true, doc: "The rows to plot, serialized into the spec."
50
+ option :config, ActiveModel::Type::Value.new, required: true,
51
+ doc: "The series config - key => { label:, color: } - naming " \
52
+ "and coloring every series."
53
+ option :series, ActiveModel::Type::Value.new, required: true,
54
+ doc: "The series list ({ data_key:, ... } hashes) - the closed " \
55
+ "spec's replacement for slots."
56
+ option :axes, ActiveModel::Type::Value.new, doc: "The axis config ({ x:, y: } hashes), also spec-carried."
57
+ option :id, :string,
58
+ doc: "Explicit DOM id token, stable across renders; otherwise the chart gets a unique per-render id."
59
+ option :label, :string, doc: "Accessible name for the mount; defaults to one built from the type and engine."
60
+
61
+ validates :type, inclusion: { in: Spec::TYPES }
62
+
63
+ part "chart-adapter-mount", "The engine's drawing surface (role=img carrying the " \
64
+ "accessible label) - the registered adapter renders into it"
65
+ part "chart-spec", "The frozen chart-spec v1, served as JSON in an " \
66
+ "application/json script for the adapter to consume"
67
+
68
+ # Built (and validated) server-side - a bad series/axis key raises
69
+ # at render, never in the browser.
70
+ # @api private
71
+ def spec
72
+ @spec ||= Spec.new(type: type, data: data, series: series, axes: axes || {}, config: config)
73
+ end
74
+
75
+ # The config: option wrapped as a {Poetry::Charts::Config}.
76
+ # @api private
77
+ def chart_config
78
+ @chart_config ||= Poetry::Charts::Config.wrap(config)
79
+ end
80
+
81
+ # The data-chart scope: explicit id when given, else unique per
82
+ # render.
83
+ # @api private
84
+ def chart_id
85
+ @chart_id ||= (dom_id_token(id) ? "chart-#{dom_id_token(id)}" : poetry_instance_id("chart"))
86
+ end
87
+
88
+ # The mount's accessible name: explicit label: or a type+engine
89
+ # default.
90
+ # @api private
91
+ def mount_label
92
+ label.presence || "#{type.to_s.capitalize} chart (#{engine})"
93
+ end
94
+
95
+ private :spec, :chart_config, :chart_id, :mount_label
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Charts
5
+ module AdapterChart
6
+ # The BYO-engine mount: the frame, the mount element, and the spec
7
+ # JSON render server-side; the registered adapter draws client-side,
8
+ # so previews show the served contract, not an engine's output.
9
+ class Preview < Poetry::Core::Preview::Base
10
+ DATA = [
11
+ { month: "Jan", desktop: 186, mobile: 80 },
12
+ { month: "Feb", desktop: 305, mobile: 200 },
13
+ { month: "Mar", desktop: 237, mobile: 120 },
14
+ { month: "Apr", desktop: 73, mobile: 190 }
15
+ ].freeze
16
+
17
+ CONFIG = {
18
+ desktop: { label: "Desktop", color: "var(--chart-1)" },
19
+ mobile: { label: "Mobile", color: "var(--chart-2)" }
20
+ }.freeze
21
+
22
+ def default
23
+ render_component(type: :bar, engine: "chartjs", data: DATA, config: CONFIG,
24
+ series: [{ data_key: :desktop }, { data_key: :mobile }],
25
+ axes: { x: { data_key: :month } })
26
+ end
27
+
28
+ def labelled
29
+ render_component(type: :line, engine: "chartjs", data: DATA, config: CONFIG,
30
+ series: [{ data_key: :desktop }],
31
+ axes: { x: { data_key: :month } },
32
+ id: "traffic", label: "Monthly desktop traffic")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Charts
5
+ module AdapterChart
6
+ # The mount fills the Container's aspect box; the engine owns
7
+ # everything inside it.
8
+ class Style < Poetry::Core::Style
9
+ base ""
10
+
11
+ element :frame, "contents"
12
+ element :mount, "size-full"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,75 @@
1
+ <%= render Poetry::Charts::Container::Component.new(config: chart_config, id: chart_id.delete_prefix("chart-")) do %>
2
+ <%= content_tag(:div, frame_attributes.merge("class" => css(:frame))) do %>
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 series_entries.any?(&:gradient) || animate? %>
5
+ <defs>
6
+ <% series_entries.select(&:gradient).each do |entry| %>
7
+ <linearGradient id="<%= gradient_id(entry) %>" x1="0" y1="0" x2="0" y2="1">
8
+ <stop offset="5%" stop-color="var(--color-<%= entry.key %>)" stop-opacity="0.8" />
9
+ <stop offset="95%" stop-color="var(--color-<%= entry.key %>)" stop-opacity="0.1" />
10
+ </linearGradient>
11
+ <% end %>
12
+ <% if animate? %>
13
+ <%# The entrance reveal: the motion stylesheet scales this rect 0->1. %>
14
+ <clipPath id="<%= chart_id %>-reveal"><rect data-slot="chart-motion-reveal" x="0" y="0" width="<%= width %>" height="<%= height %>" /></clipPath>
15
+ <% end %>
16
+ </defs>
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 %>
32
+ <%= cursor_svg(:line) %>
33
+ <g data-slot="chart-areas"<% if animate? %> clip-path="url(#<%= chart_id %>-reveal)"<% end %>>
34
+ <% series_entries.each do |entry| %>
35
+ <% if (d = series_path(entry)) %>
36
+ <path data-slot="chart-area" data-key="<%= entry.key %>" d="<%= d %>" fill="<%= fill_for(entry) %>" fill-opacity="<%= entry.fill_opacity %>" stroke="none" />
37
+ <path data-slot="chart-area-stroke" data-key="<%= entry.key %>" d="<%= series_stroke_path(entry) %>" fill="none" stroke="var(--color-<%= entry.key %>)" stroke-width="<%= entry.stroke_width %>" />
38
+ <% end %>
39
+ <% end %>
40
+ </g>
41
+ <%= reference_marks_svg %>
42
+ <% if tooltip? %>
43
+ <g data-slot="chart-active-dots" aria-hidden="true">
44
+ <% series_entries.each do |entry| %>
45
+ <% active_dot_markers(entry).each do |marker| %>
46
+ <circle data-slot="chart-active-dot" data-key="<%= entry.key %>" data-index="<%= marker[:index] %>" cx="<%= fnum(marker[:x]) %>" cy="<%= fnum(marker[:y]) %>" r="4" fill="var(--color-<%= entry.key %>)" display="none" />
47
+ <% end %>
48
+ <% end %>
49
+ </g>
50
+ <% end %>
51
+ <% if x_axis? %>
52
+ <g data-slot="chart-x-axis">
53
+ <% cartesian.categories.each_with_index do |category, i| %>
54
+ <text class="<%= css(:tick) %>" x="<%= fnum(cartesian.x_centers[i]) %>" y="<%= fnum(cartesian.plot_bottom + x_axis_config.tick_margin) %>" dy="0.71em" text-anchor="middle"><%= x_tick_label(category) %></text>
55
+ <% end %>
56
+ </g>
57
+ <% end %>
58
+ <% if y_axis? %>
59
+ <g data-slot="chart-y-axis">
60
+ <% cartesian.y_ticks.each do |tick| %>
61
+ <text class="<%= css(:tick) %>" x="<%= fnum(cartesian.plot_left - y_axis_config.tick_margin) %>" y="<%= fnum(cartesian.y_scale.call(tick)) %>" dy="0.355em" text-anchor="end"><%= y_tick_label(tick) %></text>
62
+ <% end %>
63
+ </g>
64
+ <% end %>
65
+ <%= brush_svg %>
66
+ <%= zoom_selection_svg %>
67
+ </svg>
68
+ <% if legend? %>
69
+ <%= render legend_component %>
70
+ <% end %>
71
+ <% if tooltip? %><%= render tooltip_layer_component %><% end %>
72
+ <script type="application/json" data-slot="chart-coordinates" <%= tag.attributes(stimulus_attributes_for(:coordinates)) %>><%= script_json(coordinates_json) %></script>
73
+ <% if live? %><script type="application/json" data-slot="chart-live-payload" <%= tag.attributes(stimulus_attributes_for(:live_payload)) %>><%= script_json(live_payload_json) %></script>
74
+ <% end %> <% end %>
75
+ <% end %>