graphina 0.0.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.
@@ -0,0 +1,459 @@
1
+ require 'term/ansicolor'
2
+ require 'tins'
3
+ require 'digest/md5'
4
+
5
+ require 'graphina/graph/formatters.rb'
6
+
7
+ # A class that provides graphical display functionality for terminal-based data
8
+ # visualization
9
+ #
10
+ # The Graph class enables the creation of dynamic, real-time visualizations of
11
+ # data values within a terminal environment. It manages the rendering of
12
+ # graphical representations such as line charts or graphs, updating them
13
+ # continuously based on provided data sources. The class handles terminal
14
+ # control operations, including cursor positioning, color management, and
15
+ # screen clearing to ensure smooth visual updates. It also supports
16
+ # configuration of display parameters like title, formatting strategies for
17
+ # values, update intervals, and color schemes for different data series.
18
+ #
19
+ # @example
20
+ # graph = Graphina::Graph.new(
21
+ # title: 'CPU Usage',
22
+ # value: ->(i) { rand(100) },
23
+ # format_value: :as_percent,
24
+ # sleep: 1,
25
+ # color: 33
26
+ # )
27
+ # graph.start
28
+ # # Starts the graphical display loop
29
+ #
30
+ # @example
31
+ # graph = Graphina::Graph.new(
32
+ # title: 'Memory Usage',
33
+ # value: ->(i) { `vm_stat`.match(/Pages free: (\d+)/)[1].to_i },
34
+ # format_value: :as_bytes,
35
+ # sleep: 2
36
+ # )
37
+ # graph.start
38
+ # # Starts a memory usage graph with custom data source and formatting
39
+ class Graphina::Graph
40
+ include Term::ANSIColor
41
+ include Graphina::Graph::Formatters
42
+
43
+ # The initialize method sets up a Graph instance by configuring its display parameters and internal state.
44
+ #
45
+ # This method configures the graph visualization with title, value provider,
46
+ # formatting options, update interval, and color settings. It initializes
47
+ # internal data structures for storing historical values and manages
48
+ # synchronization through a mutex for thread-safe operations.
49
+ #
50
+ # @param title [ String ] the title to display at the bottom of the graph
51
+ # @param value [ Proc ] a proc that takes an index and returns a numeric
52
+ # value for plotting
53
+ # @param format_value [ Proc, Symbol, nil ] formatting strategy for
54
+ # displaying values
55
+ # @param sleep [ Numeric ] time in seconds between updates
56
+ # @param color [ Object, Proc, nil ] color or proc or nil. nil
57
+ # determineicolor dynamically from the title
58
+ # @param color_secondary [ Object, nil ] secondary color or nil
59
+ # for enhanced visuals, nil derives secondary color from (primary) color
60
+ # @param adjust_brightness [ Symbol ] the method to call on the color for
61
+ # brightness adjustment (:lighten or :darken), defaults to :lighten
62
+ # @param adjust_brightness_percentage [ Float ] the percentage value to use
63
+ # for the brightness adjustment
64
+ # @param foreground_color [ Symbol ] the default text color for the display
65
+ # @param background_color [ Symbol ] the default background color for the
66
+ # display
67
+ # @param resolution [ Symbol ] the resolution mode (:single or :double) for
68
+ # the graph display, defaults to :double.
69
+ #
70
+ # @raise [ ArgumentError ] if the sleep parameter is negative
71
+ # @raise [ TypeError ] if the sleep parameter is not numeric
72
+ # @raise [ ArgumentError ] if the resolution parameter is not :single or :double
73
+ def initialize(
74
+ title:,
75
+ value: -> i { 0 },
76
+ format_value: nil,
77
+ sleep: 5,
78
+ true_coloring: true,
79
+ color: nil,
80
+ color_secondary: nil,
81
+ adjust_brightness: :lighten,
82
+ adjust_brightness_percentage: 15,
83
+ foreground_color: :white,
84
+ background_color: :black,
85
+ resolution: :double
86
+ )
87
+ @title = title
88
+ @value = value
89
+ @format_value = format_value
90
+ sleep = Float(sleep)
91
+ sleep >= 0 or raise ArgumentError, 'sleep has to be >= 0'
92
+ @sleep = sleep
93
+ @continue = false
94
+ @data = []
95
+ @color = color
96
+ @color_secondary = color_secondary
97
+ adjust_brightness = adjust_brightness.to_sym
98
+ if %i[ lighten darken ].include? adjust_brightness
99
+ @adjust_brightness = adjust_brightness
100
+ else
101
+ raise ArgumentError, 'adjust_brightness required to be either :lighten or :darken'
102
+ end
103
+ @adjust_brightness_percentage = Float(adjust_brightness_percentage)
104
+ @foreground_color = foreground_color
105
+ @background_color = background_color
106
+ resolution = resolution.to_sym
107
+ if %i[ single double ].include? resolution
108
+ @resolution = resolution
109
+ else
110
+ raise ArgumentError, 'resolution required to be either :single or :double'
111
+ end
112
+ Term::ANSIColor.true_coloring = true_coloring
113
+ @mutex = Mutex.new
114
+ end
115
+
116
+ # The start method initiates the graphical display process by setting up
117
+ # signal handlers, performing an initial terminal reset, and entering the
118
+ # main update loop
119
+ #
120
+ # This method serves as the entry point for starting the graph visualization
121
+ # functionality. It configures the necessary signal handlers for graceful
122
+ # shutdown and terminal resizing, performs an initial full reset of the
123
+ # display state, and then begins the continuous loop that updates and renders
124
+ # graphical data.
125
+ def start
126
+ install_handlers
127
+ full_reset
128
+ start_loop
129
+ end
130
+
131
+ # The stop method terminates the graphical display process by performing a
132
+ # full reset and setting the continue flag to false
133
+ #
134
+ # This method serves as the shutdown mechanism for the graph visualization
135
+ # functionality. It ensures that all display resources are properly cleaned
136
+ # up and the terminal state is restored to its original condition before
137
+ # stopping the continuous update loop.
138
+ def stop
139
+ full_reset
140
+ @continue = false
141
+ end
142
+
143
+ private
144
+
145
+ # Draws the graphical representation of the data on the display.
146
+ #
147
+ # This method renders the data as a graph using Unicode block characters (▀)
148
+ # to achieve 2px vertical resolution in terminal graphics. Each data point is
149
+ # plotted with appropriate color blending for visual appeal.
150
+ def draw_graph
151
+ y_width = data_range
152
+ color = pick_color
153
+ color_secondary = pick_secondary_color(
154
+ color,
155
+ adjust_brightness: @adjust_brightness,
156
+ adjust_brightness_percentage: @adjust_brightness_percentage
157
+ )
158
+ data.each_with_index do |value, i|
159
+ x = 1 + i + columns - data.size
160
+ y0 = ((value - data.min) * lines / y_width.to_f)
161
+ y = lines - y0.round + 1
162
+ y.upto(lines) do |iy|
163
+ if iy > y
164
+ @display.at(iy, x).on_color(color_secondary).write(' ')
165
+ else
166
+ case @resolution
167
+ when :double
168
+ fract = 1 - (y0 - y0.floor).abs
169
+ case
170
+ when (0...0.5) === fract
171
+ @display.at(iy, x).on_color(@background_color).color(color).write(?▄)
172
+ else
173
+ @display.at(iy, x).on_color(color).color(color_secondary).write(?▄)
174
+ end
175
+ else
176
+ @display.at(iy, x).on_color(color).color(color_secondary).write(' ')
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ # The data_range method calculates the range of data values by computing the
184
+ # difference between the maximum and minimum values in the data set and
185
+ # converting the result to a float
186
+ #
187
+ # @return [ Float ] the calculated range of the data values as a float
188
+ def data_range
189
+ (data.max - data.min).abs.to_f
190
+ end
191
+
192
+ # The start_loop method executes a continuous loop to update and display
193
+ # graphical data
194
+ #
195
+ # This method manages the main execution loop for rendering graphical
196
+ # representations of data values over time. It initializes display state,
197
+ # processes incoming data, calculates visual representations, and handles
198
+ # terminal updates while respecting configured timing intervals.
199
+ #
200
+ # It continuously updates the display and handles data processing in a loop
201
+ # until explicitly stopped.
202
+ def start_loop
203
+ full_reset
204
+ @counter = -1
205
+ @continue = true
206
+ while @continue
207
+ @start = Time.now
208
+ @full_reset and full_reset
209
+ perform hide_cursor
210
+
211
+ @data << @value.(@counter += 1)
212
+ @data = data.last(columns)
213
+
214
+ if data_range.zero?
215
+ @display.reset.bottom.styled(:bold).
216
+ write_centered("#@title / #{sleep_duration}").
217
+ reset.centered.styled(:italic).write_centered("no data")
218
+ perform_display_diff
219
+ sleep_now
220
+ next
221
+ end
222
+
223
+ @display.reset
224
+ draw_graph
225
+
226
+ @display.reset.bottom.styled(:bold).
227
+ write_centered("#@title #{format_value(data.last)} / #{sleep_duration}")
228
+ @display.reset.styled(:bold).
229
+ left.top.write(format_value(data.max)).
230
+ left.bottom.write(format_value(data.min))
231
+
232
+ perform_display_diff
233
+ sleep_now
234
+ end
235
+ rescue Interrupt
236
+ ensure
237
+ stop
238
+ end
239
+
240
+ def perform(*a)
241
+ print(*a)
242
+ end
243
+
244
+ # The columns method returns the number of columns in the display
245
+ #
246
+ # This method provides access to the horizontal dimension of the graphical
247
+ # display by returning the total number of columns available for rendering
248
+ # content
249
+ #
250
+ # @return [ Integer ] the number of columns (characters per line) in the display object
251
+ def columns
252
+ @display.columns
253
+ end
254
+
255
+ # The lines method returns the number of lines in the display
256
+ #
257
+ # This method provides access to the vertical dimension of the graphical
258
+ # display by returning the total number of rows available for rendering
259
+ # content
260
+ #
261
+ # @return [ Integer ] the number of lines (rows) in the display object
262
+ def lines
263
+ @display.lines
264
+ end
265
+
266
+ # The data reader method provides access to the data attribute that was set
267
+ # during object initialization.
268
+ #
269
+ # This method returns the value of the data instance variable, which
270
+ # typically contains structured information that has been processed or
271
+ # collected by the object.
272
+ #
273
+ # @return [ Array<Object>, Hash, nil ] the data value stored in the instance variable, or nil if not set
274
+ attr_reader :data
275
+
276
+ # The sleep_duration method returns a string representation of the configured
277
+ # sleep interval with the 's' suffix appended to indicate seconds.
278
+ #
279
+ # @return [ String ] a formatted string containing the sleep duration in seconds
280
+ def sleep_duration
281
+ "#{@sleep}s"
282
+ end
283
+
284
+ # The format_value method processes a given value using the configured
285
+ # formatting strategy
286
+ #
287
+ # This method applies the appropriate formatting to a value based on the
288
+ # \@format_value instance variable configuration It supports different
289
+ # formatting approaches including custom Proc objects, Symbol-based method
290
+ # calls, and default formatting
291
+ #
292
+ # @param value [ Object ] the value to be formatted according to the configured strategy
293
+ #
294
+ # @return [ String ] the formatted string representation of the input value
295
+ def format_value(value)
296
+ case @format_value
297
+ when Proc
298
+ @format_value.(value)
299
+ when Symbol
300
+ send(@format_value, value)
301
+ else
302
+ send(:as_default, value)
303
+ end
304
+ end
305
+
306
+ # The pick_color method determines and returns an ANSI color attribute based
307
+ # on the configured color setting
308
+ #
309
+ # This method evaluates the @color instance variable to decide how to select
310
+ # a color attribute. If @color is a Proc, it invokes the proc with the @title
311
+ # to determine the color. If @color is nil, it derives a color from the title
312
+ # string. Otherwise, it uses the @color value directly as an index into the
313
+ # ANSI color attributes.
314
+ #
315
+ # @return [ Term::ANSIColor::Attribute ] the selected color attribute object
316
+ def pick_color
317
+ Term::ANSIColor::Attribute[
318
+ case @color
319
+ when Proc
320
+ @color.(@title)
321
+ when nil
322
+ derive_color_from_string(@title)
323
+ else
324
+ @color
325
+ end
326
+ ]
327
+ end
328
+
329
+ # The pick_secondary_color method determines a secondary color based on a
330
+ # primary color and brightness adjustment parameters It returns the
331
+ # pre-configured secondary color if one exists, otherwise
332
+ # calculates a new color by adjusting the brightness of the primary color
333
+ #
334
+ # @param color [ Term::ANSIColor::Attribute ] the primary color attribute to
335
+ # be used as a base for calculation
336
+ # @param adjust_brightness [ Symbol ] the method to call on the color for
337
+ # brightness adjustment
338
+ # @param adjust_brightness_percentage [ Integer ] the percentage value to use
339
+ # for the brightness adjustment
340
+ # @return [ Term::ANSIColor::Attribute ] the secondary color attribute,
341
+ # either pre-configured or calculated from the primary color
342
+ def pick_secondary_color(color, adjust_brightness:, adjust_brightness_percentage:)
343
+ @color_secondary and return @color_secondary
344
+ color_primary = color.to_rgb_triple.to_hsl_triple
345
+ color_primary.send(adjust_brightness, adjust_brightness_percentage) rescue color
346
+ end
347
+
348
+ # The sleep_now method calculates and executes a sleep duration based on the
349
+ # configured sleep time and elapsed time since start
350
+ #
351
+ # This method determines how long to sleep by calculating the difference
352
+ # between the configured sleep interval and the time elapsed since the last
353
+ # operation started. If no start time is recorded, it uses the full
354
+ # configured sleep duration. The method ensures that negative sleep durations
355
+ # are not used by taking the maximum of the calculated duration and zero.
356
+ def sleep_now
357
+ duration = if @start
358
+ [ @sleep - (Time.now - @start).to_f, 0 ].max
359
+ else
360
+ @sleep
361
+ end
362
+ sleep duration
363
+ end
364
+
365
+ # The perform_display_diff method calculates and displays the difference
366
+ # between the current and previous display states to update only the changed
367
+ # portions of the terminal output
368
+ #
369
+ # This method synchronizes access to shared display resources using a mutex,
370
+ # then compares the current display with the previous state to determine what
371
+ # needs updating. It handles dimension mismatches by resetting the old
372
+ # display, computes the visual difference, and outputs only the modified
373
+ # portions to reduce terminal update overhead
374
+ #
375
+ # When the DEBUG_BYTESIZE environment variable is set, it also outputs
376
+ # debugging information about the size of the diff and the time elapsed since
377
+ # the last debug output
378
+ def perform_display_diff
379
+ @mutex.synchronize do
380
+ unless @old_display && @old_display.dimensions == @display.dimensions
381
+ @old_display = @display.dup.clear
382
+ end
383
+ diff = @display - @old_display
384
+ if ENV['DEBUG_BYTESIZE']
385
+ unless @last
386
+ STDERR.puts %w[ size duration ] * ?\t
387
+ else
388
+ STDERR.puts [ diff.size, (Time.now - @last).to_f ] * ?\t
389
+ end
390
+ @last = Time.now
391
+ end
392
+ perform diff
393
+ @display, @old_display = @old_display.clear, @display
394
+ perform move_to(lines, columns)
395
+ end
396
+ end
397
+
398
+
399
+ # The normalize_value method converts a value to its appropriate numeric
400
+ # representation
401
+ #
402
+ # This method takes an input value and normalizes it to either a Float or
403
+ # Integer type depending on its original form. If the value is already a
404
+ # Float, it is returned as-is. For all other types, the method attempts to
405
+ # convert the value to an integer using to_i
406
+ #
407
+ # @param value [ Object ] the value to be normalized
408
+ #
409
+ # @return [ Float, Integer ] the normalized numeric value as either a Float
410
+ # or Integer
411
+ def normalize_value(value)
412
+ case value
413
+ when Float
414
+ value
415
+ else
416
+ value.to_i
417
+ end
418
+ end
419
+
420
+ # The full_reset method performs a complete reset of the display and terminal
421
+ # state
422
+ #
423
+ # This method synchronizes access to shared resources using a mutex, then
424
+ # executes a series of terminal control operations to reset the terminal
425
+ # state, clear the screen, move the cursor to the home position, and make the
426
+ # cursor visible. It also initializes new display objects with the current
427
+ # terminal dimensions and updates the internal
428
+ # display state.
429
+ def full_reset
430
+ @mutex.synchronize do
431
+ perform reset, clear_screen, move_home, show_cursor
432
+ winsize = Tins::Terminal.winsize
433
+ opts = {
434
+ color: @foreground_color,
435
+ on_color: @background_color,
436
+ }
437
+ @display = Graphina::Graph::Display.new(*winsize, **opts)
438
+ @old_display = Graphina::Graph::Display.new(*winsize, **opts)
439
+ perform @display
440
+ @full_reset = false
441
+ end
442
+ end
443
+
444
+ # The install_handlers method sets up signal handlers for graceful shutdown
445
+ # and terminal resize handling
446
+ #
447
+ # This method configures two signal handlers: one for the exit hook that
448
+ # performs a full reset, and another for the SIGWINCH signal that handles
449
+ # terminal resize events by setting a flag and displaying a sleeping message
450
+ def install_handlers
451
+ at_exit { full_reset }
452
+ trap(:SIGWINCH) do
453
+ @full_reset = true
454
+ perform reset, clear_screen, move_home, 'Zzz…'
455
+ end
456
+ end
457
+ end
458
+
459
+ require 'graphina/graph/display'
@@ -0,0 +1,8 @@
1
+ module Graphina
2
+ # Graphina version
3
+ VERSION = '0.0.0'
4
+ VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
+ VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
+ VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
7
+ VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
8
+ end
data/lib/graphina.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Graphina
2
+ end
3
+ require 'graphina/version'
4
+ require 'graphina/graph'
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphina
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Florian Frank
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gem_hadar
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.8'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.8'
26
+ - !ruby/object:Gem::Dependency
27
+ name: debug
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tins
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.45'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.45'
54
+ - !ruby/object:Gem::Dependency
55
+ name: term-ansicolor
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.11'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.11'
68
+ description: |
69
+ Gem that provides terminal-based data visualization capabilities, enabling
70
+ developers to create dynamic, real-time graphical displays of numerical data
71
+ directly within terminal environments using Unicode characters and ANSI
72
+ styling.
73
+ email: flori@ping.de
74
+ executables:
75
+ - graphina
76
+ extensions: []
77
+ extra_rdoc_files:
78
+ - README.md
79
+ - lib/graphina.rb
80
+ - lib/graphina/graph.rb
81
+ - lib/graphina/graph/display.rb
82
+ - lib/graphina/graph/display/cell.rb
83
+ - lib/graphina/graph/formatters.rb
84
+ - lib/graphina/version.rb
85
+ files:
86
+ - Gemfile
87
+ - LICENSE
88
+ - README.md
89
+ - Rakefile
90
+ - VERSION
91
+ - bin/graphina
92
+ - graphina.gemspec
93
+ - lib/graphina.rb
94
+ - lib/graphina/graph.rb
95
+ - lib/graphina/graph/display.rb
96
+ - lib/graphina/graph/display/cell.rb
97
+ - lib/graphina/graph/formatters.rb
98
+ - lib/graphina/version.rb
99
+ homepage: https://github.com/flori/graphina
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ rdoc_options:
104
+ - "--title"
105
+ - |
106
+ Graphina - Gem for creating terminal-based data visualizations with real-time
107
+ graphical displays using Unicode characters and ANSI styling.
108
+ - "--main"
109
+ - README.md
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '3.1'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.7.2
124
+ specification_version: 4
125
+ summary: Gem for creating terminal-based data visualizations with real-time graphical
126
+ displays using Unicode characters and ANSI styling.
127
+ test_files: []