youplot 0.4.5 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725b5bffb6d597af728d9b57aedea75dbdf62182ab5c211c4ecdca35b686cb69
4
- data.tar.gz: 7eb1d48b2ba900f9955b0cb257f95d79646101598b86d841951b3271b55e0d2b
3
+ metadata.gz: c344612a2c9055f37b06ea9e6cceb5199e18a261b1ce4bb4c2d3ec74f77657ad
4
+ data.tar.gz: 736041b6139ebc559b78176f3ed0908469e6fc8c21e0119ff4f62641a357ffc8
5
5
  SHA512:
6
- metadata.gz: 0ead7ed4edd2948b03f14b77256b03aaaef25f69795c61483b1440c2f6572a29b99d90336a0852e56f5f1c1331d8a9ae47bac421e2c444c282e1814658bd464e
7
- data.tar.gz: e76b86a9c7ef5df17cc97e1f5b5c860d82c7d7650b6606d687d26ff24664032a52053f3c00bdbd8d072d8b95ce4ab2506f4119ae7ccf75791fcd4e9bd1838d29
6
+ metadata.gz: b318d624ece5511feacd5f1a9c50ea929b6c1c1f05d951a50f61869e52a66b8adf7d613c3042345b0455b2c36eca56088144ee0967f188c3cd7c12910ecf7579
7
+ data.tar.gz: 2570196b9263edea40227244909e5e9fe287854e7daa2d5d2f44bd1fa617faa5716b38979eed40a84c1687aac71be8b6005ed902d83aa1f6dce54fea60f4dd66
data/LICENSE.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
3
  Copyright (c) 2020 kojix2
4
+ Copyright (c) 2025 Red Data Tools
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  <div align="center">
2
2
  <img src="logo.svg">
3
3
  <hr>
4
- <img alt="Build Status" src="https://github.com/red-data-tools/YouPlot/workflows/test/badge.svg">
4
+ <a href="https://github.com/red-data-tools/YouPlot/actions/workflows/ci.yml"><img alt="Build Status" src="https://github.com/red-data-tools/YouPlot/workflows/test/badge.svg"></a>
5
5
  <a href="https://rubygems.org/gems/youplot/"><img alt="Gem Version" src="https://badge.fury.io/rb/youplot.svg"></a>
6
- <a href="https://zenodo.org/badge/latestdoi/283230219"><img alt="DOI" src="https://zenodo.org/badge/283230219.svg"></a>
7
6
  <a href="https://rubydoc.info/gems/youplot/"><img alt="Docs Stable" src="https://img.shields.io/badge/docs-stable-blue.svg"></a>
8
7
  <a href="LICENSE.txt"><img alt="The MIT License" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
9
8
 
@@ -14,10 +13,30 @@
14
13
 
15
14
  ## Installation
16
15
 
16
+ ```
17
+ brew install youplot
18
+ ```
19
+
17
20
  ```
18
21
  gem install youplot
19
22
  ```
20
23
 
