cmd_plot 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CmdPlot
4
+ # 2D color maps of a matrix, e.g. to eyeball a function of two variables.
5
+ module ColorMap
6
+ MAX_SHADES = TEXT_COLORS.length * FILL_SYMBOLS.length
7
+
8
+ # data - array of rows, each an array of numbers
9
+ # shades - number of distinct shades (default: one per distinct value, at most 28)
10
+ #
11
+ # The matrix is averaged down (or stretched up) to the plot size. Each shade is a
12
+ # combination of a color and a fill symbol.
13
+ def pcolor(data, shades = nil)
14
+ raise ArgumentError, 'nothing to plot: no rows' if data.empty? || data.first.empty?
15
+
16
+ shades = data.flatten.uniq.length if shades.nil?
17
+ shades = MAX_SHADES if shades > MAX_SHADES || shades <= 0
18
+ x_scale = data.first.length / @width.to_f
19
+ y_scale = data.length / @height.to_f
20
+ z_min, z_max = data.flatten.minmax
21
+ buf = +"Pcolor with x_scale: #{x_scale.round(3)}, y_scale: #{y_scale.round(3)}, " \
22
+ "z_min: #{z_min.round(3)}, z_max: #{z_max.round(3)}\n"
23
+
24
+ @height.times { |row|
25
+ line = Array.new(@width) { |col|
26
+ val = cell_average(data, row, col, x_scale, y_scale)
27
+ next ' ' if val.nil?
28
+
29
+ shade = z_max == z_min ? 0 : ((val - z_min) / (z_max - z_min).to_f * (shades - 1)).round
30
+ colorize(FILL_SYMBOLS[shade / TEXT_COLORS.length], "3#{TEXT_COLORS[shade % TEXT_COLORS.length]}")
31
+ }
32
+ buf << line.join << "\n"
33
+ }
34
+ out.print(buf)
35
+ nil
36
+ end
37
+
38
+ private
39
+
40
+ # Average of the data points that fall into one output cell, or nil if none do.
41
+ def cell_average(data, row, col, x_scale, y_scale)
42
+ first_row = (row * y_scale).floor
43
+ first_col = (col * x_scale).floor
44
+ sum = 0.0
45
+ n = 0
46
+ (first_row..[first_row, ((row + 1) * y_scale).floor - 1].max).each { |r|
47
+ next if r >= data.length
48
+
49
+ (first_col..[first_col, ((col + 1) * x_scale).floor - 1].max).each { |c|
50
+ next if c >= data[r].length
51
+
52
+ sum += data[r][c]
53
+ n += 1
54
+ }
55
+ }
56
+ n.zero? ? nil : sum / n
57
+ end
58
+ end
59
+
60
+ include ColorMap
61
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CmdPlot
4
+ # Numeric helpers used by the plots. Kept inside the CmdPlot namespace so the gem
5
+ # doesn't define top-level constants in the host application.
6
+ module Data
7
+ module_function
8
+
9
+ # n evenly spaced floats from min to max, both ends included.
10
+ def linspace(min, max, n)
11
+ return [min.to_f] if n == 1
12
+
13
+ Array.new(n) { |i| min.to_f + (i.to_f * (max.to_f - min.to_f) / (n - 1).to_f) }
14
+ end
15
+
16
+ # Counts values into bins.
17
+ #
18
+ # bins - number of equal-width bins between min and max, or an array of bin limits
19
+ # maxcap - optional cap applied to every count
20
+ #
21
+ # Values below the first limit land in the first bin, values at or above the last
22
+ # limit in the last bin.
23
+ #
24
+ # return - [counts, bin_centers]
25
+ def histogram(values, bins, maxcap = nil)
26
+ limits = bins.is_a?(Array) ? bins : linspace(values.min, values.max, bins + 1)
27
+ counts = Array.new(limits.length - 1, 0)
28
+ values.each { |v|
29
+ bin = limits.index { |limit| v < limit } || counts.length
30
+ bin = 1 if bin.zero?
31
+ counts[bin - 1] += 1
32
+ }
33
+ counts = counts.map { |c| [c, maxcap].min } unless maxcap.nil?
34
+ centers = limits.each_cons(2).map { |a, b| (a + b) / 2.0 }
35
+ [counts, centers]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CmdPlot
4
+ # The 0.0.1 classes, kept so code written against them still runs. They delegate to
5
+ # CmdPlot; new code should use CmdPlot directly.
6
+ class Plot
7
+ def initialize(width = 140, height = 40, _symbol = nil)
8
+ @plotter = CmdPlot.new(width, height)
9
+ end
10
+
11
+ def plot(x, y = nil)
12
+ @plotter.plot(x, y)
13
+ end
14
+ end
15
+
16
+ class Bar
17
+ def initialize(width = 140, height = 40, symbol = '°')
18
+ @plotter = CmdPlot.new(width, height)
19
+ @symbol = symbol
20
+ end
21
+
22
+ def bar(values, labels)
23
+ @plotter.bar(values, labels, @symbol)
24
+ end
25
+
26
+ def histogram(values, bins = 20)
27
+ counts, centers = Data.histogram(values, bins)
28
+ bar(counts, centers.map { |c| c.round(1).to_s })
29
+ end
30
+ end
31
+
32
+ class Pcolor
33
+ def initialize(width = 140, height = 40)
34
+ @plotter = CmdPlot.new(width, height)
35
+ end
36
+
37
+ def pcolor(data, shades = nil)
38
+ @plotter.pcolor(data, shades)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CmdPlot
4
+ # Line plots drawn with braille dots, one color per series.
5
+ module LinePlot
6
+ # Accepted input:
7
+ # plot([y0, y1, ...]) y values, x is the index
8
+ # plot([x0, x1, ...], [y0, y1, ...]) separate x and y arrays
9
+ # plot({ x0 => y0, x1 => y1 }) one series as a hash
10
+ # plot([{ x0 => y0 }, { x0 => y0 }]) several series, one color each
11
+ #
12
+ # x values can be Dates. nil y values leave a gap in the line.
13
+ #
14
+ # intense: - use the bright color variants
15
+ # bold: - draw the dots in bold
16
+ def plot(data, data2 = nil, intense: true, bold: true)
17
+ raise ArgumentError, 'a line plot needs at least 2x2 characters' if @width < 2 || @height < 2
18
+
19
+ series = plot_series(data, data2)
20
+ start_date = date_start(series)
21
+ series = series.map { |s| s.transform_keys { |d| (d - start_date).to_f } } unless start_date.nil?
22
+
23
+ ys = series.flat_map(&:values).compact
24
+ raise ArgumentError, 'nothing to plot: every y value is nil' if ys.empty?
25
+
26
+ x_min, x_max = spread(*series.flat_map(&:keys).compact.minmax)
27
+ y_min, y_max = spread(*ys.minmax)
28
+
29
+ cells = Array.new(@height) { Array.new(@width, ' ') }
30
+ series.each_with_index { |s, i|
31
+ code = "#{bold ? 1 : 0};#{intense ? 9 : 3}#{TEXT_COLORS[i % TEXT_COLORS.length]}"
32
+ draw_series(cells, dot_points(s, x_min, x_max, y_min, y_max), code)
33
+ }
34
+
35
+ x_ticks = Data.linspace(x_min, x_max, [2, @width / 15].max).map { |v|
36
+ start_date.nil? ? format('%.2f', v) : (start_date + v.round).to_s
37
+ }
38
+ y_ticks = Data.linspace(y_max, y_min, [2, @height / 10].max).map { |v| format('%.2f', v) }
39
+
40
+ buf = +''
41
+ plot_area(buf, cells.map(&:join), y_ticks)
42
+ x_axis(buf, x_ticks, y_ticks.map(&:length).max)
43
+ out.print(buf)
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ def plot_series(data, data2)
50
+ return [data.zip(data2).to_h] unless data2.nil?
51
+ return [data] if data.is_a?(Hash)
52
+ raise ArgumentError, 'expected an array or a hash to plot' unless data.is_a?(Array)
53
+ return [data.each_with_index.to_h { |y, i| [i, y] }] if data.all? { |d| d.nil? || d.is_a?(Numeric) }
54
+ return data if data.all?(Hash)
55
+
56
+ raise ArgumentError, 'expected numbers, a hash, or an array of hashes to plot'
57
+ end
58
+
59
+ def date_start(series)
60
+ keys = series.flat_map(&:keys).compact
61
+ return nil unless keys.any?(Date)
62
+ raise ArgumentError, 'cannot mix Date and numeric x values' unless keys.all?(Date)
63
+
64
+ keys.min
65
+ end
66
+
67
+ # Widens a zero-width range so constant data still scales.
68
+ def spread(min, max)
69
+ return [min, max] unless min == max
70
+
71
+ delta = min.zero? ? 1.0 : min.abs * 0.05
72
+ [min - delta, max + delta]
73
+ end
74
+
75
+ # Scales a series onto the braille dot grid and connects consecutive points.
76
+ def dot_points(series, x_min, x_max, y_min, y_max)
77
+ cols = @width * 2
78
+ rows = @height * 4
79
+ xs = series.keys.map { |v| v.nil? ? nil : ((v - x_min) / (x_max - x_min).to_f * (cols - 1)).round }
80
+ ys = series.values.map { |v| v.nil? ? nil : ((v - y_min) / (y_max - y_min).to_f * (rows - 1)).round }
81
+ pairs = xs.zip(ys)
82
+ points = if pairs.length == 1
83
+ pairs.reject { |pair| pair[1].nil? }
84
+ else
85
+ pairs.each_cons(2).flat_map { |(x0, y0), (x1, y1)| Braille.line_points(x0, y0, x1, y1) }
86
+ end
87
+ points.map { |x, y| [x.clamp(0, cols - 1), y.clamp(0, rows - 1)] }
88
+ end
89
+
90
+ # Later series overwrite earlier ones where they share a cell.
91
+ def draw_series(cells, points, code)
92
+ masks = Array.new(@height) { Array.new(@width, 0) }
93
+ points.each { |x, y|
94
+ masks[@height - 1 - (y / 4)][x / 2] |= Braille::DOT_BITS[3 - (y % 4)][x % 2]
95
+ }
96
+ masks.each_with_index { |row, r|
97
+ row.each_with_index { |mask, c|
98
+ next if mask.zero?
99
+
100
+ cells[r][c] = colorize((Braille::BASE + mask).chr(Encoding::UTF_8), code)
101
+ }
102
+ }
103
+ end
104
+
105
+ # Spreads the y ticks as evenly as the line count allows.
106
+ def plot_area(buf, lines, y_ticks)
107
+ tick_width = y_ticks.map(&:length).max
108
+ spacing = (@height - y_ticks.length) / (y_ticks.length - 1)
109
+ missing = @height - ((spacing * (y_ticks.length - 1)) + y_ticks.length)
110
+ missing_dist = missing.zero? ? @height : y_ticks.length / missing.to_f
111
+ next_tick = 0
112
+ tick_offset = 0
113
+ lines.each_with_index { |line, row|
114
+ if next_tick < y_ticks.length && row == tick_offset + (next_tick * (spacing + 1))
115
+ buf << y_ticks[next_tick].rjust(tick_width) << ' ├ '
116
+ next_tick += 1
117
+ tick_offset += 1 if ((tick_offset + 1) * missing_dist) < (next_tick + 2)
118
+ else
119
+ buf << (' ' * tick_width) << ' │ '
120
+ end
121
+ buf << line << "\n"
122
+ }
123
+ end
124
+
125
+ def x_axis(buf, x_ticks, indent)
126
+ buf << (' ' * indent) << ' └─'
127
+ spread_between(buf, Array.new(x_ticks.length, '┴'), '─')
128
+ buf << (' ' * indent) << ' '
129
+ spread_between(buf, x_ticks, ' ')
130
+ end
131
+
132
+ # Writes the items across the plot width, padding the gaps with fill. When the
133
+ # width doesn't divide evenly the leftover goes into the gaps one by one.
134
+ def spread_between(buf, items, fill)
135
+ total = items.sum(&:length)
136
+ gap = [(@width - total) / (items.length - 1), 1].max
137
+ missing = @width - ((gap * (items.length - 1)) + total)
138
+ missing_dist = missing.zero? ? @width : items.length / missing.to_f
139
+ added = 0
140
+ items.each_with_index { |item, i|
141
+ buf << item
142
+ if i == items.length - 1
143
+ buf << "\n"
144
+ next
145
+ end
146
+ buf << (fill * gap)
147
+ if ((added + 1) * missing_dist) < (i + 3)
148
+ added += 1
149
+ buf << fill
150
+ end
151
+ }
152
+ end
153
+ end
154
+
155
+ include LinePlot
156
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CmdPlot
4
+ # Tabular dumps of records.
5
+ #
6
+ # data is an array of hashes, or of objects that respond to #attributes (e.g. an
7
+ # ActiveRecord relation). Objects are turned into hashes with string keys and string
8
+ # values, so pass field names as strings for them.
9
+ module Tables
10
+ # Columns separated by │, each as wide as its longest value.
11
+ #
12
+ # fields - keys to show, in order (default: the keys of the first record)
13
+ # shorten - cut every column to at most this many characters
14
+ def table(data, fields = nil, shorten = nil)
15
+ rows = records(data)
16
+ fields ||= rows.first&.keys
17
+ return nil if fields.nil?
18
+
19
+ widths = fields.map { |f|
20
+ width = ([f.to_s.length] + rows.map { |r| r[f].to_s.length }).max
21
+ shorten.nil? ? width : [width, shorten].min
22
+ }
23
+ cut = ->(value) { shorten.nil? ? value.to_s : value.to_s[0, shorten] }
24
+ line = ->(values) { " #{values.zip(widths).map { |v, w| cut.call(v).rjust(w) }.join(' │ ')}" }
25
+
26
+ buf = +''
27
+ buf << line.call(fields) << "\n"
28
+ buf << widths.map { |w| '─' * (w + 2) }.join('┼') << "\n"
29
+ rows.each { |r| buf << line.call(fields.map { |f| r[f] }) << "\n" }
30
+ out.print(buf)
31
+ nil
32
+ end
33
+
34
+ # One block per record with a line per field, like psql's \x mode. Useful when
35
+ # records have too many fields for a table.
36
+ def expanded_table(data, fields = nil)
37
+ rows = records(data)
38
+ fields ||= rows.first&.keys
39
+ return nil if fields.nil?
40
+
41
+ num_width = rows.length.to_s.length
42
+ name_width = [13 + num_width, fields.map { |f| f.to_s.length }.max + 1].max
43
+ buf = +''
44
+ rows.each_with_index { |r, i|
45
+ buf << "─[ RECORD #{(i + 1).to_s.rjust(num_width)} ]─" << ('─' * (name_width - 13 - num_width))
46
+ buf << '┬' << ('─' * [70 - name_width, 0].max) << "\n"
47
+ fields.each { |f| buf << f.to_s.ljust(name_width) << '│ ' << r[f].to_s << "\n" }
48
+ }
49
+ out.print(buf)
50
+ nil
51
+ end
52
+
53
+ # Comma-separated values with a header line. Values that contain a comma, quote or
54
+ # line break are quoted.
55
+ def csv(data, fields = nil)
56
+ rows = records(data)
57
+ fields ||= rows.first&.keys
58
+ return nil if fields.nil?
59
+
60
+ buf = +''
61
+ buf << fields.map { |f| csv_value(f) }.join(', ') << "\n"
62
+ rows.each { |r| buf << fields.map { |f| csv_value(r[f]) }.join(', ') << "\n" }
63
+ out.print(buf)
64
+ nil
65
+ end
66
+
67
+ private
68
+
69
+ def records(data)
70
+ data.map { |row|
71
+ if row.respond_to?(:attributes)
72
+ row.attributes.to_h { |k, v| [k.to_s, v.to_s] }
73
+ else
74
+ row
75
+ end
76
+ }
77
+ end
78
+
79
+ def csv_value(value)
80
+ text = value.to_s
81
+ return text unless text.match?(/[",\r\n]/)
82
+
83
+ "\"#{text.gsub('"', '""')}\""
84
+ end
85
+ end
86
+
87
+ include Tables
88
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CmdPlot
4
+ VERSION = '0.1.0'
5
+ end
data/lib/cmd_plot.rb CHANGED
@@ -1,9 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # Quick text-mode plots, charts and tables for the console.
6
+ #
7
+ # plotter = CmdPlot.new(100, 25)
8
+ # plotter.plot([1, 4, 9, 16, 25])
9
+ # plotter.bar([3, 5, 2], %w[a b c])
10
+ # plotter.table([{ 'id' => 1, 'name' => 'x' }])
11
+ #
12
+ # Every method writes to the output stream and returns nil, so it reads cleanly in an
13
+ # interactive console.
1
14
  class CmdPlot
15
+ # ANSI color numbers: red, green, brown, blue, magenta, cyan, gray
16
+ TEXT_COLORS = [1, 2, 3, 4, 5, 6, 7].freeze
17
+ FILL_SYMBOLS = ['█', '▓', '░', '▒'].freeze
2
18
 
3
- end
19
+ attr_reader :width, :height
20
+
21
+ # width - width of the drawing area in characters
22
+ # height - height of the drawing area in lines
23
+ # out: - IO to write to; defaults to whatever $stdout is at the time of the call
24
+ # color: - false drops the ANSI escape codes, e.g. when writing to a file
25
+ def initialize(width = 140, height = 40, out: nil, color: true)
26
+ raise ArgumentError, 'width must be at least 1' if width.to_i < 1
27
+ raise ArgumentError, 'height must be at least 1' if height.to_i < 1
28
+
29
+ @width = width.to_i
30
+ @height = height.to_i
31
+ @out = out
32
+ @color = color
33
+ end
4
34
 
5
- require 'cmd_plot/basic_data'
35
+ private
36
+
37
+ def out
38
+ @out || $stdout
39
+ end
40
+
41
+ def color?
42
+ @color
43
+ end
44
+
45
+ def colorize(text, code)
46
+ color? ? "\e[#{code}m#{text}\e[0m" : text
47
+ end
48
+ end
6
49
 
7
- require 'cmd_plot/plot'
8
- require 'cmd_plot/bar'
9
- require 'cmd_plot/pcolor'
50
+ require 'cmd_plot/version'
51
+ require 'cmd_plot/data'
52
+ require 'cmd_plot/braille'
53
+ require 'cmd_plot/line_plot'
54
+ require 'cmd_plot/bar_chart'
55
+ require 'cmd_plot/color_map'
56
+ require 'cmd_plot/tables'
57
+ require 'cmd_plot/legacy'
metadata CHANGED
@@ -1,33 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cmd_plot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Buschermoehle
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2018-06-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: This is an interface to quickly visualize data on the command line. It
14
- supports various kinds of plots (curves, bar diagram, histogram, 2D color, etc)
15
- that are presented as text output on the console.
12
+ description: 'Quick visualization of data on the command line: braille line plots
13
+ with several series and date axes, bar charts, histograms, 2D color maps, and table/CSV
14
+ dumps of hashes or ActiveRecord-style records. No dependencies.'
16
15
  email: andreas@buschermoehle.org
17
16
  executables: []
18
17
  extensions: []
19
18
  extra_rdoc_files: []
20
19
  files:
20
+ - CHANGELOG.md
21
+ - LICENSE
22
+ - README.md
21
23
  - lib/cmd_plot.rb
22
- - lib/cmd_plot/bar.rb
23
- - lib/cmd_plot/basic_data.rb
24
- - lib/cmd_plot/pcolor.rb
25
- - lib/cmd_plot/plot.rb
26
- homepage: http://rubygems.org/gems/cmd_plot
24
+ - lib/cmd_plot/bar_chart.rb
25
+ - lib/cmd_plot/braille.rb
26
+ - lib/cmd_plot/color_map.rb
27
+ - lib/cmd_plot/data.rb
28
+ - lib/cmd_plot/legacy.rb
29
+ - lib/cmd_plot/line_plot.rb
30
+ - lib/cmd_plot/tables.rb
31
+ - lib/cmd_plot/version.rb
32
+ homepage: https://github.com/MrBusch/cmdplot
27
33
  licenses:
28
34
  - GPL-2.0
29
- metadata: {}
30
- post_install_message:
35
+ metadata:
36
+ homepage_uri: https://github.com/MrBusch/cmdplot
37
+ source_code_uri: https://github.com/MrBusch/cmdplot
38
+ changelog_uri: https://github.com/MrBusch/cmdplot/blob/main/CHANGELOG.md
39
+ bug_tracker_uri: https://github.com/MrBusch/cmdplot/issues
40
+ rubygems_mfa_required: 'true'
31
41
  rdoc_options: []
32
42
  require_paths:
33
43
  - lib
@@ -35,16 +45,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
35
45
  requirements:
36
46
  - - ">="
37
47
  - !ruby/object:Gem::Version
38
- version: '0'
48
+ version: '2.7'
39
49
  required_rubygems_version: !ruby/object:Gem::Requirement
40
50
  requirements:
41
51
  - - ">="
42
52
  - !ruby/object:Gem::Version
43
53
  version: '0'
44
54
  requirements: []
45
- rubyforge_project:
46
- rubygems_version: 2.6.12
47
- signing_key:
55
+ rubygems_version: 3.6.9
48
56
  specification_version: 4
49
- summary: Command line plotting for quick visualization of data.
57
+ summary: Line plots, bar charts, histograms, color maps and tables in the terminal.
50
58
  test_files: []
data/lib/cmd_plot/bar.rb DELETED
@@ -1,30 +0,0 @@
1
- class CmdPlot::Bar
2
- def initialize(width = 140, height = 40, symbol = '°')
3
- @width = width
4
- @height = height
5
- @symbol = symbol
6
- end
7
-
8
- def bar(height, labels)
9
- # Calculate scaling of data to plot
10
- max_height = height.max.to_f
11
- max_label_length = labels.map { |l| l.length }.max
12
- max_val_length = height.map { |l| l.to_s.length }.max
13
- max_bar_length = (@width - max_label_length - max_val_length - 3)
14
- # Plot each bar
15
- for data in height.zip(labels)
16
- label_fill_space = ' ' * (max_label_length - data[1].length)
17
- value_fill_space = ' ' * (max_val_length - data[0].to_s.length)
18
- bar_plot = @symbol * ((data[0] / max_height) * max_bar_length).round
19
- puts "#{label_fill_space}#{data[1]}: #{value_fill_space}#{data[0]} #{bar_plot}"
20
- end
21
- return nil
22
- end
23
-
24
- def histogram(data, bins = 20)
25
- # Turn data into bins with counts
26
- counts, bin_centers = BasicData.build_hist(data, bins)
27
- # Plot as bar chart
28
- bar(counts, bin_centers.map { |p| p.round(1).to_s })
29
- end
30
- end
@@ -1,33 +0,0 @@
1
- class BasicData
2
-
3
- def self.linspace(min, max, n)
4
- return Array.new(n) { |i| min.to_f + i.to_f * (max.to_f - min.to_f) / (n - 1).to_f }
5
- end
6
-
7
- def self.build_hist(data, bin_spec, maxcap = nil)
8
- min = data.min
9
- max = data.max
10
- if bin_spec.kind_of?(Array)
11
- limits = bin_spec
12
- nr_bins = limits.length - 1
13
- else
14
- limits = BasicData.linspace(min, max, bin_spec + 1)
15
- nr_bins = bin_spec
16
- end
17
- counts = Array.new(nr_bins, 0)
18
- for d in data
19
- bin = limits.map { |x| d < x }.index(true)
20
- if bin.nil?
21
- bin = counts.length
22
- end
23
- if bin == 0
24
- bin = 1
25
- end
26
- counts[bin - 1] += 1
27
- end
28
- counts = counts.map { |v| [v, maxcap].min } unless maxcap.nil?
29
- bin_centers = limits.each_cons(2).to_a.map { |a| (a[0] + a[1]) / 2.0 }
30
- return counts, bin_centers
31
- end
32
-
33
- end
@@ -1,73 +0,0 @@
1
- class CmdPlot::Pcolor
2
-
3
- #red, green, brown, blue, magenta, cyan, gray
4
- TEXT_COLORS = [31, 32, 33, 34, 35, 36, 37]
5
- FILL_SYMBOLS = ['░', '▒', '▓', '█']
6
- MAX_SHADES = TEXT_COLORS.length * FILL_SYMBOLS.length
7
-
8
- def initialize(width = 140, height = 40)
9
- @width = width
10
- @height = height
11
- end
12
-
13
- def pcolor(data, shades = nil)
14
- # Calculate scaling of data to picture
15
- shades = data.flatten.uniq.length if shades.nil?
16
- shades = MAX_SHADES if shades > MAX_SHADES or shades <= 0
17
- x_scale = data[0].length / @width.to_f
18
- y_scale = data.length / @height.to_f
19
- z_max = data.map { |r| r.max }.max
20
- z_min = data.map { |r| r.min }.min
21
- puts "x_scale: #{x_scale}, y_scale: #{y_scale}, z_min: #{z_min}, z_max: #{z_max}"
22
-
23
- # Calculcate scaled data
24
- scaled_data = Array.new(@height) { Array.new(@width) { 0.0 } }
25
- for row in 0..(scaled_data.length - 1)
26
- for col in 0..(scaled_data[row].length - 1)
27
- n = 0.0
28
- lower_row_idx = (row * y_scale).floor
29
- upper_row_idx = [lower_row_idx, ((row + 1) * y_scale).floor - 1].max
30
- lower_col_idx = (col * x_scale).floor
31
- upper_col_idx = [lower_col_idx, ((col + 1) * x_scale).floor - 1].max
32
- for d_row in lower_row_idx..upper_row_idx
33
- for d_col in lower_col_idx..upper_col_idx
34
- scaled_data[row][col] += data[d_row][d_col]
35
- n += 1.0
36
- end
37
- end
38
- if(n > 0)
39
- scaled_data[row][col] /= n
40
- else
41
- scaled_data[row][col] = nil
42
- end
43
- end
44
- end
45
-
46
- # Full picture starts out empty
47
- pic = Array.new(@height) { Array.new(@width) { nil } }
48
- # Run through data and fill in the plotting symbol/color
49
- for row in 0..(scaled_data.length - 1)
50
- for col in 0..(scaled_data[row].length - 1)
51
- val = scaled_data[row][col]
52
- next if val.nil?
53
- fill = ((val - z_min) / (z_max - z_min).to_f * (shades - 1)).round
54
- pic[row][col] = fill
55
- end
56
- end
57
-
58
- # Plot the picture
59
- for row in pic
60
- for col in row
61
- if col.nil?
62
- print ' '
63
- else
64
- color = TEXT_COLORS[col % TEXT_COLORS.length]
65
- sym = FILL_SYMBOLS[col / TEXT_COLORS.length]
66
- print "\e[#{color}m#{sym}\e[0m"
67
- end
68
- end
69
- print "\n"
70
- end
71
- return nil
72
- end
73
- end