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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3b89df50fd8dd9d1670fd4e54568fdf90538dfb8a1566bbb7431826c8578f2e
4
- data.tar.gz: 52820e7aaef7a1edbead1748cc33fc50408d7dd8a7763a93db53154cf4232752
3
+ metadata.gz: '01195bf257b2d2c5d68720d762b2787e56824304a1524ddee62bd6107cd4ce87'
4
+ data.tar.gz: 55118796cfc077ee9265f35f5126ef51b3a4ec9661eca624f098194c13136bca
5
5
  SHA512:
6
- metadata.gz: 5d5ab5c841bd63fc6154b326cd8ff7cb175dd727292a6bfbd95082274f42127f850a321de30224099904064c8809228d5a063cf353cb0296eb31e675b4e227e9
7
- data.tar.gz: b690434592d74c43929567d856771528e4e31792d575682983838a4fe619fd319f0935e594f8413f2ad7904b59b4ac13ff345e665de570edcdbeb9ef354b7248
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.2.3 (Alpha)
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
  [![Gem Version](https://badge.fury.io/rb/glimmer-libui-cc-graphs_and_charts.svg)](http://badge.fury.io/rb/glimmer-libui-cc-graphs_and_charts)
4
4
  [![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](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) for [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui)
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
  ![bar chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bar-chart.png)
9
9
 
10
- ![line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative.png)
10
+ ![line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph.png)
11
+
12
+ ![bubble chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bubble-chart.png)
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.2.3'
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
  ![basic line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph.png)
176
178
 
177
- **Relative Mode:**
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
+ ![basic line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-reverse-x.png)
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
- ![basic line graph relative](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative.png)
251
+ ![basic line graph relative](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative-reverse-x.png)
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
- ![basic line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph.png)
310
+ ![basic line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-reverse-x.png)
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
- ![basic line graph relative](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative.png)
358
+ ![basic line graph relative](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative-reverse-x.png)
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
+ ![basic bubble chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bubble-chart.png)
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
+ ![basic bubble chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bubble-chart.png)
554
+
316
555
 
317
556
  Contributing to glimmer-libui-cc-graphs_and_charts
318
557
  ------------------------------------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
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
@@ -14,6 +14,7 @@ class BasicLineGraphRelative
14
14
  body {
15
15
  window('Basic Line Graph Relative', 900, 330) { |main_window|
16
16
  @line_graph = line_graph(
17
+ reverse_x: true,
17
18
  width: 900,
18
19
  height: 300,
19
20
  graph_point_distance: :width_divided_by_point_count,
@@ -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.2.3 ruby lib
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.2.3".freeze
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 = "2024-01-03"
15
- s.description = "Graphs and Charts (Custom Controls) for Glimmer DSL for LibUI, like Line Graph.".freeze
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/basic_line_graph_relative.rb",
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 - Glimmer DSL for LibUI Custom Controls".freeze
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
- clear_drawing_cache
85
- body_root.queue_redraw_all
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
- closest_point_index = ((width - graph_padding_width - x) / graph_point_distance_for_line(lines[0])).round
112
- if closest_point_index != @closest_point_index
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 { |graph_line| single_line_graph(graph_line) }
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
- closest_points = lines.map { |line| @points[line][@closest_point_index] }
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 { |line| "#{line[:name]}: #{line[:y_values][@closest_point_index]}" }
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
- # Today, we make the assumption that all lines have points along the same x-axis values
550
- # TODO In the future, we can support different x values along different lines
551
- graph_line = lines[0]
552
- graph_line[:x_value_start] - (graph_line[:x_interval_in_seconds] * x_value_index)
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.2.3
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: 2024-01-03 00:00:00.000000000 Z
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 (Custom Controls) for Glimmer DSL for LibUI, like Line
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/basic_line_graph_relative.rb
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 - Glimmer DSL for LibUI Custom Controls
121
+ summary: Graphs and Charts (Glimmer DSL for LibUI Custom Controls)
119
122
  test_files: []