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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
# The pie chart family.
|
|
6
|
+
module PieChart
|
|
7
|
+
# Renders a pie or donut chart as server-computed polar sectors.
|
|
8
|
+
# Slices color from their row's fill key, the donut hole comes from
|
|
9
|
+
# inner_radius, stacked pies nest two rings with their own data,
|
|
10
|
+
# and the center label is plain SVG text. The 250x250 viewBox pairs
|
|
11
|
+
# with a square, max-height-capped container, so absolute-pixel
|
|
12
|
+
# radii (inner_radius: 60, active pop-outs of +10) keep the same
|
|
13
|
+
# proportion everywhere. Slices meet flush by default;
|
|
14
|
+
# stroke_width: N opts into var(--background) separators, which
|
|
15
|
+
# hold up in dark mode where a hard-coded white would not.
|
|
16
|
+
#
|
|
17
|
+
# @example A donut with a tooltip
|
|
18
|
+
# <%= poetry_chart :pie, data: browsers, config: config do |c| %>
|
|
19
|
+
# <% c.with_pie data_key: :visitors, name_key: :browser, inner_radius: 60 %>
|
|
20
|
+
# <% c.with_tooltip %>
|
|
21
|
+
# <% end %>
|
|
22
|
+
class Component < Poetry::Core::Component
|
|
23
|
+
include Poetry::Charts::ChartFamily
|
|
24
|
+
include Poetry::Charts::TooltipWiring
|
|
25
|
+
include Poetry::Charts::Motion
|
|
26
|
+
|
|
27
|
+
# Projected into the registry and agent surface.
|
|
28
|
+
AGENT_RULES = [
|
|
29
|
+
"Rows carry their slice color in a fill key (var(--color-<name>)); the config maps names to labels.",
|
|
30
|
+
"inner_radius: 60 makes the donut; with_center_label(title:, subtitle:) fills the hole.",
|
|
31
|
+
"Stacked pies: two with_pie slots with their own data: and non-overlapping radii.",
|
|
32
|
+
"active_index: pops one slice out by 10px (the donut-active look).",
|
|
33
|
+
"The tooltip walks slices by hover AND arrow keys (role=application when attached).",
|
|
34
|
+
"Entrance animation is on by default (source parity); animate: false for a static chart. " \
|
|
35
|
+
"Reduced-motion users always get the finished chart."
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# One with_pie call's captured series config.
|
|
39
|
+
Series = Data.define(:data, :data_key, :name_key, :inner_radius, :outer_radius,
|
|
40
|
+
:padding_angle, :stroke_width, :color_key, :labels, :label_key,
|
|
41
|
+
:active_index, :active_grow)
|
|
42
|
+
|
|
43
|
+
option :data, ActiveModel::Type::Value.new,
|
|
44
|
+
doc: "Default rows for pies that don't bring their own data: - one hash per slice."
|
|
45
|
+
option :config, ActiveModel::Type::Value.new, required: true,
|
|
46
|
+
doc: "The series config - name => { label:, color: } - naming " \
|
|
47
|
+
"and coloring the slices."
|
|
48
|
+
option :id, :string,
|
|
49
|
+
doc: "Explicit DOM id token, stable across renders; otherwise the chart gets a unique per-render id."
|
|
50
|
+
option :width, :integer, default: 250,
|
|
51
|
+
doc: "ViewBox width in pixels; the rendered chart scales to its container."
|
|
52
|
+
option :height, :integer, default: 250, doc: "ViewBox height in pixels."
|
|
53
|
+
option :margin, ActiveModel::Type::Value.new,
|
|
54
|
+
doc: "Margin overrides ({ top:, right:, bottom:, left: }), merged over the slim polar default."
|
|
55
|
+
option :label, :string,
|
|
56
|
+
doc: "Accessible name for the chart SVG; defaults to one built from the configured series."
|
|
57
|
+
|
|
58
|
+
motion_options(delay: 400)
|
|
59
|
+
|
|
60
|
+
part "chart-svg", "The chart canvas (<svg>) - the aria-label surface, the tooltip's " \
|
|
61
|
+
"focus/keyboard surface (role=application when it attaches), and " \
|
|
62
|
+
"the motion rig's mount",
|
|
63
|
+
states: {
|
|
64
|
+
"data-animate" => "when animate (the default) - the entrance tier's flag the " \
|
|
65
|
+
"motion stylesheet and controller key off",
|
|
66
|
+
"data-motion" => { condition: "runtime, when animate - the motion engine's lifecycle stamp",
|
|
67
|
+
values: %w[entrance morph settled] }
|
|
68
|
+
},
|
|
69
|
+
vars: {
|
|
70
|
+
"--poetry-motion-delay" => "the motion rig's entrance delay (animation_begin)",
|
|
71
|
+
"--poetry-motion-duration" => "the motion rig's entrance duration (animation_duration)",
|
|
72
|
+
"--poetry-motion-easing" => "the motion rig's easing keyword (animation_easing)"
|
|
73
|
+
}
|
|
74
|
+
part "chart-pie", "One pie's slice group (<g>) - a ring per with_pie slot",
|
|
75
|
+
states: { "data-key" => "always - the series key" }
|
|
76
|
+
part "chart-pie-sector", "One slice (<path>) - fill from its row's color, popped out when active",
|
|
77
|
+
states: {
|
|
78
|
+
"data-key" => "always - the series key",
|
|
79
|
+
"data-index" => "on the first pie's slices - the datum index the tooltip walks",
|
|
80
|
+
"data-active" => "the active slice - server-rendered via active_index:, and reflected " \
|
|
81
|
+
"onto the hovered/arrow-keyed index by the tooltip controller at runtime",
|
|
82
|
+
"data-motion-group" => "when animate - the motion rig's sweep group (one per ring)",
|
|
83
|
+
"data-motion-sector" => "when animate - the motion rig's server-computed sector params " \
|
|
84
|
+
"for the fan-out sweep"
|
|
85
|
+
}
|
|
86
|
+
part "chart-labels", "A series' value labels (<g> of <text>, aria-hidden), rendered when " \
|
|
87
|
+
"the series opts into labels",
|
|
88
|
+
states: { "data-key" => "always - the series key" }
|
|
89
|
+
part "chart-center-label", "The center text (<text>) - title tspan plus optional subtitle " \
|
|
90
|
+
"filling the chart's middle"
|
|
91
|
+
part "chart-coordinates", "The embedded JSON payload (<script>) the tooltip controller " \
|
|
92
|
+
"reads - per-category anchors and pre-formatted values, zero " \
|
|
93
|
+
"chart math in the browser"
|
|
94
|
+
|
|
95
|
+
renders_many :pies,
|
|
96
|
+
doc: "One ring of slices reading data_key: values and name_key: slice names. inner_radius: " \
|
|
97
|
+
"makes the donut; padding_angle: spaces the slices; active_index: pops one out by " \
|
|
98
|
+
"active_grow: pixels.",
|
|
99
|
+
renders: lambda { |data_key:, data: nil, name_key: :name, inner_radius: 0,
|
|
100
|
+
outer_radius: "80%", padding_angle: 0, stroke_width: 0,
|
|
101
|
+
color_key: :fill, labels: nil, label_key: nil,
|
|
102
|
+
active_index: nil, active_grow: 10|
|
|
103
|
+
(@series_entries ||= []) << Series.new(data: data, data_key: data_key.to_s,
|
|
104
|
+
name_key: name_key.to_s, inner_radius:,
|
|
105
|
+
outer_radius:, padding_angle:, stroke_width:,
|
|
106
|
+
color_key: color_key&.to_s, labels: labels&.to_sym,
|
|
107
|
+
label_key: label_key&.to_s, active_index:, active_grow:)
|
|
108
|
+
nil
|
|
109
|
+
}
|
|
110
|
+
# ActiveSupport singularizes "pies" to "py" - give the grammar its
|
|
111
|
+
# real name.
|
|
112
|
+
alias with_pie with_py
|
|
113
|
+
|
|
114
|
+
renders_one :center_label,
|
|
115
|
+
doc: "The donut-hole text: a title line plus an optional subtitle.",
|
|
116
|
+
renders: lambda { |title:, subtitle: nil|
|
|
117
|
+
@center_label_config = { title: title, subtitle: subtitle }
|
|
118
|
+
nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
renders_one :legend,
|
|
122
|
+
doc: "The legend row: align:, items:, and hide_icon:.",
|
|
123
|
+
renders: lambda { |**options|
|
|
124
|
+
@legend_config = options
|
|
125
|
+
nil
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
renders_one :tooltip,
|
|
129
|
+
doc: "The hover tooltip; the slice name carries the label, so hide_label defaults on.",
|
|
130
|
+
renders: lambda { |**options|
|
|
131
|
+
@tooltip_config = { hide_label: true }.merge(options)
|
|
132
|
+
nil
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# The polar chassis: margin/plot/center geometry plus the
|
|
136
|
+
# per-slice pointerover hit; the single-series tooltip chrome and
|
|
137
|
+
# payload ride the polar_* hooks below.
|
|
138
|
+
include Poetry::Charts::PolarFamily
|
|
139
|
+
include Poetry::Charts::PolarFamily::SingleSeriesTooltip
|
|
140
|
+
|
|
141
|
+
# The captured Series configs, forcing lazy slot evaluation.
|
|
142
|
+
# @api private
|
|
143
|
+
def series_entries
|
|
144
|
+
pies? # force slot evaluation (slots evaluate lazily)
|
|
145
|
+
@series_entries ||= []
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The center-label slot's captured config, forcing lazy slot
|
|
149
|
+
# evaluation.
|
|
150
|
+
# @api private
|
|
151
|
+
def center_label_config
|
|
152
|
+
center_label?
|
|
153
|
+
@center_label_config
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# -- geometry ---------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
# Memoized per SLOT (object identity), never per data_key: two rings
|
|
159
|
+
# may share a data_key with different data:, and a key-keyed memo
|
|
160
|
+
# would silently render the first ring's rows twice.
|
|
161
|
+
# @api private
|
|
162
|
+
def rows(entry)
|
|
163
|
+
@rows ||= {}.compare_by_identity
|
|
164
|
+
@rows[entry] ||= (entry.data || data || []).map { |row| row.to_h.transform_keys(&:to_s) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# One computed slice: its sector geometry plus name, value, and
|
|
168
|
+
# fill.
|
|
169
|
+
Slice = Data.define(:index, :name, :value, :fill, :path, :mid_angle, :middle_radius, :label_point,
|
|
170
|
+
:inner, :outer, :start_angle, :end_angle)
|
|
171
|
+
|
|
172
|
+
# Slot-identity memo, matching rows (a shared data_key must not
|
|
173
|
+
# collapse two rings into one geometry).
|
|
174
|
+
# @api private
|
|
175
|
+
def slices(entry)
|
|
176
|
+
@slices ||= {}.compare_by_identity
|
|
177
|
+
@slices[entry] ||= begin
|
|
178
|
+
entry_rows = rows(entry)
|
|
179
|
+
values = entry_rows.map { |row| row[entry.data_key] }
|
|
180
|
+
max = Polar.max_radius(plot[:width], plot[:height])
|
|
181
|
+
inner = Polar.percent_value(entry.inner_radius, max, 0)
|
|
182
|
+
outer = Polar.percent_value(entry.outer_radius, max, max * 0.8)
|
|
183
|
+
|
|
184
|
+
Polar.pie_sectors(values, padding_angle: entry.padding_angle).each_with_index.filter_map do |sector, i|
|
|
185
|
+
next if sector[:start_angle] == sector[:end_angle] && entry_rows.length != 1
|
|
186
|
+
|
|
187
|
+
grown = entry.active_index == i ? outer + entry.active_grow : outer
|
|
188
|
+
middle_radius = (inner + grown) / 2.0
|
|
189
|
+
Slice.new(
|
|
190
|
+
index: i,
|
|
191
|
+
name: entry_rows[i][entry.name_key].to_s,
|
|
192
|
+
value: sector[:value],
|
|
193
|
+
fill: slice_fill(entry, entry_rows[i]),
|
|
194
|
+
path: Polar.sector_path(cx: cx, cy: cy, inner_radius: inner, outer_radius: grown,
|
|
195
|
+
start_angle: sector[:start_angle], end_angle: sector[:end_angle]),
|
|
196
|
+
mid_angle: sector[:mid_angle],
|
|
197
|
+
middle_radius: middle_radius,
|
|
198
|
+
label_point: Polar.polar_to_cartesian(cx, cy, middle_radius, sector[:mid_angle]),
|
|
199
|
+
inner: inner,
|
|
200
|
+
outer: grown,
|
|
201
|
+
start_angle: sector[:start_angle],
|
|
202
|
+
end_angle: sector[:end_angle]
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A slice's fill: the color_key row value (CSS-validated), else
|
|
209
|
+
# the config color for its name, else the series color var.
|
|
210
|
+
# @api private
|
|
211
|
+
def slice_fill(entry, row)
|
|
212
|
+
color = row[entry.color_key].to_s if entry.color_key
|
|
213
|
+
if color.present?
|
|
214
|
+
raise ArgumentError, "slice fill #{color.inspect} is not CSS-safe" unless color.match?(Config::COLOR)
|
|
215
|
+
|
|
216
|
+
color
|
|
217
|
+
else
|
|
218
|
+
name = row[entry.name_key].to_s
|
|
219
|
+
chart_config[name]&.color || "var(--color-#{entry.data_key})"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Named labels resolve through the config (raw "chrome" ->
|
|
224
|
+
# "Chrome"), the same lookup the tooltip and legend already do.
|
|
225
|
+
# @api private
|
|
226
|
+
def slice_label(entry, slice)
|
|
227
|
+
return slice.value.to_i.to_s unless entry.label_key
|
|
228
|
+
|
|
229
|
+
raw = rows(entry)[slice.index][entry.label_key].to_s
|
|
230
|
+
chart_config.label_for(raw, raw)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# -- the tooltip payload (polar shape) ---------------------------------
|
|
234
|
+
|
|
235
|
+
# The SingleSeriesTooltip hooks: the FIRST pie's slices are the
|
|
236
|
+
# items, anchored at their label points; values read that pie's
|
|
237
|
+
# own rows.
|
|
238
|
+
# @api private
|
|
239
|
+
def polar_items(entry) = slices(entry)
|
|
240
|
+
# Where the tooltip anchors on one slice.
|
|
241
|
+
# @api private
|
|
242
|
+
def polar_anchor(slice) = slice.label_point
|
|
243
|
+
# The rows the tooltip values read from.
|
|
244
|
+
# @api private
|
|
245
|
+
def polar_value_rows(entry) = rows(entry)
|
|
246
|
+
|
|
247
|
+
# ChartFamily#svg_label's chart-type lead-in.
|
|
248
|
+
# @api private
|
|
249
|
+
def svg_label_prefix = "Pie chart"
|
|
250
|
+
|
|
251
|
+
private :center_label_config, :rows, :slices, :slice_fill, :slice_label, :polar_items
|
|
252
|
+
private :polar_anchor, :polar_value_rows, :svg_label_prefix
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module PieChart
|
|
6
|
+
# The pie family across its variants: simple, donut,
|
|
7
|
+
# donut-with-center-text, label-list (names inside slices), legend,
|
|
8
|
+
# stacked (two nested rings), and the popped active slice.
|
|
9
|
+
class Preview < Poetry::Core::Preview::Base
|
|
10
|
+
CONFIG = {
|
|
11
|
+
visitors: { label: "Visitors" },
|
|
12
|
+
chrome: { label: "Chrome", color: "var(--chart-1)" },
|
|
13
|
+
safari: { label: "Safari", color: "var(--chart-2)" },
|
|
14
|
+
firefox: { label: "Firefox", color: "var(--chart-3)" },
|
|
15
|
+
edge: { label: "Edge", color: "var(--chart-4)" },
|
|
16
|
+
other: { label: "Other", color: "var(--chart-5)" }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
DATA = [
|
|
20
|
+
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
|
|
21
|
+
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
|
|
22
|
+
{ browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
|
|
23
|
+
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
|
|
24
|
+
{ browser: "other", visitors: 90, fill: "var(--color-other)" }
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# The stacked block's second ring: desktop vs mobile by month.
|
|
28
|
+
MONTH_DATA = [
|
|
29
|
+
{ month: "january", desktop: 186, fill: "var(--color-january)" },
|
|
30
|
+
{ month: "february", desktop: 305, fill: "var(--color-february)" },
|
|
31
|
+
{ month: "march", desktop: 237, fill: "var(--color-march)" }
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
STACKED_CONFIG = CONFIG.merge(
|
|
35
|
+
desktop: { label: "Desktop" },
|
|
36
|
+
january: { label: "January", color: "var(--chart-1)" },
|
|
37
|
+
february: { label: "February", color: "var(--chart-2)" },
|
|
38
|
+
march: { label: "March", color: "var(--chart-3)" }
|
|
39
|
+
).freeze
|
|
40
|
+
|
|
41
|
+
def simple
|
|
42
|
+
render_component(data: DATA, config: CONFIG, id: "pie-simple") do |chart|
|
|
43
|
+
chart.with_pie(data_key: :visitors, name_key: :browser)
|
|
44
|
+
chart.with_tooltip
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def donut
|
|
49
|
+
render_component(data: DATA, config: CONFIG, id: "pie-donut") do |chart|
|
|
50
|
+
chart.with_pie(data_key: :visitors, name_key: :browser, inner_radius: 60)
|
|
51
|
+
chart.with_tooltip
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def donut_text
|
|
56
|
+
render_component(data: DATA, config: CONFIG, id: "pie-donut-text") do |chart|
|
|
57
|
+
chart.with_pie(data_key: :visitors, name_key: :browser, inner_radius: 60)
|
|
58
|
+
chart.with_center_label(title: "925", subtitle: "Visitors")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def label_list
|
|
63
|
+
render_component(data: DATA, config: CONFIG, id: "pie-label-list") do |chart|
|
|
64
|
+
chart.with_pie(data_key: :visitors, name_key: :browser, labels: :list, label_key: :browser)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def legend
|
|
69
|
+
render_component(data: DATA, config: CONFIG, id: "pie-legend") do |chart|
|
|
70
|
+
chart.with_pie(data_key: :visitors, name_key: :browser)
|
|
71
|
+
chart.with_legend
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stacked
|
|
76
|
+
render_component(config: STACKED_CONFIG, id: "pie-stacked") do |chart|
|
|
77
|
+
chart.with_pie(data: DATA, data_key: :visitors, name_key: :browser, outer_radius: 60)
|
|
78
|
+
chart.with_pie(data: MONTH_DATA, data_key: :desktop, name_key: :month,
|
|
79
|
+
inner_radius: 70, outer_radius: 90)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def donut_active
|
|
84
|
+
render_component(data: DATA, config: CONFIG, id: "pie-donut-active") do |chart|
|
|
85
|
+
chart.with_pie(data_key: :visitors, name_key: :browser, inner_radius: 60, active_index: 0)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A sync group member: the sync value the stimulus contract
|
|
90
|
+
# holds to the DOM.
|
|
91
|
+
def synced
|
|
92
|
+
render_component(data: DATA, config: CONFIG, id: "pie-synced", sync: "pie-demo") do |chart|
|
|
93
|
+
chart.with_pie(data_key: :visitors, name_key: :browser)
|
|
94
|
+
chart.with_tooltip
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module PieChart
|
|
6
|
+
# The polar chrome on the cn-chart-* theme rules: inside labels
|
|
7
|
+
# ride fill-background, the donut center label keeps its 3xl type.
|
|
8
|
+
class Style < Poetry::Core::Style
|
|
9
|
+
base ""
|
|
10
|
+
|
|
11
|
+
element :frame, "contents"
|
|
12
|
+
element :svg, "w-full min-h-0 flex-1"
|
|
13
|
+
element :inside_label, "cn-chart-inside-label"
|
|
14
|
+
element :center_title, "cn-chart-center-title"
|
|
15
|
+
element :center_subtitle, "cn-chart-center-subtitle"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
# The polar family chassis (pie/radar/radial): the shared margin +
|
|
6
|
+
# plot/center geometry, and the per-sector pointer hit - polar marks
|
|
7
|
+
# are hit by pointerover on the marked sector/wedge, not bisect, so
|
|
8
|
+
# the svg gains the enter action after TooltipWiring's pointer/
|
|
9
|
+
# keyboard set (include order is emission order - include this after
|
|
10
|
+
# TooltipWiring).
|
|
11
|
+
#
|
|
12
|
+
# @example Adding the polar chassis to a family
|
|
13
|
+
# class GaugeChart::Component < Poetry::Core::Component
|
|
14
|
+
# include Poetry::Charts::ChartFamily
|
|
15
|
+
# include Poetry::Charts::TooltipWiring
|
|
16
|
+
# include Poetry::Charts::PolarFamily
|
|
17
|
+
# include Poetry::Charts::PolarFamily::SingleSeriesTooltip
|
|
18
|
+
# end
|
|
19
|
+
module PolarFamily
|
|
20
|
+
# The default polar margin - a slim, even inset on all sides.
|
|
21
|
+
MARGIN = { top: 5, right: 5, bottom: 5, left: 5 }.freeze
|
|
22
|
+
|
|
23
|
+
def self.included(base)
|
|
24
|
+
base.use_stimulus do
|
|
25
|
+
on :svg do
|
|
26
|
+
controller(TooltipWiring::CONTROLLER, if: :tooltip?) { action :enter, on: :pointerover }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The margin-inset plot rect the polar geometry centers in.
|
|
32
|
+
# @api private
|
|
33
|
+
def plot
|
|
34
|
+
@plot ||= begin
|
|
35
|
+
m = MARGIN.merge((margin || {}).to_h.symbolize_keys)
|
|
36
|
+
{ left: m[:left].to_f, top: m[:top].to_f,
|
|
37
|
+
width: width - m[:left] - m[:right], height: height - m[:top] - m[:bottom] }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The polar center's x coordinate.
|
|
42
|
+
# @api private
|
|
43
|
+
def cx = plot[:left] + (plot[:width] / 2.0)
|
|
44
|
+
# The polar center's y coordinate.
|
|
45
|
+
# @api private
|
|
46
|
+
def cy = plot[:top] + (plot[:height] / 2.0)
|
|
47
|
+
|
|
48
|
+
# The single-series polar tooltip (pie/radial): the FIRST series
|
|
49
|
+
# drives the chrome, and per-index names/colors retint the one row.
|
|
50
|
+
# Families supply polar_items (the sector geometry), polar_anchor
|
|
51
|
+
# (where the tooltip anchors on one item), and polar_value_rows (the
|
|
52
|
+
# rows the values read from). Radar keeps TooltipWiring's
|
|
53
|
+
# multi-series chrome and its own payload, so it includes
|
|
54
|
+
# PolarFamily alone.
|
|
55
|
+
module SingleSeriesTooltip
|
|
56
|
+
# The embedded per-sector geometry payload the tooltip controller
|
|
57
|
+
# reads: polar layout, anchors, and per-index names/colors/values.
|
|
58
|
+
# @api private
|
|
59
|
+
def coordinates_json
|
|
60
|
+
entry = series_entries.first
|
|
61
|
+
return "{}" unless entry
|
|
62
|
+
|
|
63
|
+
items = polar_items(entry)
|
|
64
|
+
{
|
|
65
|
+
"layout" => "polar",
|
|
66
|
+
"categories" => items.map { |s| chart_config.label_for(s.name, s.name) },
|
|
67
|
+
"names" => items.map { |s| chart_config.label_for(s.name, s.name) },
|
|
68
|
+
"colors" => items.map(&:fill),
|
|
69
|
+
"anchors" => items.map { |s| polar_anchor(s).map { |v| v.round(2) } },
|
|
70
|
+
"values" => { entry.data_key => polar_value_rows(entry).map do |row|
|
|
71
|
+
Poetry::Charts.display_value(row[entry.data_key])
|
|
72
|
+
end }
|
|
73
|
+
}.to_json
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The TooltipLayer child scoped to the first series, label hidden
|
|
77
|
+
# by default (the per-index name carries it).
|
|
78
|
+
# @api private
|
|
79
|
+
def tooltip_layer_component
|
|
80
|
+
TooltipLayer::Component.new(
|
|
81
|
+
config: chart_config,
|
|
82
|
+
series_keys: [series_entries.first&.data_key].compact,
|
|
83
|
+
indicator: tooltip_config.fetch(:indicator, :dot),
|
|
84
|
+
hide_label: tooltip_config.fetch(:hide_label, true),
|
|
85
|
+
hide_indicator: tooltip_config.fetch(:hide_indicator, false)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private :coordinates_json, :tooltip_layer_component
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private :plot, :cx, :cy
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
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 grid? %>
|
|
5
|
+
<g data-slot="chart-polar-grid" aria-hidden="true">
|
|
6
|
+
<%# Linework FIRST (rings + spokes), so the tinted fills below can
|
|
7
|
+
wash over it - the translucent ring fills stack on top of the
|
|
8
|
+
grid strokes, leaving them near-invisible. %>
|
|
9
|
+
<% grid_radii.each do |radius| %>
|
|
10
|
+
<% if grid_config.type == :circle %>
|
|
11
|
+
<circle class="<%= css(:grid_line) %>" cx="<%= fnum(cx) %>" cy="<%= fnum(cy) %>" r="<%= fnum(radius) %>" />
|
|
12
|
+
<% else %>
|
|
13
|
+
<path class="<%= css(:grid_line) %>" d="<%= grid_polygon(radius) %>" />
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% if grid_config.radial_lines %>
|
|
17
|
+
<% rows.each_index do |i| %>
|
|
18
|
+
<% x, y = spoke_end(i) %>
|
|
19
|
+
<line class="<%= css(:grid_line) %>" x1="<%= fnum(cx) %>" y1="<%= fnum(cy) %>" x2="<%= fnum(x) %>" y2="<%= fnum(y) %>" />
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<%# The tinted fills: EVERY ring fills at the low opacity,
|
|
23
|
+
painted OVER the linework, overlaps compounding toward the
|
|
24
|
+
center - one fill+opacity across the whole grid. Inline
|
|
25
|
+
style, because grid_line sets fill-none and a fill
|
|
26
|
+
ATTRIBUTE loses to that CSS. %>
|
|
27
|
+
<% if grid_fill_attribute %>
|
|
28
|
+
<% grid_radii.each do |radius| %>
|
|
29
|
+
<% if grid_config.type == :circle %>
|
|
30
|
+
<circle class="<%= css(:grid_fill) %>" cx="<%= fnum(cx) %>" cy="<%= fnum(cy) %>" r="<%= fnum(radius) %>" style="fill: <%= grid_fill_attribute %>" fill-opacity="<%= grid_config.opacity %>" />
|
|
31
|
+
<% else %>
|
|
32
|
+
<path class="<%= css(:grid_fill) %>" d="<%= grid_polygon(radius) %>" style="fill: <%= grid_fill_attribute %>" fill-opacity="<%= grid_config.opacity %>" />
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</g>
|
|
37
|
+
<% end %>
|
|
38
|
+
<g data-slot="chart-radars">
|
|
39
|
+
<% series_entries.each do |entry| %>
|
|
40
|
+
<path data-slot="chart-radar" data-key="<%= entry.data_key %>" d="<%= series_path(entry) %>" fill="var(--color-<%= entry.data_key %>)" fill-opacity="<%= entry.fill_opacity %>"<% if entry.stroke_width.positive? %> stroke="var(--color-<%= entry.data_key %>)" stroke-width="<%= entry.stroke_width %>"<% end %> />
|
|
41
|
+
<% if entry.dots %>
|
|
42
|
+
<g data-slot="chart-dots" data-key="<%= entry.data_key %>">
|
|
43
|
+
<% rows.each_index do |i| %>
|
|
44
|
+
<% x, y = vertex(entry, i) %>
|
|
45
|
+
<circle data-slot="chart-dot" cx="<%= fnum(x) %>" cy="<%= fnum(y) %>" r="<%= entry.dot_radius %>" fill="var(--color-<%= entry.data_key %>)" />
|
|
46
|
+
<% end %>
|
|
47
|
+
</g>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</g>
|
|
51
|
+
<% if angle_axis? %>
|
|
52
|
+
<g data-slot="chart-angle-axis">
|
|
53
|
+
<% rows.each_index do |i| %>
|
|
54
|
+
<% placement = angle_label(i) %>
|
|
55
|
+
<text class="<%= css(:tick) %>" x="<%= fnum(placement[:x]) %>" y="<%= fnum(placement[:y]) %>" dy="0.355em" text-anchor="<%= placement[:anchor] %>"><%= placement[:text] %></text>
|
|
56
|
+
<% end %>
|
|
57
|
+
</g>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% if tooltip? %>
|
|
60
|
+
<g data-slot="chart-hit-wedges">
|
|
61
|
+
<% rows.each_index do |i| %>
|
|
62
|
+
<path data-slot="chart-hit-wedge" data-index="<%= i %>" d="<%= hit_wedge(i) %>" fill="transparent" />
|
|
63
|
+
<% end %>
|
|
64
|
+
</g>
|
|
65
|
+
<% end %>
|
|
66
|
+
</svg>
|
|
67
|
+
<% if legend? %>
|
|
68
|
+
<%= render legend_component %>
|
|
69
|
+
<% end %>
|
|
70
|
+
<% if tooltip? %><%= render tooltip_layer_component %><% end %>
|
|
71
|
+
<script type="application/json" data-slot="chart-coordinates" <%= tag.attributes(stimulus_attributes_for(:coordinates)) %>><%= script_json(coordinates_json) %></script>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% end %>
|