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,174 @@
1
+ module Kumiki
2
+ # GaugeChart - arc gauge with thresholds and center value display
3
+ # Uses fill_arc/stroke_arc APIs for drawing
4
+
5
+ GAUGE_PI = 3.14159265358979323846
6
+
7
+ class GaugeChart < BaseChart
8
+ def initialize(value, min_val, max_val)
9
+ super()
10
+ @value = value
11
+ @min_val = min_val
12
+ @max_val = max_val
13
+ @thresholds = nil # Array of [threshold_value, color] pairs
14
+ @arc_width = 20.0
15
+ @show_value = true
16
+ @value_format = nil
17
+ @unit_text = ""
18
+ @bg_arc_color = 0xFF3B4261
19
+ end
20
+
21
+ def thresholds(t)
22
+ @thresholds = t
23
+ self
24
+ end
25
+
26
+ def arc_width(w)
27
+ @arc_width = w
28
+ self
29
+ end
30
+
31
+ def show_value(v)
32
+ @show_value = v
33
+ self
34
+ end
35
+
36
+ def unit(u)
37
+ @unit_text = u
38
+ self
39
+ end
40
+
41
+ def set_value(v)
42
+ @value = v
43
+ mark_dirty
44
+ update
45
+ end
46
+
47
+ def render_chart(painter, px, py, pw, ph)
48
+ chart_size = pw
49
+ if ph < pw
50
+ chart_size = ph
51
+ end
52
+ radius = chart_size / 2.0 - @arc_width / 2.0 - 10.0
53
+ if radius < 30.0
54
+ radius = 30.0
55
+ end
56
+ cx = px + pw / 2.0
57
+ cy = py + ph / 2.0 + 10.0
58
+
59
+ # Background arc (270 degrees, from 135 to 405)
60
+ start_angle = 135.0
61
+ sweep_total = 270.0
62
+ painter.stroke_arc(cx, cy, radius, start_angle, sweep_total, @bg_arc_color, @arc_width)
63
+
64
+ # Value arc
65
+ range = @max_val - @min_val
66
+ if range <= 0.0
67
+ range = 1.0
68
+ end
69
+ fraction = (@value - @min_val) / range
70
+ if fraction < 0.0
71
+ fraction = 0.0
72
+ end
73
+ if fraction > 1.0
74
+ fraction = 1.0
75
+ end
76
+ value_sweep = fraction * sweep_total
77
+ arc_color = get_gauge_color(painter, fraction)
78
+ painter.stroke_arc(cx, cy, radius, start_angle, value_sweep, arc_color, @arc_width)
79
+
80
+ # Draw threshold tick marks
81
+ draw_gauge_ticks(painter, cx, cy, radius)
82
+
83
+ # Center value display
84
+ if @show_value
85
+ draw_gauge_value(painter, cx, cy)
86
+ end
87
+
88
+ # Min/Max labels
89
+ draw_gauge_min_max(painter, cx, cy, radius)
90
+ end
91
+
92
+ def get_gauge_color(painter, fraction)
93
+ if @thresholds == nil
94
+ return series_color(0)
95
+ end
96
+ # thresholds: [[0.5, green], [0.75, yellow], [1.0, red]]
97
+ i = 0
98
+ while i < @thresholds.length
99
+ threshold_frac = @thresholds[i][0]
100
+ if fraction <= threshold_frac
101
+ return @thresholds[i][1]
102
+ end
103
+ i = i + 1
104
+ end
105
+ # Past all thresholds, use last color
106
+ if @thresholds.length > 0
107
+ return @thresholds[@thresholds.length - 1][1]
108
+ end
109
+ series_color(0)
110
+ end
111
+
112
+ def draw_gauge_ticks(painter, cx, cy, radius)
113
+ return if @thresholds == nil
114
+ start_angle = 135.0
115
+ sweep_total = 270.0
116
+ tick_r_inner = radius - @arc_width / 2.0 - 2.0
117
+ tick_r_outer = radius + @arc_width / 2.0 + 2.0
118
+ tc = Kumiki.theme.text_secondary
119
+ i = 0
120
+ while i < @thresholds.length
121
+ frac = @thresholds[i][0]
122
+ if frac < 1.0
123
+ angle_deg = start_angle + frac * sweep_total
124
+ angle_rad = angle_deg * GAUGE_PI / 180.0
125
+ cos_a = painter.math_cos(angle_rad)
126
+ sin_a = painter.math_sin(angle_rad)
127
+ x1 = cx + tick_r_inner * cos_a
128
+ y1 = cy + tick_r_inner * sin_a
129
+ x2 = cx + tick_r_outer * cos_a
130
+ y2 = cy + tick_r_outer * sin_a
131
+ painter.draw_line(x1, y1, x2, y2, tc, 1.0)
132
+ end
133
+ i = i + 1
134
+ end
135
+ end
136
+
137
+ def draw_gauge_value(painter, cx, cy)
138
+ vl = painter.number_to_string(@value)
139
+ if @unit_text != ""
140
+ vl = vl + @unit_text
141
+ end
142
+ vw = painter.measure_text_width(vl, Kumiki.theme.font_family, 28.0)
143
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 28.0)
144
+ vx = cx - vw / 2.0
145
+ vy = cy + ascent / 2.0
146
+ tc = Kumiki.theme.text_primary
147
+ painter.draw_text(vl, vx, vy, Kumiki.theme.font_family, 28.0, tc)
148
+ end
149
+
150
+ def draw_gauge_min_max(painter, cx, cy, radius)
151
+ lc = Kumiki.theme.text_secondary
152
+ r_label = radius + @arc_width / 2.0 + 16.0
153
+ # Min label at 135 degrees
154
+ min_angle = 135.0 * GAUGE_PI / 180.0
155
+ min_label = painter.number_to_string(@min_val)
156
+ mlw = painter.measure_text_width(min_label, Kumiki.theme.font_family, 11.0)
157
+ mx = cx + r_label * painter.math_cos(min_angle) - mlw
158
+ my = cy + r_label * painter.math_sin(min_angle)
159
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
160
+ painter.draw_text(min_label, mx, my + ascent, Kumiki.theme.font_family, 11.0, lc)
161
+ # Max label at 405 degrees = 45 degrees
162
+ max_angle = 45.0 * GAUGE_PI / 180.0
163
+ max_label = painter.number_to_string(@max_val)
164
+ max_x = cx + r_label * painter.math_cos(max_angle)
165
+ max_y = cy + r_label * painter.math_sin(max_angle)
166
+ painter.draw_text(max_label, max_x, max_y + ascent, Kumiki.theme.font_family, 11.0, lc)
167
+ end
168
+ end
169
+
170
+ def GaugeChart(value, min_val, max_val)
171
+ GaugeChart.new(value, min_val, max_val)
172
+ end
173
+
174
+ end
@@ -0,0 +1,223 @@
1
+ module Kumiki
2
+ # HeatmapChart - 2D grid heatmap with color interpolation
3
+ # Supports axis labels, value display, color scale legend
4
+
5
+ class HeatmapChart < BaseChart
6
+ def initialize(x_labels, y_labels, data_2d)
7
+ super()
8
+ @x_labels = x_labels # Array of String (column headers)
9
+ @y_labels = y_labels # Array of String (row headers)
10
+ @data_2d = data_2d # Array of Array[Float] (rows x cols)
11
+ @show_cell_values = true
12
+ @color_low = 0xFF1A1B26 # Dark blue/black
13
+ @color_high = 0xFF7AA2F7 # Bright blue
14
+ @cell_padding = 2.0
15
+ @data_min = 0.0
16
+ @data_max = 1.0
17
+ @chart_px = 0.0
18
+ @chart_py = 0.0
19
+ @chart_pw = 0.0
20
+ @chart_ph = 0.0
21
+ @num_rows = 0
22
+ @num_cols = 0
23
+ compute_heatmap_range
24
+ end
25
+
26
+ def show_cell_values(v)
27
+ @show_cell_values = v
28
+ self
29
+ end
30
+
31
+ def color_range(low, high)
32
+ @color_low = low
33
+ @color_high = high
34
+ self
35
+ end
36
+
37
+ def cell_padding(p)
38
+ @cell_padding = p
39
+ self
40
+ end
41
+
42
+ def set_data(x_labels, y_labels, data_2d)
43
+ @x_labels = x_labels
44
+ @y_labels = y_labels
45
+ @data_2d = data_2d
46
+ compute_heatmap_range
47
+ mark_dirty
48
+ update
49
+ end
50
+
51
+ def render_chart(painter, px, py, pw, ph)
52
+ return if @x_labels.length == 0
53
+ return if @y_labels.length == 0
54
+ @chart_px = px
55
+ @chart_py = py
56
+ @chart_pw = pw
57
+ @chart_ph = ph
58
+ @num_rows = @y_labels.length
59
+ @num_cols = @x_labels.length
60
+ draw_heatmap_cells(painter)
61
+ draw_heatmap_x_labels(painter)
62
+ draw_heatmap_y_labels(painter)
63
+ draw_heatmap_color_legend(painter)
64
+ end
65
+
66
+ def draw_heatmap_cells(painter)
67
+ cell_w = @chart_pw / (@num_cols * 1.0)
68
+ cell_h = (@chart_ph - 20.0) / (@num_rows * 1.0)
69
+ range = @data_max - @data_min
70
+ if range <= 0.0
71
+ range = 1.0
72
+ end
73
+ ri = 0
74
+ while ri < @num_rows
75
+ ci = 0
76
+ while ci < @num_cols
77
+ draw_heatmap_cell(painter, ri, ci, cell_w, cell_h, range)
78
+ ci = ci + 1
79
+ end
80
+ ri = ri + 1
81
+ end
82
+ end
83
+
84
+ def draw_heatmap_cell(painter, ri, ci, cell_w, cell_h, range)
85
+ val = 0.0
86
+ if ri < @data_2d.length
87
+ if ci < @data_2d[ri].length
88
+ val = @data_2d[ri][ci]
89
+ end
90
+ end
91
+ t = (val - @data_min) / range
92
+ if t < 0.0
93
+ t = 0.0
94
+ end
95
+ if t > 1.0
96
+ t = 1.0
97
+ end
98
+ c = painter.interpolate_color(@color_low, @color_high, t)
99
+ cx = @chart_px + ci * 1.0 * cell_w + @cell_padding
100
+ cy = @chart_py + ri * 1.0 * cell_h + @cell_padding
101
+ cw = cell_w - @cell_padding * 2.0
102
+ ch = cell_h - @cell_padding * 2.0
103
+ painter.fill_round_rect(cx, cy, cw, ch, 3.0, c)
104
+
105
+ # Hover highlight
106
+ if @hover_series == ri
107
+ if @hover_index == ci
108
+ hc = painter.with_alpha(4294967295, 60)
109
+ painter.stroke_rect(cx, cy, cw, ch, hc, 2.0)
110
+ end
111
+ end
112
+
113
+ if @show_cell_values
114
+ vl = painter.number_to_string(val)
115
+ vw = painter.measure_text_width(vl, Kumiki.theme.font_family, 10.0)
116
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 10.0)
117
+ vx = cx + cw / 2.0 - vw / 2.0
118
+ vy = cy + ch / 2.0 + ascent / 2.0
119
+ # Use white text on dark cells, dark on light cells
120
+ text_c = 4294967295
121
+ if t > 0.6
122
+ text_c = 4278190080
123
+ end
124
+ painter.draw_text(vl, vx, vy, Kumiki.theme.font_family, 10.0, text_c)
125
+ end
126
+ end
127
+
128
+ def draw_heatmap_x_labels(painter)
129
+ cell_w = @chart_pw / (@num_cols * 1.0)
130
+ lc = Kumiki.theme.text_secondary
131
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
132
+ label_y = @chart_py + @chart_ph - 20.0 + 14.0 + ascent
133
+ i = 0
134
+ while i < @num_cols
135
+ label = @x_labels[i]
136
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
137
+ lx = @chart_px + i * 1.0 * cell_w + cell_w / 2.0 - lw / 2.0
138
+ painter.draw_text(label, lx, label_y, Kumiki.theme.font_family, 11.0, lc)
139
+ i = i + 1
140
+ end
141
+ end
142
+
143
+ def draw_heatmap_y_labels(painter)
144
+ cell_h = (@chart_ph - 20.0) / (@num_rows * 1.0)
145
+ lc = Kumiki.theme.text_secondary
146
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
147
+ i = 0
148
+ while i < @num_rows
149
+ label = @y_labels[i]
150
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
151
+ lx = @chart_px - lw - 6.0
152
+ ly = @chart_py + i * 1.0 * cell_h + cell_h / 2.0 + ascent / 2.0
153
+ painter.draw_text(label, lx, ly, Kumiki.theme.font_family, 11.0, lc)
154
+ i = i + 1
155
+ end
156
+ end
157
+
158
+ def draw_heatmap_color_legend(painter)
159
+ # Draw a small gradient bar at the right side
160
+ lg_w = 16.0
161
+ lg_h = @chart_ph - 20.0
162
+ lg_x = @chart_px + @chart_pw + 8.0
163
+ lg_y = @chart_py
164
+ steps = 20
165
+ step_h = lg_h / (steps * 1.0)
166
+ i = 0
167
+ while i < steps
168
+ t = 1.0 - (i * 1.0 / (steps * 1.0))
169
+ c = painter.interpolate_color(@color_low, @color_high, t)
170
+ sy = lg_y + i * 1.0 * step_h
171
+ painter.fill_rect(lg_x, sy, lg_w, step_h + 1.0, c)
172
+ i = i + 1
173
+ end
174
+ # Labels
175
+ lc = Kumiki.theme.text_secondary
176
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 9.0)
177
+ max_label = painter.number_to_string(@data_max)
178
+ min_label = painter.number_to_string(@data_min)
179
+ painter.draw_text(max_label, lg_x + lg_w + 4.0, lg_y + ascent, Kumiki.theme.font_family, 9.0, lc)
180
+ painter.draw_text(min_label, lg_x + lg_w + 4.0, lg_y + lg_h, Kumiki.theme.font_family, 9.0, lc)
181
+ end
182
+
183
+ def update_hover
184
+ end
185
+
186
+ private
187
+
188
+ def compute_heatmap_range
189
+ @data_min = 0.0
190
+ @data_max = 1.0
191
+ first = true
192
+ ri = 0
193
+ while ri < @data_2d.length
194
+ ci = 0
195
+ while ci < @data_2d[ri].length
196
+ v = @data_2d[ri][ci]
197
+ if first
198
+ @data_min = v
199
+ @data_max = v
200
+ first = false
201
+ else
202
+ if v < @data_min
203
+ @data_min = v
204
+ end
205
+ if v > @data_max
206
+ @data_max = v
207
+ end
208
+ end
209
+ ci = ci + 1
210
+ end
211
+ ri = ri + 1
212
+ end
213
+ if @data_min == @data_max
214
+ @data_max = @data_min + 1.0
215
+ end
216
+ end
217
+ end
218
+
219
+ def HeatmapChart(x_labels, y_labels, data_2d)
220
+ HeatmapChart.new(x_labels, y_labels, data_2d)
221
+ end
222
+
223
+ end
@@ -0,0 +1,292 @@
1
+ module Kumiki
2
+ # LineChart - line chart with point markers
3
+ # Supports multiple series, grid, hover detection
4
+
5
+ class LineChart < BaseChart
6
+ def initialize(x_labels, series_data, series_names)
7
+ super()
8
+ @x_labels = x_labels
9
+ @series_data = series_data
10
+ @series_names = series_names
11
+ @show_points = true
12
+ @point_radius = 4.0
13
+ @line_width = 2.0
14
+ @show_grid = true
15
+ @data_min = 0.0
16
+ @data_max = 0.0
17
+ # Computed per-frame
18
+ @y_scale = nil
19
+ @y_ticks = nil
20
+ @x_step = 0.0
21
+ @num_points = 0
22
+ @num_series = 0
23
+ @y_range_min = 0.0
24
+ @y_range_max = 1.0
25
+ @chart_px = 0.0
26
+ @chart_py = 0.0
27
+ @chart_pw = 0.0
28
+ @chart_ph = 0.0
29
+ compute_data_range
30
+ end
31
+
32
+ def show_points(v)
33
+ @show_points = v
34
+ self
35
+ end
36
+
37
+ def point_radius(r)
38
+ @point_radius = r
39
+ self
40
+ end
41
+
42
+ def line_width(w)
43
+ @line_width = w
44
+ self
45
+ end
46
+
47
+ def show_grid(v)
48
+ @show_grid = v
49
+ self
50
+ end
51
+
52
+ def set_data(x_labels, series_data, series_names)
53
+ @x_labels = x_labels
54
+ @series_data = series_data
55
+ @series_names = series_names
56
+ compute_data_range
57
+ mark_dirty
58
+ update
59
+ end
60
+
61
+ def render_chart(painter, px, py, pw, ph)
62
+ return if @x_labels.length == 0
63
+ return if @series_data.length == 0
64
+ @chart_px = px
65
+ @chart_py = py
66
+ @chart_pw = pw
67
+ @chart_ph = ph
68
+ setup_line_counts
69
+ setup_line_ticks
70
+ setup_line_y_scale
71
+ setup_line_x_step
72
+ draw_y_axis(painter, px, py, pw, ph, @y_ticks, @y_scale)
73
+ draw_x_axis_line(painter, px, py, pw, ph)
74
+ draw_x_labels(painter)
75
+ draw_hover_line(painter)
76
+ draw_all_series(painter)
77
+ draw_line_legend(painter, px, py)
78
+ end
79
+
80
+ def setup_line_counts
81
+ @num_points = @x_labels.length
82
+ @num_series = @series_data.length
83
+ end
84
+
85
+ def setup_line_ticks
86
+ @y_ticks = compute_ticks(@data_min, @data_max, 5)
87
+ @y_range_min = @data_min
88
+ @y_range_max = @data_max
89
+ if @y_ticks.length > 0
90
+ @y_range_min = @y_ticks[0]
91
+ @y_range_max = @y_ticks[@y_ticks.length - 1]
92
+ end
93
+ end
94
+
95
+ def setup_line_y_scale
96
+ bottom = @chart_py + @chart_ph
97
+ @y_scale = LinearScale.new(@y_range_min, @y_range_max, bottom, @chart_py)
98
+ end
99
+
100
+ def setup_line_x_step
101
+ @x_step = 0.0
102
+ if @num_points > 1
103
+ divisor = (@num_points - 1) * 1.0
104
+ @x_step = @chart_pw / divisor
105
+ end
106
+ end
107
+
108
+ def draw_x_labels(painter)
109
+ lc = Kumiki.theme.text_secondary
110
+ i = 0
111
+ while i < @num_points
112
+ draw_one_x_label(painter, i, lc)
113
+ i = i + 1
114
+ end
115
+ end
116
+
117
+ def draw_one_x_label(painter, i, lc)
118
+ i_f = i * 1.0
119
+ xx = @chart_px + i_f * @x_step
120
+ label = @x_labels[i]
121
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
122
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
123
+ label_y = @chart_py + @chart_ph + 14.0 + ascent
124
+ painter.draw_text(label, xx - lw / 2.0, label_y, Kumiki.theme.font_family, 11.0, lc)
125
+ end
126
+
127
+ def draw_hover_line(painter)
128
+ if @hover_index >= 0
129
+ if @hover_index < @num_points
130
+ hi_f = @hover_index * 1.0
131
+ hx = @chart_px + hi_f * @x_step
132
+ hc = painter.with_alpha(Kumiki.theme.accent, 80)
133
+ bottom = @chart_py + @chart_ph
134
+ painter.draw_line(hx, @chart_py, hx, bottom, hc, 1.0)
135
+ end
136
+ end
137
+ end
138
+
139
+ def draw_all_series(painter)
140
+ si = 0
141
+ while si < @num_series
142
+ c = series_color(si)
143
+ data = @series_data[si]
144
+ draw_series_lines(painter, data, c)
145
+ if @show_points
146
+ draw_series_points(painter, data, c)
147
+ end
148
+ si = si + 1
149
+ end
150
+ end
151
+
152
+ def draw_series_lines(painter, data, c)
153
+ j = 0
154
+ while j < @num_points - 1
155
+ j_f = j * 1.0
156
+ x1 = @chart_px + j_f * @x_step
157
+ y1 = @y_scale.map(data[j])
158
+ j1_f = (j + 1) * 1.0
159
+ x2 = @chart_px + j1_f * @x_step
160
+ y2 = @y_scale.map(data[j + 1])
161
+ painter.draw_line(x1, y1, x2, y2, c, @line_width)
162
+ j = j + 1
163
+ end
164
+ end
165
+
166
+ def draw_series_points(painter, data, c)
167
+ j = 0
168
+ while j < @num_points
169
+ draw_one_point(painter, data, j, c)
170
+ j = j + 1
171
+ end
172
+ end
173
+
174
+ def draw_one_point(painter, data, j, c)
175
+ j_f = j * 1.0
176
+ xx = @chart_px + j_f * @x_step
177
+ yy = @y_scale.map(data[j])
178
+ r = @point_radius
179
+ if @hover_index == j
180
+ r = @point_radius + 2.0
181
+ end
182
+ painter.fill_circle(xx, yy, r, c)
183
+ if @hover_index == j
184
+ draw_point_label(painter, data[j], xx, yy, r)
185
+ end
186
+ end
187
+
188
+ def draw_point_label(painter, val, xx, yy, r)
189
+ vl = format_axis_value(painter, val)
190
+ vw = painter.measure_text_width(vl, Kumiki.theme.font_family, 10.0)
191
+ tc = Kumiki.theme.text_primary
192
+ painter.draw_text(vl, xx - vw / 2.0, yy - r - 4.0, Kumiki.theme.font_family, 10.0, tc)
193
+ end
194
+
195
+ def draw_line_legend(painter, px, py)
196
+ if @show_legend
197
+ if @series_names.length > 1
198
+ colors = []
199
+ si = 0
200
+ while si < @series_names.length
201
+ colors << series_color(si)
202
+ si = si + 1
203
+ end
204
+ draw_legend(painter, @series_names, colors, px + 8.0, py - 20.0)
205
+ end
206
+ end
207
+ end
208
+
209
+ def update_hover
210
+ return if @y_scale == nil
211
+ if @num_points <= 1
212
+ @hover_index = -1
213
+ return
214
+ end
215
+ mx = @mouse_x
216
+ px = plot_x
217
+ pw = plot_w
218
+ left_bound = px - @x_step / 2.0
219
+ right_bound = px + pw + @x_step / 2.0
220
+ if mx < left_bound
221
+ @hover_index = -1
222
+ return
223
+ end
224
+ if mx > right_bound
225
+ @hover_index = -1
226
+ return
227
+ end
228
+ find_nearest_point(mx, px)
229
+ end
230
+
231
+ def find_nearest_point(mx, px)
232
+ best = -1
233
+ best_dist = 999999.0
234
+ i = 0
235
+ while i < @num_points
236
+ i_f = i * 1.0
237
+ xx = px + i_f * @x_step
238
+ d = mx - xx
239
+ if d < 0.0
240
+ d = 0.0 - d
241
+ end
242
+ if d < best_dist
243
+ best_dist = d
244
+ best = i
245
+ end
246
+ i = i + 1
247
+ end
248
+ @hover_index = best
249
+ end
250
+
251
+ private
252
+
253
+ def compute_data_range
254
+ @data_min = 0.0
255
+ @data_max = 1.0
256
+ first = true
257
+ si = 0
258
+ while si < @series_data.length
259
+ ci = 0
260
+ while ci < @series_data[si].length
261
+ v = @series_data[si][ci]
262
+ if first
263
+ @data_min = v
264
+ @data_max = v
265
+ first = false
266
+ else
267
+ if v > @data_max
268
+ @data_max = v
269
+ end
270
+ if v < @data_min
271
+ @data_min = v
272
+ end
273
+ end
274
+ ci = ci + 1
275
+ end
276
+ si = si + 1
277
+ end
278
+ range = @data_max - @data_min
279
+ if range > 0.0
280
+ @data_max = @data_max + range * 0.1
281
+ @data_min = @data_min - range * 0.05
282
+ else
283
+ @data_max = @data_max + 1.0
284
+ end
285
+ end
286
+ end
287
+
288
+ def LineChart(x_labels, series_data, series_names)
289
+ LineChart.new(x_labels, series_data, series_names)
290
+ end
291
+
292
+ end