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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module ScatterChart
|
|
6
|
+
# The scatter family: simple, two series, and z-sized bubbles.
|
|
7
|
+
class Preview < Poetry::Core::Preview::Base
|
|
8
|
+
SAMPLE = [
|
|
9
|
+
{ height: 161, weight: 51, bmi: 19.7 },
|
|
10
|
+
{ height: 167, weight: 59, bmi: 21.2 },
|
|
11
|
+
{ height: 170, weight: 68, bmi: 23.5 },
|
|
12
|
+
{ height: 174, weight: 66, bmi: 21.8 },
|
|
13
|
+
{ height: 178, weight: 79, bmi: 24.9 },
|
|
14
|
+
{ height: 183, weight: 84, bmi: 25.1 },
|
|
15
|
+
{ height: 189, weight: 95, bmi: 26.6 }
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
CONTROL = [
|
|
19
|
+
{ height: 158, weight: 62, bmi: 24.8 },
|
|
20
|
+
{ height: 165, weight: 71, bmi: 26.1 },
|
|
21
|
+
{ height: 172, weight: 60, bmi: 20.3 },
|
|
22
|
+
{ height: 180, weight: 88, bmi: 27.2 },
|
|
23
|
+
{ height: 186, weight: 77, bmi: 22.3 }
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
ONE = { sample: { label: "Sample", color: "var(--chart-1)" } }.freeze
|
|
27
|
+
TWO = ONE.merge(control: { label: "Control", color: "var(--chart-2)" }).freeze
|
|
28
|
+
|
|
29
|
+
def default
|
|
30
|
+
render_component(data: SAMPLE, config: ONE, id: "scatter-default",
|
|
31
|
+
margin: { left: 12, right: 12 }) do |chart|
|
|
32
|
+
chart.with_grid
|
|
33
|
+
chart.with_x_axis(data_key: :height, name: "Height")
|
|
34
|
+
chart.with_y_axis(data_key: :weight, name: "Weight")
|
|
35
|
+
chart.with_scatter(key: :sample)
|
|
36
|
+
chart.with_tooltip
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def multiple
|
|
41
|
+
render_component(data: SAMPLE, config: TWO, id: "scatter-multiple",
|
|
42
|
+
margin: { left: 12, right: 12 }) do |chart|
|
|
43
|
+
chart.with_grid
|
|
44
|
+
chart.with_x_axis(data_key: :height, name: "Height")
|
|
45
|
+
chart.with_y_axis(data_key: :weight, name: "Weight")
|
|
46
|
+
chart.with_scatter(key: :sample)
|
|
47
|
+
chart.with_scatter(key: :control, data: CONTROL)
|
|
48
|
+
chart.with_legend
|
|
49
|
+
chart.with_tooltip
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def bubbles
|
|
54
|
+
render_component(data: SAMPLE, config: ONE, id: "scatter-bubbles",
|
|
55
|
+
margin: { left: 12, right: 12 }) do |chart|
|
|
56
|
+
chart.with_grid
|
|
57
|
+
chart.with_x_axis(data_key: :height, name: "Height")
|
|
58
|
+
chart.with_y_axis(data_key: :weight, name: "Weight")
|
|
59
|
+
chart.with_z_axis(data_key: :bmi, range: [64, 400])
|
|
60
|
+
chart.with_scatter(key: :sample)
|
|
61
|
+
chart.with_tooltip
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reference_zones
|
|
66
|
+
render_component(data: SAMPLE, config: ONE, id: "scatter-refs",
|
|
67
|
+
margin: { left: 12, right: 12 }) do |chart|
|
|
68
|
+
chart.with_grid
|
|
69
|
+
chart.with_x_axis(data_key: :height, name: "Height")
|
|
70
|
+
chart.with_y_axis(data_key: :weight, name: "Weight")
|
|
71
|
+
chart.with_scatter(key: :sample, error_key: :werr)
|
|
72
|
+
chart.with_reference_area(y1: 60, y2: 80, label: "target band")
|
|
73
|
+
chart.with_reference_line(x: 175)
|
|
74
|
+
chart.with_reference_dot(x: 172, y: 70, r: 10, label: "median")
|
|
75
|
+
chart.with_tooltip
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A sync group member: the sync value the stimulus contract
|
|
80
|
+
# holds to the DOM.
|
|
81
|
+
def synced
|
|
82
|
+
render_component(data: SAMPLE, config: ONE, id: "scatter-synced",
|
|
83
|
+
margin: { left: 12, right: 12 }, sync: "scatter-demo") do |chart|
|
|
84
|
+
chart.with_x_axis(data_key: :height, name: "Height")
|
|
85
|
+
chart.with_y_axis(data_key: :weight, name: "Weight")
|
|
86
|
+
chart.with_scatter(key: :sample)
|
|
87
|
+
chart.with_tooltip
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module ScatterChart
|
|
6
|
+
# The scatter chrome on the shared cn-chart-* theme rules.
|
|
7
|
+
# Point fills are var(--color-<key>) ATTRIBUTES.
|
|
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 :grid_line, "cn-chart-grid-line"
|
|
14
|
+
element :tick, "cn-chart-tick"
|
|
15
|
+
element :reference_line, "cn-chart-reference-line"
|
|
16
|
+
element :reference_area, "cn-chart-reference-area"
|
|
17
|
+
element :reference_dot, "cn-chart-reference-dot"
|
|
18
|
+
element :error_bar, "cn-chart-error-bar"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%= content_tag(:div, **root_attributes.to_attributes) do %>
|
|
2
|
+
<% if label_text && !nest_label? %><div data-slot="chart-tooltip-label" class="<%= css(:label) %>"><%= label_text %></div><% end %>
|
|
3
|
+
<div class="<%= css(:inner) %>">
|
|
4
|
+
<% rows.each do |row| %>
|
|
5
|
+
<div data-slot="chart-tooltip-item" data-key="<%= row.key %>" class="<%= row_classes %>">
|
|
6
|
+
<% unless hide_indicator %>
|
|
7
|
+
<div data-slot="chart-tooltip-indicator" class="<%= indicator_classes(nested: nest_label?) %>"<% if row.indicator_style %> style="<%= row.indicator_style %>"<% end %>></div>
|
|
8
|
+
<% end %>
|
|
9
|
+
<div class="<%= value_wrap_classes %>">
|
|
10
|
+
<div class="<%= css(:inner) %>">
|
|
11
|
+
<% if label_text && nest_label? %><div data-slot="chart-tooltip-label" class="<%= css(:label) %>"><%= label_text %></div><% end %>
|
|
12
|
+
<span data-slot="chart-tooltip-name" class="<%= css(:name) %>"><%= row.name %></span>
|
|
13
|
+
</div>
|
|
14
|
+
<% if row.formatted_value %><span data-slot="chart-tooltip-value" class="<%= css(:value) %>"><%= row.formatted_value %></span><% end %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
# The chart tooltip chrome.
|
|
6
|
+
module TooltipContent
|
|
7
|
+
# The tooltip chrome - the styled info box: optional label, then
|
|
8
|
+
# one row per series item (indicator + name + tabular value). Pure
|
|
9
|
+
# server-rendered markup; the tooltip controller positions and
|
|
10
|
+
# repopulates it from server-embedded coordinates. Indicator colors
|
|
11
|
+
# ride the --color-bg/--color-border inline custom properties, so a
|
|
12
|
+
# row's swatch follows its series.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# <%= poetry_chart_tooltip_content(config: config,
|
|
16
|
+
# items: [{ key: :desktop, value: 186 }]) %>
|
|
17
|
+
#
|
|
18
|
+
# Items are `{ key:, name:, value:, color: }` hashes; names resolve
|
|
19
|
+
# through the config (config label, else the item name, else the
|
|
20
|
+
# key) and colors fall back to the config entry's color, all
|
|
21
|
+
# server-side.
|
|
22
|
+
class Component < Poetry::Core::Component
|
|
23
|
+
# Projected into the registry and agent surface.
|
|
24
|
+
AGENT_RULES = [
|
|
25
|
+
"Tooltip rows resolve names/colors through the chart config - pass key:, not a display string.",
|
|
26
|
+
"indicator: :dot (default) | :line | :dashed matches the ported variants.",
|
|
27
|
+
"Numeric values render delimited (1,234) in the mono tabular column automatically."
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
option :config, ActiveModel::Type::Value.new, required: true,
|
|
31
|
+
doc: "The series config - key => { label:, color: } - " \
|
|
32
|
+
"resolving row names and colors."
|
|
33
|
+
option :items, ActiveModel::Type::Value.new, required: true,
|
|
34
|
+
doc: "The rows: [{ key:, name:, value:, color: }] hashes, one " \
|
|
35
|
+
"per series."
|
|
36
|
+
option :indicator, :symbol, default: :dot, doc: "The row swatch shape."
|
|
37
|
+
option :label, :string, doc: "The category label above the rows, resolved through the config."
|
|
38
|
+
option :hide_label, :boolean, default: false, doc: "Hides the category label."
|
|
39
|
+
option :hide_indicator, :boolean, default: false, doc: "Hides the row swatches."
|
|
40
|
+
|
|
41
|
+
validates :indicator, inclusion: { in: %i[dot line dashed] }
|
|
42
|
+
|
|
43
|
+
part "chart-tooltip-content", "The tooltip box (<div>) - the styled chrome the chart's " \
|
|
44
|
+
"tooltip controller positions and text-swaps"
|
|
45
|
+
part "chart-tooltip-label", "The category label (<div>) - above the rows, or nested " \
|
|
46
|
+
"inside the single row for line/dashed indicators"
|
|
47
|
+
part "chart-tooltip-item", "One series row (<div>) - indicator + name + value",
|
|
48
|
+
states: { "data-key" => "always - the series key the controller matches values by" }
|
|
49
|
+
part "chart-tooltip-indicator", "The row's swatch (<div>) - dot/line/dashed per " \
|
|
50
|
+
"indicator:, hidden with hide_indicator",
|
|
51
|
+
vars: {
|
|
52
|
+
"--color-bg" => "the swatch fill - carries the row's series color (inline; polar " \
|
|
53
|
+
"charts retint it to the hovered slice's color at runtime)",
|
|
54
|
+
"--color-border" => "the swatch border - the same series color as the fill"
|
|
55
|
+
}
|
|
56
|
+
part "chart-tooltip-name", "The series name (<span>) - resolved through the chart config"
|
|
57
|
+
part "chart-tooltip-value", "The formatted value (<span>) - the mono tabular column, " \
|
|
58
|
+
"numbers delimited"
|
|
59
|
+
|
|
60
|
+
# The config: option wrapped as a {Poetry::Charts::Config}.
|
|
61
|
+
# @api private
|
|
62
|
+
def chart_config
|
|
63
|
+
@chart_config ||= Poetry::Charts::Config.wrap(config)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The items resolved into Row objects.
|
|
67
|
+
# @api private
|
|
68
|
+
def rows
|
|
69
|
+
@rows ||= Array(items).map { |item| Row.new(item.symbolize_keys, chart_config) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The label nests inside the single row for line/dashed
|
|
73
|
+
# indicators - the layout that makes the 1-series line-indicator
|
|
74
|
+
# tooltip read as one unit.
|
|
75
|
+
# @api private
|
|
76
|
+
def nest_label?
|
|
77
|
+
rows.length == 1 && indicator != :dot
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Label resolution: config label for the key when one matches,
|
|
81
|
+
# else the given string.
|
|
82
|
+
# @api private
|
|
83
|
+
def label_text
|
|
84
|
+
return nil if hide_label || label.blank?
|
|
85
|
+
|
|
86
|
+
chart_config.label_for(label, label)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A row's classes, with the dot layout when the indicator is one.
|
|
90
|
+
# @api private
|
|
91
|
+
def row_classes
|
|
92
|
+
classes = css(:row)
|
|
93
|
+
classes = "#{classes} #{css(:row_dot)}" if indicator == :dot
|
|
94
|
+
classes
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The swatch's classes for the indicator shape (nested dashes get
|
|
98
|
+
# their own alignment).
|
|
99
|
+
# @api private
|
|
100
|
+
def indicator_classes(nested: false)
|
|
101
|
+
classes = "#{css(:indicator)} #{css(:"indicator_#{indicator}")}"
|
|
102
|
+
classes = "#{classes} #{css(:indicator_dashed_nested)}" if nested && indicator == :dashed
|
|
103
|
+
classes
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The value column's alignment classes (nested labels bottom-align).
|
|
107
|
+
# @api private
|
|
108
|
+
def value_wrap_classes
|
|
109
|
+
"#{css(:value_wrap)} #{nest_label? ? css(:value_wrap_end) : css(:value_wrap_center)}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The box's attributes with the part self-identification.
|
|
113
|
+
# @api private
|
|
114
|
+
def root_attributes
|
|
115
|
+
html_attributes.merge_if_not_set(
|
|
116
|
+
{ "data-slot" => "chart-tooltip-content" }.merge(component_data_attributes)
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# One resolved tooltip row: name via config, color via item-else-config.
|
|
121
|
+
# @api private
|
|
122
|
+
class Row
|
|
123
|
+
def initialize(item, config)
|
|
124
|
+
@item = item
|
|
125
|
+
@config = config
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def key
|
|
129
|
+
(@item[:key] || @item[:name] || "value").to_s
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def name
|
|
133
|
+
@config.label_for(key, @item[:name])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def color
|
|
137
|
+
@item[:color] || @config[key]&.color
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def value
|
|
141
|
+
@item[:value]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def formatted_value
|
|
145
|
+
return nil if value.nil?
|
|
146
|
+
return ActiveSupport::NumberHelper.number_to_delimited(value) if value.is_a?(Numeric)
|
|
147
|
+
|
|
148
|
+
value.to_s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Guarded inline style: colors reaching a style attribute must be
|
|
152
|
+
# CSS-safe (config colors already are; item colors get the same rule).
|
|
153
|
+
def indicator_style
|
|
154
|
+
return nil unless color
|
|
155
|
+
unless color.match?(Config::COLOR)
|
|
156
|
+
raise ArgumentError, "tooltip item color #{color.inspect} is not CSS-safe"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
"--color-bg: #{color}; --color-border: #{color};"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private :chart_config, :rows, :nest_label?, :label_text, :row_classes, :indicator_classes, :value_wrap_classes
|
|
164
|
+
private :root_attributes
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module TooltipContent
|
|
6
|
+
# The tooltip chrome across its three indicator variants - every
|
|
7
|
+
# chart tooltip composes from exactly these pieces.
|
|
8
|
+
class Preview < Poetry::Core::Preview::Base
|
|
9
|
+
CONFIG = {
|
|
10
|
+
desktop: { label: "Desktop", color: "var(--chart-1)" },
|
|
11
|
+
mobile: { label: "Mobile", color: "var(--chart-2)" }
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
ITEMS = [
|
|
15
|
+
{ key: "desktop", value: 1260 },
|
|
16
|
+
{ key: "mobile", value: 570 }
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def default
|
|
20
|
+
render_component(config: CONFIG, items: ITEMS, label: "January")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def line_indicator
|
|
24
|
+
render_component(config: CONFIG, items: [{ key: "desktop", value: 1260 }],
|
|
25
|
+
label: "January", indicator: :line)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dashed_indicator
|
|
29
|
+
render_component(config: CONFIG, items: [{ key: "desktop", value: 1260 }],
|
|
30
|
+
label: "January", indicator: :dashed)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def no_label
|
|
34
|
+
render_component(config: CONFIG, items: ITEMS, hide_label: true)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module TooltipContent
|
|
6
|
+
# Re-expressed through the cn-* theme layer: the box, type,
|
|
7
|
+
# and row treatments ride cn-chart-tooltip-* rules. The indicator's
|
|
8
|
+
# border-(--color-border)/bg-(--color-bg) arbitrary properties stay
|
|
9
|
+
# inline - they read the per-row inline custom properties the
|
|
10
|
+
# component sets to the series color (mechanism).
|
|
11
|
+
class Style < Poetry::Core::Style
|
|
12
|
+
base "cn-chart-tooltip grid min-w-[8rem] items-start"
|
|
13
|
+
|
|
14
|
+
element :label, "cn-chart-tooltip-label"
|
|
15
|
+
element :inner, "grid gap-1.5"
|
|
16
|
+
element :row, "cn-chart-tooltip-row flex w-full flex-wrap items-stretch"
|
|
17
|
+
element :row_dot, "items-center"
|
|
18
|
+
element :indicator, "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)"
|
|
19
|
+
element :indicator_dot, "h-2.5 w-2.5"
|
|
20
|
+
element :indicator_line, "w-1"
|
|
21
|
+
element :indicator_dashed, "w-0 border-[1.5px] border-dashed bg-transparent"
|
|
22
|
+
element :indicator_dashed_nested, "my-0.5"
|
|
23
|
+
element :value_wrap, "flex flex-1 justify-between leading-none"
|
|
24
|
+
element :value_wrap_center, "items-center"
|
|
25
|
+
element :value_wrap_end, "items-end"
|
|
26
|
+
element :name, "cn-chart-tooltip-name"
|
|
27
|
+
element :value, "cn-chart-tooltip-value"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<%= content_tag(:div, **root_attributes.to_attributes) do %>
|
|
2
|
+
<%= render Poetry::Charts::TooltipContent::Component.new(
|
|
3
|
+
config: chart_config, items: items, label: hide_label ? nil : "…",
|
|
4
|
+
indicator: indicator, hide_label: hide_label, hide_indicator: hide_indicator
|
|
5
|
+
) %>
|
|
6
|
+
<% end %>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
# The chart families' internal hover-tooltip mount.
|
|
6
|
+
module TooltipLayer
|
|
7
|
+
# The hidden tooltip box a chart family renders when with_tooltip is
|
|
8
|
+
# set: the TooltipContent chrome pre-rendered with one row per series
|
|
9
|
+
# (names and indicator colors resolved server-side), positioned and
|
|
10
|
+
# text-swapped by the tooltip controller. Rendered by the chart
|
|
11
|
+
# families, never composed directly.
|
|
12
|
+
# @api private
|
|
13
|
+
class Component < Poetry::Core::Component
|
|
14
|
+
# The box is the parent frame controller's tooltip target - no
|
|
15
|
+
# registration here (TooltipWiring registers on the chart frame).
|
|
16
|
+
use_stimulus do
|
|
17
|
+
on :root do
|
|
18
|
+
controller(TooltipWiring::CONTROLLER) { target :tooltip }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
option :config, ActiveModel::Type::Value.new, required: true,
|
|
23
|
+
doc: "The series config - key => { label:, color: } - " \
|
|
24
|
+
"resolving row names and colors."
|
|
25
|
+
option :series_keys, ActiveModel::Type::Value.new, required: true,
|
|
26
|
+
doc: "The series keys to pre-render rows for."
|
|
27
|
+
option :indicator, :symbol, default: :dot, doc: "The row swatch shape."
|
|
28
|
+
option :hide_label, :boolean, default: false, doc: "Hides the category label."
|
|
29
|
+
option :hide_indicator, :boolean, default: false, doc: "Hides the row swatches."
|
|
30
|
+
|
|
31
|
+
# The config: option wrapped as a {Poetry::Charts::Config}.
|
|
32
|
+
# @api private
|
|
33
|
+
def chart_config
|
|
34
|
+
@chart_config ||= Poetry::Charts::Config.wrap(config)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Placeholder rows: value 0 guarantees the value span exists for
|
|
38
|
+
# the controller to swap; the box is hidden until a point is active.
|
|
39
|
+
# @api private
|
|
40
|
+
def items
|
|
41
|
+
series_keys.map { |key| { key: key, value: 0 } }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The hidden box's attributes: part self-identification plus the
|
|
45
|
+
# parent frame controller's tooltip target.
|
|
46
|
+
# @api private
|
|
47
|
+
def root_attributes
|
|
48
|
+
html_attributes.merge_if_not_set(
|
|
49
|
+
{
|
|
50
|
+
"data-slot" => "chart-tooltip",
|
|
51
|
+
"hidden" => ""
|
|
52
|
+
}.merge(stimulus_attributes_for(:root))
|
|
53
|
+
.merge(component_data_attributes)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private :chart_config, :items, :root_attributes
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
module TooltipLayer
|
|
6
|
+
# The floating box: absolute within the relative Container, above the
|
|
7
|
+
# SVG, never intercepting the pointer (the SVG owns the events).
|
|
8
|
+
class Style < Poetry::Core::Style
|
|
9
|
+
base "pointer-events-none absolute z-10 w-max"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Charts
|
|
5
|
+
# Shared tooltip wiring for the chart families: the frame
|
|
6
|
+
# wrapper carries the controller, the SVG carries targets/actions plus
|
|
7
|
+
# the accessibilityLayer floor (focusable, role=application, arrows walk
|
|
8
|
+
# categories), and the hidden chrome pre-renders per-series rows the
|
|
9
|
+
# controller text-swaps - zero chart math in the browser.
|
|
10
|
+
module TooltipWiring
|
|
11
|
+
# Full-string controller identifier - the bare :tooltip shorthand
|
|
12
|
+
# would be ambiguous with the core tooltip controller.
|
|
13
|
+
CONTROLLER = "poetry--charts--tooltip"
|
|
14
|
+
|
|
15
|
+
# The wiring is declared, not hand-built: the frame carries the
|
|
16
|
+
# controller + sync value, the SVG carries the target and the
|
|
17
|
+
# pointer/keyboard actions (the accessibilityLayer floor), and the
|
|
18
|
+
# coordinates <script> is the data target the controller reads.
|
|
19
|
+
def self.included(base)
|
|
20
|
+
# Sync-group id: charts sharing the same sync: broadcast and
|
|
21
|
+
# receive each other's active index.
|
|
22
|
+
base.option :sync, :string
|
|
23
|
+
|
|
24
|
+
base.use_stimulus do
|
|
25
|
+
on :frame do
|
|
26
|
+
controller CONTROLLER, if: :tooltip? do
|
|
27
|
+
register
|
|
28
|
+
value :sync, if: -> { sync.present? }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
on :svg do
|
|
32
|
+
controller CONTROLLER, if: :tooltip? do
|
|
33
|
+
target :svg
|
|
34
|
+
action :move, on: :pointermove
|
|
35
|
+
action :leave, on: :pointerleave
|
|
36
|
+
action :focus, on: :focus
|
|
37
|
+
action :blur, on: :blur
|
|
38
|
+
action :keydown, on: :keydown
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
on :coordinates do
|
|
42
|
+
controller CONTROLLER, if: :tooltip? do
|
|
43
|
+
target :data
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
# Rendered BY the TooltipLayer child inside the frame - mirrored
|
|
47
|
+
# here so the family's contract owns its full anatomy (the gate
|
|
48
|
+
# flags phantom if the layer ever stops rendering it).
|
|
49
|
+
on :tooltip_layer do
|
|
50
|
+
controller CONTROLLER, if: :tooltip? do
|
|
51
|
+
target :tooltip
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The tooltip slot's captured options, forcing lazy slot evaluation.
|
|
58
|
+
# @api private
|
|
59
|
+
def tooltip_config
|
|
60
|
+
tooltip?
|
|
61
|
+
@tooltip_config || {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The legend slot's options, guarded: toggle needs the live renderer
|
|
65
|
+
# (hiding a series recomputes the domain client-side).
|
|
66
|
+
# @api private
|
|
67
|
+
def legend_config
|
|
68
|
+
legend?
|
|
69
|
+
config = @legend_config || {}
|
|
70
|
+
if config[:toggle] && !(respond_to?(:live?) && live?)
|
|
71
|
+
raise ArgumentError, "with_legend toggle: true needs live: true - " \
|
|
72
|
+
"the toggle recomputes the domain client-side"
|
|
73
|
+
end
|
|
74
|
+
config
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The with_legend options forwarded to the LegendContent child.
|
|
78
|
+
LEGEND_OPTIONS = %i[align items hide_icon toggle].freeze
|
|
79
|
+
|
|
80
|
+
# The LegendContent child built from the chart's config and legend
|
|
81
|
+
# options.
|
|
82
|
+
# @api private
|
|
83
|
+
def legend_component
|
|
84
|
+
LegendContent::Component.new(config: chart_config, **legend_config.slice(*LEGEND_OPTIONS))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The hover cursor, on by default when a tooltip attaches: bars get
|
|
88
|
+
# a translucent band rect, line/area/composed a vertical rule -
|
|
89
|
+
# hidden until the tooltip controller positions it at the active
|
|
90
|
+
# index. Renders UNDER the series (after the grid) so marks stay
|
|
91
|
+
# on top.
|
|
92
|
+
# @api private
|
|
93
|
+
def cursor_svg(kind)
|
|
94
|
+
return unless tooltip? && tooltip_config.fetch(:cursor, true)
|
|
95
|
+
|
|
96
|
+
case kind
|
|
97
|
+
when :band
|
|
98
|
+
if respond_to?(:horizontal?) && horizontal?
|
|
99
|
+
tag.rect("data-slot": "chart-cursor", "aria-hidden": true, class: css(:cursor_band),
|
|
100
|
+
x: fnum(cartesian.plot_left), y: 0,
|
|
101
|
+
width: fnum(cartesian.plot_right - cartesian.plot_left), height: 0,
|
|
102
|
+
display: "none")
|
|
103
|
+
else
|
|
104
|
+
tag.rect("data-slot": "chart-cursor", "aria-hidden": true, class: css(:cursor_band),
|
|
105
|
+
x: fnum(cartesian.plot_left), y: fnum(cartesian.plot_top),
|
|
106
|
+
width: 0, height: fnum(cartesian.plot_bottom - cartesian.plot_top),
|
|
107
|
+
display: "none")
|
|
108
|
+
end
|
|
109
|
+
when :line
|
|
110
|
+
tag.line("data-slot": "chart-cursor", "aria-hidden": true, class: css(:cursor_line),
|
|
111
|
+
x1: 0, x2: 0, y1: fnum(cartesian.plot_top), y2: fnum(cartesian.plot_bottom),
|
|
112
|
+
display: "none")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Attributes for the frame div (display: contents) wrapping svg +
|
|
117
|
+
# chrome + coordinates: the declared frame wiring - tooltip
|
|
118
|
+
# registration + sync here, whichever of motion/live/window the
|
|
119
|
+
# family mixes in declared by those modules - plus the part-contract
|
|
120
|
+
# self-identification: the frame is the chart type's own root (the
|
|
121
|
+
# Container wrapper self-ids as "container", so without this the
|
|
122
|
+
# SVG anatomy would have no owner in the part contract).
|
|
123
|
+
# @api private
|
|
124
|
+
def frame_attributes
|
|
125
|
+
{ "data-component" => self.class.component_title }.merge(stimulus_attributes_for(:frame))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# role=img for static charts; when the tooltip attaches the SVG
|
|
129
|
+
# becomes the focusable accessibility layer (role=application +
|
|
130
|
+
# tabindex) so arrow keys walk the categories. Motion rides the
|
|
131
|
+
# same tag: data-animate + the --poetry-motion-* knobs.
|
|
132
|
+
# @api private
|
|
133
|
+
def svg_interaction_attributes
|
|
134
|
+
base = tooltip? ? { "role" => "application", "tabindex" => "0" } : { "role" => "img" }
|
|
135
|
+
base.merge!(stimulus_attributes_for(:svg))
|
|
136
|
+
respond_to?(:motion_svg_attributes) ? base.merge(motion_svg_attributes) : base
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The TooltipLayer child built from the chart's config and tooltip
|
|
140
|
+
# options.
|
|
141
|
+
# @api private
|
|
142
|
+
def tooltip_layer_component
|
|
143
|
+
TooltipLayer::Component.new(
|
|
144
|
+
config: chart_config,
|
|
145
|
+
series_keys: series_entries.map(&:key),
|
|
146
|
+
indicator: tooltip_config.fetch(:indicator, :dot),
|
|
147
|
+
hide_label: tooltip_config.fetch(:hide_label, false),
|
|
148
|
+
hide_indicator: tooltip_config.fetch(:hide_indicator, false)
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The hover markers lines/areas show at the active index -
|
|
153
|
+
# server-rendered hidden circles the controller toggles.
|
|
154
|
+
# @api private
|
|
155
|
+
def active_dot_markers(entry)
|
|
156
|
+
cartesian.points(entry).each_with_index.filter_map do |point, i|
|
|
157
|
+
next if point[:value].nan?
|
|
158
|
+
|
|
159
|
+
{ index: i, x: point[:x], y: point[:y1] }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private :tooltip_config, :legend_config, :legend_component, :cursor_svg, :frame_attributes
|
|
164
|
+
private :svg_interaction_attributes, :tooltip_layer_component, :active_dot_markers
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|