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
data/lib/kumiki/app.rb ADDED
@@ -0,0 +1,381 @@
1
+ module Kumiki
2
+ # rbs_inline: enabled
3
+
4
+ # App - main application loop with event dispatching
5
+
6
+ class App
7
+ #: (RanmaFrame frame, untyped widget) -> void
8
+ def initialize(frame, widget)
9
+ @@current = self
10
+ @frame = frame
11
+ @root = widget
12
+ @downed = nil
13
+ @focused = nil
14
+ @mouse_overed = nil
15
+ @prev_abs_x = 0.0
16
+ @prev_abs_y = 0.0
17
+ @prev_rel_x = 0.0
18
+ @prev_rel_y = 0.0
19
+ @focusables = []
20
+ @prev_frame_w = 0.0
21
+ @prev_frame_h = 0.0
22
+ @cursor_x = 0.0
23
+ @cursor_y = 0.0
24
+ @animations = []
25
+ @last_frame_time = 0
26
+ end
27
+
28
+ #: () -> App?
29
+ def self.current
30
+ @@current
31
+ end
32
+
33
+ # Clear references to a widget being detached
34
+ #: (untyped w) -> void
35
+ def clear_widget_refs(w)
36
+ if @mouse_overed == w
37
+ @mouse_overed = nil
38
+ end
39
+ if @focused == w
40
+ @focused = nil
41
+ end
42
+ if @downed == w
43
+ @downed = nil
44
+ end
45
+ end
46
+
47
+ #: (untyped widget) -> void
48
+ def post_update(widget)
49
+ @frame.post_update(nil)
50
+ end
51
+
52
+ #: () -> void
53
+ def run
54
+ frame = @frame
55
+ root = @root
56
+ app = self
57
+
58
+ frame.on_redraw { |painter, completely|
59
+ # Tick animations
60
+ now = painter.current_time_millis
61
+ if @last_frame_time > 0
62
+ dt = (now - @last_frame_time).to_f
63
+ if dt > 100.0
64
+ dt = 100.0 # Cap to avoid jumps
65
+ end
66
+ any_active = false
67
+ i = 0
68
+ while i < @animations.length
69
+ still_going = @animations[i].tick(dt)
70
+ if still_going
71
+ any_active = true
72
+ end
73
+ i = i + 1
74
+ end
75
+ # Remove finished animations
76
+ new_anims = []
77
+ i = 0
78
+ while i < @animations.length
79
+ if @animations[i].animating?
80
+ new_anims << @animations[i]
81
+ end
82
+ i = i + 1
83
+ end
84
+ @animations = new_anims
85
+ if any_active
86
+ completely = true
87
+ frame.post_update(nil)
88
+ end
89
+ end
90
+ @last_frame_time = now
91
+
92
+ frame_size = frame.get_size
93
+ # Detect resize → force complete redraw
94
+ if frame_size.width != @prev_frame_w || frame_size.height != @prev_frame_h
95
+ @prev_frame_w = frame_size.width
96
+ @prev_frame_h = frame_size.height
97
+ completely = true
98
+ end
99
+
100
+ # Resize root based on size policy
101
+ rw = root.get_width_policy == EXPANDING ? frame_size.width : root.get_width
102
+ rh = root.get_height_policy == EXPANDING ? frame_size.height : root.get_height
103
+ root.resize_wh(rw, rh)
104
+ root.move_xy(0.0, 0.0)
105
+
106
+ if completely
107
+ painter.clear(Kumiki.theme.bg_canvas)
108
+ end
109
+ root.redraw(painter, completely)
110
+ }
111
+
112
+ frame.on_mouse_down { |ev|
113
+ result = root.dispatch(ev.pos)
114
+ target = result[0]
115
+ p = result[1]
116
+ if target
117
+ @prev_abs_x = ev.pos.x
118
+ @prev_abs_y = ev.pos.y
119
+ ev.pos = p
120
+ @prev_rel_x = p.x
121
+ @prev_rel_y = p.y
122
+ target.mouse_down(ev)
123
+ app.set_downed(target)
124
+ end
125
+ }
126
+
127
+ frame.on_mouse_up { |ev|
128
+ downed = app.get_downed
129
+ if downed
130
+ # Focus management: unfocus old, focus new
131
+ old_focused = app.get_focused
132
+ if old_focused != nil && old_focused != downed
133
+ old_focused.unfocused
134
+ end
135
+ app.set_focused(downed)
136
+ downed.focused
137
+
138
+ # Convert to local coordinates relative to downed widget
139
+ local_p = Point.new(ev.pos.x - downed.get_x, ev.pos.y - downed.get_y)
140
+ ev.pos = local_p
141
+ downed.mouse_up(ev)
142
+ app.set_downed(nil)
143
+ end
144
+ }
145
+
146
+ frame.on_cursor_pos { |ev|
147
+ app.set_cursor_xy(ev.pos.x, ev.pos.y)
148
+ result = root.dispatch(ev.pos)
149
+ target = result[0]
150
+ p = result[1]
151
+ downed = app.get_downed
152
+ overed = app.get_mouse_overed
153
+
154
+ if target == nil
155
+ # Cursor left all widgets
156
+ if overed != nil
157
+ overed.mouse_out
158
+ app.set_mouse_overed(nil)
159
+ end
160
+ elsif downed == nil
161
+ # No button pressed - handle hover
162
+ if overed == nil
163
+ app.set_mouse_overed(target)
164
+ target.mouse_over
165
+ elsif overed != target
166
+ overed.mouse_out
167
+ app.set_mouse_overed(target)
168
+ target.mouse_over
169
+ end
170
+ # Notify target of cursor position
171
+ if p != nil
172
+ target.cursor_pos(MouseEvent.new(p, 0))
173
+ end
174
+ else
175
+ # Button pressed - handle drag
176
+ diff_x = ev.pos.x - @prev_abs_x
177
+ diff_y = ev.pos.y - @prev_abs_y
178
+ @prev_abs_x = ev.pos.x
179
+ @prev_abs_y = ev.pos.y
180
+ drag_pos = Point.new(@prev_rel_x + diff_x, @prev_rel_y + diff_y)
181
+ @prev_rel_x = drag_pos.x
182
+ @prev_rel_y = drag_pos.y
183
+ downed.mouse_drag(MouseEvent.new(drag_pos, 0))
184
+ end
185
+ }
186
+
187
+ frame.on_mouse_wheel { |ev|
188
+ cursor = Point.new(app.get_cursor_x, app.get_cursor_y)
189
+ result = root.dispatch_to_scrollable(cursor, false)
190
+ target = result[0]
191
+ if target != nil
192
+ target.mouse_wheel(ev)
193
+ end
194
+ }
195
+
196
+ frame.on_input_char { |text|
197
+ focused = app.get_focused
198
+ focused.input_char(text) if focused
199
+ }
200
+
201
+ frame.on_input_key { |key_code, modifiers|
202
+ # Tab (key ordinal 13): cycle focus
203
+ if key_code == 13
204
+ shift = (modifiers & 1) != 0
205
+ app.cycle_focus(root, shift)
206
+ else
207
+ focused = app.get_focused
208
+ focused.input_key(key_code, modifiers) if focused
209
+ end
210
+ }
211
+
212
+ frame.on_ime_preedit { |text, sel_start, sel_end|
213
+ focused = app.get_focused
214
+ focused.ime_preedit(text, sel_start, sel_end) if focused
215
+ }
216
+
217
+ frame.run
218
+ end
219
+
220
+ # Accessors for event state (used from blocks)
221
+ #: (untyped w) -> void
222
+ def set_downed(w)
223
+ @downed = w
224
+ end
225
+
226
+ #: () -> untyped
227
+ def get_downed
228
+ @downed
229
+ end
230
+
231
+ #: (untyped w) -> void
232
+ def set_focused(w)
233
+ @focused = w
234
+ end
235
+
236
+ #: () -> untyped
237
+ def get_focused
238
+ @focused
239
+ end
240
+
241
+ #: (untyped w) -> void
242
+ def set_mouse_overed(w)
243
+ @mouse_overed = w
244
+ end
245
+
246
+ #: () -> untyped
247
+ def get_mouse_overed
248
+ @mouse_overed
249
+ end
250
+
251
+ #: (Float x, Float y) -> void
252
+ def set_cursor_xy(x, y)
253
+ @cursor_x = x
254
+ @cursor_y = y
255
+ end
256
+
257
+ #: () -> Float
258
+ def get_cursor_x
259
+ @cursor_x
260
+ end
261
+
262
+ #: () -> Float
263
+ def get_cursor_y
264
+ @cursor_y
265
+ end
266
+
267
+ # --- Animation ---
268
+
269
+ #: (untyped anim) -> void
270
+ def register_animation(anim)
271
+ i = 0
272
+ while i < @animations.length
273
+ return if @animations[i] == anim
274
+ i = i + 1
275
+ end
276
+ @animations << anim
277
+ @frame.post_update(nil)
278
+ end
279
+
280
+ #: (untyped anim) -> void
281
+ def unregister_animation(anim)
282
+ new_list = []
283
+ i = 0
284
+ while i < @animations.length
285
+ if @animations[i] != anim
286
+ new_list << @animations[i]
287
+ end
288
+ i = i + 1
289
+ end
290
+ @animations = new_list
291
+ end
292
+
293
+ # --- Clipboard ---
294
+
295
+ #: () -> String
296
+ def get_clipboard_text
297
+ @frame.get_clipboard_text
298
+ end
299
+
300
+ #: (String text) -> void
301
+ def set_clipboard_text(text)
302
+ @frame.set_clipboard_text(text)
303
+ end
304
+
305
+ # --- Text Input / IME ---
306
+
307
+ #: () -> void
308
+ def enable_text_input
309
+ @frame.enable_text_input
310
+ end
311
+
312
+ #: () -> void
313
+ def disable_text_input
314
+ @frame.disable_text_input
315
+ end
316
+
317
+ #: (Integer x, Integer y, Integer w, Integer h) -> void
318
+ def set_ime_cursor_rect(x, y, w, h)
319
+ @frame.set_ime_cursor_rect(x, y, w, h)
320
+ end
321
+
322
+ # --- Focus Management ---
323
+ # Collect focusable widgets from tree into @focusables array
324
+
325
+ #: (untyped widget) -> void
326
+ def collect_focusables_from(widget)
327
+ if widget.is_focusable
328
+ @focusables << widget
329
+ end
330
+ children = widget.get_children
331
+ i = 0
332
+ while i < children.length
333
+ collect_focusables_from(children[i])
334
+ i = i + 1
335
+ end
336
+ end
337
+
338
+ #: (untyped root, bool reverse) -> void
339
+ def cycle_focus(root, reverse)
340
+ @focusables = []
341
+ collect_focusables_from(root)
342
+ count = @focusables.length
343
+ return if count == 0
344
+
345
+ # Find current focused index
346
+ current_idx = -1
347
+ i = 0
348
+ while i < count
349
+ if @focusables[i] == @focused
350
+ current_idx = i
351
+ end
352
+ i = i + 1
353
+ end
354
+
355
+ # Compute next index
356
+ next_idx = 0
357
+ if reverse
358
+ next_idx = current_idx - 1
359
+ if next_idx < 0
360
+ next_idx = count - 1
361
+ end
362
+ else
363
+ next_idx = current_idx + 1
364
+ if next_idx >= count
365
+ next_idx = 0
366
+ end
367
+ end
368
+
369
+ new_focus = @focusables[next_idx]
370
+ old = @focused
371
+ if old != nil
372
+ old.unfocused
373
+ end
374
+ @focused = new_focus
375
+ new_focus.focused
376
+ end
377
+ # Initialize @@current
378
+ @@current = nil
379
+ end
380
+
381
+ end
data/lib/kumiki/box.rb ADDED
@@ -0,0 +1,40 @@
1
+ module Kumiki
2
+ # rbs_inline: enabled
3
+
4
+ # Box layout - overlay/stack of children (all at same position)
5
+
6
+ class Box < Layout
7
+ def initialize
8
+ super
9
+ end
10
+
11
+ #: (untyped painter) -> void
12
+ def relocate_children(painter)
13
+ i = 0
14
+ while i < @children.length
15
+ c = @children[i]
16
+ if c.get_width_policy == EXPANDING
17
+ c.resize_wh(@width, c.get_height)
18
+ end
19
+ if c.get_height_policy == EXPANDING
20
+ c.resize_wh(c.get_width, @height)
21
+ end
22
+ c.move_xy(@x, @y)
23
+ i = i + 1
24
+ end
25
+ end
26
+ end
27
+
28
+ # Top-level helper
29
+ #: (*untyped children) -> Box
30
+ def Box(*children)
31
+ box = Box.new
32
+ i = 0
33
+ while i < children.length
34
+ box.add(children[i])
35
+ i = i + 1
36
+ end
37
+ box
38
+ end
39
+
40
+ end
@@ -0,0 +1,308 @@
1
+ module Kumiki
2
+ # AreaChart - line chart with filled areas below
3
+ # Supports multiple series (stacked or overlapping), grid, hover detection
4
+
5
+ class AreaChart < 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
+ @line_width = 2.0
12
+ @fill_alpha = 80
13
+ @show_grid = true
14
+ @stacked = false
15
+ @data_min = 0.0
16
+ @data_max = 0.0
17
+ @y_scale = nil
18
+ @y_ticks = nil
19
+ @x_step = 0.0
20
+ @num_points = 0
21
+ @num_series = 0
22
+ @y_range_min = 0.0
23
+ @y_range_max = 1.0
24
+ @chart_px = 0.0
25
+ @chart_py = 0.0
26
+ @chart_pw = 0.0
27
+ @chart_ph = 0.0
28
+ compute_area_range
29
+ end
30
+
31
+ def line_width(w)
32
+ @line_width = w
33
+ self
34
+ end
35
+
36
+ def fill_alpha(a)
37
+ @fill_alpha = a
38
+ self
39
+ end
40
+
41
+ def show_grid(v)
42
+ @show_grid = v
43
+ self
44
+ end
45
+
46
+ def stacked(v)
47
+ @stacked = v
48
+ self
49
+ end
50
+
51
+ def set_data(x_labels, series_data, series_names)
52
+ @x_labels = x_labels
53
+ @series_data = series_data
54
+ @series_names = series_names
55
+ compute_area_range
56
+ mark_dirty
57
+ update
58
+ end
59
+
60
+ def render_chart(painter, px, py, pw, ph)
61
+ return if @x_labels.length == 0
62
+ return if @series_data.length == 0
63
+ @chart_px = px
64
+ @chart_py = py
65
+ @chart_pw = pw
66
+ @chart_ph = ph
67
+ @num_points = @x_labels.length
68
+ @num_series = @series_data.length
69
+ setup_area_ticks
70
+ setup_area_y_scale
71
+ setup_area_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_area_x_labels(painter)
75
+ draw_area_hover_line(painter)
76
+ draw_all_areas(painter)
77
+ draw_area_legend(painter, px, py)
78
+ end
79
+
80
+ def setup_area_ticks
81
+ @y_ticks = compute_ticks(@data_min, @data_max, 5)
82
+ @y_range_min = @data_min
83
+ @y_range_max = @data_max
84
+ if @y_ticks.length > 0
85
+ @y_range_min = @y_ticks[0]
86
+ @y_range_max = @y_ticks[@y_ticks.length - 1]
87
+ end
88
+ end
89
+
90
+ def setup_area_y_scale
91
+ bottom = @chart_py + @chart_ph
92
+ @y_scale = LinearScale.new(@y_range_min, @y_range_max, bottom, @chart_py)
93
+ end
94
+
95
+ def setup_area_x_step
96
+ @x_step = 0.0
97
+ if @num_points > 1
98
+ divisor = (@num_points - 1) * 1.0
99
+ @x_step = @chart_pw / divisor
100
+ end
101
+ end
102
+
103
+ def draw_area_x_labels(painter)
104
+ lc = Kumiki.theme.text_secondary
105
+ i = 0
106
+ while i < @num_points
107
+ i_f = i * 1.0
108
+ xx = @chart_px + i_f * @x_step
109
+ label = @x_labels[i]
110
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
111
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
112
+ label_y = @chart_py + @chart_ph + 14.0 + ascent
113
+ painter.draw_text(label, xx - lw / 2.0, label_y, Kumiki.theme.font_family, 11.0, lc)
114
+ i = i + 1
115
+ end
116
+ end
117
+
118
+ def draw_area_hover_line(painter)
119
+ if @hover_index >= 0
120
+ if @hover_index < @num_points
121
+ hi_f = @hover_index * 1.0
122
+ hx = @chart_px + hi_f * @x_step
123
+ hc = painter.with_alpha(Kumiki.theme.accent, 80)
124
+ bottom = @chart_py + @chart_ph
125
+ painter.draw_line(hx, @chart_py, hx, bottom, hc, 1.0)
126
+ end
127
+ end
128
+ end
129
+
130
+ def draw_all_areas(painter)
131
+ baseline_y = @y_scale.map(0.0)
132
+ bottom = @chart_py + @chart_ph
133
+ if baseline_y > bottom
134
+ baseline_y = bottom
135
+ end
136
+ # Draw areas back to front (last series first)
137
+ si = @num_series - 1
138
+ while si >= 0
139
+ c = series_color(si)
140
+ fill_c = painter.with_alpha(c, @fill_alpha)
141
+ data = @series_data[si]
142
+ draw_area_fill(painter, data, fill_c, baseline_y)
143
+ draw_area_line(painter, data, c)
144
+ si = si - 1
145
+ end
146
+ # Draw hover points on top
147
+ if @hover_index >= 0
148
+ if @hover_index < @num_points
149
+ draw_area_hover_points(painter)
150
+ end
151
+ end
152
+ end
153
+
154
+ def draw_area_fill(painter, data, fill_c, baseline_y)
155
+ return if @num_points < 2
156
+ # Build path: top line left-to-right, then bottom line right-to-left
157
+ painter.begin_path
158
+ # First point
159
+ x0 = @chart_px
160
+ y0 = @y_scale.map(data[0])
161
+ painter.path_move_to(x0, y0)
162
+ # Line across top
163
+ i = 1
164
+ while i < @num_points
165
+ i_f = i * 1.0
166
+ xx = @chart_px + i_f * @x_step
167
+ yy = @y_scale.map(data[i])
168
+ painter.path_line_to(xx, yy)
169
+ i = i + 1
170
+ end
171
+ # Line down to baseline at last point
172
+ last_f = (@num_points - 1) * 1.0
173
+ last_x = @chart_px + last_f * @x_step
174
+ painter.path_line_to(last_x, baseline_y)
175
+ # Line back to first x at baseline
176
+ painter.path_line_to(@chart_px, baseline_y)
177
+ painter.close_fill_path(fill_c)
178
+ end
179
+
180
+ def draw_area_line(painter, data, c)
181
+ j = 0
182
+ while j < @num_points - 1
183
+ j_f = j * 1.0
184
+ x1 = @chart_px + j_f * @x_step
185
+ y1 = @y_scale.map(data[j])
186
+ j1_f = (j + 1) * 1.0
187
+ x2 = @chart_px + j1_f * @x_step
188
+ y2 = @y_scale.map(data[j + 1])
189
+ painter.draw_line(x1, y1, x2, y2, c, @line_width)
190
+ j = j + 1
191
+ end
192
+ end
193
+
194
+ def draw_area_hover_points(painter)
195
+ si = 0
196
+ while si < @num_series
197
+ c = series_color(si)
198
+ data = @series_data[si]
199
+ if @hover_index < data.length
200
+ hi_f = @hover_index * 1.0
201
+ xx = @chart_px + hi_f * @x_step
202
+ yy = @y_scale.map(data[@hover_index])
203
+ painter.fill_circle(xx, yy, 5.0, c)
204
+ vl = painter.number_to_string(data[@hover_index])
205
+ vw = painter.measure_text_width(vl, Kumiki.theme.font_family, 10.0)
206
+ tc = Kumiki.theme.text_primary
207
+ painter.draw_text(vl, xx - vw / 2.0, yy - 8.0, Kumiki.theme.font_family, 10.0, tc)
208
+ end
209
+ si = si + 1
210
+ end
211
+ end
212
+
213
+ def draw_area_legend(painter, px, py)
214
+ if @show_legend
215
+ if @series_names.length > 1
216
+ colors = []
217
+ si = 0
218
+ while si < @series_names.length
219
+ colors << series_color(si)
220
+ si = si + 1
221
+ end
222
+ draw_legend(painter, @series_names, colors, px + 8.0, py - 20.0)
223
+ end
224
+ end
225
+ end
226
+
227
+ def update_hover
228
+ return if @y_scale == nil
229
+ if @num_points <= 1
230
+ @hover_index = -1
231
+ return
232
+ end
233
+ mx = @mouse_x
234
+ px = plot_x
235
+ pw = plot_w
236
+ left_bound = px - @x_step / 2.0
237
+ right_bound = px + pw + @x_step / 2.0
238
+ if mx < left_bound
239
+ @hover_index = -1
240
+ return
241
+ end
242
+ if mx > right_bound
243
+ @hover_index = -1
244
+ return
245
+ end
246
+ best = -1
247
+ best_dist = 999999.0
248
+ i = 0
249
+ while i < @num_points
250
+ i_f = i * 1.0
251
+ xx = px + i_f * @x_step
252
+ d = mx - xx
253
+ if d < 0.0
254
+ d = 0.0 - d
255
+ end
256
+ if d < best_dist
257
+ best_dist = d
258
+ best = i
259
+ end
260
+ i = i + 1
261
+ end
262
+ @hover_index = best
263
+ end
264
+
265
+ private
266
+
267
+ def compute_area_range
268
+ @data_min = 0.0
269
+ @data_max = 1.0
270
+ first = true
271
+ si = 0
272
+ while si < @series_data.length
273
+ ci = 0
274
+ while ci < @series_data[si].length
275
+ v = @series_data[si][ci]
276
+ if first
277
+ @data_min = v
278
+ @data_max = v
279
+ first = false
280
+ else
281
+ if v > @data_max
282
+ @data_max = v
283
+ end
284
+ if v < @data_min
285
+ @data_min = v
286
+ end
287
+ end
288
+ ci = ci + 1
289
+ end
290
+ si = si + 1
291
+ end
292
+ if @data_min > 0.0
293
+ @data_min = 0.0
294
+ end
295
+ range = @data_max - @data_min
296
+ if range > 0.0
297
+ @data_max = @data_max + range * 0.1
298
+ else
299
+ @data_max = @data_max + 1.0
300
+ end
301
+ end
302
+ end
303
+
304
+ def AreaChart(x_labels, series_data, series_names)
305
+ AreaChart.new(x_labels, series_data, series_names)
306
+ end
307
+
308
+ end