Merge changes I976dffaf,I9ef234b6,I164c8fbb

* changes:
  Add basic test for plot.py
  Add python plotter using py_log_reader
  Provide Python 3 matplotlib
diff --git a/WORKSPACE b/WORKSPACE
index d43693f..fb736ee 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -277,11 +277,14 @@
     url = "http://www.frc971.org/Build-Dependencies/mingw_compiler.tar.gz",
 )
 
+# Note that we should generally keep the matplotlib repo in a folder not
+# named matplotlib, because otherwise the repository itself tends to end up
+# on the PYTHONPATH, rather than the matplotlib folder within this repo.
 http_archive(
-    name = "matplotlib",
+    name = "matplotlib_repo",
     build_file = "@//debian:matplotlib.BUILD",
-    sha256 = "fa1ff9f3bb7fddba6d0b904af5ffdca97c6233a950840273c25145b8cad80483",
-    url = "http://www.frc971.org/Build-Dependencies/matplotlib-3.tar.gz",
+    sha256 = "24f8b75754e465299ddf92bd895ab111d54945a45b0f410d7cfa16b15b162e2f",
+    url = "http://www.frc971.org/Build-Dependencies/matplotlib-4.tar.gz",
 )
 
 http_archive(
diff --git a/debian/BUILD b/debian/BUILD
index 0cb12e7..9b7ad4d 100644
--- a/debian/BUILD
+++ b/debian/BUILD
@@ -55,7 +55,7 @@
     srcs = [
         "matplotlib_init.patch",
     ],
-    visibility = ["@matplotlib//:__pkg__"],
+    visibility = ["@matplotlib_repo//:__pkg__"],
 )
 
 filegroup(
@@ -182,6 +182,7 @@
         "python-matplotlib",
         "python-tk",
         "python3-matplotlib",
+        "python3-tk",
     ],
 )
 
diff --git a/debian/matplotlib.BUILD b/debian/matplotlib.BUILD
index 5f89ba9..af2c4b7 100644
--- a/debian/matplotlib.BUILD
+++ b/debian/matplotlib.BUILD
@@ -1,124 +1,8 @@
-genrule(
-    name = "patch_init",
-    srcs = [
-        "usr/lib/python2.7/dist-packages/matplotlib/__init__.py",
-        "@//debian:matplotlib_patches",
-    ],
-    outs = ["matplotlib/__init__.py"],
-    cmd = " && ".join([
-        "cp $(location usr/lib/python2.7/dist-packages/matplotlib/__init__.py) $@",
-        "readonly PATCH=\"$$(readlink -f $(location @patch))\"",
-        "readonly FILE=\"$$(readlink -f $(location @//debian:matplotlib_patches))\"",
-        "(cd $(@D) && \"$${PATCH}\" -p1 < \"$${FILE}\") > /dev/null",
-    ]),
-    tools = [
-        "@patch",
-    ],
-)
+load("@//debian:matplotlib.bzl", "build_matplotlib")
 
-_src_files = glob(
-    include = ["usr/lib/python2.7/dist-packages/**/*.py"],
-    exclude = [
-        "usr/lib/python2.7/dist-packages/matplotlib/__init__.py",
-    ],
-)
+build_matplotlib("3", "3.5")
 
