zaniah 0.6.3 → 0.6.4
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 +4 -4
- data/CHANGELOG.md +4 -0
- data/docs/components.md +3 -1
- data/docs/tui.md +1 -1
- data/lib/zaniah/ui/charts.rb +60 -47
- data/lib/zaniah/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 985d3d62a8ffa112effba1aed1976c2b7403a7e6ef2a49dca9b817c80d49b3da
|
|
4
|
+
data.tar.gz: 8e641aad423d506fcbf18fe5c6c22afa8beff2ae5b321ae73559cbec8ed26181
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e71ed6d538a8ba9f48d3521def68d05957c637d631116dbf5676a47672d750b1128d361b7b8af48cd5231c01742db7ca88b6846961fc30c58339d4bf696e05a0
|
|
7
|
+
data.tar.gz: d6d48b77d5a4793d0e4aa2c4a5e23ccd34f4d5d690c858ecba51ad3585571275a12fa3b5ec882c1a03abdc2db174f757e6002d642d9b13b42aafe46b53f1ca23
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.4 — 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Share Cartesian chart scales, axes, grid lines, ticks, and color-keyed legends across chart types.
|
|
6
|
+
|
|
3
7
|
## 0.6.3 — 2026-09-23
|
|
4
8
|
|
|
5
9
|
- Render supplied text grid cells directly as cell surfaces, avoiding an extra wrapper per visible cell.
|
data/docs/components.md
CHANGED
|
@@ -87,7 +87,9 @@ Zaniah::UI::Button.variants[:variant][:brand] = ->(theme) {
|
|
|
87
87
|
| L4 | `Grid` | `(rows:, columns:, row_height:, column_width:, frozen_rows:, frozen_columns:)`; `scroll_to`, range `selection`, `on_select`, `on_edit`, `on_fill`, `on_resize` | two-axis virtualization, frozen panes, visible-cell resize/fill callbacks | table |
|
|
88
88
|
| L4 | `TreeView` | `(items, height:, selected:)`; `expand`, `collapse`, `replace`, `replace_children`, `invalidate`, lazy `children` proc | arrows/Home/End | tree/treeitem |
|
|
89
89
|
| L5 | `Sparkline` | `(values, width:, height:, color:, label:)` | line + tooltip | image |
|
|
90
|
-
| L5 | `LineChart`, `BarChart` | `(series, width:, height:, colors:, label:)` | axes, legend, tooltip | image |
|
|
90
|
+
| L5 | `LineChart`, `BarChart`, `StackedBarChart`, `AreaChart` | `(series, width:, height:, colors:, label:)`; `AreaChart(stacked:)` | shared axes, ticks, grid lines, color-keyed legend, tooltip | image |
|
|
91
|
+
| L5 | `PieChart`, `DonutChart` | `(data, width:, height:, colors:, label:)` | slices, shared color-keyed legend, tooltip | image |
|
|
92
|
+
| L5 | `ScatterChart` | `(series, width:, height:, colors:, label:)` | shared axes, ticks, grid lines, color-keyed legend, nearest-point tooltip | image |
|
|
91
93
|
| L5 | `Validation` | `required`, `format`, `length`, `number`, `rule` | composable rules | n/a |
|
|
92
94
|
| L5 | `FormField` | `(name, value:, label:, control:, validation:, hint:)` | errors + describedby | group/control/alert |
|
|
93
95
|
| L5 | `Form` | `field`, `on_change`, `on_submit`, `values`, `valid?` | validates before submit | form |
|
data/docs/tui.md
CHANGED
|
@@ -26,7 +26,7 @@ tests and non-window integrations.
|
|
|
26
26
|
| Toast | live status text |
|
|
27
27
|
| Table, DataGrid | header and visible rows separated with `|` |
|
|
28
28
|
| TreeView | indentation with `▸`/`▾` expansion markers |
|
|
29
|
-
| Sparkline
|
|
29
|
+
| Sparkline and all chart types | eight-level Unicode sparkline summary |
|
|
30
30
|
| Form, FormField | labeled values, `!` errors, and a submit button |
|
|
31
31
|
| CodeEditor, RichText | numbered plain-text lines or concatenated text runs |
|
|
32
32
|
|
data/lib/zaniah/ui/charts.rb
CHANGED
|
@@ -32,14 +32,14 @@ module Zaniah
|
|
|
32
32
|
result.freeze
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def paint_line(scene, bounds, values, color)
|
|
36
|
-
points = chart_points(bounds, values)
|
|
35
|
+
def paint_line(scene, bounds, values, color, range: nil)
|
|
36
|
+
points = chart_points(bounds, values, range: range)
|
|
37
37
|
path = points.map.with_index { |(x, y), index| "#{index.zero? ? "M" : "L"}#{x},#{y}" }.join
|
|
38
38
|
scene.path(path, stroke: color, width: 2)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def chart_points(bounds, values)
|
|
42
|
-
minimum, maximum = values.minmax
|
|
41
|
+
def chart_points(bounds, values, range: nil)
|
|
42
|
+
minimum, maximum = range || values.minmax
|
|
43
43
|
span = maximum == minimum ? 1.0 : maximum - minimum
|
|
44
44
|
step = values.length == 1 ? 0 : bounds.width / (values.length - 1)
|
|
45
45
|
values.map.with_index { |value, index| [bounds.x + index * step, bounds.bottom - (value - minimum) / span * bounds.height] }
|
|
@@ -83,6 +83,40 @@ module Zaniah
|
|
|
83
83
|
span = maximum == minimum ? 1.0 : maximum - minimum
|
|
84
84
|
values.map { |value| levels[((value - minimum) / span * (levels.length - 1)).round] }.join
|
|
85
85
|
end
|
|
86
|
+
|
|
87
|
+
def chart_palette(cx)
|
|
88
|
+
palette = Array(@colors || [cx.theme.colors.accent, cx.theme.colors.info, cx.theme.colors.success, cx.theme.colors.warning])
|
|
89
|
+
raise ArgumentError, "chart colors must not be empty" if palette.empty?
|
|
90
|
+
palette
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def chart_legend(entries, palette)
|
|
94
|
+
Div.new.flex_row.gap(10).children(entries.each_with_index.map do |entry, index|
|
|
95
|
+
label = block_given? ? yield(entry) : entry.to_s
|
|
96
|
+
Div.new.flex_row.items_center.gap(4)
|
|
97
|
+
.child(Div.new.w(8).h(8).bg(palette[index % palette.length]))
|
|
98
|
+
.child(Label.new(label, size: :xs))
|
|
99
|
+
end)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cartesian_axes(scene, bounds, color)
|
|
103
|
+
divisions = 4
|
|
104
|
+
grid = (1...divisions).flat_map do |index|
|
|
105
|
+
fraction = index.to_f / divisions
|
|
106
|
+
x = bounds.x + bounds.width * fraction
|
|
107
|
+
y = bounds.y + bounds.height * fraction
|
|
108
|
+
["M#{x},#{bounds.y}V#{bounds.bottom}", "M#{bounds.x},#{y}H#{bounds.right}"]
|
|
109
|
+
end.join
|
|
110
|
+
ticks = (1...divisions).flat_map do |index|
|
|
111
|
+
fraction = index.to_f / divisions
|
|
112
|
+
x = bounds.x + bounds.width * fraction
|
|
113
|
+
y = bounds.y + bounds.height * fraction
|
|
114
|
+
["M#{x},#{bounds.bottom}V#{bounds.bottom - 3}", "M#{bounds.x},#{y}H#{bounds.x + 3}"]
|
|
115
|
+
end.join
|
|
116
|
+
scene.path(grid, stroke: color, width: 0.5)
|
|
117
|
+
scene.path("M#{bounds.x},#{bounds.y}V#{bounds.bottom}H#{bounds.right}", stroke: color, width: 1)
|
|
118
|
+
scene.path(ticks, stroke: color, width: 1)
|
|
119
|
+
end
|
|
86
120
|
end
|
|
87
121
|
|
|
88
122
|
class LineChart < Sparkline
|
|
@@ -96,25 +130,21 @@ module Zaniah
|
|
|
96
130
|
end
|
|
97
131
|
|
|
98
132
|
def build(cx)
|
|
99
|
-
palette =
|
|
133
|
+
palette = chart_palette(cx)
|
|
134
|
+
range = @series.values.flatten.minmax
|
|
100
135
|
canvas = @canvas = Canvas.new do |bounds, context|
|
|
101
136
|
@plot_bounds = bounds.inset(Edges.new(12, 8, 20, 28))
|
|
102
|
-
|
|
103
|
-
@series.each_with_index
|
|
137
|
+
cartesian_axes(context.scene, @plot_bounds, cx.theme.colors.border)
|
|
138
|
+
@series.each_with_index do |(_name, values), index|
|
|
139
|
+
paint_line(context.scene, @plot_bounds, values, palette[index % palette.length], range: range)
|
|
140
|
+
end
|
|
104
141
|
end.w(@width).h(@height).on_hover { |event, context| show_line_tooltip(event, context) }
|
|
105
142
|
.focusable(context: {in_chart: true}) { |action| chart_action(action, cx) }
|
|
106
|
-
|
|
107
|
-
Div.new.flex_row.items_center.gap(4).child(Div.new.w(8).h(8).bg(palette[index % palette.length])).child(Label.new(name, size: :xs))
|
|
108
|
-
end)
|
|
109
|
-
Div.new.gap(4).child(canvas).child(legend)
|
|
143
|
+
Div.new.gap(4).child(canvas).child(chart_legend(@series.keys, palette))
|
|
110
144
|
end
|
|
111
145
|
|
|
112
146
|
private
|
|
113
147
|
|
|
114
|
-
def axes(scene, bounds, color)
|
|
115
|
-
scene.path("M#{bounds.x},#{bounds.y}V#{bounds.bottom}H#{bounds.right}", stroke: color, width: 1)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
148
|
def show_line_tooltip(event, cx)
|
|
119
149
|
return unless @plot_bounds
|
|
120
150
|
text = @series.map do |name, values|
|
|
@@ -131,14 +161,15 @@ module Zaniah
|
|
|
131
161
|
end
|
|
132
162
|
|
|
133
163
|
def build(cx)
|
|
134
|
-
palette =
|
|
164
|
+
palette = chart_palette(cx)
|
|
165
|
+
@axis_color = cx.theme.colors.border
|
|
135
166
|
canvas = @canvas = Canvas.new do |bounds, context|
|
|
136
167
|
@plot_bounds = bounds.inset(Edges.new(12, 8, 20, 28))
|
|
137
|
-
|
|
168
|
+
cartesian_axes(context.scene, @plot_bounds, @axis_color)
|
|
138
169
|
paint_bars(context.scene, @plot_bounds, palette)
|
|
139
170
|
end.w(@width).h(@height).on_hover { |event, context| show_line_tooltip(event, context) }
|
|
140
171
|
.focusable(context: {in_chart: true}) { |action| chart_action(action, cx) }
|
|
141
|
-
Div.new.gap(4).child(canvas).child(
|
|
172
|
+
Div.new.gap(4).child(canvas).child(chart_legend(@series.keys, palette))
|
|
142
173
|
end
|
|
143
174
|
|
|
144
175
|
private
|
|
@@ -185,20 +216,16 @@ module Zaniah
|
|
|
185
216
|
end
|
|
186
217
|
|
|
187
218
|
def build(cx)
|
|
188
|
-
palette =
|
|
189
|
-
raise ArgumentError, "chart colors must not be empty" if palette.empty?
|
|
219
|
+
palette = chart_palette(cx)
|
|
190
220
|
|
|
191
221
|
@canvas = Canvas.new do |bounds, context|
|
|
192
222
|
@plot_bounds = bounds
|
|
193
223
|
paint_slices(context.scene, bounds, palette)
|
|
194
224
|
end.w(@width).h(@height).on_hover { |event, context| show_slice_tooltip(event, context) }
|
|
195
225
|
.focusable(context: {in_chart: true}) { |action| chart_action(action, cx) }
|
|
196
|
-
legend =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
.child(Div.new.w(8).h(8).bg(palette[index % palette.length]))
|
|
200
|
-
.child(Label.new("#{name}: #{format("%.1f", percent)}%", size: :xs))
|
|
201
|
-
end)
|
|
226
|
+
legend = chart_legend(@slices, palette) do |name, value|
|
|
227
|
+
"#{name}: #{format("%.1f", value / @total * 100)}%"
|
|
228
|
+
end
|
|
202
229
|
Div.new.gap(4).child(@canvas).child(legend)
|
|
203
230
|
end
|
|
204
231
|
|
|
@@ -308,20 +335,15 @@ module Zaniah
|
|
|
308
335
|
end
|
|
309
336
|
|
|
310
337
|
def build(cx)
|
|
311
|
-
palette =
|
|
312
|
-
raise ArgumentError, "chart colors must not be empty" if palette.empty?
|
|
338
|
+
palette = chart_palette(cx)
|
|
313
339
|
|
|
314
340
|
canvas = @canvas = Canvas.new do |bounds, context|
|
|
315
341
|
@plot_bounds = bounds.inset(Edges.new(12, 8, 20, 28))
|
|
316
|
-
|
|
342
|
+
cartesian_axes(context.scene, @plot_bounds, cx.theme.colors.border)
|
|
317
343
|
paint_points(context.scene, @plot_bounds, palette)
|
|
318
344
|
end.w(@width).h(@height).on_hover { |event, context| show_scatter_tooltip(event, context) }
|
|
319
345
|
.focusable(context: {in_chart: true}) { |action| chart_action(action, cx) }
|
|
320
|
-
|
|
321
|
-
Div.new.flex_row.items_center.gap(4).child(Div.new.w(8).h(8).bg(palette[index % palette.length]))
|
|
322
|
-
.child(Label.new(name, size: :xs))
|
|
323
|
-
end)
|
|
324
|
-
Div.new.gap(4).child(canvas).child(legend)
|
|
346
|
+
Div.new.gap(4).child(canvas).child(chart_legend(@points.keys, palette))
|
|
325
347
|
end
|
|
326
348
|
|
|
327
349
|
def tui_cells(*) = spark(@series.values.first)
|
|
@@ -341,10 +363,6 @@ module Zaniah
|
|
|
341
363
|
|
|
342
364
|
private
|
|
343
365
|
|
|
344
|
-
def paint_axes(scene, bounds, color)
|
|
345
|
-
scene.path("M#{bounds.x},#{bounds.y}V#{bounds.bottom}H#{bounds.right}", stroke: color, width: 1)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
366
|
def paint_points(scene, bounds, palette)
|
|
349
367
|
all = @points.values.flatten(1)
|
|
350
368
|
min_x, max_x = all.map(&:first).minmax
|
|
@@ -381,20 +399,15 @@ module Zaniah
|
|
|
381
399
|
end
|
|
382
400
|
|
|
383
401
|
def build(cx)
|
|
384
|
-
palette =
|
|
385
|
-
raise ArgumentError, "chart colors must not be empty" if palette.empty?
|
|
402
|
+
palette = chart_palette(cx)
|
|
386
403
|
|
|
387
404
|
canvas = @canvas = Canvas.new do |bounds, context|
|
|
388
405
|
@plot_bounds = bounds.inset(Edges.new(12, 8, 20, 28))
|
|
389
|
-
|
|
406
|
+
cartesian_axes(context.scene, @plot_bounds, cx.theme.colors.border)
|
|
390
407
|
paint_areas(context.scene, @plot_bounds, palette)
|
|
391
408
|
end.w(@width).h(@height).on_hover { |event, context| show_line_tooltip(event, context) }
|
|
392
409
|
.focusable(context: {in_chart: true}) { |action| chart_action(action, cx) }
|
|
393
|
-
|
|
394
|
-
Div.new.flex_row.items_center.gap(4).child(Div.new.w(8).h(8).bg(palette[index % palette.length]))
|
|
395
|
-
.child(Label.new(name, size: :xs))
|
|
396
|
-
end)
|
|
397
|
-
Div.new.gap(4).child(canvas).child(legend)
|
|
410
|
+
Div.new.gap(4).child(canvas).child(chart_legend(@series.keys, palette))
|
|
398
411
|
end
|
|
399
412
|
|
|
400
413
|
private
|
|
@@ -457,7 +470,7 @@ module Zaniah
|
|
|
457
470
|
value.negative? ? negative[index] = finish : positive[index] = finish
|
|
458
471
|
end
|
|
459
472
|
end
|
|
460
|
-
scene.path("M#{bounds.x},#{zero}H#{bounds.right}", stroke: "#777", width: 1)
|
|
473
|
+
scene.path("M#{bounds.x},#{zero}H#{bounds.right}", stroke: @axis_color || "#777", width: 1)
|
|
461
474
|
end
|
|
462
475
|
end
|
|
463
476
|
end
|
data/lib/zaniah/version.rb
CHANGED