glimmer-libui-cc-graphs_and_charts 0.2.3 → 0.4.0
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 +12 -0
- data/README.md +247 -8
- data/VERSION +1 -1
- data/examples/graphs_and_charts/basic_bubble_chart.rb +95 -0
- data/examples/graphs_and_charts/{basic_line_graph_relative.rb → basic_line_graph_relative_reverse_x.rb} +1 -0
- data/examples/graphs_and_charts/basic_line_graph_reverse_x.rb +52 -0
- data/glimmer-libui-cc-graphs_and_charts.gemspec +9 -6
- data/lib/glimmer/view/bubble_chart.rb +579 -0
- data/lib/glimmer/view/line_graph.rb +64 -17
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '01195bf257b2d2c5d68720d762b2787e56824304a1524ddee62bd6107cd4ce87'
|
4
|
+
data.tar.gz: 55118796cfc077ee9265f35f5126ef51b3a4ec9661eca624f098194c13136bca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb736fcd7e72aba035fa0709c74d68fe5d4ac5fceb4baca02541aa8a6e65b62f4383af211457886413bac2f38852b4f31a81f99b2e70e8ae99a8abe4537ea53b
|
7
|
+
data.tar.gz: 605148fa95182c42c31c31eb7932e452ebdbc54806b1b8cbb51fbdb5f5fe1b4dd1a6021efad71623ded0eeb5a9481d4065b120bc25c7c209efd2df29cc377e57
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.4.0
|
4
|
+
|
5
|
+
- Support `reverse_x` option; when `false`, line graphs are drawn naturally from left to right, and when `true`, line graphs are drawn like before from right to left.
|
6
|
+
- Rename previous line graph examples to indicate that they have reverse_x as `true`
|
7
|
+
- Add new `examples/graphs_and_charts/basic_line_graph.rb` example that renders line graphs naturally from left to right.
|
8
|
+
|
9
|
+
## 0.3.0
|
10
|
+
|
11
|
+
- Initial implementation of `bubble_chart` custom control
|
12
|
+
- New `examples/graphs_and_charts/basic_bubble_chart.rb`
|
13
|
+
- Ensure that dynamically setting `lines` option in `line_graph` normalizes `lines` into `Array` if value is a `Hash`
|
14
|
+
|
3
15
|
## 0.2.3
|
4
16
|
|
5
17
|
- Automatically scale number of `bar_chart` horizontal grid markers so that if the chart width gets small enough for them to run into each other, less of them are displayed
|
data/README.md
CHANGED
@@ -1,20 +1,22 @@
|
|
1
|
-
# Graphs and Charts 0.
|
1
|
+
# Graphs and Charts 0.4.0 (Alpha)
|
2
2
|
## [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui) Custom Controls
|
3
3
|
[](http://badge.fury.io/rb/glimmer-libui-cc-graphs_and_charts)
|
4
4
|
[](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
5
5
|
|
6
|
-
Graphs and Charts (Custom Controls
|
6
|
+
Graphs and Charts (Custom Controls for [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui)). It is used in [Kuiq (Sidekiq UI)](https://github.com/mperham/kuiq).
|
7
7
|
|
8
8
|

|
9
9
|
|
10
|
-

|
11
|
+
|
12
|
+

|
11
13
|
|
12
14
|
## Setup
|
13
15
|
|
14
16
|
Add this line to Bundler `Gemfile`:
|
15
17
|
|
16
18
|
```ruby
|
17
|
-
gem 'glimmer-libui-cc-graphs_and_charts', '~> 0.
|
19
|
+
gem 'glimmer-libui-cc-graphs_and_charts', '~> 0.4.0'
|
18
20
|
```
|
19
21
|
|
20
22
|
Run:
|
@@ -174,12 +176,53 @@ line_graph(
|
|
174
176
|
|
175
177
|

|
176
178
|
|
177
|
-
**
|
179
|
+
**Absolute Mode (reverse x):**
|
180
|
+
|
181
|
+
It supports any `Numeric` y-axis values in addition to `Time` x-axis values.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
line_graph(
|
185
|
+
reverse_x: true,
|
186
|
+
width: 900,
|
187
|
+
height: 300,
|
188
|
+
lines: [
|
189
|
+
{
|
190
|
+
name: 'Stock 1',
|
191
|
+
stroke: [163, 40, 39, thickness: 2],
|
192
|
+
values: {
|
193
|
+
Time.new(2030, 12, 1) => 80,
|
194
|
+
Time.new(2030, 12, 2) => 36,
|
195
|
+
Time.new(2030, 12, 4) => 10,
|
196
|
+
Time.new(2030, 12, 5) => 60,
|
197
|
+
Time.new(2030, 12, 6) => 20,
|
198
|
+
},
|
199
|
+
x_value_format: -> (time) {time.strftime("%a %d %b %Y %T GMT")},
|
200
|
+
},
|
201
|
+
{
|
202
|
+
name: 'Stock 2',
|
203
|
+
stroke: [47, 109, 104, thickness: 2],
|
204
|
+
values: {
|
205
|
+
Time.new(2030, 12, 1) => 62,
|
206
|
+
Time.new(2030, 12, 2) => 0,
|
207
|
+
Time.new(2030, 12, 3) => 90,
|
208
|
+
Time.new(2030, 12, 5) => 0,
|
209
|
+
Time.new(2030, 12, 7) => 17,
|
210
|
+
},
|
211
|
+
x_value_format: -> (time) {time.strftime("%a %d %b %Y %T GMT")},
|
212
|
+
},
|
213
|
+
],
|
214
|
+
)
|
215
|
+
```
|
216
|
+
|
217
|
+

|
218
|
+
|
219
|
+
**Relative Mode (reverse x):**
|
178
220
|
|
179
221
|
Currently, it only supports `Integer` y-axis values in addition to `Time` x-axis values.
|
180
222
|
|
181
223
|
```ruby
|
182
224
|
line_graph(
|
225
|
+
reverse_x: true,
|
183
226
|
width: 900,
|
184
227
|
height: 300,
|
185
228
|
graph_point_distance: :width_divided_by_point_count,
|
@@ -205,7 +248,7 @@ line_graph(
|
|
205
248
|
)
|
206
249
|
```
|
207
250
|
|
208
|
-

|
251
|
+

|
209
252
|
|
210
253
|
Look into [lib/glimmer/view/line_graph.rb](/lib/glimmer/view/line_graph.rb) to learn about all supported options.
|
211
254
|
|
@@ -264,7 +307,7 @@ end
|
|
264
307
|
BasicLineGraph.launch
|
265
308
|
```
|
266
309
|
|
267
|
-

|
310
|
+

|
268
311
|
|
269
312
|
**Basic Line Graph Relative Example:**
|
270
313
|
|
@@ -312,7 +355,203 @@ end
|
|
312
355
|
BasicLineGraphRelative.launch
|
313
356
|
```
|
314
357
|
|
315
|
-

|
358
|
+

|
359
|
+
|
360
|
+
### Bubble Chart
|
361
|
+
|
362
|
+
To load the `bubble_chart` custom control, add this line to your Ruby file:
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
require 'glimmer/view/bubble_chart'
|
366
|
+
```
|
367
|
+
|
368
|
+
This makes the `bubble_chart` [Glimmer DSL for LibUI Custom Control](https://github.com/AndyObtiva/glimmer-dsl-libui#custom-components) available in the Glimmer GUI DSL.
|
369
|
+
You can then nest `bubble_chart` under `window` or some container like `vertical_box`. By the way, `bubble_chart` is implemented on top of the [`area` Glimmer DSL for LibUI control](https://github.com/AndyObtiva/glimmer-dsl-libui#area-api).
|
370
|
+
|
371
|
+
`values` are a `Hash` map of `Time` x-axis values to `Hash` map of `Numeric` y-axis values to `Numeric` z-axis values.
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
bubble_chart(
|
375
|
+
width: 900,
|
376
|
+
height: 300,
|
377
|
+
chart_color_bubble: [239, 9, 9],
|
378
|
+
values: {
|
379
|
+
Time.new(2030, 12, 1, 13, 0, 0) => {
|
380
|
+
1 => 4,
|
381
|
+
2 => 8,
|
382
|
+
8 => 3,
|
383
|
+
10 => 0
|
384
|
+
},
|
385
|
+
Time.new(2030, 12, 1, 13, 0, 2) => {
|
386
|
+
1 => 1,
|
387
|
+
2 => 5,
|
388
|
+
7 => 2,
|
389
|
+
10 => 0
|
390
|
+
},
|
391
|
+
Time.new(2030, 12, 1, 13, 0, 4) => {
|
392
|
+
1 => 2,
|
393
|
+
2 => 3,
|
394
|
+
4 => 4,
|
395
|
+
10 => 0
|
396
|
+
},
|
397
|
+
Time.new(2030, 12, 1, 13, 0, 6) => {
|
398
|
+
1 => 7,
|
399
|
+
2 => 2,
|
400
|
+
7 => 5,
|
401
|
+
10 => 0
|
402
|
+
},
|
403
|
+
Time.new(2030, 12, 1, 13, 0, 8) => {
|
404
|
+
1 => 6,
|
405
|
+
2 => 8,
|
406
|
+
8 => 1,
|
407
|
+
10 => 0
|
408
|
+
},
|
409
|
+
Time.new(2030, 12, 1, 13, 0, 10) => {
|
410
|
+
1 => 1,
|
411
|
+
2 => 2,
|
412
|
+
3 => 9,
|
413
|
+
10 => 0
|
414
|
+
},
|
415
|
+
Time.new(2030, 12, 1, 13, 0, 12) => {
|
416
|
+
1 => 5,
|
417
|
+
2 => 12,
|
418
|
+
5 => 17,
|
419
|
+
10 => 0
|
420
|
+
},
|
421
|
+
Time.new(2030, 12, 1, 13, 0, 14) => {
|
422
|
+
1 => 9,
|
423
|
+
2 => 2,
|
424
|
+
6 => 10,
|
425
|
+
10 => 0
|
426
|
+
},
|
427
|
+
Time.new(2030, 12, 1, 13, 0, 16) => {
|
428
|
+
1 => 0,
|
429
|
+
2 => 5,
|
430
|
+
7 => 8,
|
431
|
+
10 => 0
|
432
|
+
},
|
433
|
+
Time.new(2030, 12, 1, 13, 0, 18) => {
|
434
|
+
1 => 9,
|
435
|
+
3 => 3,
|
436
|
+
5 => 6,
|
437
|
+
10 => 0
|
438
|
+
},
|
439
|
+
Time.new(2030, 12, 1, 13, 0, 20) => {
|
440
|
+
2 => 2,
|
441
|
+
4 => 4,
|
442
|
+
7 => 7,
|
443
|
+
10 => 0
|
444
|
+
},
|
445
|
+
},
|
446
|
+
x_value_format: -> (time) {time.strftime('%M:%S')},
|
447
|
+
)
|
448
|
+
```
|
449
|
+
|
450
|
+

|
451
|
+
|
452
|
+
Look into [lib/glimmer/view/bar_chart.rb](/lib/glimmer/view/bar_chart.rb) to learn about all supported options.
|
453
|
+
|
454
|
+
**Basic Bubble Chart Example:**
|
455
|
+
|
456
|
+
[examples/graphs_and_charts/basic_bar_chart.rb](/examples/graphs_and_charts/basic_bar_chart.rb)
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
require 'glimmer-dsl-libui'
|
460
|
+
require 'glimmer/view/bubble_chart'
|
461
|
+
|
462
|
+
class BasicBubbleChart
|
463
|
+
include Glimmer::LibUI::Application
|
464
|
+
|
465
|
+
body {
|
466
|
+
window('Basic Line Graph', 900, 300) { |main_window|
|
467
|
+
@bubble_chart = bubble_chart(
|
468
|
+
width: 900,
|
469
|
+
height: 300,
|
470
|
+
chart_color_bubble: [239, 9, 9],
|
471
|
+
values: {
|
472
|
+
Time.new(2030, 12, 1, 13, 0, 0) => {
|
473
|
+
1 => 4,
|
474
|
+
2 => 8,
|
475
|
+
8 => 3,
|
476
|
+
10 => 0
|
477
|
+
},
|
478
|
+
Time.new(2030, 12, 1, 13, 0, 2) => {
|
479
|
+
1 => 1,
|
480
|
+
2 => 5,
|
481
|
+
7 => 2,
|
482
|
+
10 => 0
|
483
|
+
},
|
484
|
+
Time.new(2030, 12, 1, 13, 0, 4) => {
|
485
|
+
1 => 2,
|
486
|
+
2 => 3,
|
487
|
+
4 => 4,
|
488
|
+
10 => 0
|
489
|
+
},
|
490
|
+
Time.new(2030, 12, 1, 13, 0, 6) => {
|
491
|
+
1 => 7,
|
492
|
+
2 => 2,
|
493
|
+
7 => 5,
|
494
|
+
10 => 0
|
495
|
+
},
|
496
|
+
Time.new(2030, 12, 1, 13, 0, 8) => {
|
497
|
+
1 => 6,
|
498
|
+
2 => 8,
|
499
|
+
8 => 1,
|
500
|
+
10 => 0
|
501
|
+
},
|
502
|
+
Time.new(2030, 12, 1, 13, 0, 10) => {
|
503
|
+
1 => 1,
|
504
|
+
2 => 2,
|
505
|
+
3 => 9,
|
506
|
+
10 => 0
|
507
|
+
},
|
508
|
+
Time.new(2030, 12, 1, 13, 0, 12) => {
|
509
|
+
1 => 5,
|
510
|
+
2 => 12,
|
511
|
+
5 => 17,
|
512
|
+
10 => 0
|
513
|
+
},
|
514
|
+
Time.new(2030, 12, 1, 13, 0, 14) => {
|
515
|
+
1 => 9,
|
516
|
+
2 => 2,
|
517
|
+
6 => 10,
|
518
|
+
10 => 0
|
519
|
+
},
|
520
|
+
Time.new(2030, 12, 1, 13, 0, 16) => {
|
521
|
+
1 => 0,
|
522
|
+
2 => 5,
|
523
|
+
7 => 8,
|
524
|
+
10 => 0
|
525
|
+
},
|
526
|
+
Time.new(2030, 12, 1, 13, 0, 18) => {
|
527
|
+
1 => 9,
|
528
|
+
3 => 3,
|
529
|
+
5 => 6,
|
530
|
+
10 => 0
|
531
|
+
},
|
532
|
+
Time.new(2030, 12, 1, 13, 0, 20) => {
|
533
|
+
2 => 2,
|
534
|
+
4 => 4,
|
535
|
+
7 => 7,
|
536
|
+
10 => 0
|
537
|
+
},
|
538
|
+
},
|
539
|
+
x_value_format: -> (time) {time.strftime('%M:%S')},
|
540
|
+
)
|
541
|
+
|
542
|
+
on_content_size_changed do
|
543
|
+
@bubble_chart.width = main_window.content_size[0]
|
544
|
+
@bubble_chart.height = main_window.content_size[1]
|
545
|
+
end
|
546
|
+
}
|
547
|
+
}
|
548
|
+
end
|
549
|
+
|
550
|
+
BasicBubbleChart.launch
|
551
|
+
```
|
552
|
+
|
553
|
+

|
554
|
+
|
316
555
|
|
317
556
|
Contributing to glimmer-libui-cc-graphs_and_charts
|
318
557
|
------------------------------------------
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# This line is only needed when running the example from inside the project directory
|
2
|
+
$LOAD_PATH.prepend(File.expand_path(File.join(__dir__, '..', '..', 'lib'))) if File.exist?(File.join(__dir__, '..', '..', 'lib'))
|
3
|
+
|
4
|
+
require 'glimmer-dsl-libui'
|
5
|
+
require 'glimmer/view/bubble_chart'
|
6
|
+
|
7
|
+
class BasicBubbleChart
|
8
|
+
include Glimmer::LibUI::Application
|
9
|
+
|
10
|
+
body {
|
11
|
+
window('Basic Bubble Chart', 900, 300) { |main_window|
|
12
|
+
@bubble_chart = bubble_chart(
|
13
|
+
width: 900,
|
14
|
+
height: 300,
|
15
|
+
chart_color_bubble: [239, 9, 9],
|
16
|
+
values: {
|
17
|
+
Time.new(2030, 12, 1, 13, 0, 0) => {
|
18
|
+
1 => 4,
|
19
|
+
2 => 8,
|
20
|
+
8 => 3,
|
21
|
+
10 => 0
|
22
|
+
},
|
23
|
+
Time.new(2030, 12, 1, 13, 0, 2) => {
|
24
|
+
1 => 1,
|
25
|
+
2 => 5,
|
26
|
+
7 => 2,
|
27
|
+
10 => 0
|
28
|
+
},
|
29
|
+
Time.new(2030, 12, 1, 13, 0, 4) => {
|
30
|
+
1 => 2,
|
31
|
+
2 => 3,
|
32
|
+
4 => 4,
|
33
|
+
10 => 0
|
34
|
+
},
|
35
|
+
Time.new(2030, 12, 1, 13, 0, 6) => {
|
36
|
+
1 => 7,
|
37
|
+
2 => 2,
|
38
|
+
7 => 5,
|
39
|
+
10 => 0
|
40
|
+
},
|
41
|
+
Time.new(2030, 12, 1, 13, 0, 8) => {
|
42
|
+
1 => 6,
|
43
|
+
2 => 8,
|
44
|
+
8 => 1,
|
45
|
+
10 => 0
|
46
|
+
},
|
47
|
+
Time.new(2030, 12, 1, 13, 0, 10) => {
|
48
|
+
1 => 1,
|
49
|
+
2 => 2,
|
50
|
+
3 => 9,
|
51
|
+
10 => 0
|
52
|
+
},
|
53
|
+
Time.new(2030, 12, 1, 13, 0, 12) => {
|
54
|
+
1 => 5,
|
55
|
+
2 => 12,
|
56
|
+
5 => 17,
|
57
|
+
10 => 0
|
58
|
+
},
|
59
|
+
Time.new(2030, 12, 1, 13, 0, 14) => {
|
60
|
+
1 => 9,
|
61
|
+
2 => 2,
|
62
|
+
6 => 10,
|
63
|
+
10 => 0
|
64
|
+
},
|
65
|
+
Time.new(2030, 12, 1, 13, 0, 16) => {
|
66
|
+
1 => 0,
|
67
|
+
2 => 5,
|
68
|
+
7 => 8,
|
69
|
+
10 => 0
|
70
|
+
},
|
71
|
+
Time.new(2030, 12, 1, 13, 0, 18) => {
|
72
|
+
1 => 9,
|
73
|
+
3 => 3,
|
74
|
+
5 => 6,
|
75
|
+
10 => 0
|
76
|
+
},
|
77
|
+
Time.new(2030, 12, 1, 13, 0, 20) => {
|
78
|
+
2 => 2,
|
79
|
+
4 => 4,
|
80
|
+
7 => 7,
|
81
|
+
10 => 0
|
82
|
+
},
|
83
|
+
},
|
84
|
+
x_value_format: -> (time) {time.strftime('%M:%S')},
|
85
|
+
)
|
86
|
+
|
87
|
+
on_content_size_changed do
|
88
|
+
@bubble_chart.width = main_window.content_size[0]
|
89
|
+
@bubble_chart.height = main_window.content_size[1]
|
90
|
+
end
|
91
|
+
}
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
BasicBubbleChart.launch
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# This line is only needed when running the example from inside the project directory
|
2
|
+
$LOAD_PATH.prepend(File.expand_path(File.join(__dir__, '..', '..', 'lib'))) if File.exist?(File.join(__dir__, '..', '..', 'lib'))
|
3
|
+
|
4
|
+
require 'glimmer-dsl-libui'
|
5
|
+
require 'glimmer/view/line_graph'
|
6
|
+
|
7
|
+
class BasicLineGraph
|
8
|
+
include Glimmer::LibUI::Application
|
9
|
+
|
10
|
+
body {
|
11
|
+
window('Basic Line Graph', 900, 300) { |main_window|
|
12
|
+
@line_graph = line_graph(
|
13
|
+
reverse_x: true,
|
14
|
+
width: 900,
|
15
|
+
height: 300,
|
16
|
+
lines: [
|
17
|
+
{
|
18
|
+
name: 'Stock 1',
|
19
|
+
stroke: [163, 40, 39, thickness: 2],
|
20
|
+
values: {
|
21
|
+
Time.new(2030, 12, 1) => 2,
|
22
|
+
Time.new(2030, 12, 2) => 2.5,
|
23
|
+
Time.new(2030, 12, 4) => 1,
|
24
|
+
Time.new(2030, 12, 5) => 0,
|
25
|
+
Time.new(2030, 12, 6) => 2,
|
26
|
+
},
|
27
|
+
x_value_format: -> (time) {time.strftime("%a %d %b %Y %T GMT")},
|
28
|
+
},
|
29
|
+
{
|
30
|
+
name: 'Stock 2',
|
31
|
+
stroke: [47, 109, 104, thickness: 2],
|
32
|
+
values: {
|
33
|
+
Time.new(2030, 12, 1) => 2,
|
34
|
+
Time.new(2030, 12, 2) => 0,
|
35
|
+
Time.new(2030, 12, 3) => 2.5,
|
36
|
+
Time.new(2030, 12, 5) => 1,
|
37
|
+
Time.new(2030, 12, 7) => 2,
|
38
|
+
},
|
39
|
+
x_value_format: -> (time) {time.strftime("%a %d %b %Y %T GMT")},
|
40
|
+
},
|
41
|
+
],
|
42
|
+
)
|
43
|
+
|
44
|
+
on_content_size_changed do
|
45
|
+
@line_graph.width = main_window.content_size[0]
|
46
|
+
@line_graph.height = main_window.content_size[1]
|
47
|
+
end
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
BasicLineGraph.launch
|
@@ -2,17 +2,17 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: glimmer-libui-cc-graphs_and_charts 0.
|
5
|
+
# stub: glimmer-libui-cc-graphs_and_charts 0.4.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "glimmer-libui-cc-graphs_and_charts".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.4.0".freeze
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Andy Maleh".freeze]
|
14
|
-
s.date = "
|
15
|
-
s.description = "Graphs and Charts (
|
14
|
+
s.date = "2025-01-27"
|
15
|
+
s.description = "Graphs and Charts (Glimmer DSL for LibUI Custom Controls), like Line Graph, Bar Chart, and Bubble Chart.".freeze
|
16
16
|
s.email = "andy.am@gmail.com".freeze
|
17
17
|
s.extra_rdoc_files = [
|
18
18
|
"CHANGELOG.md",
|
@@ -25,17 +25,20 @@ Gem::Specification.new do |s|
|
|
25
25
|
"README.md",
|
26
26
|
"VERSION",
|
27
27
|
"examples/graphs_and_charts/basic_bar_chart.rb",
|
28
|
+
"examples/graphs_and_charts/basic_bubble_chart.rb",
|
28
29
|
"examples/graphs_and_charts/basic_line_graph.rb",
|
29
|
-
"examples/graphs_and_charts/
|
30
|
+
"examples/graphs_and_charts/basic_line_graph_relative_reverse_x.rb",
|
31
|
+
"examples/graphs_and_charts/basic_line_graph_reverse_x.rb",
|
30
32
|
"glimmer-libui-cc-graphs_and_charts.gemspec",
|
31
33
|
"lib/glimmer-libui-cc-graphs_and_charts.rb",
|
32
34
|
"lib/glimmer/view/bar_chart.rb",
|
35
|
+
"lib/glimmer/view/bubble_chart.rb",
|
33
36
|
"lib/glimmer/view/line_graph.rb"
|
34
37
|
]
|
35
38
|
s.homepage = "http://github.com/AndyObtiva/glimmer-libui-cc-graphs_and_charts".freeze
|
36
39
|
s.licenses = ["MIT".freeze]
|
37
40
|
s.rubygems_version = "3.5.3".freeze
|
38
|
-
s.summary = "Graphs and Charts
|
41
|
+
s.summary = "Graphs and Charts (Glimmer DSL for LibUI Custom Controls)".freeze
|
39
42
|
|
40
43
|
s.specification_version = 4
|
41
44
|
|
@@ -0,0 +1,579 @@
|
|
1
|
+
require 'glimmer-dsl-libui'
|
2
|
+
|
3
|
+
module Glimmer
|
4
|
+
module View
|
5
|
+
# General-Purpose Bubble Chart Custom Control
|
6
|
+
class BubbleChart
|
7
|
+
class << self
|
8
|
+
def interpret_color(color_object)
|
9
|
+
@color_cache ||= {}
|
10
|
+
@color_cache[color_object] ||= Glimmer::LibUI.interpret_color(color_object)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
include Glimmer::LibUI::CustomControl
|
15
|
+
|
16
|
+
DEFAULT_CHART_PADDING_WIDTH = 5.0
|
17
|
+
DEFAULT_CHART_PADDING_HEIGHT = 5.0
|
18
|
+
DEFAULT_CHART_GRID_MARKER_PADDING_WIDTH = 37.0
|
19
|
+
DEFAULT_CHART_POINT_DISTANCE = 15.0
|
20
|
+
DEFAULT_CHART_POINT_RADIUS = 1.0
|
21
|
+
DEFAULT_CHART_SELECTED_POINT_RADIUS = 3.0
|
22
|
+
|
23
|
+
DEFAULT_CHART_STROKE_GRID = [185, 184, 185]
|
24
|
+
DEFAULT_CHART_STROKE_MARKER = [185, 184, 185]
|
25
|
+
DEFAULT_CHART_STROKE_MARKER_LINE = [217, 217, 217, thickness: 1, dashes: [1, 1]]
|
26
|
+
DEFAULT_CHART_STROKE_PERIODIC_LINE = [121, 121, 121, thickness: 1, dashes: [1, 1]]
|
27
|
+
DEFAULT_CHART_STROKE_HOVER_LINE = [133, 133, 133]
|
28
|
+
|
29
|
+
DEFAULT_CHART_FILL_SELECTED_POINT = :white
|
30
|
+
|
31
|
+
DEFAULT_CHART_COLOR_BUBBLE = [92, 122, 190]
|
32
|
+
DEFAULT_CHART_COLOR_MARKER_TEXT = [96, 96, 96]
|
33
|
+
DEFAULT_CHART_COLOR_PERIOD_TEXT = [163, 40, 39]
|
34
|
+
|
35
|
+
DEFAULT_CHART_FONT_MARKER_TEXT = {family: "Arial", size: 14}
|
36
|
+
|
37
|
+
DEFAULT_CHART_STATUS_HEIGHT = 30.0
|
38
|
+
|
39
|
+
DAY_IN_SECONDS = 60*60*24
|
40
|
+
|
41
|
+
option :width, default: 600
|
42
|
+
option :height, default: 200
|
43
|
+
|
44
|
+
option :lines, default: [] # TODO remove this once conversion of code to bubble chart is complete
|
45
|
+
option :values, default: []
|
46
|
+
|
47
|
+
option :chart_padding_width, default: DEFAULT_CHART_PADDING_WIDTH
|
48
|
+
option :chart_padding_height, default: DEFAULT_CHART_PADDING_HEIGHT
|
49
|
+
option :chart_grid_marker_padding_width, default: DEFAULT_CHART_GRID_MARKER_PADDING_WIDTH
|
50
|
+
option :chart_point_distance, default: DEFAULT_CHART_POINT_DISTANCE
|
51
|
+
option :chart_point_radius, default: DEFAULT_CHART_POINT_RADIUS
|
52
|
+
option :chart_selected_point_radius, default: DEFAULT_CHART_SELECTED_POINT_RADIUS
|
53
|
+
|
54
|
+
option :chart_stroke_grid, default: DEFAULT_CHART_STROKE_GRID
|
55
|
+
option :chart_stroke_marker, default: DEFAULT_CHART_STROKE_MARKER
|
56
|
+
option :chart_stroke_marker_line, default: DEFAULT_CHART_STROKE_MARKER_LINE
|
57
|
+
option :chart_stroke_periodic_line, default: DEFAULT_CHART_STROKE_PERIODIC_LINE
|
58
|
+
option :chart_stroke_hover_line, default: DEFAULT_CHART_STROKE_HOVER_LINE
|
59
|
+
|
60
|
+
option :chart_fill_selected_point, default: DEFAULT_CHART_FILL_SELECTED_POINT
|
61
|
+
|
62
|
+
option :chart_color_bubble, default: DEFAULT_CHART_COLOR_BUBBLE
|
63
|
+
option :chart_color_marker_text, default: DEFAULT_CHART_COLOR_MARKER_TEXT
|
64
|
+
option :chart_color_period_text, default: DEFAULT_CHART_COLOR_PERIOD_TEXT
|
65
|
+
|
66
|
+
option :chart_font_marker_text, default: DEFAULT_CHART_FONT_MARKER_TEXT
|
67
|
+
|
68
|
+
option :chart_status_height, default: DEFAULT_CHART_STATUS_HEIGHT
|
69
|
+
|
70
|
+
option :display_attributes_on_hover, default: false
|
71
|
+
|
72
|
+
before_body do
|
73
|
+
generate_lines
|
74
|
+
end
|
75
|
+
|
76
|
+
after_body do
|
77
|
+
observe(self, :values) do
|
78
|
+
generate_lines
|
79
|
+
clear_drawing_cache
|
80
|
+
body_root.queue_redraw_all
|
81
|
+
end
|
82
|
+
observe(self, :width) do
|
83
|
+
clear_drawing_cache
|
84
|
+
end
|
85
|
+
observe(self, :height) do
|
86
|
+
clear_drawing_cache
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
body {
|
91
|
+
area { |chart_area|
|
92
|
+
on_draw do
|
93
|
+
calculate_dynamic_options
|
94
|
+
chart_background
|
95
|
+
grid_lines
|
96
|
+
all_bubble_charts
|
97
|
+
periodic_lines
|
98
|
+
hover_stats
|
99
|
+
end
|
100
|
+
|
101
|
+
on_mouse_moved do |event|
|
102
|
+
@hover_point = {x: event[:x], y: event[:y]}
|
103
|
+
|
104
|
+
if @hover_point && lines && lines[0] && @points && @points[lines[0]] && !@points[lines[0]].empty?
|
105
|
+
x = @hover_point[:x]
|
106
|
+
closest_point_index = ((width - chart_padding_width - x) / chart_point_distance_for_line(lines[0])).round
|
107
|
+
if closest_point_index != @closest_point_index
|
108
|
+
@closest_point_index = closest_point_index
|
109
|
+
chart_area.queue_redraw_all
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
on_mouse_exited do |outside|
|
115
|
+
if !@hover_point.nil?
|
116
|
+
@hover_point = nil
|
117
|
+
@closest_point_index = nil
|
118
|
+
chart_area.queue_redraw_all
|
119
|
+
end
|
120
|
+
end
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def generate_lines
|
127
|
+
normalized_values = []
|
128
|
+
values.each do |x_value, y_z_hash|
|
129
|
+
y_z_hash.each do |y_value, z_value|
|
130
|
+
normalized_values << {x_value: x_value, y_value: y_value, z_value: z_value}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
normalized_lines_values = []
|
134
|
+
normalized_values.each do |normalized_value|
|
135
|
+
normalized_line_values = normalized_lines_values.detect do |line|
|
136
|
+
!line.include?(normalized_value[:x_value])
|
137
|
+
end
|
138
|
+
if normalized_line_values.nil?
|
139
|
+
normalized_line_values = {}
|
140
|
+
normalized_lines_values << normalized_line_values
|
141
|
+
end
|
142
|
+
normalized_line_values[normalized_value[:x_value]] = normalized_value[:y_value]
|
143
|
+
end
|
144
|
+
self.lines = normalized_lines_values.map do |normalized_line_values|
|
145
|
+
# TODO take name from component options/constants
|
146
|
+
{name: 'Bubble Chart', values: normalized_line_values}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def clear_drawing_cache
|
151
|
+
@chart_point_distance_per_line = nil
|
152
|
+
@y_value_max_for_all_lines = nil
|
153
|
+
@x_resolution = nil
|
154
|
+
@y_resolution = nil
|
155
|
+
@x_value_range_for_all_lines = nil
|
156
|
+
@x_value_min_for_all_lines = nil
|
157
|
+
@x_value_max_for_all_lines = nil
|
158
|
+
@grid_marker_points = nil
|
159
|
+
@points = nil
|
160
|
+
@grid_marker_number_values = nil
|
161
|
+
@grid_marker_numbers = nil
|
162
|
+
@chart_stroke_marker_values = nil
|
163
|
+
@mod_values = nil
|
164
|
+
end
|
165
|
+
|
166
|
+
def calculate_dynamic_options
|
167
|
+
calculate_chart_point_distance_per_line
|
168
|
+
end
|
169
|
+
|
170
|
+
def calculate_chart_point_distance_per_line
|
171
|
+
return unless lines[0]&.[](:y_values) && chart_point_distance == :width_divided_by_point_count
|
172
|
+
|
173
|
+
@chart_point_distance_per_line ||= lines.inject({}) do |hash, line|
|
174
|
+
value = (width - 2.0*chart_padding_width - chart_grid_marker_padding_width) / (line[:y_values].size - 1).to_f
|
175
|
+
value = [value, width_drawable].min
|
176
|
+
hash.merge(line => value)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def width_drawable
|
181
|
+
width - 2.0*chart_padding_width - chart_grid_marker_padding_width
|
182
|
+
end
|
183
|
+
|
184
|
+
def height_drawable
|
185
|
+
height - 2.0*chart_padding_height
|
186
|
+
end
|
187
|
+
|
188
|
+
def chart_point_distance_for_line(line)
|
189
|
+
@chart_point_distance_per_line&.[](line) || chart_point_distance
|
190
|
+
end
|
191
|
+
|
192
|
+
def chart_background
|
193
|
+
rectangle(0, 0, width, height + (display_attributes_on_hover ? chart_status_height : 0)) {
|
194
|
+
fill 255, 255, 255
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
def grid_lines
|
199
|
+
line(chart_padding_width, chart_padding_height, chart_padding_width, height - chart_padding_height) {
|
200
|
+
stroke chart_stroke_grid
|
201
|
+
}
|
202
|
+
line(chart_padding_width, height - chart_padding_height, width - chart_padding_width, height - chart_padding_height) {
|
203
|
+
stroke chart_stroke_grid
|
204
|
+
}
|
205
|
+
grid_marker_number_font = chart_font_marker_text.merge(size: 11)
|
206
|
+
@grid_marker_number_values ||= []
|
207
|
+
@grid_marker_numbers ||= []
|
208
|
+
@chart_stroke_marker_values ||= []
|
209
|
+
@mod_values ||= []
|
210
|
+
grid_marker_points.each_with_index do |marker_point, index|
|
211
|
+
@grid_marker_number_values[index] ||= begin
|
212
|
+
value = (grid_marker_points.size - index).to_i
|
213
|
+
value = y_value_max_for_all_lines if !y_value_max_for_all_lines.nil? && y_value_max_for_all_lines.to_i != y_value_max_for_all_lines && index == 0
|
214
|
+
value
|
215
|
+
end
|
216
|
+
grid_marker_number_value = @grid_marker_number_values[index]
|
217
|
+
# TODO consider not caching the following line as that might save memory and run faster without caching
|
218
|
+
@grid_marker_numbers[index] ||= (grid_marker_number_value >= 1000) ? "#{grid_marker_number_value / 1000}K" : grid_marker_number_value.to_s
|
219
|
+
grid_marker_number = @grid_marker_numbers[index]
|
220
|
+
@chart_stroke_marker_values[index] ||= BubbleChart.interpret_color(chart_stroke_marker).tap do |color_hash|
|
221
|
+
color_hash[:thickness] = (index != grid_marker_points.size - 1 ? 2 : 1) if color_hash[:thickness].nil?
|
222
|
+
end
|
223
|
+
chart_stroke_marker_value = @chart_stroke_marker_values[index]
|
224
|
+
@mod_values[index] ||= begin
|
225
|
+
mod_value_multiplier = ((grid_marker_points.size / max_marker_count) + 1)
|
226
|
+
[((mod_value_multiplier <= 2 ? 2 : 5) * mod_value_multiplier), 1].max
|
227
|
+
end
|
228
|
+
mod_value = @mod_values[index]
|
229
|
+
comparison_value = (mod_value > 2) ? 0 : 1
|
230
|
+
if mod_value > 2
|
231
|
+
if grid_marker_number_value % mod_value == comparison_value
|
232
|
+
line(marker_point[:x], marker_point[:y], marker_point[:x] + 4, marker_point[:y]) {
|
233
|
+
stroke chart_stroke_marker_value
|
234
|
+
}
|
235
|
+
end
|
236
|
+
else
|
237
|
+
line(marker_point[:x], marker_point[:y], marker_point[:x] + 4, marker_point[:y]) {
|
238
|
+
stroke chart_stroke_marker_value
|
239
|
+
}
|
240
|
+
end
|
241
|
+
if grid_marker_number_value % mod_value == comparison_value && grid_marker_number_value != grid_marker_points.size
|
242
|
+
line(marker_point[:x], marker_point[:y], marker_point[:x] + width - chart_padding_width, marker_point[:y]) {
|
243
|
+
stroke chart_stroke_marker_line
|
244
|
+
}
|
245
|
+
end
|
246
|
+
if grid_marker_number_value % mod_value == comparison_value || grid_marker_number_value != grid_marker_number_value.to_i
|
247
|
+
grid_marker_number_width = estimate_width_of_text(grid_marker_number, grid_marker_number_font)
|
248
|
+
text(marker_point[:x] + 4 + 3, marker_point[:y] - 6, grid_marker_number_width) {
|
249
|
+
string(grid_marker_number) {
|
250
|
+
font grid_marker_number_font
|
251
|
+
color chart_color_marker_text
|
252
|
+
}
|
253
|
+
}
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def grid_marker_points
|
259
|
+
if @grid_marker_points.nil?
|
260
|
+
if lines[0]&.[](:y_values)
|
261
|
+
chart_y_max = [y_value_max_for_all_lines, 1].max
|
262
|
+
current_chart_height = (height - chart_padding_height * 2)
|
263
|
+
division_height = current_chart_height / chart_y_max
|
264
|
+
@grid_marker_points = chart_y_max.to_i.times.map do |marker_index|
|
265
|
+
x = chart_padding_width
|
266
|
+
y = chart_padding_height + marker_index * division_height
|
267
|
+
{x: x, y: y}
|
268
|
+
end
|
269
|
+
else
|
270
|
+
chart_y_max = y_value_max_for_all_lines
|
271
|
+
y_value_count = chart_y_max.ceil
|
272
|
+
@grid_marker_points = y_value_count.times.map do |marker_index|
|
273
|
+
x = chart_padding_width
|
274
|
+
y_value = y_value_count - marker_index
|
275
|
+
if marker_index == 0 && chart_y_max.ceil != chart_y_max.to_i
|
276
|
+
y_value = chart_y_max
|
277
|
+
end
|
278
|
+
scaled_y_value = y_value.to_f * y_resolution.to_f
|
279
|
+
y = height - chart_padding_height - scaled_y_value
|
280
|
+
{x: x, y: y}
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
@grid_marker_points
|
286
|
+
end
|
287
|
+
|
288
|
+
def max_marker_count
|
289
|
+
[(0.15*height).to_i, 1].max
|
290
|
+
end
|
291
|
+
|
292
|
+
def all_bubble_charts
|
293
|
+
lines.each { |chart_line| single_bubble_chart(chart_line) }
|
294
|
+
end
|
295
|
+
|
296
|
+
def single_bubble_chart(chart_line)
|
297
|
+
points = calculate_points(chart_line)
|
298
|
+
points.to_a.each do |point|
|
299
|
+
# circle(point[:x], point[:y], chart_point_radius) {
|
300
|
+
circle(point[:x], point[:y], point[:z]) {
|
301
|
+
fill chart_color_bubble
|
302
|
+
}
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def calculate_points(chart_line)
|
307
|
+
if lines[0]&.[](:y_values)
|
308
|
+
calculate_points_relative(chart_line)
|
309
|
+
else
|
310
|
+
calculate_points_absolute(chart_line)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def calculate_points_relative(chart_line)
|
315
|
+
@points ||= {}
|
316
|
+
if @points[chart_line].nil?
|
317
|
+
y_values = chart_line[:y_values] || []
|
318
|
+
y_values = y_values[0, max_visible_point_count(chart_line)]
|
319
|
+
chart_y_max = [y_value_max_for_all_lines, 1].max
|
320
|
+
points = y_values.each_with_index.map do |y_value, index|
|
321
|
+
x = width - chart_padding_width - (index * chart_point_distance_for_line(chart_line))
|
322
|
+
y = ((height - chart_padding_height) - y_value * ((height - chart_padding_height * 2) / chart_y_max))
|
323
|
+
{x: x, y: y, index: index, y_value: y_value}
|
324
|
+
end
|
325
|
+
@points[chart_line] = translate_points(chart_line, points)
|
326
|
+
end
|
327
|
+
@points[chart_line]
|
328
|
+
end
|
329
|
+
|
330
|
+
def calculate_points_absolute(chart_line)
|
331
|
+
@points ||= {}
|
332
|
+
# and then use them to produce a :z key in the hash below
|
333
|
+
if @points[chart_line].nil?
|
334
|
+
values = chart_line[:values] || []
|
335
|
+
# all points are visible when :values is supplied because we stretch the chart to show them all
|
336
|
+
chart_y_max = [y_value_max_for_all_lines, 1].max
|
337
|
+
x_value_range_for_all_lines
|
338
|
+
points = values.each_with_index.map do |(x_value, y_value), index|
|
339
|
+
z_value = self.values[x_value][y_value]
|
340
|
+
relative_x_value = x_value - x_value_min_for_all_lines
|
341
|
+
scaled_x_value = x_value_range_for_all_lines == 0 ? 0 : relative_x_value.to_f * x_resolution.to_f
|
342
|
+
scaled_y_value = y_value_max_for_all_lines == 0 ? 0 : y_value.to_f * y_resolution.to_f
|
343
|
+
x = width - chart_padding_width - scaled_x_value
|
344
|
+
y = height - chart_padding_height - scaled_y_value
|
345
|
+
# z = z_value == 0 ? z_value : z_value + 1 # TODO change 1 with magnification factor or something
|
346
|
+
z = z_value
|
347
|
+
{x: x, y: y, z: z, index: index, x_value: x_value, y_value: y_value}
|
348
|
+
end
|
349
|
+
# Translation is not supported today
|
350
|
+
# TODO consider supporting in the future
|
351
|
+
# @points[chart_line] = translate_points(chart_line, points)
|
352
|
+
@points[chart_line] = points
|
353
|
+
end
|
354
|
+
@points[chart_line]
|
355
|
+
end
|
356
|
+
|
357
|
+
# this is the multiplier that we must multiply by the relative x value
|
358
|
+
def x_resolution
|
359
|
+
@x_resolution ||= width_drawable.to_f / x_value_range_for_all_lines.to_f
|
360
|
+
end
|
361
|
+
|
362
|
+
# this is the multiplier that we must multiply by the relative y value
|
363
|
+
def y_resolution
|
364
|
+
# TODO in the future, we will use the y range, but today, we assume it starts at 0
|
365
|
+
@y_resolution ||= height_drawable.to_f / y_value_max_for_all_lines.to_f
|
366
|
+
end
|
367
|
+
|
368
|
+
def x_value_range_for_all_lines
|
369
|
+
@x_value_range_for_all_lines ||= x_value_max_for_all_lines - x_value_min_for_all_lines
|
370
|
+
end
|
371
|
+
|
372
|
+
def x_value_min_for_all_lines
|
373
|
+
if @x_value_min_for_all_lines.nil?
|
374
|
+
line_visible_x_values = lines.map { |line| line[:values].to_h.keys }
|
375
|
+
all_visible_x_values = line_visible_x_values.reduce(:+) || []
|
376
|
+
# Right now, we assume Time objects
|
377
|
+
# TODO support String representations of Time (w/ some auto-detection of format)
|
378
|
+
@x_value_min_for_all_lines = all_visible_x_values.min
|
379
|
+
end
|
380
|
+
@x_value_min_for_all_lines
|
381
|
+
end
|
382
|
+
|
383
|
+
def x_value_max_for_all_lines
|
384
|
+
if @x_value_max_for_all_lines.nil?
|
385
|
+
line_visible_x_values = lines.map { |line| line[:values].to_h.keys }
|
386
|
+
all_visible_x_values = line_visible_x_values.reduce(:+) || []
|
387
|
+
# Right now, we assume Time objects
|
388
|
+
# TODO support String representations of Time (w/ some auto-detection of format)
|
389
|
+
@x_value_max_for_all_lines = all_visible_x_values.max
|
390
|
+
end
|
391
|
+
@x_value_max_for_all_lines
|
392
|
+
end
|
393
|
+
|
394
|
+
def y_value_max_for_all_lines
|
395
|
+
if @y_value_max_for_all_lines.nil?
|
396
|
+
if lines[0]&.[](:y_values)
|
397
|
+
line_visible_y_values = lines.map { |line| line[:y_values][0, max_visible_point_count(line)] }
|
398
|
+
else
|
399
|
+
# When using :values , we always stretch the chart so that all points are visible
|
400
|
+
line_visible_y_values = lines.map { |line| line[:values].to_h.values }
|
401
|
+
end
|
402
|
+
all_visible_y_values = line_visible_y_values.reduce(:+) || []
|
403
|
+
@y_value_max_for_all_lines = all_visible_y_values.max.to_f
|
404
|
+
end
|
405
|
+
@y_value_max_for_all_lines
|
406
|
+
end
|
407
|
+
|
408
|
+
def translate_points(chart_line, points)
|
409
|
+
max_job_count_before_translation = ((width / chart_point_distance_for_line(chart_line)).to_i + 1)
|
410
|
+
x_translation = [(points.size - max_job_count_before_translation) * chart_point_distance_for_line(chart_line), 0].max
|
411
|
+
if x_translation > 0
|
412
|
+
points.each do |point|
|
413
|
+
# need to check if point[:x] is present because if the user shrinks the window, we drop points
|
414
|
+
point[:x] = point[:x] - x_translation if point[:x]
|
415
|
+
end
|
416
|
+
end
|
417
|
+
points
|
418
|
+
end
|
419
|
+
|
420
|
+
def max_visible_point_count(chart_line) = ((width - chart_grid_marker_padding_width) / chart_point_distance_for_line(chart_line)).to_i + 1
|
421
|
+
|
422
|
+
def periodic_lines
|
423
|
+
return unless lines && lines[0] && lines[0][:x_interval_in_seconds] && lines[0][:x_interval_in_seconds] == DAY_IN_SECONDS
|
424
|
+
day_count = lines[0][:y_values].size
|
425
|
+
case day_count
|
426
|
+
when ..7
|
427
|
+
@points[lines[0]].each_with_index do |point, index|
|
428
|
+
next if index == 0
|
429
|
+
|
430
|
+
line(point[:x], chart_padding_height, point[:x], height - chart_padding_height) {
|
431
|
+
stroke chart_stroke_periodic_line
|
432
|
+
}
|
433
|
+
day = calculated_x_value(point[:index]).strftime("%e")
|
434
|
+
font_size = chart_font_marker_text[:size]
|
435
|
+
text(point[:x], height - chart_padding_height - font_size*1.4, font_size*2) {
|
436
|
+
string(day) {
|
437
|
+
font chart_font_marker_text
|
438
|
+
color chart_color_period_text
|
439
|
+
}
|
440
|
+
}
|
441
|
+
end
|
442
|
+
when ..30
|
443
|
+
@points[lines[0]].each_with_index do |point, index|
|
444
|
+
day_number = index + 1
|
445
|
+
if day_number % 7 == 0
|
446
|
+
line(point[:x], chart_padding_height, point[:x], height - chart_padding_height) {
|
447
|
+
stroke chart_stroke_periodic_line
|
448
|
+
}
|
449
|
+
date = calculated_x_value(point[:index]).strftime("%b %e")
|
450
|
+
font_size = chart_font_marker_text[:size]
|
451
|
+
text(point[:x] + 4, height - chart_padding_height - font_size*1.4, font_size*6) {
|
452
|
+
string(date) {
|
453
|
+
font chart_font_marker_text
|
454
|
+
color chart_color_period_text
|
455
|
+
}
|
456
|
+
}
|
457
|
+
end
|
458
|
+
end
|
459
|
+
else
|
460
|
+
@points[lines[0]].each do |point|
|
461
|
+
if calculated_x_value(point[:index]).strftime("%d") == "01"
|
462
|
+
line(point[:x], chart_padding_height, point[:x], height - chart_padding_height) {
|
463
|
+
stroke chart_stroke_periodic_line
|
464
|
+
}
|
465
|
+
date = calculated_x_value(point[:index]).strftime("%b")
|
466
|
+
font_size = chart_font_marker_text[:size]
|
467
|
+
text(point[:x] + 4, height - chart_padding_height - font_size*1.4, font_size*6) {
|
468
|
+
string(date) {
|
469
|
+
font chart_font_marker_text
|
470
|
+
color chart_color_period_text
|
471
|
+
}
|
472
|
+
}
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def hover_stats
|
479
|
+
return unless display_attributes_on_hover && @closest_point_index
|
480
|
+
|
481
|
+
require "bigdecimal"
|
482
|
+
require "perfect_shape/point"
|
483
|
+
|
484
|
+
if @hover_point && lines && lines[0] && @points && @points[lines[0]] && !@points[lines[0]].empty?
|
485
|
+
x = @hover_point[:x]
|
486
|
+
closest_points = lines.map { |line| @points[line][@closest_point_index] }
|
487
|
+
closest_x = closest_points[0]&.[](:x)
|
488
|
+
line(closest_x, chart_padding_height, closest_x, height - chart_padding_height) {
|
489
|
+
stroke chart_stroke_hover_line
|
490
|
+
}
|
491
|
+
closest_points.each_with_index do |closest_point, index|
|
492
|
+
next unless closest_point && closest_point[:x] && closest_point[:y]
|
493
|
+
|
494
|
+
circle(closest_point[:x], closest_point[:y], chart_selected_point_radius) {
|
495
|
+
fill chart_fill_selected_point == :line_stroke ? chart_color_bubble : chart_fill_selected_point
|
496
|
+
stroke_value = chart_color_bubble.dup
|
497
|
+
stroke_value << {} unless stroke_value.last.is_a?(Hash)
|
498
|
+
stroke_value.last[:thickness] = 2
|
499
|
+
stroke stroke_value
|
500
|
+
}
|
501
|
+
end
|
502
|
+
text_label = formatted_x_value(@closest_point_index)
|
503
|
+
text_label_width = estimate_width_of_text(text_label, DEFAULT_CHART_FONT_MARKER_TEXT)
|
504
|
+
lines_with_closest_points = lines.each_with_index.map do |line, index|
|
505
|
+
next if closest_points[index].nil?
|
506
|
+
|
507
|
+
line
|
508
|
+
end.compact
|
509
|
+
closest_point_texts = lines_with_closest_points.map { |line| "#{line[:name]}: #{line[:y_values][@closest_point_index]}" }
|
510
|
+
closest_point_text_widths = closest_point_texts.map do |text|
|
511
|
+
estimate_width_of_text(text, chart_font_marker_text)
|
512
|
+
end
|
513
|
+
square_size = 12.0
|
514
|
+
square_to_label_padding = 10.0
|
515
|
+
label_padding = 10.0
|
516
|
+
text_label_x = width - chart_padding_width - text_label_width - label_padding -
|
517
|
+
(lines_with_closest_points.size*(square_size + square_to_label_padding) + (lines_with_closest_points.size - 1)*label_padding + closest_point_text_widths.sum)
|
518
|
+
text_label_y = height + chart_padding_height
|
519
|
+
|
520
|
+
text(text_label_x, text_label_y, text_label_width) {
|
521
|
+
string(text_label) {
|
522
|
+
font DEFAULT_CHART_FONT_MARKER_TEXT
|
523
|
+
color chart_color_marker_text
|
524
|
+
}
|
525
|
+
}
|
526
|
+
|
527
|
+
relative_x = text_label_x + text_label_width
|
528
|
+
lines_with_closest_points.size.times do |index|
|
529
|
+
square_x = relative_x + label_padding
|
530
|
+
|
531
|
+
square(square_x, text_label_y + 2, square_size) {
|
532
|
+
fill chart_color_bubble
|
533
|
+
}
|
534
|
+
|
535
|
+
attribute_label_x = square_x + square_size + square_to_label_padding
|
536
|
+
attribute_text = closest_point_texts[index]
|
537
|
+
attribute_text_width = closest_point_text_widths[index]
|
538
|
+
relative_x = attribute_label_x + attribute_text_width
|
539
|
+
|
540
|
+
text(attribute_label_x, text_label_y, attribute_text_width) {
|
541
|
+
string(attribute_text) {
|
542
|
+
font chart_font_marker_text
|
543
|
+
color chart_color_marker_text
|
544
|
+
}
|
545
|
+
}
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def formatted_x_value(x_value_index)
|
551
|
+
# Today, we make the assumption that all lines have points along the same x-axis values
|
552
|
+
# TODO In the future, we can support different x values along different lines
|
553
|
+
chart_line = lines[0]
|
554
|
+
x_value_format = chart_line[:x_value_format] || :to_s
|
555
|
+
x_value = calculated_x_value(x_value_index)
|
556
|
+
if (x_value_format.is_a?(Symbol) || x_value_format.is_a?(String))
|
557
|
+
x_value.send(x_value_format)
|
558
|
+
else
|
559
|
+
x_value_format.call(x_value)
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def calculated_x_value(x_value_index)
|
564
|
+
# Today, we make the assumption that all lines have points along the same x-axis values
|
565
|
+
# TODO In the future, we can support different x values along different lines
|
566
|
+
chart_line = lines[0]
|
567
|
+
chart_line[:x_value_start] - (chart_line[:x_interval_in_seconds] * x_value_index)
|
568
|
+
end
|
569
|
+
|
570
|
+
def estimate_width_of_text(text_string, font_properties)
|
571
|
+
return 0 if text_string.to_s.empty?
|
572
|
+
font_size = font_properties[:size] || 16
|
573
|
+
estimated_font_width = 0.63 * font_size
|
574
|
+
text_string.chars.size * estimated_font_width
|
575
|
+
end
|
576
|
+
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
@@ -74,6 +74,7 @@ module Glimmer
|
|
74
74
|
option :graph_status_height, default: DEFAULT_GRAPH_STATUS_HEIGHT
|
75
75
|
|
76
76
|
option :display_attributes_on_hover, default: false
|
77
|
+
option :reverse_x, default: false
|
77
78
|
|
78
79
|
before_body do
|
79
80
|
self.lines = [lines] if lines.is_a?(Hash)
|
@@ -81,8 +82,12 @@ module Glimmer
|
|
81
82
|
|
82
83
|
after_body do
|
83
84
|
observe(self, :lines) do
|
84
|
-
|
85
|
-
|
85
|
+
if lines.is_a?(Hash)
|
86
|
+
self.lines = [lines]
|
87
|
+
else
|
88
|
+
clear_drawing_cache
|
89
|
+
body_root.queue_redraw_all
|
90
|
+
end
|
86
91
|
end
|
87
92
|
observe(self, :width) do
|
88
93
|
clear_drawing_cache
|
@@ -108,8 +113,13 @@ module Glimmer
|
|
108
113
|
|
109
114
|
if @hover_point && lines && lines[0] && @points && @points[lines[0]] && !@points[lines[0]].empty?
|
110
115
|
x = @hover_point[:x]
|
111
|
-
|
112
|
-
|
116
|
+
if lines[0][:x_interval_in_seconds]
|
117
|
+
closest_point_index = ((width - graph_padding_width - x) / graph_point_distance_for_line(lines[0])).round
|
118
|
+
else
|
119
|
+
closest_point_index = :absolute
|
120
|
+
end
|
121
|
+
if closest_point_index == :absolute || closest_point_index != @closest_point_index
|
122
|
+
# TODO look into optimizing this for absolute mode
|
113
123
|
@closest_point_index = closest_point_index
|
114
124
|
graph_area.queue_redraw_all
|
115
125
|
end
|
@@ -271,12 +281,14 @@ module Glimmer
|
|
271
281
|
end
|
272
282
|
|
273
283
|
def all_line_graphs
|
274
|
-
lines.each
|
284
|
+
lines.each(&method(:single_line_graph))
|
275
285
|
end
|
276
286
|
|
277
287
|
def single_line_graph(graph_line)
|
278
288
|
last_point = nil
|
279
289
|
points = calculate_points(graph_line)
|
290
|
+
# points are already calculated as reversed before here, so here we reverse again if needed
|
291
|
+
points = reverse_x_in_points(points) if !reverse_x
|
280
292
|
points.to_a.each do |point|
|
281
293
|
if last_point
|
282
294
|
line(last_point[:x], last_point[:y], point[:x], point[:y]) {
|
@@ -339,6 +351,22 @@ module Glimmer
|
|
339
351
|
@points[graph_line]
|
340
352
|
end
|
341
353
|
|
354
|
+
def reverse_x_in_points(points)
|
355
|
+
# TODO look into optimizing operations below by not iterating 3 times (perhaps one iteration could do everything)
|
356
|
+
points = points.map do |point|
|
357
|
+
point.merge(x: width_drawable.to_f - point[:x])
|
358
|
+
end
|
359
|
+
min_point = points.min_by {|point| point[:x]}
|
360
|
+
if min_point[:x] < 0
|
361
|
+
points.each do |point|
|
362
|
+
point[:x] = point[:x] - min_point[:x]
|
363
|
+
end
|
364
|
+
end
|
365
|
+
points.each do |point|
|
366
|
+
point[:x] = point[:x] + graph_padding_width.to_f
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
342
370
|
# this is the multiplier that we must multiply by the relative x value
|
343
371
|
def x_resolution
|
344
372
|
@x_resolution ||= width_drawable.to_f / x_value_range_for_all_lines.to_f
|
@@ -468,7 +496,19 @@ module Glimmer
|
|
468
496
|
|
469
497
|
if @hover_point && lines && lines[0] && @points && @points[lines[0]] && !@points[lines[0]].empty?
|
470
498
|
x = @hover_point[:x]
|
471
|
-
|
499
|
+
if @closest_point_index == :absolute # used in absolute mode
|
500
|
+
# TODO this is making a wrong assumption that there will be a point for every line
|
501
|
+
# some lines might end up with no points, so we need to filter them out
|
502
|
+
# we should start with the point across all lines that is closest to the mouse hover point
|
503
|
+
# and then pick up points that match its X value
|
504
|
+
closest_points = lines.map do |line|
|
505
|
+
line_points = @points[line]
|
506
|
+
point_distances_from_hover_point = line_points.map { |point| [point, PerfectShape::Point.point_distance(point[:x], point[:y], @hover_point[:x], @hover_point[:y])] }
|
507
|
+
closest_point = point_distances_from_hover_point.min_by(&:last).first
|
508
|
+
end
|
509
|
+
else
|
510
|
+
closest_points = lines.map { |line| @points[line][@closest_point_index] }
|
511
|
+
end
|
472
512
|
closest_x = closest_points[0]&.[](:x)
|
473
513
|
line(closest_x, graph_padding_height, closest_x, height - graph_padding_height) {
|
474
514
|
stroke graph_stroke_hover_line
|
@@ -484,14 +524,21 @@ module Glimmer
|
|
484
524
|
stroke stroke_value
|
485
525
|
}
|
486
526
|
end
|
487
|
-
text_label = formatted_x_value(@closest_point_index)
|
527
|
+
text_label = formatted_x_value(@closest_point_index, closest_points)
|
488
528
|
text_label_width = estimate_width_of_text(text_label, DEFAULT_GRAPH_FONT_MARKER_TEXT)
|
489
529
|
lines_with_closest_points = lines.each_with_index.map do |line, index|
|
490
530
|
next if closest_points[index].nil?
|
491
531
|
|
492
532
|
line
|
493
533
|
end.compact
|
494
|
-
closest_point_texts = lines_with_closest_points.map
|
534
|
+
closest_point_texts = lines_with_closest_points.each_with_index.map do |line, index|
|
535
|
+
if @closest_point_index == :absolute
|
536
|
+
line_point = closest_points[index]
|
537
|
+
"#{line[:name]}: #{line_point[:y_value]}"
|
538
|
+
else
|
539
|
+
"#{line[:name]}: #{line[:y_values][@closest_point_index]}"
|
540
|
+
end
|
541
|
+
end
|
495
542
|
closest_point_text_widths = closest_point_texts.map do |text|
|
496
543
|
estimate_width_of_text(text, graph_font_marker_text)
|
497
544
|
end
|
@@ -532,12 +579,10 @@ module Glimmer
|
|
532
579
|
end
|
533
580
|
end
|
534
581
|
|
535
|
-
def formatted_x_value(x_value_index)
|
536
|
-
# Today, we make the assumption that all lines have points along the same x-axis values
|
537
|
-
# TODO In the future, we can support different x values along different lines
|
582
|
+
def formatted_x_value(x_value_index, closest_points)
|
538
583
|
graph_line = lines[0]
|
539
584
|
x_value_format = graph_line[:x_value_format] || :to_s
|
540
|
-
x_value = calculated_x_value(x_value_index)
|
585
|
+
x_value = calculated_x_value(x_value_index, closest_points)
|
541
586
|
if (x_value_format.is_a?(Symbol) || x_value_format.is_a?(String))
|
542
587
|
x_value.send(x_value_format)
|
543
588
|
else
|
@@ -545,11 +590,13 @@ module Glimmer
|
|
545
590
|
end
|
546
591
|
end
|
547
592
|
|
548
|
-
def calculated_x_value(x_value_index)
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
593
|
+
def calculated_x_value(x_value_index, closest_points = nil)
|
594
|
+
if x_value_index == :absolute # absolute mode
|
595
|
+
closest_points.first[:x_value]
|
596
|
+
else # relative mode
|
597
|
+
graph_line = lines[0]
|
598
|
+
graph_line[:x_value_start] - (graph_line[:x_interval_in_seconds] * x_value_index)
|
599
|
+
end
|
553
600
|
end
|
554
601
|
|
555
602
|
def estimate_width_of_text(text_string, font_properties)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: glimmer-libui-cc-graphs_and_charts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: glimmer-dsl-libui
|
@@ -72,8 +72,8 @@ dependencies:
|
|
72
72
|
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '0'
|
75
|
-
description: Graphs and Charts (
|
76
|
-
Graph.
|
75
|
+
description: Graphs and Charts (Glimmer DSL for LibUI Custom Controls), like Line
|
76
|
+
Graph, Bar Chart, and Bubble Chart.
|
77
77
|
email: andy.am@gmail.com
|
78
78
|
executables: []
|
79
79
|
extensions: []
|
@@ -87,11 +87,14 @@ files:
|
|
87
87
|
- README.md
|
88
88
|
- VERSION
|
89
89
|
- examples/graphs_and_charts/basic_bar_chart.rb
|
90
|
+
- examples/graphs_and_charts/basic_bubble_chart.rb
|
90
91
|
- examples/graphs_and_charts/basic_line_graph.rb
|
91
|
-
- examples/graphs_and_charts/
|
92
|
+
- examples/graphs_and_charts/basic_line_graph_relative_reverse_x.rb
|
93
|
+
- examples/graphs_and_charts/basic_line_graph_reverse_x.rb
|
92
94
|
- glimmer-libui-cc-graphs_and_charts.gemspec
|
93
95
|
- lib/glimmer-libui-cc-graphs_and_charts.rb
|
94
96
|
- lib/glimmer/view/bar_chart.rb
|
97
|
+
- lib/glimmer/view/bubble_chart.rb
|
95
98
|
- lib/glimmer/view/line_graph.rb
|
96
99
|
homepage: http://github.com/AndyObtiva/glimmer-libui-cc-graphs_and_charts
|
97
100
|
licenses:
|
@@ -115,5 +118,5 @@ requirements: []
|
|
115
118
|
rubygems_version: 3.5.3
|
116
119
|
signing_key:
|
117
120
|
specification_version: 4
|
118
|
-
summary: Graphs and Charts
|
121
|
+
summary: Graphs and Charts (Glimmer DSL for LibUI Custom Controls)
|
119
122
|
test_files: []
|