Class: Hackmac::Graph

Inherits:
Object
  • Object
show all
Includes:
Formatters, Term::ANSIColor
Defined in:
lib/hackmac/graph.rb,
lib/hackmac/graph/display.rb,
lib/hackmac/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 = Hackmac::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 = Hackmac::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: nil, color: nil) ⇒ 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: nil)

    time in seconds between updates

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

    color index or proc to determine color dynamically

Raises:

  • (ArgumentError)

    if the sleep parameter is negative



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/hackmac/graph.rb', line 58

def initialize(
  title:,
  value: -> i { 0 },
  format_value: nil,
  sleep: nil,
  color: nil
)
  sleep >= 0 or raise ArgumentError, 'sleep has to be >= 0'
  @title        = title
  @value        = value
  @format_value = format_value
  @sleep        = sleep
  @continue     = false
  @data         = []
  @color        = color
  @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



225
226
227
# File 'lib/hackmac/graph.rb', line 225

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



202
203
204
# File 'lib/hackmac/graph.rb', line 202

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



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

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.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/hackmac/graph.rb', line 110

def draw_graph
  y_width     = data_range
  color       = pick_color
  color_light = color.to_rgb_triple.to_hsl_triple.lighten(15) rescue color
  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_light).write(' ')
      else
        fract = 1 - (y0 - y0.floor).abs
        case
        when (0...0.5) === fract
          @display.at(iy, x).color(0).on_color(color).write(?▀)
        else
          @display.at(iy, x).color(color).on_color(color_light).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, 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



246
247
248
249
250
251
252
253
254
255
# File 'lib/hackmac/graph.rb', line 246

def format_value(value)
  case @format_value
  when Proc
    @format_value.(value)
  when Symbol
    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.



364
365
366
367
368
369
370
371
372
373
# File 'lib/hackmac/graph.rb', line 364

def full_reset
  @mutex.synchronize do
    perform reset, clear_screen, move_home, show_cursor
    winsize = Tins::Terminal.winsize
    @display     = Hackmac::Graph::Display.new(*winsize)
    @old_display = Hackmac::Graph::Display.new(*winsize)
    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



381
382
383
384
385
386
387
# File 'lib/hackmac/graph.rb', line 381

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



213
214
215
# File 'lib/hackmac/graph.rb', line 213

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



346
347
348
349
350
351
352
353
# File 'lib/hackmac/graph.rb', line 346

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

#perform(*a) ⇒ Object (private)



191
192
193
# File 'lib/hackmac/graph.rb', line 191

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

#perform_display_diffvoid (private)

This method returns an undefined value.

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



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/hackmac/graph.rb', line 313

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



267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/hackmac/graph.rb', line 267

def pick_color
  Term::ANSIColor::Attribute[
    case @color
    when Proc
      @color.(@title)
    when nil
      derive_color_from_string(@title)
    else
      @color
    end
  ]
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



231
232
233
# File 'lib/hackmac/graph.rb', line 231

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.



288
289
290
291
292
293
294
295
# File 'lib/hackmac/graph.rb', line 288

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.



85
86
87
88
89
# File 'lib/hackmac/graph.rb', line 85

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.



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
182
183
184
185
186
187
188
189
# File 'lib/hackmac/graph.rb', line 153

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.



98
99
100
101
# File 'lib/hackmac/graph.rb', line 98

def stop
  full_reset
  @continue = false
end