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.
- checksums.yaml +7 -0
- data/.gitattributes +6 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/THIRD_PARTY_NOTICES.md +39 -0
- data/app/assets/stylesheets/poetry-charts.css +117 -0
- data/app/components/poetry/charts/adapter_chart/component.html.erb +6 -0
- data/app/components/poetry/charts/adapter_chart/component.rb +99 -0
- data/app/components/poetry/charts/adapter_chart/preview.rb +37 -0
- data/app/components/poetry/charts/adapter_chart/style.rb +16 -0
- data/app/components/poetry/charts/area_chart/component.html.erb +75 -0
- data/app/components/poetry/charts/area_chart/component.rb +251 -0
- data/app/components/poetry/charts/area_chart/preview.rb +95 -0
- data/app/components/poetry/charts/area_chart/style.rb +29 -0
- data/app/components/poetry/charts/bar_chart/component.html.erb +95 -0
- data/app/components/poetry/charts/bar_chart/component.rb +318 -0
- data/app/components/poetry/charts/bar_chart/preview.rb +115 -0
- data/app/components/poetry/charts/bar_chart/style.rb +28 -0
- data/app/components/poetry/charts/bar_math.rb +95 -0
- data/app/components/poetry/charts/cartesian_family.rb +105 -0
- data/app/components/poetry/charts/chart_family.rb +58 -0
- data/app/components/poetry/charts/composed_chart/component.html.erb +95 -0
- data/app/components/poetry/charts/composed_chart/component.rb +309 -0
- data/app/components/poetry/charts/composed_chart/preview.rb +62 -0
- data/app/components/poetry/charts/composed_chart/style.rb +24 -0
- data/app/components/poetry/charts/container/component.html.erb +4 -0
- data/app/components/poetry/charts/container/component.rb +80 -0
- data/app/components/poetry/charts/container/preview.rb +45 -0
- data/app/components/poetry/charts/container/style.rb +22 -0
- data/app/components/poetry/charts/error_bars.rb +35 -0
- data/app/components/poetry/charts/legend_content/component.html.erb +10 -0
- data/app/components/poetry/charts/legend_content/component.rb +122 -0
- data/app/components/poetry/charts/legend_content/preview.rb +23 -0
- data/app/components/poetry/charts/legend_content/style.rb +26 -0
- data/app/components/poetry/charts/line_chart/component.html.erb +85 -0
- data/app/components/poetry/charts/line_chart/component.rb +252 -0
- data/app/components/poetry/charts/line_chart/preview.rb +115 -0
- data/app/components/poetry/charts/line_chart/style.rb +28 -0
- data/app/components/poetry/charts/live.rb +296 -0
- data/app/components/poetry/charts/motion.rb +98 -0
- data/app/components/poetry/charts/pie_chart/component.html.erb +36 -0
- data/app/components/poetry/charts/pie_chart/component.rb +256 -0
- data/app/components/poetry/charts/pie_chart/preview.rb +100 -0
- data/app/components/poetry/charts/pie_chart/style.rb +19 -0
- data/app/components/poetry/charts/polar_family.rb +95 -0
- data/app/components/poetry/charts/radar_chart/component.html.erb +73 -0
- data/app/components/poetry/charts/radar_chart/component.rb +329 -0
- data/app/components/poetry/charts/radar_chart/preview.rb +97 -0
- data/app/components/poetry/charts/radar_chart/style.rb +21 -0
- data/app/components/poetry/charts/radial_bar_chart/component.html.erb +65 -0
- data/app/components/poetry/charts/radial_bar_chart/component.rb +424 -0
- data/app/components/poetry/charts/radial_bar_chart/preview.rb +99 -0
- data/app/components/poetry/charts/radial_bar_chart/style.rb +27 -0
- data/app/components/poetry/charts/reference_marks.rb +156 -0
- data/app/components/poetry/charts/scatter_chart/component.html.erb +55 -0
- data/app/components/poetry/charts/scatter_chart/component.rb +396 -0
- data/app/components/poetry/charts/scatter_chart/preview.rb +93 -0
- data/app/components/poetry/charts/scatter_chart/style.rb +22 -0
- data/app/components/poetry/charts/tooltip_content/component.html.erb +19 -0
- data/app/components/poetry/charts/tooltip_content/component.rb +168 -0
- data/app/components/poetry/charts/tooltip_content/preview.rb +39 -0
- data/app/components/poetry/charts/tooltip_content/style.rb +31 -0
- data/app/components/poetry/charts/tooltip_layer/component.html.erb +6 -0
- data/app/components/poetry/charts/tooltip_layer/component.rb +61 -0
- data/app/components/poetry/charts/tooltip_layer/style.rb +13 -0
- data/app/components/poetry/charts/tooltip_wiring.rb +167 -0
- data/app/helpers/poetry/charts/components_helper.rb +125 -0
- data/app/javascript/poetry/charts/adapter_controller.js +75 -0
- data/app/javascript/poetry/charts/adapter_registry.js +52 -0
- data/app/javascript/poetry/charts/adapters/chartjs.js +86 -0
- data/app/javascript/poetry/charts/d3.js +14 -0
- data/app/javascript/poetry/charts/index.js +67 -0
- data/app/javascript/poetry/charts/live/renderer.js +456 -0
- data/app/javascript/poetry/charts/live_controller.js +195 -0
- data/app/javascript/poetry/charts/motion/flip.js +169 -0
- data/app/javascript/poetry/charts/motion/sector.js +62 -0
- data/app/javascript/poetry/charts/motion/tween.js +105 -0
- data/app/javascript/poetry/charts/motion_controller.js +230 -0
- data/app/javascript/poetry/charts/tooltip_controller.js +331 -0
- data/app/javascript/poetry/charts/window_controller.js +195 -0
- data/config/component_descriptions.yml +14 -0
- data/config/component_registry.yml +3055 -0
- data/config/controllers_manifest.json +100 -0
- data/config/importmap.rb +10 -0
- data/lib/poetry/charts/cartesian.rb +277 -0
- data/lib/poetry/charts/config.rb +149 -0
- data/lib/poetry/charts/engine.rb +76 -0
- data/lib/poetry/charts/geometry/area.rb +68 -0
- data/lib/poetry/charts/geometry/curve.rb +327 -0
- data/lib/poetry/charts/geometry/line.rb +64 -0
- data/lib/poetry/charts/geometry/nice_ticks.rb +220 -0
- data/lib/poetry/charts/geometry/path.rb +96 -0
- data/lib/poetry/charts/geometry/scale/band.rb +81 -0
- data/lib/poetry/charts/geometry/scale/linear.rb +127 -0
- data/lib/poetry/charts/geometry/stack.rb +129 -0
- data/lib/poetry/charts/geometry/ticks.rb +103 -0
- data/lib/poetry/charts/geometry.rb +66 -0
- data/lib/poetry/charts/polar.rb +179 -0
- data/lib/poetry/charts/spec.rb +108 -0
- data/lib/poetry/charts/theme_style.rb +53 -0
- data/lib/poetry/charts/version.rb +8 -0
- data/lib/poetry/charts.rb +114 -0
- data/lib/poetry-charts.rb +4 -0
- data/themes/default.css +146 -0
- data/themes/luma.css +139 -0
- data/themes/lyra.css +140 -0
- data/themes/maia.css +139 -0
- data/themes/mira.css +139 -0
- data/themes/nova.css +139 -0
- data/themes/rhea.css +139 -0
- data/themes/sera.css +140 -0
- data/themes/vega.css +144 -0
- data/vendor/d3-kernel/LICENSE-d3.txt +17 -0
- data/vendor/d3-kernel/LICENSE-decimal.js-light.txt +25 -0
- data/vendor/d3-kernel/LICENSE-recharts.txt +26 -0
- data/vendor/d3-kernel/README.md +30 -0
- data/vendor/d3-kernel/build.mjs +66 -0
- data/vendor/d3-kernel/entry.js +27 -0
- data/vendor/d3-kernel/recharts/state/cartesianAxisSlice.ts +6 -0
- data/vendor/d3-kernel/recharts/util/scale/getNiceTickValues.ts +310 -0
- data/vendor/d3-kernel/recharts/util/scale/util/arithmetic.ts +54 -0
- data/vendor/d3-kernel/recharts/util/types.ts +5 -0
- 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
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 %>
|