// Copyright 2020 The Chromium Authors. All rights reserved. // 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 #include #include #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::BASELINE_BOTTOM) ? -1 : 1; // Bottom path is always base. So it's visually below the current line when // BASELINE_BOTTOM 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::BASELINE_BOTTOM ? -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::BASELINE_BOTTOM ? 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::NONE && !top_path_.empty()) { gfx::RectF graph_bounds_f(graph_bounds); if (baseline_ == Baseline::BASELINE_BOTTOM) { 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& top_path, const auto& draw_point, SkPath& out_path) { SkPoint previous_point = top_path.front(); for (std::vector::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& bottom_path, const auto& draw_point, SkPath& result) { SkPoint previous_point = bottom_path.back(); for (std::vector::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::LINES paths. const auto draw_lines_point = [](const SkPoint& point, const SkPoint& /*previous_point*/, SkPath& out_path) { out_path.lineTo(point); }; // Top and bottom Style::SKYLINE 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::LINES: draw_top_line(top_path_, draw_lines_point, path); break; case Style::SKYLINE: draw_top_line(top_path_, draw_skyline_point, path); break; } if (fill_ == Graph::Fill::SOLID) { switch (bottom_path_style_) { case Style::LINES: draw_bottom_line(bottom_path_, draw_lines_point, path); break; case Style::SKYLINE: 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::NONE) ? 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(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::SOLID) { // 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