Class: Graphina::Graph

Inherits:
Object
  • Object
show all
Includes:
Formatters, Term::ANSIColor
Defined in:
lib/graphina/graph.rb,
lib/graphina/graph/display.rb,
lib/graphina/graph/formatters.rb

Overview

A class that provides graphical display functionality for terminal-based data visualization

The Graph class enables the creation of dynamic, real-time visualizations of data values within a terminal environment. It manages the rendering of graphical representations such as line charts or graphs, updating them continuously based on provided data sources. The class handles terminal control operations, including cursor positioning, color management, and screen clearing to ensure smooth visual updates. It also supports configuration of display parameters like title, formatting strategies for values, update intervals, and color schemes for different data series.

Examples:

graph = Graphina::Graph.new(
  title: 'CPU Usage',
  value: ->(i) { rand(100) },
  format_value: :as_percent,
  sleep: 1,
  color: 33
)
graph.start
# Starts the graphical display loop
graph = Graphina::Graph.new(
  title: 'Memory Usage',
  value: ->(i) { `vm_stat`.match(/Pages free: (\d+)/)[1].to_i },
  format_value: :as_bytes,
  sleep: 2
)
graph.start
# Starts a memory usage graph with custom data source and formatting

Defined Under Namespace

Modules: Formatters Classes: Display

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Formatters

#as_bytes, #as_celsius, #as_default, #as_hertz, #as_percent, #derive_color_from_string

Constructor Details

#initialize(title:, value: -> i { 0 }, format_value: nil, sleep: 5, true_coloring: true, color: nil, color_secondary: nil, adjust_brightness: :lighten, adjust_brightness_percentage: 15, foreground_color: :white, background_color: :black, resolution: :double) ⇒ Graph

The initialize method sets up a Graph instance by configuring its display parameters and internal state.

This method configures the graph visualization with title, value provider, formatting options, update interval, and color settings. It initializes internal data structures for storing historical values and manages synchronization through a mutex for thread-safe operations.

Parameters:

  • title (String)

    the title to display at the bottom of the graph

  • value (Proc) (defaults to: -> i { 0 })

    a proc that takes an index and returns a numeric value for plotting

  • format_value (Proc, Symbol, nil) (defaults to: nil)

    formatting strategy for displaying values

  • sleep (Numeric) (defaults to: 5)

    time in seconds between updates

  • color (Object, Proc, nil) (defaults to: nil)

    color or proc or nil. nil determineicolor dynamically from the title

  • color_secondary (Object, nil) (defaults to: nil)

    secondary color or nil for enhanced visuals, nil derives secondary color from (primary) color

  • adjust_brightness (Symbol) (defaults to: :lighten)

    the method to call on the color for brightness adjustment (:lighten or :darken), defaults to :lighten

  • adjust_brightness_percentage (Float) (defaults to: 15)

    the percentage value to use for the brightness adjustment

  • foreground_color (Symbol) (defaults to: :white)

    the default text color for the display

  • background_color (Symbol) (defaults to: :black)

    the default background color for the display

  • resolution (Symbol) (defaults to: :double)

    the resolution mode (:single or :double) for the graph display, defaults to :double.

Raises:

  • (ArgumentError)

    if the sleep parameter is negative

  • (TypeError)

    if the sleep parameter is not numeric

  • (ArgumentError)

    if the resolution parameter is not :single or :double



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/graphina/graph.rb', line 73

