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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +256 -0
- data/lib/kumiki/animation/animated_state.rb +83 -0
- data/lib/kumiki/animation/easing.rb +62 -0
- data/lib/kumiki/animation/value_tween.rb +69 -0
- data/lib/kumiki/app.rb +381 -0
- data/lib/kumiki/box.rb +40 -0
- data/lib/kumiki/chart/area_chart.rb +308 -0
- data/lib/kumiki/chart/bar_chart.rb +291 -0
- data/lib/kumiki/chart/base_chart.rb +213 -0
- data/lib/kumiki/chart/chart_helpers.rb +74 -0
- data/lib/kumiki/chart/gauge_chart.rb +174 -0
- data/lib/kumiki/chart/heatmap_chart.rb +223 -0
- data/lib/kumiki/chart/line_chart.rb +292 -0
- data/lib/kumiki/chart/pie_chart.rb +222 -0
- data/lib/kumiki/chart/scales.rb +79 -0
- data/lib/kumiki/chart/scatter_chart.rb +306 -0
- data/lib/kumiki/chart/stacked_bar_chart.rb +279 -0
- data/lib/kumiki/column.rb +351 -0
- data/lib/kumiki/core.rb +2511 -0
- data/lib/kumiki/dsl.rb +408 -0
- data/lib/kumiki/frame_ranma.rb +570 -0
- data/lib/kumiki/markdown/ast.rb +127 -0
- data/lib/kumiki/markdown/mermaid/layout.rb +389 -0
- data/lib/kumiki/markdown/mermaid/models.rb +235 -0
- data/lib/kumiki/markdown/mermaid/parser.rb +522 -0
- data/lib/kumiki/markdown/mermaid/renderer.rb +339 -0
- data/lib/kumiki/markdown/parser.rb +808 -0
- data/lib/kumiki/markdown/renderer.rb +642 -0
- data/lib/kumiki/markdown/theme.rb +168 -0
- data/lib/kumiki/render_node.rb +262 -0
- data/lib/kumiki/row.rb +288 -0
- data/lib/kumiki/spacer.rb +20 -0
- data/lib/kumiki/style.rb +799 -0
- data/lib/kumiki/theme.rb +567 -0
- data/lib/kumiki/themes/material.rb +40 -0
- data/lib/kumiki/themes/tokyo_night.rb +11 -0
- data/lib/kumiki/version.rb +5 -0
- data/lib/kumiki/widgets/button.rb +105 -0
- data/lib/kumiki/widgets/calendar.rb +1028 -0
- data/lib/kumiki/widgets/checkbox.rb +119 -0
- data/lib/kumiki/widgets/container.rb +111 -0
- data/lib/kumiki/widgets/data_table.rb +670 -0
- data/lib/kumiki/widgets/divider.rb +31 -0
- data/lib/kumiki/widgets/image.rb +105 -0
- data/lib/kumiki/widgets/input.rb +485 -0
- data/lib/kumiki/widgets/markdown.rb +58 -0
- data/lib/kumiki/widgets/modal.rb +165 -0
- data/lib/kumiki/widgets/multiline_input.rb +970 -0
- data/lib/kumiki/widgets/multiline_text.rb +180 -0
- data/lib/kumiki/widgets/net_image.rb +100 -0
- data/lib/kumiki/widgets/progress_bar.rb +72 -0
- data/lib/kumiki/widgets/radio_buttons.rb +93 -0
- data/lib/kumiki/widgets/slider.rb +135 -0
- data/lib/kumiki/widgets/switch.rb +84 -0
- data/lib/kumiki/widgets/tabs.rb +175 -0
- data/lib/kumiki/widgets/text.rb +120 -0
- data/lib/kumiki/widgets/tree.rb +434 -0
- data/lib/kumiki/widgets/webview.rb +87 -0
- data/lib/kumiki.rb +130 -0
- 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
|