Skip to content

Commit 8bb6e0e

Browse files
authored
Add support for drawing histogram with aggregated (buckets + counts) data (#51)
1 parent 589fe49 commit 8bb6e0e

File tree

5 files changed

+118
-13
lines changed

5 files changed

+118
-13
lines changed

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,56 @@ In [9]: print(plotille.hist(np.random.normal(size=10000)))
272272

273273
![Example hist](https://github.com/tammoippen/plotille/raw/master/imgs/hist.png)
274274

275+
#### Hist (aggregated)
276+
277+
This function allows you to create a histogram when your data is already aggregated (aka you don't have access to raw values, but you have access to bins and counts for each bin).
278+
279+
This comes handy when working with APIs such as [OpenTelemetry Metrics API](https://opentelemetry-python.readthedocs.io/en/latest/api/metrics.html)
280+
where views such as [ExplicitBucketHistogramAggregation](https://opentelemetry-python.readthedocs.io/en/latest/sdk/metrics.view.html#opentelemetry.sdk.metrics.view.ExplicitBucketHistogramAggregation)
281+
only expose access to aggregated values (counts for each bin / bucket).
282+
283+
```python
284+
In [8]: plotille.hist_aggregated?
285+
Signature:
286+
plotille.hist_aggregated(
287+
counts,
288+
bins,
289+
width=80,
290+
log_scale=False,
291+
linesep='\n',
292+
lc=None,
293+
bg=None,
294+
color_mode='names',
295+
)
296+
Docstring:
297+
Create histogram for aggregated data.
298+
299+
Parameters:
300+
counts: List[int] Counts for each bucket.
301+
bins: List[float] Limits for the bins for the provided counts: limits for
302+
bin `i` are `[bins[i], bins[i+1])`.
303+
Hence, `len(bins) == len(counts) + 1`.
304+
width: int The number of characters for the width (columns).
305+
log_scale: bool Scale the histogram with `log` function.
306+
linesep: str The requested line seperator. default: os.linesep
307+
lc: multiple Give the line color.
308+
bg: multiple Give the background color.
309+
color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'
310+
see plotille.color.__docs__
311+
Returns:
312+
str: histogram over `X` from left to right.
313+
314+
In [9]: counts = [1945, 0, 0, 0, 0, 0, 10555, 798, 0, 28351, 0]
315+
In [10]: bins = [float('-inf'), 10, 50, 100, 200, 300, 500, 800, 1000, 2000, 10000, float('+inf')]
316+
In [11]: print(plotille.hist_aggregated(counts, bins))
317+
```
318+
319+
Keep in mind that there must always be n+1 bins (n is a total number of count values, 11 in the example above).
320+
321+
In this example the first bin is from [-inf, 10) with a count of 1945 and the last bin is from [10000, +inf] with a count of 0.
322+
323+
![Example hist](https://github.com/tammoippen/plotille/raw/master/imgs/hist_aggregated.png)
324+
275325
#### Histogram
276326
277327
There is also another more 'usual' histogram function available:

imgs/hist_aggregated.png

31.4 KB
Loading

plotille/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ._cmaps import Colormap, ListedColormap
2828
from ._colors import color, hsl
2929
from ._figure import Figure
30-
from ._graphs import hist, histogram, plot, scatter
30+
from ._graphs import hist, hist_aggregated, histogram, plot, scatter
3131

3232

3333
__all__ = [
@@ -36,6 +36,7 @@
3636
'Colormap',
3737
'Figure',
3838
'hist',
39+
'hist_aggregated',
3940
'histogram',
4041
'hsl',
4142
'ListedColormap',

plotille/_graphs.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,23 @@
3232
from ._util import hist as compute_hist
3333

3434

35-
def hist(X, bins=40, width=80, log_scale=False, linesep=os.linesep,
36-
lc=None, bg=None, color_mode='names'):
37-
"""Create histogram over `X` from left to right
38-
39-
The values on the left are the center of the bucket, i.e. `(bin[i] + bin[i+1]) / 2`.
40-
The values on the right are the total counts of this bucket.
35+
def hist_aggregated(counts, bins, width=80, log_scale=False, linesep=os.linesep,
36+
lc=None, bg=None, color_mode='names'):
37+
"""
38+
Create histogram for aggregated data.
4139
4240
Parameters:
43-
X: List[float] The items to count over.
44-
bins: int The number of bins to put X entries in (rows).
41+
counts: List[int] Counts for each bucket.
42+
bins: List[float] Limits for the bins for the provided counts: limits for
43+
bin `i` are `[bins[i], bins[i+1])`.
44+
Hence, `len(bins) == len(counts) + 1`.
4545
width: int The number of characters for the width (columns).
4646
log_scale: bool Scale the histogram with `log` function.
4747
linesep: str The requested line seperator. default: os.linesep
4848
lc: multiple Give the line color.
4949
bg: multiple Give the background color.
5050
color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'
5151
see plotille.color.__docs__
52-
5352
Returns:
5453
str: histogram over `X` from left to right.
5554
"""
@@ -58,14 +57,18 @@ def _scale(a):
5857
return log(a)
5958
return a
6059

60+
h = counts
61+
b = bins
62+
6163
ipf = InputFormatter()
62-
h, b = compute_hist(X, bins)
6364
h_max = _scale(max(h)) or 1
6465
delta = b[-1] - b[0]
6566

67+
bins_count = len(h)
68+
6669
canvas = [' bucket | {} {}'.format('_' * width, 'Total Counts')]
6770
lasts = ['', '⠂', '⠆', '⠇', '⡇', '⡗', '⡷', '⡿']
68-
for i in range(bins):
71+
for i in range(bins_count):
6972
hight = int(width * 8 * _scale(h[i]) / h_max)
7073
canvas += ['[{}, {}) | {} {}'.format(
7174
ipf.fmt(b[i], delta=delta, chars=8, left=True),
@@ -77,6 +80,32 @@ def _scale(a):
7780
return linesep.join(canvas)
7881

7982

83+
def hist(X, bins=40, width=80, log_scale=False, linesep=os.linesep,
84+
lc=None, bg=None, color_mode='names'):
85+
"""Create histogram over `X` from left to right
86+
87+
The values on the left are the center of the bucket, i.e. `(bin[i] + bin[i+1]) / 2`.
88+
The values on the right are the total counts of this bucket.
89+
90+
Parameters:
91+
X: List[float] The items to count over.
92+
bins: int The number of bins to put X entries in (rows).
93+
width: int The number of characters for the width (columns).
94+
log_scale: bool Scale the histogram with `log` function.
95+
linesep: str The requested line seperator. default: os.linesep
96+
lc: multiple Give the line color.
97+
bg: multiple Give the background color.
98+
color_mode: str Specify color input mode; 'names' (default), 'byte' or 'rgb'
99+
see plotille.color.__docs__
100+
101+
Returns:
102+
str: histogram over `X` from left to right.
103+
"""
104+
counts, bins = compute_hist(X, bins)
105+
return hist_aggregated(counts=counts, bins=bins, width=width, log_scale=log_scale,
106+
linesep=linesep, lc=lc, bg=bg, color_mode=color_mode)
107+
108+
80109
def histogram(X, bins=160, width=80, height=40, X_label='X', Y_label='Counts', linesep=os.linesep,
81110
x_min=None, x_max=None, y_min=None, y_max=None,
82111
lc=None, bg=None, color_mode='names'):

tests/test_hist.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pendulum import datetime, duration
77
import pytest
88

9-
from plotille import hist
9+
from plotille import hist, hist_aggregated
1010

1111
try:
1212
import numpy as np
@@ -30,6 +30,24 @@ def expected_hist(cleandoc):
3030
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾""") # noqa: E501
3131

3232

33+
@pytest.fixture()
34+
def expected_hist_aggregated(cleandoc):
35+
return cleandoc("""
36+
bucket | ________________________________________________________________________________ Total Counts
37+
[-inf , 10) | ⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 1945
38+
[10 , 50) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
39+
[50 , 100) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
40+
[100 , 200) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
41+
[200 , 300) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
42+
[300 , 500) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
43+
[500 , 800) | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 10555
44+
[800 , 1000) | ⣿⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 798
45+
[1000 , 2000) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
46+
[2000 , 10000) | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀ 28351
47+
[10000 , inf) | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
48+
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾""") # noqa: E501
49+
50+
3351
@pytest.mark.skipif(not have_numpy, reason='No numpy installed.')
3452
def test_timehist_numpy(expected_hist):
3553
day = np.timedelta64(1, 'D')
@@ -65,3 +83,10 @@ def test_timehist_orig_dt(expected_hist):
6583
# print()
6684
# print(res)
6785
assert expected_hist == res
86+
87+
88+
def test_hist_aggregated(expected_hist, expected_hist_aggregated):
89+
counts = [1945, 0, 0, 0, 0, 0, 10555, 798, 0, 28351, 0]
90+
bins = [float('-inf'), 10, 50, 100, 200, 300, 500, 800, 1000, 2000, 10000, float('+inf')]
91+
res = hist_aggregated(counts=counts, bins=bins)
92+
assert expected_hist_aggregated == res

0 commit comments

Comments
 (0)