Skip to content

Commit d60f96d

Browse files
authored
ostream improvements: support more accumulators, unicode plots (#317)
1 parent 8a402c7 commit d60f96d

File tree

11 files changed

+451
-110
lines changed

11 files changed

+451
-110
lines changed

.github/workflows/cov.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ env:
1616

1717
jobs:
1818
cov:
19-
runs-on: ubuntu-latest
19+
runs-on: macos-10.15
2020
steps:
2121
- uses: actions/checkout@v2
2222
- name: Fetch Boost superproject
@@ -42,10 +42,10 @@ jobs:
4242
./b2 headers
4343
4444
# simulate bundled boost by moving the headers instead of symlinking
45-
rm -rf boost/histogram.pp boost/histogram
45+
rm -rf boost/histogram*
4646
mv -f libs/histogram/include/boost/* boost
4747
48-
- name: Test gcc-8 cxxstd=latest coverage=on
48+
- name: Test cxxstd=latest coverage=on
4949
run: |
5050
cd libs/histogram
5151

.github/workflows/slow.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ jobs:
6363
cd libs/histogram
6464
../../b2 $B2_OPTS cxxstd=17 test//all
6565
66-
gcc7:
67-
runs-on: ubuntu-latest
66+
gcc5:
67+
runs-on: ubuntu-16.04
6868
steps:
6969
- uses: actions/checkout@v2
7070
- name: Fetch Boost superproject
@@ -80,10 +80,10 @@ jobs:
8080
run: |
8181
./bootstrap.sh
8282
./b2 headers
83-
- name: Test gcc-7 cxxstd=14
83+
- name: Test cxxstd=14
8484
run: |
8585
cd libs/histogram
86-
../../b2 $B2_OPTS toolset=gcc-7 cxxstd=14 test//all examples
86+
../../b2 $B2_OPTS toolset=gcc-5 cxxstd=14 test//all examples
8787
8888
gcc10:
8989
runs-on: ubuntu-latest
@@ -102,7 +102,7 @@ jobs:
102102
run: |
103103
./bootstrap.sh
104104
./b2 headers
105-
- name: Test gcc-10 cxxstd=20 -O3 -funsafe-math-optimizations
105+
- name: Test cxxstd=20 -O3 -funsafe-math-optimizations
106106
run: |
107107
cd libs/histogram
108108
../../b2 $B2_OPTS toolset=gcc-10 cxxstd=20 cxxflags="-O3 -funsafe-math-optimizations" test//all examples
@@ -124,7 +124,7 @@ jobs:
124124
run: |
125125
./bootstrap.sh
126126
./b2 headers
127-
- name: Test clang-6 cxxstd=17 ubsan asan
127+
- name: Test cxxstd=17 ubsan asan
128128
run: |
129129
cd libs/histogram
130130
../../b2 $B2_OPTS toolset=clang-6 cxxstd=17 variant=histogram_ubasan test//all

examples/guide_histogram_streaming.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ int main() {
1818

1919
std::ostringstream os;
2020

21+
// width of histogram can be set like this; if it is not set, the library attempts to
22+
// determine the terminal width and choses the histogram width accordingly
23+
os.width(78);
24+
2125
auto h1 = make_histogram(axis::regular<>(5, -1.0, 1.0, "axis 1"));
2226
h1.at(0) = 2;
2327
h1.at(1) = 4;
@@ -38,15 +42,15 @@ int main() {
3842
assert(
3943
os.str() ==
4044
"histogram(regular(5, -1, 1, metadata=\"axis 1\", options=underflow | overflow))\n"
41-
" +-------------------------------------------------------------+\n"
42-
"[-inf, -1) 0 | |\n"
43-
"[ -1, -0.6) 2 |============================== |\n"
44-
"[-0.6, -0.2) 4 |============================================================ |\n"
45-
"[-0.2, 0.2) 3 |============================================= |\n"
46-
"[ 0.2, 0.6) 0 | |\n"
47-
"[ 0.6, 1) 1 |=============== |\n"
48-
"[ 1, inf) 0 | |\n"
49-
" +-------------------------------------------------------------+\n"
45+
" ┌─────────────────────────────────────────────────────────────┐\n"
46+
"[-inf, -1) 0 \n"
47+
"[ -1, -0.6) 2 │██████████████████████████████ \n"
48+
"[-0.6, -0.2) 4 │████████████████████████████████████████████████████████████ │\n"
49+
"[-0.2, 0.2) 3 │█████████████████████████████████████████████ \n"
50+
"[ 0.2, 0.6) 0 \n"
51+
"[ 0.6, 1) 1 │███████████████ \n"
52+
"[ 1, inf) 0 \n"
53+
" └─────────────────────────────────────────────────────────────┘\n"
5054
"histogram(\n"
5155
" regular(2, -1, 1, metadata=\"axis 1\", options=underflow | overflow)\n"
5256
" category(\"red\", \"blue\", metadata=\"axis 2\", options=overflow)\n"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2021 Hans Dembinski
2+
//
3+
// Distributed under the Boost Software License, Version 1.0.
4+
// (See accompanying file LICENSE_1_0.txt
5+
// or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
7+
#ifndef BOOST_HISTOGRAM_DETAIL_TERM_INFO_HPP
8+
#define BOOST_HISTOGRAM_DETAIL_TERM_INFO_HPP
9+
10+
#if defined __has_include
11+
#if __has_include(<sys/ioctl.h>) && __has_include(<unistd.h>)
12+
#include <sys/ioctl.h>
13+
#include <unistd.h>
14+
#endif
15+
#endif
16+
#include <boost/config/workaround.hpp>
17+
#include <cstdlib>
18+
#include <cstring>
19+
20+
namespace boost {
21+
namespace histogram {
22+
namespace detail {
23+
24+
namespace term_info {
25+
class env_t {
26+
public:
27+
env_t(const char* key) {
28+
#if BOOST_WORKAROUND(BOOST_MSVC, >= 0) // msvc complains about using std::getenv
29+
_dupenv_s(&data, &size, key);
30+
#else
31+
data = std::getenv(key);
32+
if (data) size = std::strlen(data);
33+
#endif
34+
}
35+
36+
~env_t() {
37+
#if BOOST_WORKAROUND(BOOST_MSVC, >= 0)
38+
std::free(data);
39+
#endif
40+
}
41+
42+
bool contains(const char* s) {
43+
const std::size_t n = std::strlen(s);
44+
if (size < n) return false;
45+
return std::strstr(data, s);
46+
}
47+
48+
operator bool() { return size > 0; }
49+
50+
explicit operator int() { return size ? std::atoi(data) : 0; }
51+
52+
private:
53+
char* data;
54+
std::size_t size = 0;
55+
};
56+
57+
inline bool utf8() {
58+
// return false only if LANG exists and does not contain the string UTF
59+
env_t env("LANG");
60+
bool b = true;
61+
if (env) b = env.contains("UTF") || env.contains("utf");
62+
return b;
63+
}
64+
65+
inline int width() {
66+
int w = 0;
67+
#if defined TIOCGWINSZ
68+
struct winsize ws;
69+
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
70+
w = std::max(static_cast<int>(ws.ws_col), 0); // not sure if ws_col can be less than 0
71+
#endif
72+
env_t env("COLUMNS");
73+
const int col = std::max(static_cast<int>(env), 0);
74+
// if both t and w are set, COLUMNS may be used to restrict width
75+
return w == 0 ? col : std::min(col, w);
76+
}
77+
} // namespace term_info
78+
79+
} // namespace detail
80+
} // namespace histogram
81+
} // namespace boost
82+
83+
#endif

include/boost/histogram/ostream.hpp

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
#include <boost/histogram/accumulators/ostream.hpp>
1212
#include <boost/histogram/axis/ostream.hpp>
1313
#include <boost/histogram/detail/counting_streambuf.hpp>
14+
#include <boost/histogram/detail/detect.hpp>
1415
#include <boost/histogram/detail/priority.hpp>
16+
#include <boost/histogram/detail/term_info.hpp>
1517
#include <boost/histogram/indexed.hpp>
1618
#include <cmath>
1719
#include <iomanip>
@@ -162,19 +164,14 @@ void ostream_bin(OStream& os, const Axis&, axis::index_type i, B, priority<0>) {
162164
os << i;
163165
}
164166

165-
template <class CharT>
166-
struct line_t {
167-
CharT ch;
168-
int size;
167+
struct line {
168+
const char* ch;
169+
const int size;
170+
line(const char* a, int b) : ch{a}, size{std::max(b, 0)} {}
169171
};
170172

171-
template <class CharT>
172-
auto line(CharT c, int n) {
173-
return line_t<CharT>{c, n};
174-
}
175-
176-
template <class C, class T>
177-
std::basic_ostream<C, T>& operator<<(std::basic_ostream<C, T>& os, line_t<C>&& l) {
173+
template <class T>
174+
std::basic_ostream<char, T>& operator<<(std::basic_ostream<char, T>& os, line&& l) {
178175
for (int i = 0; i < l.size; ++i) os << l.ch;
179176
return os;
180177
}
@@ -191,13 +188,50 @@ void ostream_head(OStream& os, const Axis& ax, int index, double val) {
191188
ax);
192189
}
193190

191+
template <class OStream>
192+
void ostream_bar(OStream& os, int zero_offset, double z, int width, bool utf8) {
193+
int k = static_cast<int>(std::lround(z * width));
194+
if (utf8) {
195+
os << "";
196+
if (z > 0) {
197+
const char* scale[8] = {" ", "", "", "", "", "", "", ""};
198+
int j = static_cast<int>(std::lround(8 * (z * width - k)));
199+
if (j < 0) {
200+
--k;
201+
j += 8;
202+
}
203+
os << line(" ", zero_offset) << line("", k);
204+
os << scale[j];
205+
os << line(" ", width - zero_offset - k);
206+
} else if (z < 0) {
207+
os << line(" ", zero_offset + k) << line("", -k)
208+
<< line(" ", width - zero_offset + 1);
209+
} else {
210+
os << line(" ", width + 1);
211+
}
212+
os << "\n";
213+
} else {
214+
os << " |";
215+
if (z >= 0) {
216+
os << line(" ", zero_offset) << line("=", k) << line(" ", width - zero_offset - k);
217+
} else {
218+
os << line(" ", zero_offset + k) << line("=", -k) << line(" ", width - zero_offset);
219+
}
220+
os << " |\n";
221+
}
222+
}
223+
194224
// cannot display generalized histograms yet; line not reachable by coverage tests
195225
template <class OStream, class Histogram>
196-
void ascii_plot(OStream&, const Histogram&, int, std::false_type) {} // LCOV_EXCL_LINE
226+
void plot(OStream&, const Histogram&, int, std::false_type) {} // LCOV_EXCL_LINE
197227

198228
template <class OStream, class Histogram>
199-
void ascii_plot(OStream& os, const Histogram& h, int w_total, std::true_type) {
200-
if (w_total == 0) w_total = 78; // TODO detect actual width of terminal
229+
void plot(OStream& os, const Histogram& h, int w_total, std::true_type) {
230+
if (w_total == 0) {
231+
w_total = term_info::width();
232+
if (w_total == 0 || w_total > 78) w_total = 78;
233+
}
234+
bool utf8 = term_info::utf8();
201235

202236
const auto& ax = h.axis();
203237

@@ -207,39 +241,42 @@ void ascii_plot(OStream& os, const Histogram& h, int w_total, std::true_type) {
207241
tabular_ostream_wrapper<OStream, 7> tos(os);
208242
// first pass to get widths
209243
for (auto&& v : indexed(h, coverage::all)) {
210-
ostream_head(tos.row(), ax, v.index(), *v);
211-
vmin = std::min(vmin, static_cast<double>(*v));
212-
vmax = std::max(vmax, static_cast<double>(*v));
244+
auto w = static_cast<double>(*v);
245+
ostream_head(tos.row(), ax, v.index(), w);
246+
vmin = std::min(vmin, w);
247+
vmax = std::max(vmax, w);
213248
}
214249
tos.complete();
215250
if (vmax == 0) vmax = 1;
216251

217252
// calculate width useable by bar (notice extra space at top)
218253
// <-- head --> |<--- bar ---> |
219254
// w_head + 2 + 2
220-
const auto w_head = std::accumulate(tos.begin(), tos.end(), 0);
221-
const auto w_bar = w_total - 4 - w_head;
255+
const int w_head = std::accumulate(tos.begin(), tos.end(), 0);
256+
const int w_bar = w_total - 4 - w_head;
222257
if (w_bar < 0) return;
223258

224259
// draw upper line
225-
os << '\n' << line(' ', w_head + 1) << '+' << line('-', w_bar + 1) << "+\n";
260+
os << '\n' << line(" ", w_head + 1);
261+
if (utf8)
262+
os << "" << line("", w_bar + 1) << "\n";
263+
else
264+
os << '+' << line("-", w_bar + 1) << "+\n";
226265

227266
const int zero_offset = static_cast<int>(std::lround((-vmin) / (vmax - vmin) * w_bar));
228267
for (auto&& v : indexed(h, coverage::all)) {
229-
ostream_head(tos.row(), ax, v.index(), *v);
268+
auto w = static_cast<double>(*v);
269+
ostream_head(tos.row(), ax, v.index(), w);
230270
// rest uses os, not tos
231-
os << " |";
232-
const int k = static_cast<int>(std::lround(*v / (vmax - vmin) * w_bar));
233-
if (k < 0) {
234-
os << line(' ', zero_offset + k) << line('=', -k) << line(' ', w_bar - zero_offset);
235-
} else {
236-
os << line(' ', zero_offset) << line('=', k) << line(' ', w_bar - zero_offset - k);
237-
}
238-
os << " |\n";
271+
ostream_bar(os, zero_offset, w / (vmax - vmin), w_bar, utf8);
239272
}
240273

241274
// draw lower line
242-
os << line(' ', w_head + 1) << '+' << line('-', w_bar + 1) << "+\n";
275+
os << line(" ", w_head + 1);
276+
if (utf8)
277+
os << "" << line("", w_bar + 1) << "\n";
278+
else
279+
os << '+' << line("-", w_bar + 1) << "+\n";
243280
}
244281

245282
template <class OStream, class Histogram>
@@ -300,11 +337,12 @@ std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>&
300337

301338
using value_type = typename histogram<A, S>::value_type;
302339

340+
using convertible = detail::is_explicitly_convertible<value_type, double>;
303341
// must be non-const to avoid a msvc warning about possible use of if constexpr
304-
bool show_ascii = std::is_convertible<value_type, double>::value && h.rank() == 1;
305-
if (show_ascii) {
342+
bool show_plot = convertible::value && h.rank() == 1;
343+
if (show_plot) {
306344
detail::ostream(os, h, false);
307-
detail::ascii_plot(os, h, w, std::is_convertible<value_type, double>{});
345+
detail::plot(os, h, w, convertible{});
308346
} else {
309347
detail::ostream(os, h);
310348
}
@@ -314,9 +352,9 @@ std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>&
314352
return os;
315353
}
316354

355+
#endif // BOOST_HISTOGRAM_DOXYGEN_INVOKED
356+
317357
} // namespace histogram
318358
} // namespace boost
319359

320-
#endif // BOOST_HISTOGRAM_DOXYGEN_INVOKED
321-
322360
#endif

test/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ endif()
1818

1919
set(BOOST_TEST_LINK_LIBRARIES Boost::histogram)
2020

21-
# keep in sync with Jamfile, this should be automatized...
21+
# keep in sync with Jamfile, this should be automated...
2222
boost_test(TYPE compile-fail SOURCES axis_category_fail0.cpp)
2323
boost_test(TYPE compile-fail SOURCES axis_category_fail1.cpp)
2424
boost_test(TYPE compile-fail SOURCES axis_category_fail2.cpp)
@@ -119,6 +119,7 @@ endif()
119119
# LINK_LIBRARIES Boost::serialization)
120120
# boost_test(TYPE run SOURCES accumulators_serialization_test.cpp
121121
# LINK_LIBRARIES Boost::serialization)
122+
# boost_test(TYPE run SOURCES histogram_ostream_ascii_test.cpp)
122123

123124
# Workaround for gcc-5
124125
if (NOT(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6))

test/Jamfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright 2016-2017 Klemens David Morgenstern
22
# Copyright 2018 Mateusz Loskot <[email protected]>
3-
# Copyright 2018-2019 Hans Dembinski
3+
# Copyright 2018-2021 Hans Dembinski
44
#
55
# Use, modification and distribution is subject to the Boost Software License,
66
# Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
@@ -82,7 +82,8 @@ alias cxx14 :
8282
[ run histogram_growing_test.cpp ]
8383
[ run histogram_mixed_test.cpp ]
8484
[ run histogram_operators_test.cpp ]
85-
[ run histogram_ostream_test.cpp ]
85+
[ run histogram_ostream_test.cpp : : : <testing.launcher>"env LANG=UTF" ]
86+
[ run histogram_ostream_ascii_test.cpp : : : <testing.launcher>"env LANG=FOO COLUMNS=20" ]
8687
[ run histogram_test.cpp ]
8788
[ run indexed_test.cpp ]
8889
[ run storage_adaptor_test.cpp ]

0 commit comments

Comments
 (0)