Skip to content

Commit 7de14ff

Browse files
committed
Provide labels for aggregated hist
fixes #62
1 parent 4131aa1 commit 7de14ff

File tree

2 files changed

+135
-19
lines changed

2 files changed

+135
-19
lines changed

plotille/_graphs.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636

3737
def hist_aggregated(
3838
counts: list[int],
39-
bins: Sequence[float],
39+
bins: Sequence[float] | None = None,
40+
labels: Sequence[str] | None = None,
4041
width: int = 80,
4142
log_scale: bool = False,
4243
linesep: str = os.linesep,
@@ -48,11 +49,15 @@ def hist_aggregated(
4849
"""
4950
Create histogram for aggregated data.
5051
52+
**Either provide bins xor labels.**
53+
5154
Parameters:
5255
counts: List[int] Counts for each bucket.
53-
bins: List[float] Limits for the bins for the provided counts: limits for
56+
bins: List[float] | None Limits for the bins for the provided counts: limits for
5457
bin `i` are `[bins[i], bins[i+1])`.
5558
Hence, `len(bins) == len(counts) + 1`.
59+
labels: List[str] | None Labels for the bins for the provided counts
60+
Hence, `len(labels) == len(counts)`.
5661
width: int The number of characters for the width (columns).
5762
log_scale: bool Scale the histogram with `log` function.
5863
linesep: str The requested line separator. default: os.linesep
@@ -73,37 +78,59 @@ def _scale(a: int) -> float | int:
7378
if meta is None:
7479
meta = DataMetadata(is_datetime=False)
7580

76-
h = counts
77-
b = bins
81+
assert not (bins is None and labels is None), (
82+
"At least one of bins/labels has to be set"
83+
)
84+
assert not (bins is not None and labels is not None), (
85+
"At most one of bins/labels has to be set"
86+
)
7887

7988
ipf = InputFormatter()
80-
h_max = _scale(max(h)) or 1
81-
max_ = b[-1]
82-
min_ = b[0]
83-
# bins are always normalized to float
84-
delta = max_ - min_
85-
delta_display = timedelta(seconds=delta) if meta.is_datetime else delta
89+
h_max = _scale(max(counts)) or 1
90+
delta_display = None
91+
l_max = None
92+
if bins:
93+
max_ = bins[-1]
94+
min_ = bins[0]
95+
# bins are always normalized to float
96+
delta = max_ - min_
97+
delta_display = timedelta(seconds=delta) if meta.is_datetime else delta
98+
canvas = [" bucket | {} {}".format("_" * width, "Total Counts")]
99+
else:
100+
assert labels
101+
l_max = max(5, max(len(label) for label in labels))
102+
canvas = [
103+
"{}| {} {}".format("label".ljust(l_max + 1), "_" * width, "Total Counts")
104+
]
86105

87-
bins_count = len(h)
106+
bins_count = len(counts)
88107

89-
canvas = [" bucket | {} {}".format("_" * width, "Total Counts")]
90108
lasts = ["", "⠂", "⠆", "⠇", "⡇", "⡗", "⡷", "⡿"]
91109
for i in range(bins_count):
92-
height = int(width * 8 * _scale(h[i]) / h_max)
93-
canvas += [
94-
"[{}, {}) | {} {}".format(
110+
height = int(width * 8 * _scale(counts[i]) / h_max)
111+
if bins:
112+
assert delta_display is not None
113+
line_start = "[{}, {})".format(
95114
ipf.fmt(
96-
meta.convert_for_display(b[i]),
115+
meta.convert_for_display(bins[i]),
97116
delta=delta_display,
98117
chars=8,
99118
left=True,
100119
),
101120
ipf.fmt(
102-
meta.convert_for_display(b[i + 1]),
121+
meta.convert_for_display(bins[i + 1]),
103122
delta=delta_display,
104123
chars=8,
105124
left=False,
106125
),
126+
)
127+
else:
128+
assert labels is not None
129+
assert l_max is not None
130+
line_start = ipf.fmt(labels[i], delta=None, chars=l_max, left=True)
131+
canvas += [
132+
"{} | {} {}".format(
133+
line_start,
107134
color(
108135
"⣿" * (height // 8) + lasts[height % 8],
109136
fg=lc,
@@ -115,10 +142,14 @@ def _scale(a: int) -> float | int:
115142
bg=bg,
116143
mode=color_mode,
117144
),
118-
h[i],
145+
counts[i],
119146
)
120147
]
121-
canvas += ["‾" * (2 * 8 + 2 + 3 + width + 12)]
148+
if bins:
149+
canvas += ["‾" * (2 * 8 + 2 + 3 + width + 12)]
150+
else:
151+
assert l_max is not None
152+
canvas += ["‾" * (l_max + 3 + width + 12)]
122153
return linesep.join(canvas)
123154

124155

tests/test_hist.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,88 @@ def test_hist_aggregated(expected_hist, expected_hist_aggregated):
8888
]
8989
res = hist_aggregated(counts=counts, bins=bins)
9090
assert expected_hist_aggregated == res
91+
92+
93+
def test_hist_aggregated_labels(cleandoc, expected_hist_aggregated):
94+
counts = [1945, 0, 0, 0, 0, 0, 10555, 798, 0, 28351, 0]
95+
labels = [
96+
"-inf",
97+
"10",
98+
"50",
99+
"100",
100+
"200",
101+
"300",
102+
"500",
103+
"800",
104+
"1000",
105+
"2000",
106+
"10000",
107+
]
108+
expected = cleandoc("""
109+
label | ________________________________________________________________________________ Total Counts
110+
-inf | ⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 1945
111+
10 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
112+
50 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
113+
100 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
114+
200 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
115+
300 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
116+
500 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 10555
117+
800 | ⣿⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 798
118+
1000 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
119+
2000 | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀ 28351
120+
10000 | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
121+
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾""")
122+
res = hist_aggregated(counts=counts, labels=labels)
123+
assert expected == res
124+
125+
126+
def test_hist_aggregated_small_labels(cleandoc, expected_hist_aggregated):
127+
counts = [1945, 0, 0, 0, 0, 0, 10555, 798, 0, 28351, 0]
128+
labels = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"]
129+
expected = cleandoc("""
130+
label | ________________________________________________________________________________ Total Counts
131+
a | ⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 1945
132+
b | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
133+
c | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
134+
d | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
135+
e | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
136+
f | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
137+
g | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 10555
138+
h | ⣿⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 798
139+
i | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
140+
j | ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀ 28351
141+
k | ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ 0
142+
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾""")
143+
res = hist_aggregated(counts=counts, labels=labels)
144+
assert expected == res
145+
146+
147+
def test_hist_aggregated_labels_bins(cleandoc, expected_hist_aggregated):
148+
counts = [1945, 0, 0, 0, 0, 0, 10555, 798, 0, 28351, 0]
149+
labels = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"]
150+
bins = [
151+
float("-inf"),
152+
10,
153+
50,
154+
100,
155+
200,
156+
300,
157+
500,
158+
800,
159+
1000,
160+
2000,
161+
10000,
162+
float("+inf"),
163+
]
164+
with pytest.raises(
165+
AssertionError, match="At least one of bins/labels has to be set"
166+
):
167+
hist_aggregated(counts=counts, labels=None, bins=None)
168+
with pytest.raises(
169+
AssertionError, match="At least one of bins/labels has to be set"
170+
):
171+
hist_aggregated(counts=counts)
172+
with pytest.raises(
173+
AssertionError, match="At most one of bins/labels has to be set"
174+
):
175+
hist_aggregated(counts=counts, labels=labels, bins=bins)

0 commit comments

Comments
 (0)