|
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 |