-_data_files = glob([
-    "usr/share/matplotlib/mpl-data/**",
-    "usr/share/tcltk/**",
-])
-
-_src_copied = ["/".join(f.split("/")[4:]) for f in _src_files]
-
-_builtin_so_files = glob([
-    "usr/lib/python2.7/dist-packages/**/*.x86_64-linux-gnu.so",
-    "usr/lib/python2.7/lib-dynload/*.so",
-])
-
-_system_so_files = glob([
-    "usr/lib/**/*.so*",
-    "lib/x86_64-linux-gnu/**/*.so*",
-])
-
-_builtin_so_copied = ["/".join(f.split("/")[4:]) for f in _builtin_so_files]
-
-_system_so_copied = ["rpathed/" + f for f in _system_so_files]
-
-_builtin_rpaths = [":".join([
-    "\\$$ORIGIN/%s" % rel,
-    "\\$$ORIGIN/%s/rpathed/usr/lib/x86_64-linux-gnu" % rel,
-    "\\$$ORIGIN/%s/rpathed/usr/lib" % rel,
-    "\\$$ORIGIN/%s/rpathed/lib/x86_64-linux-gnu" % rel,
-]) for rel in ["/".join([".." for _ in so.split("/")[1:]]) for so in _builtin_so_copied]]
-
-_system_rpaths = [":".join([
-    "\\$$ORIGIN/%s/rpathed/usr/lib/x86_64-linux-gnu" % rel,
-    "\\$$ORIGIN/%s/rpathed/lib/x86_64-linux-gnu" % rel,
-]) for rel in ["/".join([".." for _ in so.split("/")[1:]]) for so in _system_so_copied]]
-
-genrule(
-    name = "run_patchelf_builtin",
-    srcs = _builtin_so_files,
-    outs = _builtin_so_copied,
-    cmd = "\n".join(
-        [
-            "cp $(location %s) $(location %s)" % (src, dest)
-            for src, dest in zip(_builtin_so_files, _builtin_so_copied)
-        ] +
-        ["$(location @patchelf) --set-rpath %s $(location %s)" % (rpath, so) for rpath, so in zip(_builtin_rpaths, _builtin_so_copied)],
-    ),
-    tools = [
-        "@patchelf",
-    ],
-)
-
-genrule(
-    name = "run_patchelf_system",
-    srcs = _system_so_files,
-    outs = _system_so_copied,
-    cmd = "\n".join(
-        [
-            "cp $(location %s) $(location %s)" % (src, dest)
-            for src, dest in zip(_system_so_files, _system_so_copied)
-        ] +
-        ["$(location @patchelf) --set-rpath %s $(location %s)" % (rpath, so) for rpath, so in zip(_system_rpaths, _system_so_copied)],
-    ),
-    tools = [
-        "@patchelf",
-    ],
-)
-
-genrule(
-    name = "copy_files",
-    srcs = _src_files,
-    outs = _src_copied,
-    cmd = " && ".join(["cp $(location %s) $(location %s)" % (src, dest) for src, dest in zip(
-        _src_files,
-        _src_copied,
-    )]),
-)
-
-genrule(
-    name = "create_rc",
-    outs = ["usr/share/matplotlib/mpl-data/matplotlibrc"],
-    cmd = "\n".join([
-        "cat > $@ << END",
-        # This is necessary to make matplotlib actually plot things to the
-        # screen by default.
-        "backend      : TkAgg",
-        "END",
-    ]),
-)
-
-py_library(
-    name = "matplotlib",
-    srcs = _src_copied + [
-        "matplotlib/__init__.py",
-    ],
-    data = _data_files + _builtin_so_copied + _system_so_copied + [
-        ":usr/share/matplotlib/mpl-data/matplotlibrc",
-    ],
-    imports = ["usr/lib/python2.7/dist-packages"],
-    restricted_to = ["@//tools:k8"],
-    visibility = ["//visibility:public"],
+build_matplotlib(
+    "2.7",
+    copy_shared_files = False,
 )
diff --git a/debian/matplotlib.bzl b/debian/matplotlib.bzl
index b1687d6..a2c6683 100644
--- a/debian/matplotlib.bzl
+++ b/debian/matplotlib.bzl
@@ -90,6 +90,7 @@
     "python3-matplotlib_2.0.0+dfsg1-2_amd64.deb": "8f5d3509d4f5451468c6de44fc8dfe391c3df4120079adc01ab5f13ff4194f5a",
     "python3-pyparsing_2.1.10+dfsg1-1_all.deb": "ee8d7f04f841248127e81b3d356d37e623ed29da284b28c7d2b8a5b34f0eebba",
     "python3-six_1.10.0-3_all.deb": "597005e64cf70e4be97170a47c33287f70a1c87a2979d47a434c10c9201af3ca",
+    "python3-tk_3.5.3-1_amd64.deb": "67489a1c86a9e501dbe2989cd72b5b2c70511fe3829af3567a009271b61fdbb5",
     "python3-tz_2016.7-0.3_all.deb": "5f1c7db456aac5fe9b0ea66d7413c12660c7652ae382c640f71c517a05d39551",
     "shared-mime-info_1.8-1+deb9u1_amd64.deb": "d6591f13ee1200c4f0b5581c2299eb7b8097a6b04742dc333e34a7bb7ba47532",
     "tk8.6-blt2.5_2.5.3+dfsg-3_amd64.deb": "88587a928e2bd692650d98c1483b67f1dee1fed57730077c895e689462af1569",
@@ -97,3 +98,148 @@
     "tzdata_2019c-0+deb9u1_all.deb": "80c9809dafc62ec741cbf3024130253de6047af31a10f0c86bb17f2d12ad10d5",
     "ucf_3.0036_all.deb": "796a65e765d6045007175531d512c720f4eb04e7f3326b79b848bc6123947225",
 }
