blob: c666a306a00eb945bbbad485e4dfa53a02a0799f [file] [log] [blame]
/*
* 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 <algorithm>
#include <cmath>
#include <iomanip> // setw
#include <iostream>
#include <map>
#include <sstream>
#include <string>
// 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<float, bool>, 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 <class RandomAccessIterator>
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<int>(number)) + 3, 2), leftPadding + 1);
}
// TODO Make this templated and add comments.
inline std::string audio_utils_plot_histogram(const std::map<double, int> &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<int>(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