glimmer-libui-cc-graphs_and_charts 0.2.3 → 0.3.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: 05df9fa97ed359a26f750b606fabd320c0c114ab6f0f9a68806f144b0782f4c0
4
+ data.tar.gz: ca02e1bcd1c9c0997f9241a8f16718eb6e5ca72ddc0f539b052f144d92aedb8f
5
5
  SHA512:
6
- metadata.gz: 5d5ab5c841bd63fc6154b326cd8ff7cb175dd727292a6bfbd95082274f42127f850a321de30224099904064c8809228d5a063cf353cb0296eb31e675b4e227e9
7
- data.tar.gz: b690434592d74c43929567d856771528e4e31792d575682983838a4fe619fd319f0935e594f8413f2ad7904b59b4ac13ff345e665de570edcdbeb9ef354b7248
6
+ metadata.gz: 8066883e70794b6c6d4d6ebab139d97bf1c9fe01d3b7b8ed9b94fc61f8a69b5c7a82a2b89c9e2c7eaa7dab326cf40f3da2f348e7a1f2945c16604850b9bab0b9
7
+ data.tar.gz: 3dd5b11b42f701d3f30064060c6c08bb01bcb1391316ec307725b9b8a668c8052d22fffafa794555423ed15b38155924fdaf48019437177114415331488fed84
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Initial implementation of `bubble_chart` custom control
6
+ - New `examples/graphs_and_charts/basic_bubble_chart.rb`
7
+ - Ensure that dynamically setting `lines` option in `line_graph` normalizes `lines` into `Array` if value is a `Hash`
8
+
3
9
  ## 0.2.3
4
10
 
5
11
  - 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.3.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))
7
7
 
8
8
  ![bar chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bar-chart.png)
9
9
 
10
10
  ![line graph](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative.png)
11
11
 
