/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef LOG_PLOT_H #define LOG_PLOT_H #include #include #include // setw #include #include #include #include // TODO Make a class called LogPlot and put this functionality in it. // Actually maybe this file can be called AsciiPlot or something... /** * \brief Creates a std::string graph representation of equally-spaced time-series data points. * * \param first RandomAccessIterator iterator to initial position of sequence. * Iterator shall point to a pair, where the float is the data value * and the bool is whether the data value is the start of a new data point in time * (i.e. a break in time continuity). * \param last RandomAccessIterator iterator to final position of sequence. * \return the std::string of the graph. * */ template std::string audio_utils_log_plot(RandomAccessIterator first, RandomAccessIterator last) { using T = decltype((*first).first); constexpr int HEIGHT = 14; // Character height of the plot // Leave 20% display space before min and after max data points constexpr float RANGE_BUFFER_ROOM = 0.2f; // Minimum range of lowest and highest y-axis value to display constexpr int RANGE_MIN = 14; constexpr unsigned int WIDTH_MAX = 200U; // Max character width of plot const size_t size = last - first; if (size <= 0) { return ""; } // Find min and max element in the vector. const auto result = std::minmax_element(first, last); const T minVal = (*result.first).first; const T maxVal = (*result.second).first; const T range = maxVal - minVal; T graphMin, graphMax; if (range < RANGE_MIN) { T avg = (maxVal + minVal) / 2; graphMin = avg - RANGE_MIN / 2; graphMax = avg + RANGE_MIN / 2; } else { graphMin = minVal - range * RANGE_BUFFER_ROOM; graphMax = maxVal + range * RANGE_BUFFER_ROOM; } // Value of one character height increase on the graph const T increment = (graphMax - graphMin) / HEIGHT; // Something went wrong if we reached this statement.. if (increment <= 0.0f) { return ""; } std::stringstream ss; ss << std::fixed << std::setprecision(1); // Start storing the graph into string. // TODO store everything into a preallocated string rather than use stringstream. // This may make the code easier to maintain. ss << "\n"; for (int height = HEIGHT - 1; height >= 0; height--) { int spaces = 1; // Amount of spaces before the data point ss << std::setw(9) << graphMin + increment * height; ss << std::setw(3) << "-|"; auto it = size <= WIDTH_MAX ? first : first + size - WIDTH_MAX; for (; it < last; ++it) { const T power = it->first; const bool start = it->second; // TODO explicitly do type conversion for parameter passed to round()? int px = (int)round((power - graphMin) / increment); // The it != last - 1 is a temporary workaround to prevent vertical bar // separators after the last data point entry. if ((start || px == height) && it != last - 1) { ss << std::setw(spaces) << (start ? "|" : "*"); spaces = 1; } else { spaces++; } } ss << "\n"; } ss << std::setw(12) << "|"; ss << std::string(std::min(size - (size_t)1, (size_t)WIDTH_MAX), '_') << "\n\n"; return ss.str(); } // determines how many character spaces an integer takes up. inline int widthOf(int x) { int width = 0; if (x < 0) { ++width; x = x == INT_MIN ? INT_MAX : -x; } // assert (x >= 0) do { ++width; x /= 10; } while (x > 0); return width; } // computes the column width required for a specific histogram value inline int numberWidth(double number, int leftPadding) { // Added values account for whitespaces needed around numbers, and for the // dot and decimal digit not accounted for by widthOf return std::max(std::max(widthOf(static_cast(number)) + 3, 2), leftPadding + 1); } // TODO Make this templated and add comments. inline std::string audio_utils_plot_histogram(const std::map &buckets, const char *title = "", const char *label = "", int maxHeight = 10) { if (buckets.empty()) { return ""; } auto it = buckets.begin(); double maxDelta = it->first; int maxCount = it->second; // Compute maximum values while (++it != buckets.end()) { if (it->first > maxDelta) { maxDelta = it->first; } if (it->second > maxCount) { maxCount = it->second; } } int height = log2(maxCount) + 1; // maxCount > 0, safe to call log2 const int leftPadding = widthOf(1 << height); const int bucketWidth = numberWidth(maxDelta, leftPadding); int scalingFactor = 1; // scale data if it exceeds maximum height if (height > maxHeight) { scalingFactor = (height + maxHeight) / maxHeight; height /= scalingFactor; } std::stringstream ss; ss << title << "\n " << std::setw(leftPadding) << " "; // write histogram label line with bucket values for (auto const &x : buckets) { const int colWidth = numberWidth(x.first, leftPadding); ss << std::setw(colWidth) << x.second; } // write histogram ascii art // underscores and spaces length corresponds to maximum width of histogram constexpr int kLen = 200; static const std::string underscores(kLen, '_'); static const std::string spaces(kLen, ' '); auto getTail = [](const size_t n, const std::string &s) { return s.c_str() + s.size() - std::min(n, s.size()); }; ss << "\n "; for (int row = height * scalingFactor; row >= 0; row -= scalingFactor) { // TODO explain how value is derived from log2 and why it doesn't overflow. const int value = 1 << row; ss << getTail(leftPadding, spaces); for (auto const &x : buckets) { const int colWidth = numberWidth(x.first, leftPadding); ss << getTail(colWidth - 1, spaces) << (x.second < value ? " " : "|"); } ss << "\n "; } // print x-axis const int columns = static_cast(buckets.size()); ss << std::setw(leftPadding) << " " << getTail((columns + 1) * bucketWidth, underscores) << "\n "; // write footer with bucket labels ss << std::setw(leftPadding) << " "; for (auto const &x : buckets) { const int colWidth = numberWidth(x.first, leftPadding); ss << std::setw(colWidth) << std::fixed << std::setprecision(1) << x.first; } ss << getTail(bucketWidth, spaces) << label << "\n"; return ss.str(); } #endif // !LOG_PLOT_H