Provide utilities for convenient plotting of channels

Implement a set of utilities (along with basic examples) to
make it so that we can readily make plots of individual channels
from logfiles (or live on the robot).

Change-Id: Ic648c9ccb9dcb73419dc2c8c4c395fdea0536110
diff --git a/frc971/analysis/plot_index.ts b/frc971/analysis/plot_index.ts
new file mode 100644
index 0000000..759202a
--- /dev/null
+++ b/frc971/analysis/plot_index.ts
@@ -0,0 +1,118 @@
+// This file provides an index of all standard plot configs.
+// In the future, this may be split into more pieces (e.g., to have
+// year-specific indices). For now, the pattern for creating a new plot
+// is that you provide an exported function (a la the plot*() functions imported
+// below) which, when called, will generate the plot in the provided div.
+// This file handles providing a master list of all known plots so that
+// the user can just open a single web-page and select the plot that they want
+// from a drop-down. A given plot will not be loaded until it has been selected
+// once, at which point it will stay loaded. This means that if the user wants
+// to switch between several plot configs, they don't incur any performance
+// penalty associated with swapping.
+// By default, no plot is selected, but the plot= URL parameter may be used
+// to specify a specific plot, so that people can create links to a specific
+// plot.
+// The plot*() functions are called *after* we have already received a valid
+// config from the web server, so config handlers do not need to be used.
+//
+// The exact setup of this will be in flux as we add more configs and figure out
+// what setups work best--we will likely end up with separate index files for
+// each robot year, and may even end up allowing plots to be specified solely
+// using JSON rather than requiring people to write a script just to create
+// a plot.
+import * as configuration from 'org_frc971/aos/configuration_generated';
+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 Connection = proxy.Connection;
+import Configuration = configuration.aos.Configuration;
+
+const rootDiv = document.createElement('div');
+document.body.appendChild(rootDiv);
+
+const helpDiv = document.createElement('div');
+rootDiv.appendChild(helpDiv);
+helpDiv.innerHTML =
+    'Help: click + drag to pan, double click to reset, scroll to zoom. ' +
+    'Hold the x/y keys to only pan/zoom along the x/y axes.';
+
+class PlotState {
+  public readonly div: HTMLElement;
+  private initialized = false;
+  constructor(
+      parentDiv: HTMLElement,
+      private readonly initializer:
+          (conn: Connection, element: Element) => void) {
+    this.div = document.createElement('div');
+    parentDiv.appendChild(this.div);
+    this.hide();
+  }
+  initialize(conn: Connection): void {
+    if (!this.initialized) {
+      this.initializer(conn, this.div);
+      this.initialized = true;
+    }
+  }
+  hide(): void {
+    this.div.style.display = "none";
+  }
+  show(): void {
+    this.div.style.display = "block";
+  }
+}
+
+const plotSelect = document.createElement('select');
+rootDiv.appendChild(plotSelect);
+
+const plotDiv = document.createElement('div');
+plotDiv.style.top = (plotSelect.getBoundingClientRect().bottom + 10).toString();
+plotDiv.style.left = '0';
+plotDiv.style.position = 'absolute';
+rootDiv.appendChild(plotDiv);
+
+// The master list of all the plots that we provide. For a given config, it
+// is possible that not all of these plots will be usable depending on the
+// presence of certain channels.
+const plotIndex = new Map<string, PlotState>([
+  ['Demo', new PlotState(plotDiv, plotDemo)],
+  ['IMU', new PlotState(plotDiv, plotImu)]
+]);
+
+const invalidSelectValue = 'null';
+function getDefaultPlot(): string {
+  const urlParams = (new URL(document.URL)).searchParams;
+  const urlParamKey = 'plot';
+  if (!urlParams.has(urlParamKey)) {
+    return invalidSelectValue;
+  }
+  const desiredPlot = urlParams.get(urlParamKey);
+  if (!plotIndex.has(desiredPlot)) {
+    return invalidSelectValue;
+  }
+  return desiredPlot;
+}
+
+const conn = new Connection();
+
+conn.connect();
+
+conn.addConfigHandler((config: Configuration) => {
+  plotSelect.add(new Option("Select Plot", invalidSelectValue));
+  for (const name of plotIndex.keys()) {
+    plotSelect.add(new Option(name, name));
+  }
+  plotSelect.addEventListener('input', () => {
+    for (const plot of plotIndex.values()) {
+      plot.hide();
+    }
+    if (plotSelect.value == invalidSelectValue) {
+      return;
+    }
+    plotIndex.get(plotSelect.value).initialize(conn);
+    plotIndex.get(plotSelect.value).show();
+  });
+  plotSelect.value = getDefaultPlot();
+  // Force the event to occur once at the start.
+  plotSelect.dispatchEvent(new Event('input'));
+});