Skip to content

Instantly share code, notes, and snippets.

@ddlsmurf
Last active February 15, 2022 00:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ddlsmurf/45ea6ae81ba8edad83e9cb13553acf14 to your computer and use it in GitHub Desktop.
Save ddlsmurf/45ea6ae81ba8edad83e9cb13553acf14 to your computer and use it in GitHub Desktop.

Very simple CLI histogram printer for lists of integers in elixir

Examples

    1..10000
    |> Enum.map(fn _ -> round(:rand.normal(2.5, 5)) end)

Default

    |> CLIHistogram.print()
             avg: 2.49, stddev: 2.23
         10000 numbers between -5 and 11
                     ██ ▇▇                         ▔▔ 1733
                  ▂▂ ██ ██ ▃▃
                  ██ ██ ██ ██
                  ██ ██ ██ ██
               ▅▅ ██ ██ ██ ██ ▃▃
               ██ ██ ██ ██ ██ ██
               ██ ██ ██ ██ ██ ██
            ██ ██ ██ ██ ██ ██ ██ ██
         ▃▃ ██ ██ ██ ██ ██ ██ ██ ██ ▃▃
   ▁▁ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ▃▃ ▁▁       ▁▁ 2
-5 -4 -3 -2 -1  0  1  2  3  4  5  6  7  8  9 10 11

Some options:

    |> CLIHistogram.print(col_width: 3,
                          col_sep: "",
                          title: "A nice title for\nthis fine histogram in your CLI",
                          from_zero: true,
                          block_chars: " .x|",
                          print_stats: false,
                          height: 5)
                 A nice title for
          this fine histogram in your CLI
                     ||||||...                      ▔▔ 1733
                  ||||||||||||
               |||||||||||||||xxx
            xxx||||||||||||||||||xxx
      ...xxx||||||||||||||||||||||||xxx...          ▁▁ 0
 -5 -4 -3 -2 -1  0  1  2  3  4  5  6  7  8  9 10 11
defmodule CLIHistogram do
defp block_index_from_fractional(blocks, fractional) when fractional >= 0 and fractional <= 1, do:
round(fractional * (tuple_size(blocks) - 1))
defp block_index_from_fractional(_blocks, fractional) when fractional < 0, do: 0
defp block_index_from_fractional(blocks, fractional) when fractional > 1, do: tuple_size(blocks) - 1
defp block_from_fractional(blocks, fractional), do: elem(blocks, block_index_from_fractional(blocks, fractional))
defp repeat_string(str, count) when count <= 0, do: str
defp repeat_string(str, count), do: String.pad_leading("", count, str)
defp str_to_blocks(blocks), do: blocks |> String.graphemes |> List.to_tuple()
defp str_center(width, str), do:
repeat_string(" ", div((width - String.length(str)) , 2)) <> str
defp parag_center(width, text), do:
text |> String.split("\n") |> Enum.map_join("\n", &str_center(width, &1))
defp average(nums), do: Enum.sum(nums) / Enum.count(nums)
defp stats(nums) do
avg = average(nums)
{avg, nums |> Enum.map(&((&1 - avg) ** 2)) |> average() |> :math.sqrt()}
end
defp print_stats(numbers, total_width) do
{avg, stdev} = stats(numbers)
IO.puts(parag_center(total_width, "avg: #{Float.round(avg, 2)}, stddev: #{Float.round(stdev, 2)}"))
{min, max} = Enum.min_max(numbers)
IO.puts(parag_center(total_width, "#{Enum.count(numbers)} numbers between #{min} and #{max}"))
end
defp build_rows(height, cols_normalised, col_sep, col_width, blocks, count_min, count_max) do
1..height
|> Enum.map_join("\n", fn row_index ->
row = 1 + height - row_index
row_string =
cols_normalised
|> Enum.map_join(col_sep, fn col_value ->
block = block_from_fractional(blocks, col_value - row + 1)
repeat_string(block, col_width)
end)
row_string <>
if row_index == 1, do: " ▔▔ #{count_max}", else: (
if row_index == height, do: " ▁▁ #{count_min}", else: ""
)
end)
end
@print_default_options [
block_chars: " ▁▂▃▄▅▆▇█",
col_sep: " ",
col_width: 2,
height: 10,
from_zero: false,
print_stats: true,
]
def print(numbers, options \\ []) do
options = Keyword.merge(@print_default_options, options, fn _k, _v1, v2 -> v2 end)
height = Keyword.get(options, :height)
col_sep = Keyword.get(options, :col_sep)
col_width = Keyword.get(options, :col_width)
from_zero = Keyword.get(options, :from_zero)
block_chars = Keyword.get(options, :block_chars)
title = Keyword.get(options, :title)
print_stats = Keyword.get(options, :print_stats)
blocks = str_to_blocks(block_chars)
frequencies = Enum.frequencies(numbers)
freq_keys = Map.keys(frequencies)
{value_min, value_max} = Enum.min_max(freq_keys)
{count_min, count_max} = Enum.min_max(Map.values(frequencies))
{count_min, count_window} = if from_zero, do: {0, count_max}, else: {count_min, count_max - count_min}
count_window = if count_window != 0, do: count_window, else: 1
cols = value_min..value_max
cols_normalised = cols
|> Enum.map(fn col_index ->
max(0, ((Map.get(frequencies, col_index, 0) - count_min) / count_window) * height)
end)
col_sep_width = String.length(col_sep)
total_width = Enum.count(cols) * (col_width + col_sep_width) - col_sep_width
if title, do: IO.puts(parag_center(total_width, title))
if print_stats, do: print_stats(numbers, total_width)
# IO.inspect([frequencies: frequencies, cols_normalised: cols_normalised])
build_rows(height, cols_normalised, col_sep, col_width, blocks, count_min, count_max)
|> IO.puts()
cols
|> Enum.map_join(col_sep, &String.pad_leading(to_string(&1), col_width))
|> IO.puts
numbers
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment