Implement interface for using web plotter from C++

It's not actually usable yet due to ODR violations created by
abseil being compiled into libwebrtc_full.a, but it does work based
on testing I've done with using websockets for data transfer.

Change-Id: I574570c7b5c85df9e53321bfb971a608d20b9803
diff --git a/aos/network/BUILD b/aos/network/BUILD
index bf2cb00..6053260 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -389,11 +389,11 @@
     hdrs = ["web_proxy.h"],
     copts = [
         "-DWEBRTC_POSIX",
-        "-Wno-unused-parameter",
     ],
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
         ":connect_fbs",
+        ":gen_embedded",
         ":web_proxy_fbs",
         ":web_proxy_utils",
         "//aos/events:shm_event_loop",
@@ -419,7 +419,6 @@
     srcs = ["web_proxy_main.cc"],
     copts = [
         "-DWEBRTC_POSIX",
-        "-Wno-unused-parameter",
     ],
     data = [
         "//aos/network/www:files",
@@ -428,12 +427,9 @@
     ],
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
-        ":gen_embedded",
         ":web_proxy",
         "//aos:init",
         "//aos/events:shm_event_loop",
-        "//aos/seasocks:seasocks_logger",
-        "//third_party/seasocks",
         "@com_github_google_flatbuffers//:flatbuffers",
     ],
 )
@@ -446,16 +442,12 @@
     ],
     copts = [
         "-DWEBRTC_POSIX",
-        "-Wno-unused-parameter",
     ],
     deps = [
-        ":gen_embedded",
         ":web_proxy",
         "//aos:init",
         "//aos/events:simulated_event_loop",
         "//aos/events/logging:logger",
-        "//aos/seasocks:seasocks_logger",
-        "//third_party/seasocks",
         "@com_github_google_flatbuffers//:flatbuffers",
     ],
 )
diff --git a/aos/network/log_web_proxy_main.cc b/aos/network/log_web_proxy_main.cc
index dda9ae4..1942c57 100644
--- a/aos/network/log_web_proxy_main.cc
+++ b/aos/network/log_web_proxy_main.cc
@@ -9,21 +9,14 @@
 #include "aos/flatbuffer_merge.h"
 #include "aos/init.h"
 #include "aos/network/web_proxy.h"
-#include "aos/seasocks/seasocks_logger.h"
 #include "gflags/gflags.h"
 
-#include "internal/Embedded.h"
-#include "seasocks/Server.h"
-#include "seasocks/WebSocket.h"
-
 DEFINE_string(data_dir, "www", "Directory to serve data files from");
 DEFINE_string(node, "", "Directory to serve data files from");
 DEFINE_int32(buffer_size, -1, "-1 if infinite, in # of messages / channel.");
 
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
-  // Make sure to reference this to force the linker to include it.
-  findEmbeddedContent("");
 
   const std::vector<std::string> unsorted_logfiles =
       aos::logger::FindLogs(argc, argv);
diff --git a/aos/network/web_proxy.cc b/aos/network/web_proxy.cc
index af1b646..5902d30 100644
--- a/aos/network/web_proxy.cc
+++ b/aos/network/web_proxy.cc
@@ -7,6 +7,7 @@
 #include "aos/seasocks/seasocks_logger.h"
 #include "api/create_peerconnection_factory.h"
 #include "glog/logging.h"
+#include "internal/Embedded.h"
 
 namespace aos {
 namespace web_proxy {
@@ -22,7 +23,7 @@
     return new rtc::RefCountedObject<DummySetSessionDescriptionObserver>();
   }
   virtual void OnSuccess() {}
-  virtual void OnFailure(webrtc::RTCError error) {}
+  virtual void OnFailure(webrtc::RTCError /*error*/) {}
 };
 
 }  // namespace