+
+def build_matplotlib(version, tkinter_py_version = None, copy_shared_files = True):
+  """Creates a py_library rule for matplotlib for the given python version.
+
+  See debian/matplotlib.BUILD for the usage.
+
+  All the rules generated by this will be suffixed by version. Only one
+  instance of this macro should set copy_shared_files, which generate the
+  files that are shared between python versions.
+
+  tkinter_py_version is used because for the Python3 instance, some files
+  are in folders named python3 and some are in folders named python3.5...
+
+  version numbers should both be strings.
+  """
+  if tkinter_py_version == None:
+    tkinter_py_version = version
+
+  native.genrule(
+      name = "patch_init" + version,
+      srcs = [
+          "usr/lib/python" + version + "/dist-packages/matplotlib/__init__.py",
+          "@//debian:matplotlib_patches",
+      ],
+      outs = [version + "/matplotlib/__init__.py"],
+      cmd = " && ".join([
+          "cp $(location usr/lib/python" + version + "/dist-packages/matplotlib/__init__.py) $@",
+          "readonly PATCH=\"$$(readlink -f $(location @patch))\"",
+          "readonly FILE=\"$$(readlink -f $(location @//debian:matplotlib_patches))\"",
+          "(cd $(@D) && \"$${PATCH}\" -p1 < \"$${FILE}\") > /dev/null",
+      ]),
+      tools = [
+          "@patch",
+      ],
+  )
+
+  _src_files = native.glob(
+      include = ["usr/lib/python" + version + "/dist-packages/**/*.py"],
+      exclude = [
+          "usr/lib/python" + version + "/dist-packages/matplotlib/__init__.py",
+      ],
+  )
+
+  _data_files = native.glob([
+      "usr/share/matplotlib/mpl-data/**",
+      "usr/share/tcltk/**",
+  ])
+
+  _src_copied = ["/".join([version] + f.split("/")[4:]) for f in _src_files]
+
+  _builtin_so_files = native.glob([
+      "usr/lib/python" + version + "/dist-packages/**/*x86_64-linux-gnu.so",
+      "usr/lib/python" + tkinter_py_version + "/lib-dynload/*.so",
+  ])
+
+  _system_so_files = native.glob([
+      "usr/lib/**/*.so*",
+      "lib/x86_64-linux-gnu/**/*.so*",
+  ])
+
+  _builtin_so_copied = ["/".join([version] + f.split("/")[4:]) for f in _builtin_so_files]
+
+  rpath_prefix = "rpathed" + version + "/"
+
+  _system_so_copied = [rpath_prefix + f for f in _system_so_files]
+
+  _builtin_rpaths = [":".join([
+      "\\$$ORIGIN/%s" % rel,
+      "\\$$ORIGIN/%s/%s/usr/lib/x86_64-linux-gnu" % (rel, rpath_prefix),
+      "\\$$ORIGIN/%s/%s/usr/lib" % (rel, rpath_prefix),
+      "\\$$ORIGIN/%s/%s/lib/x86_64-linux-gnu" % (rel, rpath_prefix),
+  ]) for rel in ["/".join([".." for _ in so.split("/")[1:]]) for so in _builtin_so_copied]]
+
+  _system_rpaths = [":".join([
+      "\\$$ORIGIN/%s/%s/usr/lib/x86_64-linux-gnu" % (rel, rpath_prefix),
+      "\\$$ORIGIN/%s/%s/lib/x86_64-linux-gnu" % (rel, rpath_prefix),
+  ]) for rel in ["/".join([".." for _ in so.split("/")[1:]]) for so in _system_so_copied]]
+
+  native.genrule(
+      name = "run_patchelf_builtin" + version,
+      srcs = _builtin_so_files,
+      outs = _builtin_so_copied,
+      cmd = "\n".join(
+          [
+              "cp $(location %s) $(location %s)" % (src, dest)
+              for src, dest in zip(_builtin_so_files, _builtin_so_copied)
+          ] +
+          ["$(location @patchelf) --set-rpath %s $(location %s)" % (rpath, so) for rpath, so in zip(_builtin_rpaths, _builtin_so_copied)],
+      ),
+      tools = [
+          "@patchelf",
+      ],
+  )
+
+  native.genrule(
+      name = "run_patchelf_system" + version,
+      srcs = _system_so_files,
+      outs = _system_so_copied,
+      cmd = "\n".join(
+          [
+              "cp $(location %s) $(location %s)" % (src, dest)
+              for src, dest in zip(_system_so_files, _system_so_copied)
+          ] +
+          ["$(location @patchelf) --set-rpath %s $(location %s)" % (rpath, so) for rpath, so in zip(_system_rpaths, _system_so_copied)],
+      ),
+      tools = [
+          "@patchelf",
+      ],
+  )
+
+  native.genrule(
+      name = "copy_files" + version,
+      srcs = _src_files,
+      outs = _src_copied,
+      cmd = " && ".join(["cp $(location %s) $(location %s)" % (src, dest) for src, dest in zip(
+          _src_files,
+          _src_copied,
+      )]),
+  )
+
+  if copy_shared_files:
+    native.genrule(
+        name = "create_rc" + version,
+        outs = ["usr/share/matplotlib/mpl-data/matplotlibrc"],
+        cmd = "\n".join([
+            "cat > $@ << END",
+            # This is necessary to make matplotlib actually plot things to the
+            # screen by default.
+            "backend      : TkAgg",
+            "END",
+        ]),
+    )
+
+  native.py_library(
+      name = "matplotlib" + version,
+      srcs = _src_copied + [
+          version + "/matplotlib/__init__.py",
+      ],
+      data = _data_files + _builtin_so_copied + _system_so_copied + [
+          ":usr/share/matplotlib/mpl-data/matplotlibrc",
+      ],
+      imports = ["usr/lib/python" + version + "/dist-packages", version, "."],
+      restricted_to = ["@//tools:k8"],
+      visibility = ["//visibility:public"],
+  )
diff --git a/debian/matplotlib_init.patch b/debian/matplotlib_init.patch
index 0106b64..e547108 100644
--- a/debian/matplotlib_init.patch
+++ b/debian/matplotlib_init.patch
@@ -18,7 +18,7 @@
 +os.environ['MATPLOTLIBDATA'] = \
 +        os.path.join( \
 +            _matplotlib_base,
-+            "usr", "share", "matplotlib", "mpl-data")
++            "..", "usr", "share", "matplotlib", "mpl-data")
 +# Avoid reading /etc/matplotlib in all cases. Matplotlib is pretty happy to
 +# escape the sandbox by using absolute paths.
 +os.environ['MATPLOTLIBRC'] = os.environ['MATPLOTLIBDATA']
