WidgetBarchart

Demonstrates categorical data visualization with interactive attribute cycling.

Raw tables of numbers are hard to scan. Comparing magnitudes requires mental arithmetic, which slows down decision-making.

This demo showcases the BarChart widget. It provides an interactive playground where you can cycle through different data formats, styles, and orientations in real-time.

Use it to understand how to visualize and compare discrete datasets effectively.

Example

Run the demo from the terminal:

ruby examples/widget_barchart/app.rb

Source Code

# frozen_string_literal: true

#--
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
# SPDX-License-Identifier: MIT-0
#++

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"

# Demonstrates categorical data visualization with interactive attribute cycling.
#
# Raw tables of numbers are hard to scan. Comparing magnitudes requires mental arithmetic, which slows down decision-making.
#
# This demo showcases the <tt>BarChart</tt> widget. It provides an interactive playground where you can cycle through different data formats, styles, and orientations in real-time.
#
# Use it to understand how to visualize and compare discrete datasets effectively.
#
# === Example
#
# Run the demo from the terminal:
#
#   ruby examples/widget_barchart/app.rb
#
# rdoc-image:/doc/images/widget_barchart.png
class WidgetBarchart
  def initialize
    @data_index = 2
    @styles = nil # Initialized in run
    @style_index = 3
    @label_style_index = 3
    @value_style_index = 3
    @bar_sets = [
      { name: "Default", set: nil },
      { name: "Numbers (Short)", set: { 8 => "8", 7 => "7", 6 => "6", 5 => "5", 4 => "4", 3 => "3", 2 => "2", 1 => "1", 0 => "0" } },
      { name: "Letters (Long)", set: { full: "H", seven_eighths: "G", three_quarters: "F", five_eighths: "E", half: "D", three_eighths: "C", one_quarter: "B", one_eighth: "A", empty: " " } },
      { name: "ASCII (Heights)", set: [" ", "_", ".", "-", "=", "+", "*", "#", "@"] },
    ]
    @bar_set_index = 0
    @direction = :vertical
    @bar_width = 8
    @bar_gap = 1
    @group_gap = 2
    @height_mode = :full
    @hotkey_style = nil
  end

  def run
    RatatuiRuby.run do |tui|
      @tui = tui
      init_styles

      loop do
        render
        break if handle_input == :quit
      end
    end
  end

  private def init_styles
    @styles = [
      { name: "Green", style: @tui.style(fg: :green) },
      { name: "Blue", style: @tui.style(fg: :blue) },
      { name: "Red", style: @tui.style(fg: :red) },
      { name: "Cyan", style: @tui.style(fg: :cyan) },
      { name: "Yellow Bold", style: @tui.style(fg: :yellow, modifiers: [:bold]) },
      { name: "Reversed", style: @tui.style(modifiers: [:reversed]) },
    ]
    @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
  end

  private def current_data
    case @data_index
    when 0 # Simple Hash
      {
        "Q1" => 50,
        "Q2" => 80,
        "Q3" => 45,
        "Q4" => 60,
        "Q1'" => 55,
        "Q2'" => 85,
        "Q3'" => 50,
        "Q4'" => 65,
      }
    when 1 # Array with styles
      [
        ["Mon", 120],
        ["Tue", 150],
        ["Wed", 130],
        ["Thu", 160],
        ["Fri", 140],
        ["Sat", 110, @tui.style(fg: :red)],
        ["Sun", 100, @tui.style(fg: :red)],
      ]
    when 2 # Groups
      [
        @tui.bar_chart_bar_group(label: "2024", bars: [
          @tui.bar_chart_bar(value: 40, label: "Q1"),
          @tui.bar_chart_bar(value: 45, label: "Q2"),
          @tui.bar_chart_bar(value: 50, label: "Q3"),
          @tui.bar_chart_bar(value: 55, label: "Q4"),
        ]),
        @tui.bar_chart_bar_group(label: "2025", bars: [
          @tui.bar_chart_bar(value: 60, label: "Q1", style: @tui.style(fg: :yellow)),
          @tui.bar_chart_bar(value: 65, label: "Q2", style: @tui.style(fg: :yellow)),
          @tui.bar_chart_bar(value: 70, label: "Q3", style: @tui.style(fg: :yellow)),
          @tui.bar_chart_bar(value: 75, label: "Q4", style: @tui.style(fg: :yellow)),
        ]),
      ]
    end
  end

  private def data_name
    ["Simple Hash", "Styled Array", "Groups"][@data_index]
  end

  private def render
    @tui.draw do |frame|
      chart_area, controls_area = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(6),
        ]
      )

      # Handle Mini Mode
      effective_chart_area = if @height_mode == :mini
        mini_area, = @tui.layout_split(
          chart_area,
          direction: :vertical,
          constraints: [
            @tui.constraint_length(3),
            @tui.constraint_fill(1),
          ]
        )
        mini_area
      else
        chart_area
      end

      bar_chart = @tui.bar_chart(
        data: current_data,
        bar_width: @bar_width,
        style: @styles[@style_index][:style],
        bar_gap: @bar_gap,
        group_gap: @group_gap,
        direction: @direction,
        label_style: @styles[@label_style_index][:style],
        value_style: @styles[@value_style_index][:style],
        bar_set: @bar_sets[@bar_set_index][:set],
        block: @tui.block(
          title: "BarChart: #{data_name}",
          borders: [:all]
        )
      )
      frame.render_widget(bar_chart, effective_chart_area)

      render_controls(frame, controls_area)
    end
  end

  private def render_controls(frame, area)
    controls = @tui.block(
      title: "Controls",
      borders: [:all],
      children: [
        @tui.paragraph(
          text: [
            @tui.text_line(spans: [
              @tui.text_span(content: "d", style: @hotkey_style),
              @tui.text_span(content: ": Data (#{data_name})  "),
              @tui.text_span(content: "v", style: @hotkey_style),
              @tui.text_span(content: ": Direction (#{@direction})  "),
              @tui.text_span(content: "q", style: @hotkey_style),
              @tui.text_span(content: ": Quit"),
            ]),
            @tui.text_line(spans: [
              @tui.text_span(content: "w", style: @hotkey_style),
              @tui.text_span(content: ": Width (#{@bar_width})  "),
              @tui.text_span(content: "a", style: @hotkey_style),
              @tui.text_span(content: ": Gap (#{@bar_gap})  "),
              @tui.text_span(content: "g", style: @hotkey_style),
              @tui.text_span(content: ": Group Gap (#{@group_gap})"),
            ]),
            @tui.text_line(spans: [
              @tui.text_span(content: "s", style: @hotkey_style),
              @tui.text_span(content: ": Style (#{@styles[@style_index][:name]})  "),
              @tui.text_span(content: "x", style: @hotkey_style),
              @tui.text_span(content: ": Label (#{@styles[@label_style_index][:name]})  "),
              @tui.text_span(content: "z", style: @hotkey_style),
              @tui.text_span(content: ": Value (#{@styles[@value_style_index][:name]})  "),
            ]),
            @tui.text_line(spans: [
              @tui.text_span(content: "b", style: @hotkey_style),
              @tui.text_span(content: ": Set (#{@bar_sets[@bar_set_index][:name]})  "),
              @tui.text_span(content: "m", style: @hotkey_style),
              @tui.text_span(content: ": Mode (#{(@height_mode == :full) ? 'Full' : 'Mini'})"),
            ]),
          ]
        ),
      ]
    )
    frame.render_widget(controls, area)
  end

  private def handle_input
    case @tui.poll_event
    in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
      :quit
    in type: :key, code: "d"
      @data_index = (@data_index + 1) % 3
    in type: :key, code: "v"
      @direction = (@direction == :vertical) ? :horizontal : :vertical
      # Adjust width default based on direction for better UX, though user can manually adjust 'w'
      @bar_width = (@direction == :vertical) ? 8 : 1
    in type: :key, code: "w"
      @bar_width = (@bar_width % 10) + 1
    in type: :key, code: "a"
      @bar_gap = (@bar_gap + 1) % 5
    in type: :key, code: "g"
      @group_gap = (@group_gap + 1) % 5
    in type: :key, code: "s"
      @style_index = (@style_index + 1) % @styles.size
    in type: :key, code: "x"
      @label_style_index = (@label_style_index + 1) % @styles.size
    in type: :key, code: "z"
      @value_style_index = (@value_style_index + 1) % @styles.size
    in type: :key, code: "b"
      @bar_set_index = (@bar_set_index + 1) % @bar_sets.size
    in type: :key, code: "m"
      @height_mode = (@height_mode == :full) ? :mini : :full
    else
      # Ignore
    end
  end
end

WidgetBarchart.new.run if __FILE__ == $0