kumiki 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +256 -0
  4. data/lib/kumiki/animation/animated_state.rb +83 -0
  5. data/lib/kumiki/animation/easing.rb +62 -0
  6. data/lib/kumiki/animation/value_tween.rb +69 -0
  7. data/lib/kumiki/app.rb +381 -0
  8. data/lib/kumiki/box.rb +40 -0
  9. data/lib/kumiki/chart/area_chart.rb +308 -0
  10. data/lib/kumiki/chart/bar_chart.rb +291 -0
  11. data/lib/kumiki/chart/base_chart.rb +213 -0
  12. data/lib/kumiki/chart/chart_helpers.rb +74 -0
  13. data/lib/kumiki/chart/gauge_chart.rb +174 -0
  14. data/lib/kumiki/chart/heatmap_chart.rb +223 -0
  15. data/lib/kumiki/chart/line_chart.rb +292 -0
  16. data/lib/kumiki/chart/pie_chart.rb +222 -0
  17. data/lib/kumiki/chart/scales.rb +79 -0
  18. data/lib/kumiki/chart/scatter_chart.rb +306 -0
  19. data/lib/kumiki/chart/stacked_bar_chart.rb +279 -0
  20. data/lib/kumiki/column.rb +351 -0
  21. data/lib/kumiki/core.rb +2511 -0
  22. data/lib/kumiki/dsl.rb +408 -0
  23. data/lib/kumiki/frame_ranma.rb +570 -0
  24. data/lib/kumiki/markdown/ast.rb +127 -0
  25. data/lib/kumiki/markdown/mermaid/layout.rb +389 -0
  26. data/lib/kumiki/markdown/mermaid/models.rb +235 -0
  27. data/lib/kumiki/markdown/mermaid/parser.rb +522 -0
  28. data/lib/kumiki/markdown/mermaid/renderer.rb +339 -0
  29. data/lib/kumiki/markdown/parser.rb +808 -0
  30. data/lib/kumiki/markdown/renderer.rb +642 -0
  31. data/lib/kumiki/markdown/theme.rb +168 -0
  32. data/lib/kumiki/render_node.rb +262 -0
  33. data/lib/kumiki/row.rb +288 -0
  34. data/lib/kumiki/spacer.rb +20 -0
  35. data/lib/kumiki/style.rb +799 -0
  36. data/lib/kumiki/theme.rb +567 -0
  37. data/lib/kumiki/themes/material.rb +40 -0
  38. data/lib/kumiki/themes/tokyo_night.rb +11 -0
  39. data/lib/kumiki/version.rb +5 -0
  40. data/lib/kumiki/widgets/button.rb +105 -0
  41. data/lib/kumiki/widgets/calendar.rb +1028 -0
  42. data/lib/kumiki/widgets/checkbox.rb +119 -0
  43. data/lib/kumiki/widgets/container.rb +111 -0
  44. data/lib/kumiki/widgets/data_table.rb +670 -0
  45. data/lib/kumiki/widgets/divider.rb +31 -0
  46. data/lib/kumiki/widgets/image.rb +105 -0
  47. data/lib/kumiki/widgets/input.rb +485 -0
  48. data/lib/kumiki/widgets/markdown.rb +58 -0
  49. data/lib/kumiki/widgets/modal.rb +165 -0
  50. data/lib/kumiki/widgets/multiline_input.rb +970 -0
  51. data/lib/kumiki/widgets/multiline_text.rb +180 -0
  52. data/lib/kumiki/widgets/net_image.rb +100 -0
  53. data/lib/kumiki/widgets/progress_bar.rb +72 -0
  54. data/lib/kumiki/widgets/radio_buttons.rb +93 -0
  55. data/lib/kumiki/widgets/slider.rb +135 -0
  56. data/lib/kumiki/widgets/switch.rb +84 -0
  57. data/lib/kumiki/widgets/tabs.rb +175 -0
  58. data/lib/kumiki/widgets/text.rb +120 -0
  59. data/lib/kumiki/widgets/tree.rb +434 -0
  60. data/lib/kumiki/widgets/webview.rb +87 -0
  61. data/lib/kumiki.rb +130 -0
  62. metadata +113 -0
