histogram-rb 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 697a2cb2bd4da96b74ed8bbbea30058ab1b98c9e83cbaffec4656614152a9d7e
4
+ data.tar.gz: ad1aa054124f41c1e32626d452d5b187b3b154ad32ca722e65eb182a9bc8c0f0
5
+ SHA512:
6
+ metadata.gz: 196ec245d6ffacca8e9032e16fe37351b8eb21c9b48ef60c14c777f3830c6392de6dc2a8dcf64a6733fc4e0b1872b2477779cf54777bc2e424bd336a8a16ea53
7
+ data.tar.gz: 6baf9065d3154e1307875a14bbd49af4b7375fe954db26d517979ac453e459d0e81693648a7eaae75bb70425664707096fab0b9cbd4a3b14cc84840f152f1609
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem "rake", "~> 13.0"
9
+ gem "pry-byebug"
10
+ end
11
+
12
+ group :test do
13
+ gem "minitest", "~> 5.0"
14
+ gem "minitest-reporters"
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 derekstride
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Histogram
2
+ [![Ruby CI](https://github.com/DerekStride/histogram/actions/workflows/ruby.yml/badge.svg)](https://github.com/DerekStride/histogram/actions/workflows/ruby.yml) [![Publish new releases to rubygems](https://github.com/DerekStride/histogram/actions/workflows/publish-gem.yml/badge.svg)](https://github.com/DerekStride/histogram/actions/workflows/publish-gem.yml)
3
+
4
+ Easily create a histogram from an array of values and allow easy rendering to the terminal.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'histogram-rb'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install histogram-rb
21
+
22
+ ## Usage
23
+
24
+ Initialize a Histogram with a list of values:
25
+
26
+ ```ruby
27
+ histogram = Histogram.new(1000.times.map { Random.rand })
28
+ ```
29
+
30
+ Then call render to get a print-friendly string, all parameters are optional.
31
+
32
+ ```ruby
33
+ histogram.render(
34
+ title: "Uniform Distribution",
35
+ precision: 2,
36
+ )
37
+ ```
38
+
39
+ ```
40
+ Uniform Distribution
41
+ ═════════════════════════════════════════════════════════════════════════════════
42
+ x x x x x x
43
+ x x x x x x x
44
+ x x x x x x x
45
+ x x x x x x x
46
+ x x x x x x x
47
+ x x x x x x x
48
+ x x x x x x x
49
+ x x x x x x x
50
+ x x x x x x x
51
+ x x x x x x x
52
+ x x x x x x x
53
+ x x x x x x x
54
+ ─────────────────────────────────────────────────────────────────────────────────
55
+ 0.0-0.14 0.14-0.29 0.29-0.43 0.43-0.57 0.57-0.71 0.71-0.86 0.86-1.0
56
+ (150) (128) (154) (145) (136) (150) (137)
57
+ ```
58
+
59
+ You can use the `border` parameter to specify a `terminal-table` border class to change the output. e.g.
60
+
61
+ ```ruby
62
+ histogram.render(
63
+ title: "Normal Distribution",
64
+ precision: 2,
65
+ border: Terminal::Table::AsciiBorder.new,
66
+ )
67
+ ```
68
+
69
+ ```
70
+ Normal Distribution
71
+ -----------+----------+----------+-----------+-----------+-----------+-----------
72
+ x
73
+ x
74
+ x x
75
+ x x
76
+ x x x
77
+ x x x x
78
+ x x x x
79
+ x x x x x
80
+ x x x x x
81
+ x x x x x x x
82
+ x x x x x x x
83
+ x x x x x x x
84
+ -----------+----------+----------+-----------+-----------+-----------+-----------
85
+ 0.02-0.16 0.16-0.3 0.3-0.44 0.44-0.57 0.57-0.71 0.71-0.85 0.85-0.98
86
+ (26) (51) (108) (141) (81) (73) (24)
87
+ ```
88
+ ## Development
89
+
90
+ After checking out the repo, run `bundle install` to install dependencies. You can also run `bin/console` for an
91
+ interactive prompt that will allow you to experiment.
92
+
93
+ ## Testing
94
+
95
+ Running the unit tests with `bundle exec rake test`.
96
+
97
+ The rendering portion of the histogram is harder to test with unit tests. Instead there is a script that will allow you
98
+ to render histograms easily to enable visual tests.
99
+
100
+ Examples:
101
+
102
+ `bin/test`
103
+ `bin/test -b ascii -p 2`
104
+ `bin/test -t "Example Title" -b thick -p 2`
105
+ `bin/test -t "Example Title" -b ascii -p 3`
106
+ `bin/test -t "Normal Distribution" -d normal -p 2 -c 10000`
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
data/histogram.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/histogram/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "histogram-rb"
7
+ spec.version = Histogram::VERSION
8
+ spec.authors = ["derekstride"]
9
+ spec.email = ["derek@stride.host"]
10
+
11
+ spec.summary = "Easily convert an array of values into a histogram."
12
+ spec.description = "Easily convert an array of values into a histogram."
13
+ spec.homepage = "https://github.com/derekstride/histogram"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject do |f|
21
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
22
+ end
23
+ end
24
+
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency("terminal-table")
30
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Bin = Struct.new(:range, :count)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BinCounter
4
+ extend self
5
+
6
+ def evaluate(values)
7
+ min, max = values.minmax
8
+
9
+ candidate =
10
+ if max - min <= 12 && (max.integer? || min.integer?)
11
+ (max - min).ceil
12
+ elsif max - min <= 1
13
+ 10
14
+ elsif max - min <= 12
15
+ 12
16
+ elsif Math.sqrt(max - min) <= 12
17
+ Math.sqrt(max - min).ceil
18
+ end
19
+
20
+ candidate || [Math.sqrt(values.size).ceil, 12].min
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BinPacker
4
+ attr_reader :bin_count, :min, :max
5
+
6
+ def initialize(bin_count, min, max)
7
+ @bin_count = bin_count
8
+ @max = max
9
+ @min = min
10
+ end
11
+
12
+ def bins(values)
13
+ groups = Array.new(bin_count) do |i|
14
+ f = min + i * bin_width
15
+ range_max = if f.is_a?(Integer)
16
+ f + bin_width - 1
17
+ else
18
+ f + bin_width.prev_float
19
+ end
20
+
21
+ Bin.new(f..range_max, 0)
22
+ end
23
+ binmax = groups.last.range.max
24
+ unless binmax >= max
25
+ groups << if bin_width == 1
26
+ Bin.new(max..max, 0)
27
+ else
28
+ Bin.new(binmax..(binmax + bin_width), 0)
29
+ end
30
+ end
31
+
32
+ values.each do |v|
33
+ bin = groups.detect { |bin| bin.range.include?(v) }
34
+ bin.count += 1
35
+ end
36
+
37
+ groups
38
+ end
39
+
40
+ def bin_width
41
+ if max.is_a?(Float) || min.is_a?(Float)
42
+ (max - min) / bin_count.to_f
43
+ else
44
+ ((max - min) / bin_count.to_f).ceil
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Histogram
4
+ VERSION = "0.1.1"
5
+ end
data/lib/histogram.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "histogram/bin"
4
+ require "histogram/bin_packer"
5
+ require "histogram/bin_counter"
6
+ require "terminal-table"
7
+
8
+ class Histogram
9
+ class Error < StandardError; end
10
+
11
+ attr_reader :bin_count
12
+
13
+ def initialize(values)
14
+ @values = values
15
+ @bin_count = BinCounter.evaluate(values)
16
+ @bin_packer = BinPacker.new(@bin_count, values.min, values.max)
17
+ end
18
+
19
+ def bin_width
20
+ @bin_packer.bin_width
21
+ end
22
+
23
+ def bins
24
+ @bins ||= @bin_packer.bins(values)
25
+ end
26
+
27
+ def render(title: nil, precision: 2, border: Terminal::Table::UnicodeBorder.new)
28
+ largest_bin = bins.max_by(&:count).count
29
+ max_marker_count = [largest_bin, 12].min
30
+ marker_width = largest_bin / max_marker_count
31
+
32
+ rows = max_marker_count.times.map do |y|
33
+ bins.size.times.map do |x|
34
+ value = if (bins[x].count / marker_width.to_f).ceil > y
35
+ "x"
36
+ else
37
+ ""
38
+ end
39
+ { value: value, alignment: :center }
40
+ end
41
+ end.reverse
42
+ rows << bins.map { |bin| "(#{bin.count})" }
43
+
44
+ border.top = false
45
+ border.left = false
46
+ border.right = false
47
+ border.data[:y] = " "
48
+ border.data[:n] = border.data[:nx]
49
+ border.data[:ai] = border.data[:ax]
50
+ border.data[:s] = border.data[:sx]
51
+
52
+ headings = bins.map do |bin|
53
+ min, max = bin.range.minmax
54
+ if min == max
55
+ min.to_s
56
+ else
57
+ "#{min.round(precision)}-#{max.round(precision)}"
58
+ end
59
+ end
60
+ options = {
61
+ headings: headings,
62
+ rows: rows,
63
+ style: {
64
+ border: border,
65
+ },
66
+ }
67
+ options[:title] = title if title
68
+
69
+ output = Terminal::Table.new(**options).to_s
70
+
71
+ lines = output.lines
72
+ lines.delete_at(1) # delete header seperator
73
+ header = if title
74
+ lines.delete_at(1)
75
+ else
76
+ lines.shift
77
+ end
78
+ totals = lines.delete_at(-2)
79
+ lines.last << "\n"
80
+ lines << header
81
+ lines << totals.chomp
82
+ lines.join
83
+ end
84
+
85
+ private
86
+
87
+ attr_reader :values, :title
88
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: histogram-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - derekstride
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: terminal-table
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Easily convert an array of values into a histogram.
28
+ email:
29
+ - derek@stride.host
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - histogram.gemspec
39
+ - lib/histogram.rb
40
+ - lib/histogram/bin.rb
41
+ - lib/histogram/bin_counter.rb
42
+ - lib/histogram/bin_packer.rb
43
+ - lib/histogram/version.rb
44
+ homepage: https://github.com/derekstride/histogram
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://github.com/derekstride/histogram
49
+ source_code_uri: https://github.com/derekstride/histogram
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.3.7
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Easily convert an array of values into a histogram.
69
+ test_files: []