1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 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 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
module UnicodePlot
class Barplot < Plot
include ValueTransformer
MIN_WIDTH = 10
DEFAULT_COLOR = :green
DEFAULT_SYMBOL = "■"
def initialize(bars, width, color, symbol, transform, **kw)
if symbol.length > 1
raise ArgumentError, "symbol must be a single character"
end
@bars = bars
@symbol = symbol
@max_freq, i = find_max(transform_values(transform, bars))
@max_len = bars[i].to_s.length
@width = [width, max_len + 7, MIN_WIDTH].max
@color = color
@symbol = symbol
@transform = transform
super(**kw)
end
attr_reader :max_freq
attr_reader :max_len
attr_reader :width
def n_rows
@bars.length
end
def n_columns
@width
end
def add_row!(bars)
@bars.concat(bars)
@max_freq, i = find_max(transform_values(@transform, bars))
@max_len = @bars[i].to_s.length
end
def print_row(out, row_index)
check_row_index(row_index)
bar = @bars[row_index]
max_bar_width = [width - 2 - max_len, 1].max
val = transform_values(@transform, bar)
bar_len = max_freq > 0 ?
([val, 0].max.fdiv(max_freq) * max_bar_width).round :
0
bar_str = max_freq > 0 ? @symbol * bar_len : ""
bar_lbl = bar.to_s
print_styled(out, bar_str, color: @color)
print_styled(out, " ", bar_lbl, color: :normal)
pan_len = [max_bar_width + 1 + max_len - bar_len - bar_lbl.length, 0].max
pad = " " * pan_len.round
out.print(pad)
end
private def find_max(values)
i = j = 0
max = values[i]
while j < values.length
if values[j] > max
i, max = j, values[j]
end
j += 1
end
[max, i]
end
end
# @overload barplot(text, heights, xscale: nil, title: nil, xlabel: nil, ylabel: nil, labels: true, border: :barplot, margin: Plot::DEFAULT_MARGIN, padding: Plot::DEFAULT_PADDING, color: Barplot::DEFAULT_COLOR, width: Plot::DEFAULT_WIDTH, symbol: Barplot::DEFAULT_SYMBOL)
#
# Draws a horizontal barplot.
#
# @param text [Array<String>] The lables / captions of the bars.
# @param heights [Array<Numeric>] The values / heights of the bars.
# @param xscale [nil,:log,:ln,:log10,:lg,:log2,:lb,callable]
# A function name symbol or callable object to transform the bar
# length before plotting. This effectively scales the x-axis
# without influencing the captions of the individual bars.
# e.g. use `xscale: :log10` for logscale.
# @param title
# @param xlabel
# @param ylabel
# @param labels
# @param border
# @param margin
# @param padding
# @param color
# @param width
# @param symbol [String] Specifies the character that should be used
# to render the bars.
#
# @return [Barplot] A plot object.
#
# @example Example usage of barplot on IRB:
#
# >> UnicodePlot.barplot(["Paris", "New York", "Moskau", "Madrid"],
# [2.244, 8.406, 11.92, 3.165],
# xlabel: "population [in mil]").render
# ┌ ┐
# Paris ┤■■■■■■ 2.244
# New York ┤■■■■■■■■■■■■■■■■■■■■■■■ 8.406
# Moskau ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 11.92
# Madrid ┤■■■■■■■■■ 3.165
# └ ┘
# population [in mil]
# => nil
#
# @see Plot
# @see histogram
# @see Barplot
#
# @overload barplot(data, **kwargs)
#
# The different variation of barplot described above.
#
# @param data [Hash] A hash in which the keys will be used as `text` and
# the values will be utilized as `heights`.
# @param kwargs Optional keyword arguments same as ones described above.
# @return [Barplot] A plot object.
module_function def barplot(*args,
width: Plot::DEFAULT_WIDTH,
color: Barplot::DEFAULT_COLOR,
symbol: Barplot::DEFAULT_SYMBOL,
border: :barplot,
xscale: nil,
xlabel: nil,
data: nil,
**kw)
case args.length
when 0
data = Hash(data)
keys = data.keys.map(&:to_s)
heights = data.values
when 2
keys = Array(args[0])
heights = Array(args[1])
else
raise ArgumentError, "invalid arguments"
end
unless keys.length == heights.length
raise ArgumentError, "The given vectors must be of the same length"
end
unless heights.min >= 0
raise ArgumentError, "All values have to be positive. Negative bars are not supported."
end
xlabel ||= ValueTransformer.transform_name(xscale)
plot = Barplot.new(heights, width, color, symbol, xscale,
border: border, xlabel: xlabel,
**kw)
keys.each_with_index do |key, i|
plot.annotate_row!(:l, i, key)
end
plot
end
# @overload barplot!(plot, text, heights)
#
# Draw additional bars on the given existing plot.
#
# @param plot [Barplot] the existing plot.
# @return [Barplot] A plot object.
#
# @see barplot
#
# @overload barplot!(plot, data)
#
# The different variation of `barplot!` that takes the plotting data in a hash.
#
# @param plot [Barplot] the existing plot.
# @return [Barplot] A plot object.
module_function def barplot!(plot,
*args,
data: nil)
case args.length
when 0
data = Hash(data)
keys = data.keys.map(&:to_s)
heights = data.values
when 2
keys = Array(args[0])
heights = Array(args[1])
else
raise ArgumentError, "invalid arguments"
end
unless keys.length == heights.length
raise ArgumentError, "The given vectors must be of the same length"
end
if keys.empty?
raise ArgumentError, "Can't append empty array to barplot"
end
cur_idx = plot.n_rows
plot.add_row!(heights)
keys.each_with_index do |key, i|
plot.annotate_row!(:l, cur_idx + i, key)
end
plot
end
end
|