24
+ ```
25
+ nix shell nixpkgs#youplot
26
+ ```
27
+
28
+ ```
29
+ guix install youplot
30
+ ```
31
+
32
+ ```
33
+ conda install -c conda-forge ruby
34
+ conda install -c conda-forge compilers
35
+ gem install youplot
36
+ ```
37
+
38
+ :crystal_ball: [YouPlot2](https://github.com/red-data-tools/YouPlot2) - Experimental project with pre-built binaries
39
+
21
40
  ## Quick Start
22
41
 
23
42
  <img alt="barplot" src="https://user-images.githubusercontent.com/5798442/101999903-d36a2d00-3d24-11eb-9361-b89116f44122.png" width=160> <img alt="histogram" src="https://user-images.githubusercontent.com/5798442/101999820-21cafc00-3d24-11eb-86db-e410d19b07df.png" width=160> <img alt="scatter" src="https://user-images.githubusercontent.com/5798442/101999827-27284680-3d24-11eb-9903-551857eaa69c.png" width=160> <img alt="density" src="https://user-images.githubusercontent.com/5798442/101999828-2abbcd80-3d24-11eb-902c-2f44266fa6ae.png" width=160> <img alt="boxplot" src="https://user-images.githubusercontent.com/5798442/101999830-2e4f5480-3d24-11eb-8891-728c18bf5b35.png" width=160>
@@ -37,13 +56,19 @@ curl -sL https://git.io/ISLANDScsv \
37
56
  <img alt="barplot" src="https://user-images.githubusercontent.com/5798442/101999903-d36a2d00-3d24-11eb-9361-b89116f44122.png">
38
57
  </p>
39
58
 
59
+ For offline users: sorts files in a directory by size and shows a bar graph.
60
+
61
+ ```sh
62
+ ls -l | awk '{print $9, $5}' | sort -nk 2 | uplot bar -d ' '
63
+ ```
64
+
40
65
  ### histogram
41
66
 
42
67
  ```sh
43
68
  echo -e "from numpy import random;" \
44
69
  "n = random.randn(10000);" \
45
70
  "print('\\\n'.join(str(i) for i in n))" \
46
- | python \
71
+ | python3 \
47
72
  | uplot hist --nbins 20
48
73
  ```
49
74
 
@@ -63,6 +88,17 @@ curl -sL https://git.io/AirPassengers \
63
88
  <img alt="lineplot" src="https://user-images.githubusercontent.com/5798442/101999825-24c5ec80-3d24-11eb-99f4-c642e8d221bc.png">
64
89
  </p>
65
90
 
91
+ For offline users: calculates sin values from 0 to 2*pi and plots a sine wave.
92
+
93
+ ```sh
94
+ python3 - <<'PY' | uplot line
95
+ from math import sin, pi
96
+
97
+ for i in range(101):
98
+ print(f"{i*pi/50}\t{sin(i*pi/50)}")
99
+ PY
100
+ ```
101
+
66
102
  ### scatter
67
103
 
68
104
  ```sh
@@ -75,6 +111,13 @@ curl -sL https://git.io/IRIStsv \
75
111
  <img alt="scatter" src="https://user-images.githubusercontent.com/5798442/101999827-27284680-3d24-11eb-9903-551857eaa69c.png">
76
112
  </p>
77
113
 
114
+
115
+ For offline users:
116
+
117
+ ```sh
118
+ cat test/fixtures/iris.csv | cut -f1-4 -d, | uplot scatter -H -d, -t IRIS
119
+ ```
120
+
78
121
  ### density
79
122
 
80
123
  ```sh
@@ -87,6 +130,12 @@ curl -sL https://git.io/IRIStsv \
87
130
  <img alt="density" src="https://user-images.githubusercontent.com/5798442/101999828-2abbcd80-3d24-11eb-902c-2f44266fa6ae.png">
88
131
  </p>
89
132
 
133
+ For offline users:
134
+
135
+ ```sh
136
+ cat test/fixtures/iris.csv | cut -f1-4 -d, | uplot density -H -d, -t IRIS
137
+ ```
138
+
90
139
  ### boxplot
91
140
 
92
141
  ```sh
@@ -99,8 +148,22 @@ curl -sL https://git.io/IRIStsv \
99
148
  <img alt="boxplot" src="https://user-images.githubusercontent.com/5798442/101999830-2e4f5480-3d24-11eb-8891-728c18bf5b35.png">
100
149
  </p>
101
150
 
151
+ For offline users:
152
+
153
+ ```sh
154
+ cat test/fixtures/iris.csv | cut -f1-4 -d, | uplot boxplot -H -d, -t IRIS
155
+ ```
156
+
102
157
  ### count
103
158
 
159
+ Count processes by user ID.
160
+
161
+ ```sh
162
+ ps aux | awk '{print $1}' | uplot count
163
+ ```
164
+
165
+ Count the number of chromosomes where genes are located.
166
+
104
167
  ```sh
105
168
  cat gencode.v35.annotation.gff3 \
106
169
  | grep -v '#' | grep 'gene' | cut -f1 \
@@ -111,7 +174,6 @@ cat gencode.v35.annotation.gff3 \
111
174
  <img alt="count" src="https://user-images.githubusercontent.com/5798442/101999832-30b1ae80-3d24-11eb-96fe-e5000bed1f5c.png">
112
175
  </p>
113
176
 
114
- In this example, YouPlot counts the number of chromosomes where genes are located.
115
177
  * [GENCODE - Human Release](https://www.gencodegenes.org/human/)
116
178
 
117
179
  Note: `count` is not very fast because it runs in a Ruby script.
@@ -130,7 +192,7 @@ cat gencode.v35.annotation.gff3 | grep -v '#' | grep 'gene' | cut -f1 \
130
192
  `uplot` is the shortened form of `youplot`. You can use either.
131
193
 
132
194
  | Command | Description |
133
- |------------------------------------------------|-----------------------------------|
195
+ | ---------------------------------------------- | --------------------------------- |
134
196
  | `cat data.tsv \| uplot <command> [options]` | Take input from stdin |
135
197
  | `uplot <command> [options] data.tsv ...` | Take input from files |
136
198
  | `pipeline1 \| uplot <command> -O \| pipeline2` | Outputs data from stdin to stdout |
@@ -139,25 +201,25 @@ cat gencode.v35.annotation.gff3 | grep -v '#' | grep 'gene' | cut -f1 \
139
201
 
140
202
  The following sub-commands are available.
141
203
 
142
- | command | short | how it works |
143
- |-----------|-------|----------------------------------------|
144
- | barplot | bar | draw a horizontal barplot |
145
- | histogram | hist | draw a horizontal histogram |
146
- | lineplot | line | draw a line chart |
147
- | lineplots | lines | draw a line chart with multiple series |
148
- | scatter | s | draw a scatter plot |
149
- | density | d | draw a density plot |
150
- | boxplot | box | draw a horizontal boxplot |
151
- | | | |
204
+ | command | short | how it works |
205
+ | --------- | ----- | -------------------------------------------------------- |
206
+ | barplot | bar | draw a horizontal barplot |
207
+ | histogram | hist | draw a horizontal histogram |
208
+ | lineplot | line | draw a line chart |
209
+ | lineplots | lines | draw a line chart with multiple series |
210
+ | scatter | s | draw a scatter plot |
211
+ | density | d | draw a density plot |
212
+ | boxplot | box | draw a horizontal boxplot |
213
+ | | | |
152
214
  | count | c | draw a barplot based on the number of occurrences (slow) |
153
- | | | |
154
- | colors | color | show the list of available colors |
215
+ | | | |
216
+ | colors | color | show the list of available colors |
155
217
 
156
218
  ### Output the plot
157
219
 
158
220
  * `-o`
159
221
  * By default, the plot is output to **standard error output**.
160
- * If you want to output to standard input, Use hyphen ` -o -` or no argument `uplot s -o | `.
222
+ * If you want to output to standard output, Use hyphen ` -o -` or no argument `uplot s -o | `.
161
223
 
162
224
  ### Output the input data
163
225
 
@@ -240,13 +302,14 @@ Please feel free to send us your pull requests.
240
302
 
241
303
  ### Development
242
304
 
305
+ Fork the main repository by clicking the Fork button.
306
+
243
307
  ```sh
244
- # fork the main repository by clicking the Fork button.
245
308
  git clone https://github.com/your_name/YouPlot
246
- bundle install # Install the gem dependencies
247
- bundle exec rake test # Run the test
248
- bundle exec rake install # Installation from source code
249
- bundle exec exe/uplot # Run youplot (Try out the edited code)
309
+ bundle install
310
+ bundle exec rake test
311
+ bundle exec rake install
312
+ bundle exec exe/uplot
250
313
  ```
251
314
 
252
315
  Do you need commit rights to my repository?
@@ -255,7 +318,7 @@ bundle exec exe/uplot # Run youplot (Try out the edited code)
255
318
 
256
319
  ### Acknowledgements
257
320
 
258
- * [sampo grafiikka](https://jypg.net/sampo_grafiikka) - Project logo creation
321
+ * [sampo grafiikka](https://lepo.sampo-grafiikka.com/) - Project logo creation
259
322
  * [yutaas](https://github.com/yutaas) - English proofreading
260
323
 
261
324
  ## License
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YouPlot
4
+ module Aggregation
5
+ module_function
6
+
7
+ def count_values(arr, tally: true, reverse: false)
8
+ # tally was added in Ruby 2.7
9
+ result = \
10
+ if tally && Enumerable.method_defined?(:tally)
11
+ arr.tally
12
+ else
13
+ # value_counts Enumerable::Statistics
14
+ arr.value_counts(dropna: false)
15
+ end
16
+
17
+ sort_cache = {}
18
+
19
+ # sorting
20
+ result = result.sort do |a, b|
21
+ # compare values
22
+ r = b[1] <=> a[1]
23
+ # If the values are the same, compare by name
24
+ r = natural_compare(a[0], b[0], sort_cache) if r.zero?
25
+ r
26
+ end
27
+
28
+ # --reverse option
29
+ result.reverse! if reverse
30
+
31
+ # prepare for barplot
32
+ result.transpose
33
+ end
34
+
35
+ # Natural order comparison for tie-breaking when counts are equal.
36
+ # Fast paths handle text-only and pure numeric labels.
37
+ # Mixed labels still use chunked comparison (e.g. "chr1" vs "chr10").
38
+ def natural_compare(a, b, cache = nil)
39
+ aa = natural_sort_key(a, cache)
40
+ bb = natural_sort_key(b, cache)
41
+
42
+ # Fast path: both labels are text-only, so plain string comparison is enough.
43
+ return aa[:string] <=> bb[:string] if aa[:type] == :text && bb[:type] == :text
44
+
45
+ # Fast path: both labels are pure numbers, so compare numerically first.
46
+ if aa[:type] == :numeric && bb[:type] == :numeric
47
+ r = aa[:numeric] <=> bb[:numeric]
48
+ return r unless r.zero?
49
+
50
+ # Tiebreaker for equivalent numeric values (e.g. "1" and "01")
51
+ return aa[:string] <=> bb[:string]
52
+ end
53
+
54
+ # Fallback path: at least one label mixes text and digits.
55
+ ta = ensure_natural_tokens(aa)
56
+ tb = ensure_natural_tokens(bb)
57
+ max = [ta.size, tb.size].max
58
+
59
+ 0.upto(max - 1) do |i|
60
+ xa = ta[i]
61
+ xb = tb[i]
62
+
63
+ return -1 if xa.nil?
64
+ return 1 if xb.nil?
65
+
66
+ r = if xa[0] == :num && xb[0] == :num
67
+ compare_integer_strings(xa[1], xb[1])
68
+ else
69
+ xa[1] <=> xb[1]
70
+ end
71
+
72
+ return r unless r.zero?
73
+ end
74
+
75
+ aa[:string] <=> bb[:string]
76
+ end
77
+
78
+ # Classifies a value for natural sorting and caches the result per label.
79
+ def natural_sort_key(value, cache = nil)
80
+ str = value.to_s
81
+ return cache[str] if cache && cache.key?(str)
82
+
83
+ key = if str.match?(/\d/)
84
+ numeric = parse_numeric(str)
85
+ if numeric
86
+ # Pure numeric labels get a dedicated fast path.
87
+ { type: :numeric, string: str, numeric: numeric }
88
+ else
89
+ # Mixed labels fall back to chunked natural comparison.
90
+ { type: :mixed, string: str, tokens: nil }
91
+ end
92
+ else
93
+ # Text-only labels get a dedicated fast path.
94
+ { type: :text, string: str, tokens: nil }
95
+ end
96
+
97
+ cache ? cache[str] = key : key
98
+ end
99
+
100
+ # Memoizes token pairs for fallback chunked comparison.
101
+ def ensure_natural_tokens(key)
102
+ key[:tokens] ||= natural_tokens(key[:string])
103
+ end
104
+
105
+ # Parses a string as a numeric value if it matches pure number format.
106
+ # Returns Float or nil.
107
+ def parse_numeric(str)
108
+ return nil unless str.match?(/\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)\z/)
109
+
110
+ str.to_f
111
+ end
112
+
113
+ # Splits a string into [type, token] pairs for natural comparison.
114
+ # Type is :num for digit-only chunks, :text for anything else.
115
+ # E.g. "chr10" => [[:text, "chr"], [:num, "10"]]
116
+ def natural_tokens(str)
117
+ str.scan(/\d+|\D+/).map do |tok|
118
+ kind = tok.match?(/\A\d+\z/) ? :num : :text
119
+ [kind, tok]
120
+ end
121
+ end
122
+
123
+ # Compares two numeric strings, handling leading zeros.
124
+ # Order: by length (sans leading zeros), then numeric value, then original.
125
+ def compare_integer_strings(a, b)
126
+ aa = a.sub(/\A0+/, '')
127
+ bb = b.sub(/\A0+/, '')
128
+ aa = '0' if aa.empty?
129
+ bb = '0' if bb.empty?
130
+
131
+ r = aa.length <=> bb.length
132
+ return r unless r.zero?
133
+
134
+ r = aa <=> bb
135
+ return r unless r.zero?
136
+
137
+ a <=> b
138
+ end
139
+ end
140
+ end
@@ -3,7 +3,7 @@
3
3
  # UnicodePlot - Plot your data by Unicode characters
4
4
  # https://github.com/red-data-tools/unicode_plot.rb
5
5
 
6
- require_relative 'processing'
6
+ require_relative '../aggregation'
7
7
  require 'unicode_plot'
8
8
 
9
9
  # If the line color is specified as a number, the program will display an error
@@ -40,13 +40,14 @@ module YouPlot
40
40
  series = data.series
41
41
  # `uplot count`
42
42
  if count
43
- series = Processing.count_values(series[0], reverse: reverse)
43
+ series = YouPlot::Aggregation.count_values(series[0], reverse: reverse)
44
44
  params.title = headers[0] if headers
45
45
  end
46
46
  if series.size == 1
47
47
  # If there is only one series.use the line number for label.
48
48
  params.title ||= headers[0] if headers
49
49
  labels = Array.new(series[0].size) { |i| (i + 1).to_s }
50
+ raw_values = series[0]
50
51
  values = series[0].map(&:to_f)
51
52
  else
52
53
  # If there are 2 or more series...
@@ -61,11 +62,28 @@ module YouPlot
61
62
  end
62
63
  params.title ||= headers[y_col] if headers
63
64
  labels = series[x_col]
64
- values = series[y_col].map(&:to_f)
65
+ raw_values = series[y_col]
66
+ values = if count
67
+ series[y_col].map(&:to_i)
68
+ else
69
+ series[y_col].map(&:to_f)
70
+ end
65
71
  end
72
+ values = values.map(&:to_i) if count || integer_display_values?(values, raw_values)
66
73
  ::UnicodePlot.barplot(labels, values, **params.to_hc)
67
74
  end
68
75
 
76
+ # True only for integer literals: "3" => true, "3.0" => false.
77
+ def integer_display_values?(values, raw_values)
78
+ values.all? { |v| v.finite? && v == v.to_i } &&
79
+ raw_values.all? { |v| integer_literal?(v) }
80
+ end
81
+
82
+ def integer_literal?(value)
83
+ # Matches "3" and "-12", but not "3.0".
84
+ value.to_s.match?(/\A[+-]?\d+\z/)
85
+ end
86
+
69
87
  def histogram(data, params)
70
88
  headers = data.headers
71
89
  series = data.series
@@ -200,27 +218,37 @@ module YouPlot
200
218
 
201
219
  def check_series_size(data, fmt)
202
220
  series = data.series
203
- if series.size == 1
204
- warn <<~EOS
205
- YouPlot: There is only one series of input data. Please check the delimiter.
206
-
207
- Headers: \e[35m#{data.headers.inspect}\e[0m
208
- The first item is: \e[35m\"#{series[0][0]}\"\e[0m
209
- The last item is : \e[35m\"#{series[0][-1]}\"\e[0m
210
- EOS
211
- # NOTE: Error messages cannot be colored.
212
- YouPlot.run_as_executable ? exit(1) : raise(Error)
213
- end
214
- if fmt == 'xyxy' && series.size.odd?
215
- warn <<~EOS
216
- YouPlot: In the xyxy format, the number of series must be even.
217
-
218
- Number of series: \e[35m#{series.size}\e[0m
219
- Headers: \e[35m#{data.headers.inspect}\e[0m
220
- EOS
221
- # NOTE: Error messages cannot be colored.
222
- YouPlot.run_as_executable ? exit(1) : raise(Error)
223
- end
221
+ raise_if_single_series(data, series) if series.size == 1
222
+ raise_if_odd_series_for_xyxy(data, fmt, series)
223
+ end
224
+
225
+ def raise_if_single_series(data, series)
226
+ warn <<~EOS
227
+ YouPlot: There is only one series of input data. Please check the delimiter.
228
+
229
+ Headers: \e[35m#{data.headers.inspect}\e[0m
230
+ The first item is: \e[35m\"#{series[0][0]}\"\e[0m
231
+ The last item is : \e[35m\"#{series[0][-1]}\"\e[0m
232
+ EOS
233
+ raise_plot_error
234
+ end
235
+
236
+ def raise_if_odd_series_for_xyxy(data, fmt, series)
237
+ return unless fmt == 'xyxy'
238
+ return if series.size.even?
239
+
240
+ warn <<~EOS
241
+ YouPlot: In the xyxy format, the number of series must be even.
242
+
243
+ Number of series: \e[35m#{series.size}\e[0m
244
+ Headers: \e[35m#{data.headers.inspect}\e[0m
245
+ EOS
246
+ raise_plot_error
247
+ end
248
+
249
+ def raise_plot_error
250
+ # NOTE: Error messages cannot be colored.
251
+ YouPlot.run_as_executable ? exit(1) : raise(Error)
224
252
  end
225
253
  end
226
254
  end