@@ -32,6 +33,8 @@
     : server_(server),
       config_(aos::CopyFlatBuffer(event_loop->configuration())),
       event_loop_(event_loop) {
+  // We need to reference findEmbeddedContent() to make the linker happy...
+  findEmbeddedContent("");
   const aos::Node *self = event_loop->node();
 
   for (uint i = 0; i < event_loop->configuration()->channels()->size(); ++i) {
@@ -276,11 +279,14 @@
 // Function called for web socket data. Parses the flatbuffer and
 // handles it appropriately.
 void Connection::HandleWebSocketData(const uint8_t *data, size_t size) {
-  const WebSocketMessage *message =
-      flatbuffers::GetRoot<WebSocketMessage>(data);
-  switch (message->payload_type()) {
+  const FlatbufferSpan<WebSocketMessage> message({data, size});
+  if (!message.Verify()) {
+    LOG(ERROR) << "Invalid WebsocketMessage received from browser.";
+    return;
+  }
+  switch (message.message().payload_type()) {
     case Payload::WebSocketSdp: {
-      const WebSocketSdp *offer = message->payload_as_WebSocketSdp();
+      const WebSocketSdp *offer = message.message().payload_as_WebSocketSdp();
       if (offer->type() != SdpType::OFFER) {
         LOG(WARNING) << "Got the wrong sdp type from client";
         break;
@@ -325,7 +331,7 @@
       break;
     }
     case Payload::WebSocketIce: {
-      const WebSocketIce *ice = message->payload_as_WebSocketIce();
+      const WebSocketIce *ice = message.message().payload_as_WebSocketIce();
       std::string candidate = ice->candidate()->str();
       std::string sdpMid = ice->sdpMid()->str();
       int sdpMLineIndex = ice->sdpMLineIndex();
diff --git a/aos/network/web_proxy.h b/aos/network/web_proxy.h
index ab524de..e6d47c4 100644
--- a/aos/network/web_proxy.h
+++ b/aos/network/web_proxy.h
@@ -178,7 +178,7 @@
       rtc::scoped_refptr<webrtc::DataChannelInterface> channel) override;
   void OnRenegotiationNeeded() override {}
   void OnIceConnectionChange(
-      webrtc::PeerConnectionInterface::IceConnectionState state) override {}
+      webrtc::PeerConnectionInterface::IceConnectionState /*state*/) override {}
   void OnIceGatheringChange(
       webrtc::PeerConnectionInterface::IceGatheringState) override {}
   void OnIceCandidate(const webrtc::IceCandidateInterface *candidate) override;
@@ -186,7 +186,7 @@
 
   // CreateSessionDescriptionObserver implementation
   void OnSuccess(webrtc::SessionDescriptionInterface *desc) override;
-  void OnFailure(webrtc::RTCError error) override {}
+  void OnFailure(webrtc::RTCError /*error*/) override {}
   // CreateSessionDescriptionObserver is a refcounted object
   void AddRef() const override {}
   // We handle ownership with a unique_ptr so don't worry about actually
@@ -198,7 +198,7 @@
   // DataChannelObserver implementation
   void OnStateChange() override;
   void OnMessage(const webrtc::DataBuffer &buffer) override;
-  void OnBufferedAmountChange(uint64_t sent_data_size) override {}
+  void OnBufferedAmountChange(uint64_t /*sent_data_size*/) override {}
 
  private:
   ::seasocks::WebSocket *sock_;
diff --git a/aos/network/web_proxy_main.cc b/aos/network/web_proxy_main.cc
index ddab5dc..06fe942 100644
--- a/aos/network/web_proxy_main.cc
+++ b/aos/network/web_proxy_main.cc
@@ -2,21 +2,14 @@
 #include "aos/flatbuffer_merge.h"
 #include "aos/init.h"
 #include "aos/network/web_proxy.h"
-#include "aos/seasocks/seasocks_logger.h"
 #include "gflags/gflags.h"
 
-#include "internal/Embedded.h"
-#include "seasocks/Server.h"
-#include "seasocks/WebSocket.h"
-
 DEFINE_string(config, "./config.json", "File path of aos configuration");
 DEFINE_string(data_dir, "www", "Directory to serve data files from");
 DEFINE_int32(buffer_size, 0, "-1 if infinite, in # of messages / channel.");
 
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
-  // Make sure to reference this to force the linker to include it.
-  findEmbeddedContent("");
 
   aos::FlatbufferDetachedBuffer<aos::Configuration> config =
       aos::configuration::ReadConfig(FLAGS_config);
diff --git a/frc971/analysis/BUILD b/frc971/analysis/BUILD
index 465df0d..0ef9bca 100644
--- a/frc971/analysis/BUILD
+++ b/frc971/analysis/BUILD
@@ -3,6 +3,8 @@
 load("@npm_bazel_typescript//:defs.bzl", "ts_library")
 load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
 load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary", "rollup_bundle")
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_ts_library")
+load("//aos:config.bzl", "aos_config")
 
 py_binary(
     name = "plot_action",
@@ -89,6 +91,7 @@
     srcs = ["plot_index.ts"],
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
+        ":plot_data_utils",
         "//aos:configuration_ts_fbs",
         "//aos/network/www:demo_plot",
         "//aos/network/www:proxy",
@@ -134,3 +137,75 @@
     ],
     target_compatible_with = ["@platforms//os:linux"],
 )
+
+flatbuffer_cc_library(
+    name = "plot_data_fbs",
+    srcs = [
+        "plot_data.fbs",
+    ],
+    gen_reflections = 1,
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+flatbuffer_ts_library(
+    name = "plot_data_ts_fbs",
+    srcs = [
+        "plot_data.fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
+ts_library(
+    name = "plot_data_utils",
+    srcs = ["plot_data_utils.ts"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":plot_data_ts_fbs",
+        "//aos:configuration_ts_fbs",
+        "//aos/network/www:aos_plotter",
+        "//aos/network/www:plotter",
+        "//aos/network/www:proxy",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+    ],
+)
+
+aos_config(
+    name = "plotter",
+    src = "plotter_config.json",
+    flatbuffers = [":plot_data_fbs"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = ["//aos/events:config"],
+)
+
+cc_library(
+    name = "in_process_plotter",
+    srcs = ["in_process_plotter.cc"],
+    hdrs = ["in_process_plotter.h"],
+    copts = [
+        "-DWEBRTC_POSIX",
+    ],
+    deps = [
+        ":plot_data_fbs",
+        "//aos/events:simulated_event_loop",
+        "//aos/network:web_proxy",
+    ],
+)
+
+cc_binary(
+    name = "in_process_plotter_demo",
+    srcs = ["in_process_plotter_demo.cc"],
+    copts = [
+        "-DWEBRTC_POSIX",
+    ],
+    data = [
+        ":plotter",
+        ":plotter_files",
+    ],
+    # Tagged manual until we either get the linker working with the current
+    # WebRTC implementation or we get a new implementation.
+    tags = ["manual"],
+    deps = [
+        ":in_process_plotter",
+        "//aos:init",
+    ],
+)
diff --git a/frc971/analysis/in_process_plotter.cc b/frc971/analysis/in_process_plotter.cc
new file mode 100644
index 0000000..e82cb2f
--- /dev/null
+++ b/frc971/analysis/in_process_plotter.cc
@@ -0,0 +1,131 @@
+#include "frc971/analysis/in_process_plotter.h"
+
+#include "aos/configuration.h"
+
+namespace frc971 {
+namespace analysis {
+
+namespace {
+const char *kDataPath = "frc971/analysis";
+const char *kConfigPath = "frc971/analysis/plotter.json";
+}  // namespace
+
+Plotter::Plotter()
+    : config_(aos::configuration::ReadConfig(kConfigPath)),
+      event_loop_factory_(&config_.message()),
+      event_loop_(event_loop_factory_.MakeEventLoop("plotter")),
+      plot_sender_(event_loop_->MakeSender<Plot>("/analysis")),
+      web_proxy_(event_loop_.get(), -1),
+      builder_(plot_sender_.MakeBuilder()) {
+  web_proxy_.SetDataPath(kDataPath);
+  event_loop_->SkipTimingReport();
+  color_wheel_.push_back(Color(1, 0, 0));
+  color_wheel_.push_back(Color(0, 1, 0));
+  color_wheel_.push_back(Color(0, 0, 1));
+  color_wheel_.push_back(Color(1, 1, 0));
+  color_wheel_.push_back(Color(0, 1, 1));
+  color_wheel_.push_back(Color(1, 0, 1));
+}
+
+void Plotter::Spin() { event_loop_factory_.Run(); }
+
+void Plotter::Title(std::string_view title) {
+  title_ = builder_.fbb()->CreateString(title);
+}
+
+void Plotter::AddFigure(std::string_view title, double width, double height) {
+  MaybeFinishFigure();
+
+  if (!title.empty()) {
+    figure_title_ = builder_.fbb()->CreateString(title);
+  }
+
+  // For positioning, just stack figures vertically.
+  auto position_builder = builder_.MakeBuilder<Position>();
+  position_builder.add_top(next_top_);
+  position_builder.add_left(0);
+  position_builder.add_width(width);
+  position_builder.add_height(height);
+  position_ = position_builder.Finish();
+
+  next_top_ += height;
+}
+
+void Plotter::XLabel(std::string_view label) {
+  xlabel_ = builder_.fbb()->CreateString(label);
+}
+
+void Plotter::YLabel(std::string_view label) {
+  ylabel_ = builder_.fbb()->CreateString(label);
+}
+
+void Plotter::AddLine(const std::vector<double> &x,
+                      const std::vector<double> &y, std::string_view label) {
+  CHECK_EQ(x.size(), y.size());
+  CHECK(!position_.IsNull())
+      << "You must call AddFigure() before calling AddLine().";
+
+  flatbuffers::Offset<flatbuffers::String> label_offset;
+  if (!label.empty()) {
+    label_offset = builder_.fbb()->CreateString(label);
+  }
+
+  std::vector<Point> points;
+  for (size_t ii = 0; ii < x.size(); ++ii) {
+    points.emplace_back(x[ii], y[ii]);
+  }
+  const flatbuffers::Offset<flatbuffers::Vector<const Point*>>
+      points_offset = builder_.fbb()->CreateVectorOfStructs(points);
+
+  const Color *color = &color_wheel_.at(color_wheel_position_);
+  color_wheel_position_ = (color_wheel_position_ + 1) % color_wheel_.size();
+
+  auto line_builder = builder_.MakeBuilder<Line>();
+  line_builder.add_label(label_offset);
+  line_builder.add_points(points_offset);
+  line_builder.add_color(color);
+  lines_.push_back(line_builder.Finish());
+}
+
+void Plotter::MaybeFinishFigure() {
+  if (!lines_.empty()) {
+    const flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Line>>>
+        lines_offset = builder_.fbb()->CreateVector(lines_);
+    auto figure_builder = builder_.MakeBuilder<Figure>();
+    figure_builder.add_title(figure_title_);
+    figure_builder.add_position(position_);
+    figure_builder.add_lines(lines_offset);
+    figure_builder.add_xlabel(xlabel_);
+    figure_builder.add_share_x_axis(share_x_axis_);
+    figure_builder.add_ylabel(ylabel_);
+    figures_.push_back(figure_builder.Finish());
+  }
+  lines_.clear();
+  figure_title_.o = 0;
+  xlabel_.o = 0;
+  ylabel_.o = 0;
+  position_.o = 0;
+  share_x_axis_ = false;
+  color_wheel_position_ = 0;
+}
+
+void Plotter::Publish() {
+  MaybeFinishFigure();
+  const flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Figure>>>
+      figures_offset = builder_.fbb()->CreateVector(figures_);
+
+  auto plot_builder = builder_.MakeBuilder<Plot>();
+  plot_builder.add_title(title_);
+  plot_builder.add_figures(figures_offset);
+
+  builder_.Send(plot_builder.Finish());
+
+  builder_ = plot_sender_.MakeBuilder();
+
+  title_.o = 0;
+  figures_.clear();
+  next_top_ = 0;
+}
+
+}  // namespace analysis
+}  // namespace frc971
diff --git a/frc971/analysis/in_process_plotter.h b/frc971/analysis/in_process_plotter.h
new file mode 100644
index 0000000..3d7a037
--- /dev/null
+++ b/frc971/analysis/in_process_plotter.h
@@ -0,0 +1,79 @@
+#ifndef FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
+#define FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
+
+#include <vector>
+
+#include "aos/events/simulated_event_loop.h"
+#include "aos/network/web_proxy.h"
+#include "frc971/analysis/plot_data_generated.h"
+
+namespace frc971 {
+namespace analysis {
+
+// This class wraps the WebProxy class to provide a convenient C++ interface to
+// dynamically generate plots.
+// Currently, the main useful interface that this provides is a matplotlib-like
+// interface--see in_process_plotter_demo.cc for sample usage. It doesn't
+// precisely follow matplotlib's conventions, but the basic style does mimic
+// matplotlib. Future iterations may ditch this in favor of a more modern
+// interface where we actually return handles for plots and lines and the such.
+//
+// Note that currently the port for the seb server is hard-coded to 8080, so
+// only one instance of the Plotter can be present at once.
+//
+// You must call Spin() for the web server to actually do anything helpful.
+class Plotter {
+ public:
+  Plotter();
+
+  // matplotlib-like interface
+  // The basic pattern is:
+  // 1) Call Figure()
+  // 2) Setup the lines, labels, etc. for the figure.
+  // 3) Repeat 1-2 however many times.
+  // 4) Call Publish().
+  // 5) Repeat 1-5 however many times.
+  //
+  // Publish() actually pushes the figures that you setup to the web-page,
+  // either with an autogenerated title or the title set by Title(). All state
+  // is cleared (or should be cleared) by the call to Publish().
+
+  // Sets the title for the current set of plots; if you
+  void Title(std::string_view title);
+  void AddFigure(std::string_view title = "", double width = 900,
+                 double height = 400);
+  void AddLine(const std::vector<double> &x, const std::vector<double> &y,
+               std::string_view label = "");
+  void ShareXAxis(bool share) { share_x_axis_ = share; }
+  void XLabel(std::string_view label);
+  void YLabel(std::string_view label);
+  void Publish();
+
+  void Spin();
+ private:
+  void MaybeFinishFigure();
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+  std::unique_ptr<aos::EventLoop> event_loop_;
+  aos::Sender<Plot> plot_sender_;
+  aos::web_proxy::WebProxy web_proxy_;
+
+  aos::Sender<Plot>::Builder builder_;
+  flatbuffers::Offset<flatbuffers::String> title_;
+  flatbuffers::Offset<flatbuffers::String> figure_title_;
+  flatbuffers::Offset<flatbuffers::String> xlabel_;
+  flatbuffers::Offset<flatbuffers::String> ylabel_;
+  bool share_x_axis_ = false;
+  float next_top_ = 0;
+  flatbuffers::Offset<Position> position_;
+  std::vector<flatbuffers::Offset<Figure>> figures_;
+  std::vector<flatbuffers::Offset<Line>> lines_;
+
+  size_t color_wheel_position_ = 0;
+  std::vector<Color> color_wheel_;
+};
+
+}  // namespace analysis
+}  // namespace frc971
+#endif  // FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
diff --git a/frc971/analysis/in_process_plotter_demo.cc b/frc971/analysis/in_process_plotter_demo.cc
new file mode 100644
index 0000000..492ddfa
--- /dev/null
+++ b/frc971/analysis/in_process_plotter_demo.cc
@@ -0,0 +1,42 @@
+#include "frc971/analysis/in_process_plotter.h"
+
+#include "aos/init.h"
+
+// To run this example, do:
+// bazel run -c opt //frc971/analysis:in_process_plotter_demo
+// And then open localhost:8080, select "C++ Plotter" from the drop-down, and
+// then select "TITLE!" or "Trig Functions" from the second drop-down to see
+// each plot.
+int main(int argc, char *argv[]) {
+  aos::InitGoogle(&argc, &argv);
+  frc971::analysis::Plotter plotter;
+  plotter.Title("TITLE!");
+  plotter.AddFigure("Fig Foo");
+  plotter.ShareXAxis(true);
+  plotter.AddLine({1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, "y = x");
+  plotter.AddLine({5, 4, 3, 2, 1}, {1, 2, 3, 4, 5}, "y = -x");
+  plotter.YLabel("Y Axis");
+  plotter.AddFigure("Fig Bar");
+  plotter.ShareXAxis(true);
+  plotter.AddLine({1, 2, 3}, {3, 4, 5}, "y = x + 2");
+  plotter.XLabel("X Axis (Linked to both above plots)");
+  plotter.Publish();
+
+  plotter.Title("Trig Functions");
+
+  plotter.AddFigure("Sin & Cos");
+  std::vector<double> x;
+  std::vector<double> sinx;
+  std::vector<double> cosx;
+  constexpr int kNumPoints = 100000;
+  for (int ii = 0; ii < kNumPoints; ++ii) {
+    x.push_back(ii * 2 * M_PI / kNumPoints);
+    sinx.push_back(std::sin(x.back()));
+    cosx.push_back(std::cos(x.back()));
+  }
+  plotter.AddLine(x, sinx, "sin(x)");
+  plotter.AddLine(x, cosx, "cos(x)");
+  plotter.Publish();
+
+  plotter.Spin();
+}
diff --git a/frc971/analysis/plot_data.fbs b/frc971/analysis/plot_data.fbs
new file mode 100644
index 0000000..c21add1
--- /dev/null
+++ b/frc971/analysis/plot_data.fbs
@@ -0,0 +1,53 @@
+// This flatbuffer defines the interface that is used by the in-process
+// web plotter to plot data dynamically. Both the structure of the plot and
+// the data to plot is all packaged within a single Plot message. Each Plot
+// message will correspond to a single view/tab on the web-page, and can have
+// multiple figures, each of which can have multiple lines.
+namespace frc971.analysis;
+
+// Position within the web-page to plot a figure at. [0, 0] will be the upper
+// left corner of the allowable places where plots can be put, and should
+// generally be the default location. All values in pixels.
+table Position {
+  top:float (id: 0);
+  left:float (id: 1);
+  width:float (id: 2);
+  height:float (id: 3);
+}
+
+struct Point {
+  x:double (id: 0);
+  y:double (id: 1);
+}
+
+// RGB values are in the range [0, 1].
+struct Color {
+  r:float (id: 0);
+  g:float (id: 1);
+  b:float (id: 2);
+}
+
+table Line {
+  label:string (id: 0);
+  points:[Point] (id: 1);
+  color:Color (id: 2);
+}
+
+table Figure {
+  position:Position (id: 0);
+  lines:[Line] (id: 1);
+  // Specifies whether to link the x-axis of this Figure with that of other
+  // figures in this Plot. Only the axes of Figure's with this flag set will
+  // be linked.
+  share_x_axis:bool (id: 2);
+  title:string (id: 3);
+  xlabel:string (id: 4);
+  ylabel:string (id: 5);
+}
+
+table Plot {
+  figures:[Figure] (id: 0);
+  title:string (id: 1);
+}
+
+root_type Plot;
diff --git a/frc971/analysis/plot_data_utils.ts b/frc971/analysis/plot_data_utils.ts
new file mode 100644
index 0000000..8d42a4a
--- /dev/null
+++ b/frc971/analysis/plot_data_utils.ts
@@ -0,0 +1,95 @@
+// Provides a plot which handles plotting the plot defined by a
+// frc971.analysis.Plot message.
+import * as configuration from 'org_frc971/aos/configuration_generated';
+import * as plot_data from 'org_frc971/frc971/analysis/plot_data_generated';
+import {MessageHandler, TimestampedMessage} from 'org_frc971/aos/network/www/aos_plotter';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+import {Plot} from 'org_frc971/aos/network/www/plotter';
+import * as proxy from 'org_frc971/aos/network/www/proxy';
+
+import Connection = proxy.Connection;
+import Schema = configuration.reflection.Schema;
+import PlotFb = plot_data.frc971.analysis.Plot;
+
+export function plotData(conn: Connection, parentDiv: Element) {
+  // Set up a selection box to allow the user to choose between plots to show.
+  const plotSelect = document.createElement('select');
+  parentDiv.appendChild(plotSelect);
+  const plots = new Map<string, HTMLElement>();
+  const invalidSelectValue = 'null';
+  plotSelect.addEventListener('input', () => {
+    for (const plot of plots.values()) {
+      plot.style.display = 'none';
+    }
+    if (plotSelect.value == invalidSelectValue) {
+      return;
+    }
+    plots.get(plotSelect.value).style.display = 'block';
+  });
+  plotSelect.add(new Option('Select Plot', invalidSelectValue));
+
+  const plotDiv = document.createElement('div');
+  plotDiv.style.position = 'absolute';
+  plotDiv.style.top = '30';
+  plotDiv.style.left = '0';
+  parentDiv.appendChild(plotDiv);
+
+  conn.addReliableHandler(
+      '/analysis', 'frc971.analysis.Plot', (data: Uint8Array, time: number) => {
+        const plotFb = PlotFb.getRootAsPlot(
+            new ByteBuffer(data) as unknown as flatbuffers.ByteBuffer);
+        const name = (!plotFb.title()) ? 'Plot ' + plots.size : plotFb.title();
+        const div = document.createElement('div');
+        div.style.display = 'none';
+        plots.set(name, div);
+        plotDiv.appendChild(div);
+        plotSelect.add(new Option(name, name));
+
+        const linkedXAxes: Plot[] = [];
+
+        for (let ii = 0; ii < plotFb.figuresLength(); ++ii) {
+          const figure = plotFb.figures(ii);
+          const figureDiv = document.createElement('div');
+          figureDiv.style.top = figure.position().top().toString();
+          figureDiv.style.left = figure.position().left().toString();
+          figureDiv.style.position = 'absolute';
+          div.appendChild(figureDiv);
+          const plot = new Plot(
+              figureDiv, figure.position().width(), figure.position().height());
+
+          if (figure.title()) {
+            plot.getAxisLabels().setTitle(figure.title());
+          }
+          if (figure.xlabel()) {
+            plot.getAxisLabels().setXLabel(figure.xlabel());
+          }
+          if (figure.ylabel()) {
+            plot.getAxisLabels().setYLabel(figure.xlabel());
+          }
+          if (figure.shareXAxis()) {
+            for (const other of linkedXAxes) {
+              plot.linkXAxis(other);
+            }
+            linkedXAxes.push(plot);
+          }
+
+          for (let jj = 0; jj < figure.linesLength(); ++jj) {
+            const lineFb = figure.lines(jj);
+            const line = plot.getDrawer().addLine();
+            if (lineFb.label()) {
+              line.setLabel(lineFb.label());
+            }
+            const points = new Float32Array(lineFb.pointsLength() * 2);
+            for (let kk = 0; kk < lineFb.pointsLength(); ++kk) {
+              points[kk * 2] = lineFb.points(kk).x();
+              points[kk * 2 + 1] = lineFb.points(kk).y();
+            }
+            if (lineFb.color()) {
+              line.setColor(
+                  [lineFb.color().r(), lineFb.color().g(), lineFb.color().b()]);
+            }
+            line.setPoints(points);
+          }
+        }
+      });
+}
diff --git a/frc971/analysis/plot_index.ts b/frc971/analysis/plot_index.ts
index 8fbdb7c..6b0e0e2 100644
--- a/frc971/analysis/plot_index.ts
+++ b/frc971/analysis/plot_index.ts
@@ -24,6 +24,7 @@
 import * as proxy from 'org_frc971/aos/network/www/proxy';
 import {plotImu} from 'org_frc971/frc971/wpilib/imu_plotter';
 import {plotDemo} from 'org_frc971/aos/network/www/demo_plot';
+import {plotData} from 'org_frc971/frc971/analysis/plot_data_utils';
 
 import Connection = proxy.Connection;
 import Configuration = configuration.aos.Configuration;
@@ -77,7 +78,8 @@
 // presence of certain channels.
 const plotIndex = new Map<string, PlotState>([
   ['Demo', new PlotState(plotDiv, plotDemo)],
-  ['IMU', new PlotState(plotDiv, plotImu)]
+  ['IMU', new PlotState(plotDiv, plotImu)],
+  ['C++ Plotter', new PlotState(plotDiv, plotData)],
 ]);
 
 const invalidSelectValue = 'null';
diff --git a/frc971/analysis/plotter_config.json b/frc971/analysis/plotter_config.json
new file mode 100644
index 0000000..49266ee
--- /dev/null
+++ b/frc971/analysis/plotter_config.json
@@ -0,0 +1,12 @@
+{
+  "channels": [
+    {
+      "name": "/analysis",
+      "type": "frc971.analysis.Plot",
+      "max_size": 10000000
+    }
+  ],
+  "imports": [
+    "../../aos/events/aos.json"
+  ]
+}