def initialize(
  title:,
  value:                        -> i { 0 },
  format_value:                 nil,
  sleep:                        5,
  true_coloring:                true,
  color:                        nil,
  color_secondary:              nil,
  adjust_brightness:            :lighten,
  adjust_brightness_percentage: 15,
  foreground_color:             :white,
  background_color:             :black,
  resolution:                   :double
)
  @title                        = title
  @value                        = value
  @format_value                 = format_value
  sleep = Float(sleep)
  sleep >= 0 or raise ArgumentError, 'sleep has to be >= 0'
  @sleep                        = sleep
  @continue                     = false
  @data                         = []
  @color                        = color
  @color_secondary              = color_secondary
  adjust_brightness             = adjust_brightness.to_sym
  if %i[ lighten darken ].include? adjust_brightness
    @adjust_brightness          = adjust_brightness
  else
    raise ArgumentError, 'adjust_brightness required to be either :lighten or :darken'
  end
  @adjust_brightness_percentage = Float(adjust_brightness_percentage)
  @foreground_color             = foreground_color
  @background_color             = background_color
  resolution                    = resolution.to_sym
  if %i[ single double ].include? resolution
    @resolution                 = resolution
  else
    raise ArgumentError, 'resolution required to be either :single or :double'
  end
  Term::ANSIColor.true_coloring = true_coloring
  @mutex                        = Mutex.new
end

Instance Attribute Details

#dataArray<Object>, ... (readonly, private)

The data reader method provides access to the data attribute that was set during object initialization.

This method returns the value of the data instance variable, which typically contains structured information that has been processed or collected by the object.

Returns:

  • (Array<Object>, Hash, nil)

    the data value stored in the instance variable, or nil if not set



274
275
276
# File 'lib/graphina/graph.rb', line 274

def data
  @data
end

Instance Method Details

#columnsInteger (private)

The columns method returns the number of columns in the display

This method provides access to the horizontal dimension of the graphical display by returning the total number of columns available for rendering content

Returns:

  • (Integer)

    the number of columns (characters per line) in the display object



251
252
253
# File 'lib/graphina/graph.rb', line 251

def columns
  @display.columns
end

#data_rangeFloat (private)

The data_range method calculates the range of data values by computing the difference between the maximum and minimum values in the data set and converting the result to a float

Returns:

  • (Float)

    the calculated range of the data values as a float



188
189
190
# File 'lib/graphina/graph.rb', line 188

def data_range
  (data.max - data.min).abs.to_f
end

#draw_graphObject (private)

Draws the graphical representation of the data on the display.

This method renders the data as a graph using Unicode block characters (▀) to achieve 2px vertical resolution in terminal graphics. Each data point is plotted with appropriate color blending for visual appeal.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/graphina/graph.rb', line 150

def draw_graph
  y_width         = data_range
  color           = pick_color
  color_secondary = pick_secondary_color(
    color,
    adjust_brightness:            @adjust_brightness,
    adjust_brightness_percentage: @adjust_brightness_percentage
  )
  data.each_with_index do |value, i|
    x  = 1 + i + columns - data.size
    y0 = ((value - data.min) * lines / y_width.to_f)
    y  = lines - y0.round + 1
    y.upto(lines) do |iy|
      if iy > y
        @display.at(iy, x).on_color(color_secondary).write(' ')
      else
        case @resolution
        when :double
          fract = 1 - (y0 - y0.floor).abs
          case
          when (0...0.5) === fract
            @display.at(iy, x).on_color(@background_color).color(color).write(?▄)
          else
            @display.at(iy, x).on_color(color).color(color_secondary).write(?▄)
          end
        else
          @display.at(iy, x).on_color(color).color(color_secondary).write(' ')
        end
      end
    end
  end
end

#format_value(value) ⇒ String (private)

The format_value method processes a given value using the configured formatting strategy

This method applies the appropriate formatting to a value based on the @format_value instance variable configuration It supports different formatting approaches including custom Proc objects, String- or Symbol-based method calls, and default formatting

Parameters:

  • value (Object)

    the value to be formatted according to the configured strategy

Returns:

  • (String)

    the formatted string representation of the input value



295
296
297
298
299
300
301
302
303
304
# File 'lib/graphina/graph.rb', line 295

def format_value(value)
  case @format_value
  when Proc
    @format_value.(value)
  when Symbol, String
    send(@format_value, value)
  else
    send(:as_default, value)
  end
end

#full_resetObject (private)

The full_reset method performs a complete reset of the display and terminal state

This method synchronizes access to shared resources using a mutex, then executes a series of terminal control operations to reset the terminal state, clear the screen, move the cursor to the home position, and make the cursor visible. It also initializes new display objects with the current terminal dimensions and updates the internal display state.



