chromium/ash/hud_display/graph.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/hud_display/graph.h"

#include <algorithm>
#include <limits>
#include <sstream>

#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPoint.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect_f.h"

namespace ash {
namespace hud_display {

Graph::Graph(size_t max_data_points,
             Baseline baseline,
             Fill fill,
             Style style,
             SkColor color)
    : baseline_(baseline), fill_(fill), style_(style), color_(color) {
  DCHECK_LT(max_data_points, data_.BufferSize());
  max_data_points_ = std::min(max_data_points, data_.BufferSize() - 1);
}

Graph::~Graph() {}

void Graph::AddValue(float value, float unscaled_value) {
  data_.SaveToBuffer(value);
  unscaled_data_.SaveToBuffer(unscaled_value);
}

void Graph::Layout(const gfx::Rect& graph_bounds, const Graph* base) {
  graph_bounds_ = graph_bounds;

  const float scale_x = graph_bounds.width() / (float)max_data_points_;

  // Assume data is already scaled to [0-1], which will map to full
  // |graph_bounds| height.
  const float scale_y = graph_bounds.height();

  // Let every graph to occupy at least given amount of pixels.
  const int pixel_adjust = (baseline_ == Baseline::kBaselineBottom) ? -1 : 1;

  // Bottom path is always base. So it's visually below the current line when
  // kBaselineBottom and above current graph when BASELINE_TOP.
  // top_path_ is similarly inverted.
  bottom_path_.resize(0);
  if (base) {
    bottom_path_.reserve(base->top_path().size());
    bottom_path_style_ = base->style_;
    for (const SkPoint& point : base->top_path())
      bottom_path_.push_back({point.x(), point.y() + pixel_adjust});
  }
  top_path_.resize(0);
  top_path_.reserve(max_data_points_);
  size_t i = 0;
  // base::RingBuffer<> does not conform to C++ containers specification, so
  // it's End() is actually Back().
  for (Graph::Data::Iterator it = data_.End(); it; --it) {
    const float value = **it;

    float x = graph_bounds.x() + (max_data_points_ - i) * scale_x;
    float y =
        (baseline_ == Baseline::kBaselineBottom ? -1 : 1) * value * scale_y;
    if (bottom_path_.size()) {
      CHECK_LT(i, bottom_path_.size());
      // Adjust to the single pixel line added above.
      y = bottom_path_[i].y() - pixel_adjust + y;
    } else {
      y = (baseline_ == Baseline::kBaselineBottom ? graph_bounds.bottom()
                                                  : graph_bounds.y()) +
          y;
    }
    top_path_.push_back({x, y});
    ++i;
    if (i >= max_data_points_)
      break;
  }

  // This is the first layer from the start and it is filled and is non-empty.
  if (!base && fill_ != Graph::Fill::kNone && !top_path_.empty()) {
    gfx::RectF graph_bounds_f(graph_bounds);
    if (baseline_ == Baseline::kBaselineBottom) {
      bottom_path_.push_back({graph_bounds_f.right(), graph_bounds_f.bottom()});
      bottom_path_.push_back({top_path_.back().x(), graph_bounds_f.bottom()});
    } else {
      bottom_path_.push_back({graph_bounds_f.right(), graph_bounds_f.y()});
      bottom_path_.push_back({top_path_.back().x(), graph_bounds_f.y()});
    }
  }
}

void Graph::Draw(gfx::Canvas* canvas) const {
  if (top_path_.empty())
    return;

  SkPath path;
  path.moveTo(top_path_.front());

  const auto draw_top_line = [](const std::vector<SkPoint>& top_path,
                                const auto& draw_point, SkPath& out_path) {
    SkPoint previous_point = top_path.front();
    for (std::vector<SkPoint>::const_iterator it = top_path.begin();
         it != top_path.end(); ++it) {
      // For the top line we are already here.
      if (it == top_path.begin())
        continue;
      // Depending on the line type, |draw_point| may use the previous point.
      draw_point(*it, previous_point, out_path);
    }
  };
  const auto draw_bottom_line = [](const std::vector<SkPoint>& bottom_path,
                                   const auto& draw_point, SkPath& result) {
    SkPoint previous_point = bottom_path.back();
    for (std::vector<SkPoint>::const_reverse_iterator it =
             bottom_path.crbegin();
         it != bottom_path.crend(); ++it) {
      // Bottom line needs line to the first point too.
      // Depending on the line type, |draw_point| may use the previous point.
      draw_point(*it, previous_point, result);
    }
  };

  // This is used to draw both top and bottom Style::kLines paths.
  const auto draw_lines_point =
      [](const SkPoint& point, const SkPoint& /*previous_point*/,
         SkPath& out_path) { out_path.lineTo(point); };
  // Top and bottom Style::kSkyline drawing functions are symmetric.
  const auto draw_skyline_point =
      [](const SkPoint& point, SkPoint& previous_point, SkPath& out_path) {
        out_path.lineTo(SkPoint::Make(point.x(), previous_point.y()));
        out_path.lineTo(point);
        previous_point = point;
      };
  const auto draw_bottom_skyline_point =
      [](const SkPoint& point, SkPoint& previous_point, SkPath& out_path) {
        out_path.lineTo(SkPoint::Make(previous_point.x(), point.y()));
        out_path.lineTo(point);
        previous_point = point;
      };
  switch (style_) {
    case Style::kLines:
      draw_top_line(top_path_, draw_lines_point, path);
      break;
    case Style::kSkyline:
      draw_top_line(top_path_, draw_skyline_point, path);
      break;
  }
  if (fill_ == Graph::Fill::kSolid) {
    switch (bottom_path_style_) {
      case Style::kLines:
        draw_bottom_line(bottom_path_, draw_lines_point, path);
        break;
      case Style::kSkyline:
        draw_bottom_line(bottom_path_, draw_bottom_skyline_point, path);
        break;
    }
  }
  cc::PaintFlags flags;
  flags.setAntiAlias(true);
  flags.setBlendMode(SkBlendMode::kSrc);
  const cc::PaintFlags::Style style = (fill_ == Graph::Fill::kNone)
                                          ? cc::PaintFlags::kStroke_Style
                                          : cc::PaintFlags::kFill_Style;
  flags.setStyle(style);
  flags.setStrokeWidth(1);
  flags.setColor(color_);
  canvas->DrawPath(path, flags);
}

void Graph::UpdateLastValue(float value, float unscaled_value) {
  const size_t last_index = data_.BufferSize() - 1;
  if (!data_.IsFilledIndex(last_index))
    return;

  *data_.MutableReadBuffer(last_index) = value;
  *unscaled_data_.MutableReadBuffer(last_index) = unscaled_value;
}

float Graph::GetUnscaledValueAt(size_t index) const {
  if (index >= max_data_points_)
    return 0;

  // 0 - the oldest value
  // BufferSize() - 1  - the newest value.
  const size_t raw_index = index < unscaled_data_.BufferSize()
                               ? unscaled_data_.BufferSize() - 1 - index
                               : 0;

  // It will CHECK() if index is not populated.
  if (!unscaled_data_.IsFilledIndex(raw_index))
    return 0;

  return unscaled_data_.ReadBuffer(raw_index);
}

bool Graph::IsFilledIndex(size_t index) const {
  if (index >= max_data_points_)
    return false;

  // 0 - the oldest value
  // BufferSize() - 1  - the newest value.
  const size_t raw_index =
      index < data_.BufferSize() ? data_.BufferSize() - 1 - index : 0;
  return data_.IsFilledIndex(raw_index);
}

void Graph::Reset() {
  data_.Clear();
  unscaled_data_.Clear();
}

#if !defined(NDEBUG)
std::string Graph::DebugDump(const std::string& name) const {
  std::ostringstream os;
  os << name << ": location BLxy  [" << graph_bounds_.x() << ", "
     << graph_bounds_.bottom() << "] TRxy [" << graph_bounds_.right() << ", "
     << graph_bounds_.y() << "]";
  const int topsize = static_cast<int>(top_path_.size());
  for (int i = 0; i < topsize; ++i) {
    if ((i > 5) && (i < topsize - 5)) {
      // Skip the middle part.
      os << "\t" << name << ": ...";
      i = topsize - 5;
    }
    if (fill_ == Graph::Fill::kSolid) {
      // Print filled graph as a set of vertical lines.
      if (top_path_.size() == bottom_path_.size()) {
        // Each point on the top has matching point on the bottom.
        os << "\t" << name << ": " << i << ": [" << bottom_path_[i].x() << ", "
           << bottom_path_[i].y() << "] -> [" << top_path_[i].x() << ", "
           << top_path_[i].y() << "]";
      } else {
        // This is the first graph in stack. Use bottom_path_[0].y() as
        // reference.
        os << "\t" << name << ": " << i << ": ["
           << ((i == 0) ? bottom_path_[0].x() : top_path_[i].x()) << ", "
           << bottom_path_[0].y() << "] -> [" << top_path_[i].x() << ", "
           << top_path_[i].y() << "]";
      }
    } else {
      // Print lines graph as a list of dots.
      os << "\t" << name << ": " << i << ": -> [" << top_path_[i].x() << ", "
         << top_path_[i].y() << "]";
    }
    os << "\n";
  }
  return os.str();
}
#endif

}  // namespace hud_display
}  // namespace ash