12
+ ![bubble chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bubble-chart.png)
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.3.0'
18
20
  ```
19
21
 
20
22
  Run:
@@ -314,6 +316,202 @@ BasicLineGraphRelative.launch
314
316
 
315
317
  ![basic line graph relative](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-line-graph-relative.png)
316
318
 
319
+ ### Bubble Chart
320
+
321
+ To load the `bubble_chart` custom control, add this line to your Ruby file:
322
+
323
+ ```ruby
324
+ require 'glimmer/view/bubble_chart'
325
+ ```
326
+
327
+ 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.
328
+ 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).
329
+
330
+ `values` are a `Hash` map of `Time` x-axis values to `Hash` map of `Numeric` y-axis values to `Numeric` z-axis values.
331
+
332
+ ```ruby
333
+ bubble_chart(
334
+ width: 900,
335
+ height: 300,
336
+ chart_color_bubble: [239, 9, 9],
337
+ values: {
338
+ Time.new(2030, 12, 1, 13, 0, 0) => {
339
+ 1 => 4,
340
+ 2 => 8,
341
+ 8 => 3,
342
+ 10 => 0
343
+ },
344
+ Time.new(2030, 12, 1, 13, 0, 2) => {
345
+ 1 => 1,
346
+ 2 => 5,
347
+ 7 => 2,
348
+ 10 => 0
349
+ },
350
+ Time.new(2030, 12, 1, 13, 0, 4) => {
351
+ 1 => 2,
352
+ 2 => 3,
353
+ 4 => 4,
354
+ 10 => 0
355
+ },
356
+ Time.new(2030, 12, 1, 13, 0, 6) => {
357
+ 1 => 7,
358
+ 2 => 2,
359
+ 7 => 5,
360
+ 10 => 0
361
+ },
362
+ Time.new(2030, 12, 1, 13, 0, 8) => {
363
+ 1 => 6,
364
+ 2 => 8,
365
+ 8 => 1,
366
+ 10 => 0
367
+ },
368
+ Time.new(2030, 12, 1, 13, 0, 10) => {
369
+ 1 => 1,
370
+ 2 => 2,
371
+ 3 => 9,
372
+ 10 => 0
373
+ },
374
+ Time.new(2030, 12, 1, 13, 0, 12) => {
375
+ 1 => 5,
376
+ 2 => 12,
377
+ 5 => 17,
378
+ 10 => 0
379
+ },
380
+ Time.new(2030, 12, 1, 13, 0, 14) => {
381
+ 1 => 9,
382
+ 2 => 2,
383
+ 6 => 10,
384
+ 10 => 0
385
+ },
386
+ Time.new(2030, 12, 1, 13, 0, 16) => {
387
+ 1 => 0,
388
+ 2 => 5,
389
+ 7 => 8,
390
+ 10 => 0
391
+ },
392
+ Time.new(2030, 12, 1, 13, 0, 18) => {
393
+ 1 => 9,
394
+ 3 => 3,
395
+ 5 => 6,
396
+ 10 => 0
397
+ },
398
+ Time.new(2030, 12, 1, 13, 0, 20) => {
399
+ 2 => 2,
400
+ 4 => 4,
401
+ 7 => 7,
402
+ 10 => 0
403
+ },
404
+ },
405
+ x_value_format: -> (time) {time.strftime('%M:%S')},
406
+ )
407
+ ```
408
+
409
+ ![basic bubble chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bubble-chart.png)
410
+
411
+ Look into [lib/glimmer/view/bar_chart.rb](/lib/glimmer/view/bar_chart.rb) to learn about all supported options.
412
+
413
+ **Basic Bubble Chart Example:**
414
+
415
+ [examples/graphs_and_charts/basic_bar_chart.rb](/examples/graphs_and_charts/basic_bar_chart.rb)
416
+
417
+ ```ruby
418
+ require 'glimmer-dsl-libui'
419
+ require 'glimmer/view/bubble_chart'
420
+
421
+ class BasicBubbleChart
422
+ include Glimmer::LibUI::Application
423
+
424
+ body {
425
+ window('Basic Line Graph', 900, 300) { |main_window|
426
+ @bubble_chart = bubble_chart(
427
+ width: 900,
428
+ height: 300,
429
+ chart_color_bubble: [239, 9, 9],
430
+ values: {
431
+ Time.new(2030, 12, 1, 13, 0, 0) => {
432
+ 1 => 4,
433
+ 2 => 8,
434
+ 8 => 3,
435
+ 10 => 0
436
+ },
437
+ Time.new(2030, 12, 1, 13, 0, 2) => {
438
+ 1 => 1,
439
+ 2 => 5,
440
+ 7 => 2,
441
+ 10 => 0
442
+ },
443
+ Time.new(2030, 12, 1, 13, 0, 4) => {
444
+ 1 => 2,
445
+ 2 => 3,
446
+ 4 => 4,
447
+ 10 => 0
448
+ },
449
+ Time.new(2030, 12, 1, 13, 0, 6) => {
450
+ 1 => 7,
451
+ 2 => 2,
452
+ 7 => 5,
453
+ 10 => 0
454
+ },
455
+ Time.new(2030, 12, 1, 13, 0, 8) => {
456
+ 1 => 6,
457
+ 2 => 8,
458
+ 8 => 1,
459
+ 10 => 0
460
+ },
461
+ Time.new(2030, 12, 1, 13, 0, 10) => {
462
+ 1 => 1,
463
+ 2 => 2,
464
+ 3 => 9,
465
+ 10 => 0
466
+ },
467
+ Time.new(2030, 12, 1, 13, 0, 12) => {
468
+ 1 => 5,
469
+ 2 => 12,
470
+ 5 => 17,
471
+ 10 => 0
472
+ },
473
+ Time.new(2030, 12, 1, 13, 0, 14) => {
474
+ 1 => 9,
475
+ 2 => 2,
476
+ 6 => 10,
477
+ 10 => 0
478
+ },
479
+ Time.new(2030, 12, 1, 13, 0, 16) => {
480
+ 1 => 0,
481
+ 2 => 5,
482
+ 7 => 8,
483
+ 10 => 0
484
+ },
485
+ Time.new(2030, 12, 1, 13, 0, 18) => {
486
+ 1 => 9,
487
+ 3 => 3,
488
+ 5 => 6,
489
+ 10 => 0
490
+ },
491
+ Time.new(2030, 12, 1, 13, 0, 20) => {
492
+ 2 => 2,
493
+ 4 => 4,
494
+ 7 => 7,
495
+ 10 => 0
496
+ },
497
+ },
498
+ x_value_format: -> (time) {time.strftime('%M:%S')},
499
+ )
500
+
501
+ on_content_size_changed do
502
+ @bubble_chart.width = main_window.content_size[0]
503
+ @bubble_chart.height = main_window.content_size[1]
504
+ end
505
+ }
506
+ }
507
+ end
508
+
509
+ BasicBubbleChart.launch
510
+ ```
511
+
512
+ ![basic bubble chart](/screenshots/glimmer-libui-cc-graphs_and_charts-mac-basic-bubble-chart.png)
513
+
514
+
317
515
  Contributing to glimmer-libui-cc-graphs_and_charts
318
516
  ------------------------------------------
319
517
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.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
@@ -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.3.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.3.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 = "2024-02-17"
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,19 @@ 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
30
  "examples/graphs_and_charts/basic_line_graph_relative.rb",
30
31
  "glimmer-libui-cc-graphs_and_charts.gemspec",
31
32
  "lib/glimmer-libui-cc-graphs_and_charts.rb",
32
33
  "lib/glimmer/view/bar_chart.rb",
34
+ "lib/glimmer/view/bubble_chart.rb",
33
35
  "lib/glimmer/view/line_graph.rb"
34
36
  ]
35
37
  s.homepage = "http://github.com/AndyObtiva/glimmer-libui-cc-graphs_and_charts".freeze
36
38
  s.licenses = ["MIT".freeze]
37
39
  s.rubygems_version = "3.5.3".freeze
38
- s.summary = "Graphs and Charts - Glimmer DSL for LibUI Custom Controls".freeze
40
+ s.summary = "Graphs and Charts (Glimmer DSL for LibUI Custom Controls)".freeze
39
41
 
40
42
  s.specification_version = 4
41
43
 
@@ -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
@@ -81,8 +81,12 @@ module Glimmer
81
81
 
82
82
  after_body do
83
83
  observe(self, :lines) do
84
- clear_drawing_cache
85
- body_root.queue_redraw_all
84
+ if lines.is_a?(Hash)
85
+ self.lines = [lines]
86
+ else
87
+ clear_drawing_cache
88
+ body_root.queue_redraw_all
89
+ end
86
90
  end
87
91
  observe(self, :width) do
88
92
  clear_drawing_cache
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.3.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: 2024-02-17 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,13 @@ 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
92
  - examples/graphs_and_charts/basic_line_graph_relative.rb
92
93
  - glimmer-libui-cc-graphs_and_charts.gemspec
93
94
  - lib/glimmer-libui-cc-graphs_and_charts.rb
94
95
  - lib/glimmer/view/bar_chart.rb
96
+ - lib/glimmer/view/bubble_chart.rb
95
97
  - lib/glimmer/view/line_graph.rb
96
98
  homepage: http://github.com/AndyObtiva/glimmer-libui-cc-graphs_and_charts
97
99
  licenses:
@@ -115,5 +117,5 @@ requirements: []
115
117
  rubygems_version: 3.5.3
116
118
  signing_key:
117
119
  specification_version: 4
118
- summary: Graphs and Charts - Glimmer DSL for LibUI Custom Controls
120
+ summary: Graphs and Charts (Glimmer DSL for LibUI Custom Controls)
119
121
  test_files: []