429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/graphina/graph.rb', line 429

def full_reset
  @mutex.synchronize do
    perform reset, clear_screen, move_home, show_cursor
    winsize = Tins::Terminal.winsize
    opts = {
         color: @foreground_color,
      on_color: @background_color,
    }
    @display     = Graphina::Graph::Display.new(*winsize, **opts)
    @old_display = Graphina::Graph::Display.new(*winsize, **opts)
    perform @display
    @full_reset = false
  end
end

#install_handlersObject (private)

The install_handlers method sets up signal handlers for graceful shutdown and terminal resize handling

This method configures two signal handlers: one for the exit hook that performs a full reset, and another for the SIGWINCH signal that handles terminal resize events by setting a flag and displaying a sleeping message



450
451
452
453
454
455
456
# File 'lib/graphina/graph.rb', line 450

def install_handlers
  at_exit { full_reset }
  trap(:SIGWINCH) do
    @full_reset = true
    perform reset, clear_screen, move_home, 'Zzz…'
  end
end

#linesInteger (private)

The lines method returns the number of lines in the display

This method provides access to the vertical dimension of the graphical display by returning the total number of rows available for rendering content

Returns:

  • (Integer)

    the number of lines (rows) in the display object



262
263
264
# File 'lib/graphina/graph.rb', line 262

def lines
  @display.lines
end

#normalize_value(value) ⇒ Float, Integer (private)

The normalize_value method converts a value to its appropriate numeric representation

This method takes an input value and normalizes it to either a Float or Integer type depending on its original form. If the value is already a Float, it is returned as-is. For all other types, the method attempts to convert the value to an integer using to_i

Parameters:

  • value (Object)

    the value to be normalized

Returns:

  • (Float, Integer)

    the normalized numeric value as either a Float or Integer



411
412
413
414
415
416
417
418
# File 'lib/graphina/graph.rb', line 411

def normalize_value(value)
  case value
  when Float
    value
  else
    value.to_i
  end
end

#perform(*a) ⇒ Object (private)



240
241
242
# File 'lib/graphina/graph.rb', line 240

def perform(*a)
  print(*a)
end

#perform_display_diffObject (private)

The perform_display_diff method calculates and displays the difference between the current and previous display states to update only the changed portions of the terminal output

This method synchronizes access to shared display resources using a mutex, then compares the current display with the previous state to determine what needs updating. It handles dimension mismatches by resetting the old display, computes the visual difference, and outputs only the modified portions to reduce terminal update overhead

When the DEBUG_BYTESIZE environment variable is set, it also outputs debugging information about the size of the diff and the time elapsed since the last debug output



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/graphina/graph.rb', line 378

def perform_display_diff
  @mutex.synchronize do
    unless @old_display && @old_display.dimensions == @display.dimensions
      @old_display = @display.dup.clear
    end
    diff = @display - @old_display
    if ENV['DEBUG_BYTESIZE']
      unless @last
        STDERR.puts %w[ size duration ] * ?\t
      else
        STDERR.puts [ diff.size, (Time.now - @last).to_f ] * ?\t
      end
      @last = Time.now
    end
    perform diff
    @display, @old_display = @old_display.clear, @display
    perform move_to(lines, columns)
  end
end

#pick_colorTerm::ANSIColor::Attribute (private)

The pick_color method determines and returns an ANSI color attribute based on the configured color setting

This method evaluates the @color instance variable to decide how to select a color attribute. If @color is a Proc, it invokes the proc with the @title to determine the color. If @color is nil, it derives a color from the title string. Otherwise, it uses the @color value directly as an index into the ANSI color attributes.

Returns:

  • (Term::ANSIColor::Attribute)

    the selected color attribute object



316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/graphina/graph.rb', line 316

def pick_color
  Term::ANSIColor::Attribute[
    case @color
    when Proc
      @color.(@title)
    when nil
      derive_color_from_string(@title)
    else
      @color
    end
  ]
end

#pick_secondary_color(color, adjust_brightness:, adjust_brightness_percentage:) ⇒ Term::ANSIColor::Attribute (private)