@@ -29,7 +29,7 @@
 +
 +# Tell Tcl where to find the sandboxed version. Otherwise, it will try using
 +# one from the host system, even if that's an incompatible version.
-+os.environ['TCL_LIBRARY'] = os.path.join(_matplotlib_base, 'usr', 'share',
++os.environ['TCL_LIBRARY'] = os.path.join(_matplotlib_base, '..', 'usr', 'share',
 +                                         'tcltk', 'tcl8.6')
 +
  try:
diff --git a/frc971/analysis/BUILD b/frc971/analysis/BUILD
index b526712..2686bcd 100644
--- a/frc971/analysis/BUILD
+++ b/frc971/analysis/BUILD
@@ -1,5 +1,7 @@
 package(default_visibility = ["//visibility:public"])
 
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+
 py_binary(
     name = "plot_action",
     srcs = [
@@ -12,7 +14,7 @@
     restricted_to = ["//tools:k8"],
     deps = [
         ":python_init",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -48,3 +50,32 @@
     restricted_to = ["//tools:k8"],
     deps = ["//aos:configuration_fbs_python"],
 )
+
+py_proto_library(
+    name = "plot_config_proto",
+    srcs = ["plot_config.proto"],
+)
+
+py_binary(
+    name = "plot",
+    srcs = ["plot.py"],
+    data = [
+        ":py_log_reader.so",
+    ] + glob(["plot_configs/**"]),
+    restricted_to = ["//tools:k8"],
+    deps = [
+        ":plot_config_proto",
+        ":python_init",
+        "@matplotlib_repo//:matplotlib3",
+    ],
+)
+
+py_test(
+    name = "plot_test",
+    srcs = ["plot_test.py"],
+    data = [
+        "@sample_logfile//file",
+    ],
+    restricted_to = ["//tools:k8"],
+    deps = [":plot"],
+)
diff --git a/frc971/analysis/plot.py b/frc971/analysis/plot.py
new file mode 100644
index 0000000..4367c08
--- /dev/null
+++ b/frc971/analysis/plot.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python3
+# Sample usage:
+# bazel run -c opt //frc971/analysis:plot  -- --logfile /tmp/log.fbs --config gyro.pb
+import argparse
+import json
+import os.path
+from pathlib import Path
+import sys
+
+from frc971.analysis.py_log_reader import LogReader
+from frc971.analysis.plot_config_pb2 import PlotConfig, Signal
+from google.protobuf import text_format
+
+import matplotlib
+from matplotlib import pyplot as plt
+
+
+class Plotter:
+    def __init__(self, plot_config: PlotConfig, reader: LogReader):
+        self.config = plot_config
+        self.reader = reader
+        # Data streams, indexed by alias.
+        self.data = {}
+
+    def process_logfile(self):
+        aliases = set()
+        for channel in self.config.channel:
+            if channel.alias in aliases:
+                raise ValueError("Duplicate alias " + channel.alias)
+            aliases.add(channel.alias)
+            if not self.reader.subscribe(channel.name, channel.type):
+                raise ValueError("No such channel with name " + channel.name +
+                                 " and type " + channel.type)
+
+        self.reader.process()
+
+        for channel in self.config.channel:
+            self.data[channel.alias] = []
+            for message in self.reader.get_data_for_channel(
+                    channel.name, channel.type):
+                valid_json = message[2].replace('nan', '"nan"')
+                parsed_json = json.loads(valid_json)
+                self.data[channel.alias].append((message[0], message[1],
+                                                 parsed_json))
+
+    def plot_signal(self, axes: matplotlib.axes.Axes, signal: Signal):
+        if not signal.channel in self.data:
+            raise ValueError("No channel alias " + signal.channel)
+        field_path = signal.field.split('.')
+        monotonic_time = []
+        signal_data = []
+        for entry in self.data[signal.channel]:
+            monotonic_time.append(entry[0] * 1e-9)
+            value = entry[2]
+            for name in field_path:
+                value = value[name]
+            # Catch NaNs and convert them to floats.
+            value = float(value)
+            signal_data.append(value)
+        label_name = signal.channel + "." + signal.field
+        axes.plot(monotonic_time, signal_data, label=label_name)
+
+    def plot(self):
+        for figure_config in self.config.figure:
+            fig = plt.figure()
+            num_subplots = len(figure_config.axes)
+            for ii in range(num_subplots):
+                axes = fig.add_subplot(num_subplots, 1, ii + 1)
+                axes_config = figure_config.axes[ii]
+                for signal in axes_config.signal:
+                    self.plot_signal(axes, signal)
+                axes.legend()
+                axes.set_xlabel("Monotonic Time (sec)")
+                if axes_config.HasField("ylabel"):
+                    axes.set_ylabel(axes_config.ylabel)
+
+
+def main(argv):
+    parser = argparse.ArgumentParser(
+        description="Plot data from an aos logfile.")
+    parser.add_argument(
+        "--logfile",
+        type=str,
+        required=True,
+        help="Path to the logfile to parse.")
+    parser.add_argument(
+        "--config",
+        type=str,
+        required=True,
+        help="Name of the plot config to use.")
+    parser.add_argument(
+        "--config_dir",
+        type=str,
+        default="frc971/analysis/plot_configs",
+        help="Directory to look for plot configs in.")
+    args = parser.parse_args(argv[1:])
+
+    if not os.path.isdir(args.config_dir):
+        print(args.config_dir + " is not a directory.")
+        return 1
+    config_path = os.path.join(args.config_dir, args.config)
+    if not os.path.isfile(config_path):
+        print(config_path +
+              " does not exist or is not a file--available configs are")
+        for file_name in os.listdir(args.config_dir):
+            print(os.path.basename(file_name))
+        return 1
+
+    config = PlotConfig()
+    with open(config_path) as config_file:
+        text_format.Merge(config_file.read(), config)
+
+    if not os.path.isfile(args.logfile):
+        print(args.logfile + " is not a file.")
+        return 1
+
+    reader = LogReader(args.logfile)
+
+    plotter = Plotter(config, reader)
+    plotter.process_logfile()
+    plotter.plot()
+    plt.show()
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/frc971/analysis/plot_config.proto b/frc971/analysis/plot_config.proto
new file mode 100644
index 0000000..9aaea89
--- /dev/null
+++ b/frc971/analysis/plot_config.proto
@@ -0,0 +1,45 @@
+syntax = "proto2";
+
+package frc971.analysis;
+
+// Specification fo a Channel to pull from the logfile. The name and type will
+// be the full name/type of the channel to pull from the logfile. The alias is a
+// shorter, easier to type, name that the rest of the logfile will use to refer
+// to the channel.
+message Channel {
+  optional string name = 1;
+  optional string type = 2;
+  optional string alias = 3;
+}
+
+// A specification for a single signal within a Channel.
+message Signal {
+  // Alias for the channel to pull the signal from--this should match an alias
+  // specified in one of the Channels.
+  optional string channel = 1;
+  // Specification of the field to plot. Currently, this only supports simple
+  // submessages, using dots. To access, e.g., the "bar" member of the "foo"
+  // submessage, field would be "foo.bar". This does not currently support
+  // working with repeated fields.
+  optional string field = 2;
+}
+
+// Message representing a single pyplot Axes, with specifications for exactly
+// which signals to show in the supplied subplot.
+message Axes {
+  repeated Signal signal = 1;
+  optional string ylabel = 2;
+}
+
+// Message representing a single pyplot figure.
+message Figure {
+  repeated Axes axes = 1;
+}
+
+// This configuration specifies what to plot when reading from a logfile.
+message PlotConfig {
+  // List of channels and their aliases to use in the plot.
+  repeated Channel channel = 1;
+  // Figures to plot.
+  repeated Figure figure = 2;
+}
diff --git a/frc971/analysis/plot_configs/gyro.pb b/frc971/analysis/plot_configs/gyro.pb
new file mode 100644
index 0000000..e398a2e
--- /dev/null
+++ b/frc971/analysis/plot_configs/gyro.pb
@@ -0,0 +1,38 @@
+channel {
+  name: "/drivetrain"
+  type: "frc971.IMUValues"
+  alias: "IMU"
+}
+
+figure {
+  axes {
+    signal {
+      channel: "IMU"
+      field: "gyro_x"
+    }
+    signal {
+      channel: "IMU"
+      field: "gyro_y"
+    }
+    signal {
+      channel: "IMU"
+      field: "gyro_z"
+    }
+    ylabel: "rad / sec"
+  }
+  axes {
+    signal {
+      channel: "IMU"
+      field: "accelerometer_x"
+    }
+    signal {
+      channel: "IMU"
+      field: "accelerometer_y"
+    }
+    signal {
+      channel: "IMU"
+      field: "accelerometer_z"
+    }
+    ylabel: "g"
+  }
+}
diff --git a/frc971/analysis/plot_test.py b/frc971/analysis/plot_test.py
new file mode 100644
index 0000000..1c57ae2
--- /dev/null
+++ b/frc971/analysis/plot_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python3
+import unittest
+
+import matplotlib
+# Use a non-interactive backend so that the test can actually run...
+matplotlib.use('Agg')
+
+import frc971.analysis.plot
+
+
+class PlotterTest(unittest.TestCase):
+    def test_plotter(self):
+        """Basic test that makes sure that we can run the test without crashing."""
+        self.assertEqual(0,
+                         frc971.analysis.plot.main([
+                             "binary", "--logfile",
+                             "external/sample_logfile/file/log.fbs",
+                             "--config", "gyro.pb"
+                         ]))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/frc971/control_loops/python/BUILD b/frc971/control_loops/python/BUILD
index b3aa2c0..50764c7 100644
--- a/frc971/control_loops/python/BUILD
+++ b/frc971/control_loops/python/BUILD
@@ -11,7 +11,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -59,7 +59,7 @@
     deps = [
         ":controls",
         ":python_init",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -72,7 +72,7 @@
     deps = [
         ":controls",
         ":python_init",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -133,7 +133,7 @@
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
         "//y2016/control_loops/python:polydrivetrain_lib",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -146,7 +146,7 @@
         ":controls",
         "//aos/util:py_trapezoid_profile",
         "//frc971/control_loops:python_init",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -159,7 +159,7 @@
         ":controls",
         "//aos/util:py_trapezoid_profile",
         "//frc971/control_loops:python_init",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
diff --git a/motors/python/BUILD b/motors/python/BUILD
index d5d74ae..5eafc3a 100644
--- a/motors/python/BUILD
+++ b/motors/python/BUILD
@@ -13,7 +13,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
diff --git a/y2014/control_loops/python/BUILD b/y2014/control_loops/python/BUILD
index 5216eb9..cba8cea 100644
--- a/y2014/control_loops/python/BUILD
+++ b/y2014/control_loops/python/BUILD
@@ -60,7 +60,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -76,7 +76,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -92,7 +92,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
diff --git a/y2016/control_loops/python/BUILD b/y2016/control_loops/python/BUILD
index c49a250..65964fd 100644
--- a/y2016/control_loops/python/BUILD
+++ b/y2016/control_loops/python/BUILD
@@ -61,7 +61,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -94,7 +94,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -111,7 +111,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -140,7 +140,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -157,7 +157,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
diff --git a/y2017/control_loops/python/BUILD b/y2017/control_loops/python/BUILD
index 50f5584..6afb242 100644
--- a/y2017/control_loops/python/BUILD
+++ b/y2017/control_loops/python/BUILD
@@ -58,7 +58,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -106,7 +106,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -123,7 +123,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
@@ -168,7 +168,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )
 
diff --git a/y2018/control_loops/python/BUILD b/y2018/control_loops/python/BUILD
index 5690b08..ba34d86 100644
--- a/y2018/control_loops/python/BUILD
+++ b/y2018/control_loops/python/BUILD
@@ -94,7 +94,7 @@
         "//external:python-gflags",
         "//external:python-glog",
         "//frc971/control_loops/python:controls",
-        "@matplotlib",
+        "@matplotlib_repo//:matplotlib2.7",
     ],
 )