@@ -0,0 +1,222 @@
1
+ module Kumiki
2
+ # PieChart - pie/donut chart widget
3
+ # Supports slice labels, percentages, hover, donut mode
4
+
5
+ PIE_PI = 3.14159265358979323846
6
+
7
+ class PieChart < BaseChart
8
+ def initialize(labels, values)
9
+ super()
10
+ @labels = labels
11
+ @values = values
12
+ @donut = false
13
+ @donut_ratio = 0.5
14
+ @show_pct = true
15
+ @show_labels = true
16
+ @start_angle = -90.0
17
+ # Computed per-frame
18
+ @cx = 0.0
19
+ @cy = 0.0
20
+ @radius = 20.0
21
+ @total = 0.0
22
+ end
23
+
24
+ def donut(v)
25
+ @donut = v
26
+ self
27
+ end
28
+
29
+ def donut_ratio(r)
30
+ @donut_ratio = r
31
+ self
32
+ end
33
+
34
+ def show_percentages(v)
35
+ @show_pct = v
36
+ self
37
+ end
38
+
39
+ def show_labels(v)
40
+ @show_labels = v
41
+ self
42
+ end
43
+
44
+ def start_angle(a)
45
+ @start_angle = a
46
+ self
47
+ end
48
+
49
+ def set_data(labels, values)
50
+ @labels = labels
51
+ @values = values
52
+ mark_dirty
53
+ update
54
+ end
55
+
56
+ def render_chart(painter, px, py, pw, ph)
57
+ return if @labels.length == 0
58
+ setup_pie(px, py, pw, ph)
59
+ return if @total <= 0.0
60
+ draw_slices(painter)
61
+ draw_donut_hole(painter)
62
+ draw_pie_legend(painter, px)
63
+ end
64
+
65
+ def setup_pie(px, py, pw, ph)
66
+ @total = compute_total
67
+ chart_size = pw
68
+ if ph < pw
69
+ chart_size = ph
70
+ end
71
+ @radius = chart_size / 2.0 - 10.0
72
+ if @radius < 20.0
73
+ @radius = 20.0
74
+ end
75
+ @cx = px + pw / 2.0
76
+ @cy = py + ph / 2.0
77
+ end
78
+
79
+ def compute_total
80
+ total = 0.0
81
+ i = 0
82
+ while i < @values.length
83
+ total = total + @values[i]
84
+ i = i + 1
85
+ end
86
+ total
87
+ end
88
+
89
+ def draw_slices(painter)
90
+ angle = @start_angle
91
+ i = 0
92
+ while i < @values.length
93
+ angle = draw_one_slice(painter, i, angle)
94
+ i = i + 1
95
+ end
96
+ end
97
+
98
+ def draw_one_slice(painter, i, angle)
99
+ fraction = @values[i] / @total
100
+ sweep = fraction * 360.0
101
+ c = series_color(i)
102
+ if @hover_index == i
103
+ c = painter.lighten_color(c, 0.3)
104
+ end
105
+ painter.fill_arc(@cx, @cy, @radius, angle, sweep, c)
106
+ if @show_pct
107
+ if fraction >= 0.03
108
+ draw_slice_label(painter, angle, sweep, fraction)
109
+ end
110
+ end
111
+ angle + sweep
112
+ end
113
+
114
+ def draw_slice_label(painter, angle, sweep, fraction)
115
+ mid_angle = angle + sweep / 2.0
116
+ rad = mid_angle * PIE_PI / 180.0
117
+ label_r = compute_label_radius
118
+ lx = @cx + label_r * painter.math_cos(rad)
119
+ ly = @cy + label_r * painter.math_sin(rad)
120
+ pct_val = fraction * 100.0
121
+ pct = painter.number_to_string(pct_val) + "%"
122
+ pw2 = painter.measure_text_width(pct, Kumiki.theme.font_family, 11.0)
123
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
124
+ painter.draw_text(pct, lx - pw2 / 2.0, ly + ascent / 2.0, Kumiki.theme.font_family, 11.0, 4294967295)
125
+ end
126
+
127
+ def compute_label_radius
128
+ if @donut
129
+ @radius * (1.0 + @donut_ratio) / 2.0
130
+ else
131
+ @radius * 0.65
132
+ end
133
+ end
134
+
135
+ def draw_donut_hole(painter)
136
+ if @donut
137
+ inner_r = @radius * @donut_ratio
138
+ painter.fill_circle(@cx, @cy, inner_r, Kumiki.theme.bg_primary)
139
+ end
140
+ end
141
+
142
+ def draw_pie_legend(painter, px)
143
+ if @show_legend
144
+ legend_y = @cy + @radius + 16.0
145
+ colors = []
146
+ i = 0
147
+ while i < @labels.length
148
+ colors << series_color(i)
149
+ i = i + 1
150
+ end
151
+ draw_legend(painter, @labels, colors, px + 8.0, legend_y)
152
+ end
153
+ end
154
+
155
+ def update_hover
156
+ return if @painter == nil
157
+ mx = @mouse_x
158
+ my = @mouse_y
159
+ dx = mx - @cx
160
+ dy = my - @cy
161
+ dist = @painter.math_sqrt(dx * dx + dy * dy)
162
+ if dist > @radius
163
+ @hover_index = -1
164
+ return
165
+ end
166
+ if @donut
167
+ inner = @radius * @donut_ratio
168
+ if dist < inner
169
+ @hover_index = -1
170
+ return
171
+ end
172
+ end
173
+ find_hover_slice(dx, dy)
174
+ end
175
+
176
+ def find_hover_slice(dx, dy)
177
+ angle_rad = @painter.math_atan2(dy, dx)
178
+ angle_deg = angle_rad * 180.0 / PIE_PI
179
+ relative = angle_deg - @start_angle
180
+ relative = normalize_angle(relative)
181
+ total = compute_total
182
+ if total <= 0.0
183
+ @hover_index = -1
184
+ return
185
+ end
186
+ find_slice_at_angle(relative, total)
187
+ end
188
+
189
+ def normalize_angle(a)
190
+ while a < 0.0
191
+ a = a + 360.0
192
+ end
193
+ while a >= 360.0
194
+ a = a - 360.0
195
+ end
196
+ a
197
+ end
198
+
199
+ def find_slice_at_angle(relative, total)
200
+ cumulative = 0.0
201
+ i = 0
202
+ while i < @values.length
203
+ fraction = @values[i] / total
204
+ sweep = fraction * 360.0
205
+ if relative >= cumulative
206
+ if relative < cumulative + sweep
207
+ @hover_index = i
208
+ return
209
+ end
210
+ end
211
+ cumulative = cumulative + sweep
212
+ i = i + 1
213
+ end
214
+ @hover_index = -1
215
+ end
216
+ end
217
+
218
+ def PieChart(labels, values)
219
+ PieChart.new(labels, values)
220
+ end
221
+
222
+ end
@@ -0,0 +1,79 @@
1
+ module Kumiki
2
+ # Chart Scales - domain-to-pixel coordinate mapping
3
+ # Pure math classes, no UI dependency
4
+
5
+ class LinearScale
6
+ def initialize(domain_min, domain_max, range_min, range_max)
7
+ @domain_min = domain_min
8
+ @domain_max = domain_max
9
+ @range_min = range_min
10
+ @range_max = range_max
11
+ span = domain_max - domain_min
12
+ if span == 0.0
13
+ @factor = 0.0
14
+ else
15
+ @factor = (range_max - range_min) / span
16
+ end
17
+ end
18
+
19
+ def map(value)
20
+ @range_min + (value - @domain_min) * @factor
21
+ end
22
+
23
+ def domain_min
24
+ @domain_min
25
+ end
26
+
27
+ def domain_max
28
+ @domain_max
29
+ end
30
+
31
+ def range_min
32
+ @range_min
33
+ end
34
+
35
+ def range_max
36
+ @range_max
37
+ end
38
+ end
39
+
40
+ class BandScale
41
+ def initialize(count, range_min, range_max, padding)
42
+ @count = count
43
+ @range_min = range_min
44
+ @range_max = range_max
45
+ @padding = padding
46
+ total = range_max - range_min
47
+ @band_width = compute_band_width(count, total, padding)
48
+ end
49
+
50
+ def compute_band_width(count, total, padding)
51
+ if count > 0.0
52
+ slots = count + 1.0
53
+ pad_total = padding * slots
54
+ usable = total - pad_total
55
+ bw = usable / count
56
+ if bw < 1.0
57
+ bw = 1.0
58
+ end
59
+ bw
60
+ else
61
+ total
62
+ end
63
+ end
64
+
65
+ def map(index)
66
+ idx = index * 1.0
67
+ @range_min + @padding + idx * (@band_width + @padding)
68
+ end
69
+
70
+ def band_width
71
+ @band_width
72
+ end
73
+
74
+ def count
75
+ @count
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,306 @@
1
+ module Kumiki
2
+ # ScatterChart - XY scatter plot with point markers
3
+ # Supports multiple series, hover detection, value labels
4
+
5
+ class ScatterChart < BaseChart
6
+ def initialize(x_data, y_data, series_names)
7
+ super()
8
+ @x_data = x_data # Array of Array[Float] (one per series)
9
+ @y_data = y_data # Array of Array[Float] (one per series)
10
+ @series_names = series_names
11
+ @point_radius = 5.0
12
+ @show_grid = true
13
+ @data_x_min = 0.0
14
+ @data_x_max = 1.0
15
+ @data_y_min = 0.0
16
+ @data_y_max = 1.0
17
+ @x_scale = nil
18
+ @y_scale = nil
19
+ @x_ticks = nil
20
+ @y_ticks = nil
21
+ @num_series = 0
22
+ @chart_px = 0.0
23
+ @chart_py = 0.0
24
+ @chart_pw = 0.0
25
+ @chart_ph = 0.0
26
+ compute_scatter_range
27
+ end
28
+
29
+ def point_radius(r)
30
+ @point_radius = r
31
+ self
32
+ end
33
+
34
+ def show_grid(v)
35
+ @show_grid = v
36
+ self
37
+ end
38
+
39
+ def set_data(x_data, y_data, series_names)
40
+ @x_data = x_data
41
+ @y_data = y_data
42
+ @series_names = series_names
43
+ compute_scatter_range
44
+ mark_dirty
45
+ update
46
+ end
47
+
48
+ def render_chart(painter, px, py, pw, ph)
49
+ return if @x_data.length == 0
50
+ @chart_px = px
51
+ @chart_py = py
52
+ @chart_pw = pw
53
+ @chart_ph = ph
54
+ @num_series = @x_data.length
55
+ setup_scatter_scales
56
+ draw_scatter_grid(painter)
57
+ draw_y_axis(painter, px, py, pw, ph, @y_ticks, @y_scale)
58
+ draw_x_axis_line(painter, px, py, pw, ph)
59
+ draw_scatter_x_ticks(painter)
60
+ draw_all_scatter_points(painter)
61
+ draw_scatter_legend(painter, px, py)
62
+ end
63
+
64
+ def setup_scatter_scales
65
+ @x_ticks = compute_ticks(@data_x_min, @data_x_max, 5)
66
+ @y_ticks = compute_ticks(@data_y_min, @data_y_max, 5)
67
+ x_min = @data_x_min
68
+ x_max = @data_x_max
69
+ y_min = @data_y_min
70
+ y_max = @data_y_max
71
+ if @x_ticks.length > 0
72
+ x_min = @x_ticks[0]
73
+ x_max = @x_ticks[@x_ticks.length - 1]
74
+ end
75
+ if @y_ticks.length > 0
76
+ y_min = @y_ticks[0]
77
+ y_max = @y_ticks[@y_ticks.length - 1]
78
+ end
79
+ bottom = @chart_py + @chart_ph
80
+ right = @chart_px + @chart_pw
81
+ @x_scale = LinearScale.new(x_min, x_max, @chart_px, right)
82
+ @y_scale = LinearScale.new(y_min, y_max, bottom, @chart_py)
83
+ end
84
+
85
+ def draw_scatter_grid(painter)
86
+ return if !@show_grid
87
+ bc = Kumiki.theme.border
88
+ gc = painter.with_alpha(bc, 40)
89
+ # Vertical grid lines at x ticks
90
+ i = 0
91
+ while i < @x_ticks.length
92
+ xx = @x_scale.map(@x_ticks[i])
93
+ bottom = @chart_py + @chart_ph
94
+ painter.draw_line(xx, @chart_py, xx, bottom, gc, 1.0)
95
+ i = i + 1
96
+ end
97
+ end
98
+
99
+ def draw_scatter_x_ticks(painter)
100
+ lc = Kumiki.theme.text_secondary
101
+ i = 0
102
+ while i < @x_ticks.length
103
+ xx = @x_scale.map(@x_ticks[i])
104
+ label = painter.number_to_string(@x_ticks[i])
105
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
106
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
107
+ label_y = @chart_py + @chart_ph + 14.0 + ascent
108
+ painter.draw_text(label, xx - lw / 2.0, label_y, Kumiki.theme.font_family, 11.0, lc)
109
+ i = i + 1
110
+ end
111
+ end
112
+
113
+ def draw_all_scatter_points(painter)
114
+ si = 0
115
+ while si < @num_series
116
+ c = series_color(si)
117
+ draw_scatter_series(painter, si, c)
118
+ si = si + 1
119
+ end
120
+ end
121
+
122
+ def draw_scatter_series(painter, si, c)
123
+ xs = @x_data[si]
124
+ ys = @y_data[si]
125
+ n = xs.length
126
+ if ys.length < n
127
+ n = ys.length
128
+ end
129
+ j = 0
130
+ while j < n
131
+ xx = @x_scale.map(xs[j])
132
+ yy = @y_scale.map(ys[j])
133
+ r = @point_radius
134
+ if @hover_series == si
135
+ if @hover_index == j
136
+ r = @point_radius + 3.0
137
+ end
138
+ end
139
+ painter.fill_circle(xx, yy, r, c)
140
+ if @hover_series == si
141
+ if @hover_index == j
142
+ draw_scatter_label(painter, xs[j], ys[j], xx, yy, r)
143
+ end
144
+ end
145
+ j = j + 1
146
+ end
147
+ end
148
+
149
+ def draw_scatter_label(painter, xv, yv, xx, yy, r)
150
+ xl = painter.number_to_string(xv)
151
+ yl = painter.number_to_string(yv)
152
+ label = "(" + xl + ", " + yl + ")"
153
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 10.0)
154
+ tc = Kumiki.theme.text_primary
155
+ painter.draw_text(label, xx - lw / 2.0, yy - r - 4.0, Kumiki.theme.font_family, 10.0, tc)
156
+ end
157
+
158
+ def draw_scatter_legend(painter, px, py)
159
+ if @show_legend
160
+ if @series_names.length > 1
161
+ colors = []
162
+ si = 0
163
+ while si < @series_names.length
164
+ colors << series_color(si)
165
+ si = si + 1
166
+ end
167
+ draw_legend(painter, @series_names, colors, px + 8.0, py - 20.0)
168
+ end
169
+ end
170
+ end
171
+
172
+ def update_hover
173
+ return if @x_scale == nil
174
+ mx = @mouse_x
175
+ my = @mouse_y
176
+ px = plot_x
177
+ py = plot_y
178
+ pw = plot_w
179
+ ph = plot_h
180
+ if mx < px
181
+ @hover_index = -1
182
+ @hover_series = -1
183
+ return
184
+ end
185
+ if mx > px + pw
186
+ @hover_index = -1
187
+ @hover_series = -1
188
+ return
189
+ end
190
+ if my < py
191
+ @hover_index = -1
192
+ @hover_series = -1
193
+ return
194
+ end
195
+ if my > py + ph
196
+ @hover_index = -1
197
+ @hover_series = -1
198
+ return
199
+ end
200
+ find_nearest_scatter_point(mx, my)
201
+ end
202
+
203
+ def find_nearest_scatter_point(mx, my)
204
+ best_si = -1
205
+ best_j = -1
206
+ best_dist = 999999.0
207
+ si = 0
208
+ while si < @num_series
209
+ xs = @x_data[si]
210
+ ys = @y_data[si]
211
+ n = xs.length
212
+ if ys.length < n
213
+ n = ys.length
214
+ end
215
+ j = 0
216
+ while j < n
217
+ xx = @x_scale.map(xs[j])
218
+ yy = @y_scale.map(ys[j])
219
+ dx = mx - xx
220
+ dy = my - yy
221
+ d = dx * dx + dy * dy
222
+ if d < best_dist
223
+ best_dist = d
224
+ best_si = si
225
+ best_j = j
226
+ end
227
+ j = j + 1
228
+ end
229
+ si = si + 1
230
+ end
231
+ threshold = (@point_radius + 8.0) * (@point_radius + 8.0)
232
+ if best_dist < threshold
233
+ @hover_series = best_si
234
+ @hover_index = best_j
235
+ else
236
+ @hover_series = -1
237
+ @hover_index = -1
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ def compute_scatter_range
244
+ @data_x_min = 0.0
245
+ @data_x_max = 1.0
246
+ @data_y_min = 0.0
247
+ @data_y_max = 1.0
248
+ first = true
249
+ si = 0
250
+ while si < @x_data.length
251
+ xs = @x_data[si]
252
+ ys = @y_data[si]
253
+ n = xs.length
254
+ if ys.length < n
255
+ n = ys.length
256
+ end
257
+ j = 0
258
+ while j < n
259
+ xv = xs[j]
260
+ yv = ys[j]
261
+ if first
262
+ @data_x_min = xv
263
+ @data_x_max = xv
264
+ @data_y_min = yv
265
+ @data_y_max = yv
266
+ first = false
267
+ else
268
+ if xv < @data_x_min
269
+ @data_x_min = xv
270
+ end
271
+ if xv > @data_x_max
272
+ @data_x_max = xv
273
+ end
274
+ if yv < @data_y_min
275
+ @data_y_min = yv
276
+ end
277
+ if yv > @data_y_max
278
+ @data_y_max = yv
279
+ end
280
+ end
281
+ j = j + 1
282
+ end
283
+ si = si + 1
284
+ end
285
+ xr = @data_x_max - @data_x_min
286
+ yr = @data_y_max - @data_y_min
287
+ if xr > 0.0
288
+ @data_x_max = @data_x_max + xr * 0.05
289
+ @data_x_min = @data_x_min - xr * 0.05
290
+ else
291
+ @data_x_max = @data_x_max + 1.0
292
+ end
293
+ if yr > 0.0
294
+ @data_y_max = @data_y_max + yr * 0.05
295
+ @data_y_min = @data_y_min - yr * 0.05
296
+ else
297
+ @data_y_max = @data_y_max + 1.0
298
+ end
299
+ end
300
+ end
301
+
302
+ def ScatterChart(x_data, y_data, series_names)
303
+ ScatterChart.new(x_data, y_data, series_names)
304
+ end
305
+
306
+ end