The pick_secondary_color method determines a secondary color based on a primary color and brightness adjustment parameters It returns the pre-configured secondary color if one exists, otherwise calculates a new color by adjusting the brightness of the primary color

Parameters:

  • color (Term::ANSIColor::Attribute)

    the primary color attribute to be used as a base for calculation

  • adjust_brightness (Symbol)

    the method to call on the color for brightness adjustment

  • adjust_brightness_percentage (Integer)

    the percentage value to use for the brightness adjustment

Returns:

  • (Term::ANSIColor::Attribute)

    the secondary color attribute, either pre-configured or calculated from the primary color



342
343
344
345
346
# File 'lib/graphina/graph.rb', line 342

def pick_secondary_color(color, adjust_brightness:, adjust_brightness_percentage:)
  @color_secondary and return @color_secondary
  color_primary = color.to_rgb_triple.to_hsl_triple
  color_primary.send(adjust_brightness, adjust_brightness_percentage) rescue color
end

#sleep_durationString (private)

The sleep_duration method returns a string representation of the configured sleep interval with the ‘s’ suffix appended to indicate seconds.

Returns:

  • (String)

    a formatted string containing the sleep duration in seconds



280
281
282
# File 'lib/graphina/graph.rb', line 280

def sleep_duration
  "#{@sleep}s"
end

#sleep_nowObject (private)

The sleep_now method calculates and executes a sleep duration based on the configured sleep time and elapsed time since start

This method determines how long to sleep by calculating the difference between the configured sleep interval and the time elapsed since the last operation started. If no start time is recorded, it uses the full configured sleep duration. The method ensures that negative sleep durations are not used by taking the maximum of the calculated duration and zero.



356
357
358
359
360
361
362
363
# File 'lib/graphina/graph.rb', line 356

def sleep_now
  duration = if @start
               [ @sleep - (Time.now - @start).to_f, 0 ].max
             else
               @sleep
             end
  sleep duration
end

#startObject

The start method initiates the graphical display process by setting up signal handlers, performing an initial terminal reset, and entering the main update loop

This method serves as the entry point for starting the graph visualization functionality. It configures the necessary signal handlers for graceful shutdown and terminal resizing, performs an initial full reset of the display state, and then begins the continuous loop that updates and renders graphical data.



125
126
127
128
129
# File 'lib/graphina/graph.rb', line 125

def start
  install_handlers
  full_reset
  start_loop
end

#start_loopObject (private)

The start_loop method executes a continuous loop to update and display graphical data

This method manages the main execution loop for rendering graphical representations of data values over time. It initializes display state, processes incoming data, calculates visual representations, and handles terminal updates while respecting configured timing intervals.

It continuously updates the display and handles data processing in a loop until explicitly stopped.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/graphina/graph.rb', line 202

def start_loop
  full_reset
  @counter    = -1
  @continue = true
  while @continue
    @start = Time.now
    @full_reset and full_reset
    perform hide_cursor

    @data << @value.(@counter += 1)
    @data = data.last(columns)

    if data_range.zero?
      @display.reset.bottom.styled(:bold).
        write_centered("#@title / #{sleep_duration}").
        reset.centered.styled(:italic).write_centered("no data")
      perform_display_diff
      sleep_now
      next
    end

    @display.reset
    draw_graph

    @display.reset.bottom.styled(:bold).
      write_centered("#@title #{format_value(data.last)} / #{sleep_duration}")
    @display.reset.styled(:bold).
      left.top.write(format_value(data.max)).
      left.bottom.write(format_value(data.min))

    perform_display_diff
    sleep_now
  end
rescue Interrupt
ensure
  stop
end

#stopObject

The stop method terminates the graphical display process by performing a full reset and setting the continue flag to false

This method serves as the shutdown mechanism for the graph visualization functionality. It ensures that all display resources are properly cleaned up and the terminal state is restored to its original condition before stopping the continuous update loop.



138
139
140
141
# File 'lib/graphina/graph.rb', line 138

def stop
  full_reset
  @continue = false
end