Add plots for statistical significance of measurements.

* This is useful when comparing benchmark runs.
* Inspired by [Statistics for Hackers](https://youtu.be/Iq9DzN6mvYA?t=800)

Change-Id: I4f2bfdf174263e1fa1542e115c0caabcafdb53b3
Test: `npm run-script dev`
diff --git a/development/plot-benchmarks/src/lib/App.svelte b/development/plot-benchmarks/src/lib/App.svelte
index ffa3e57..f1ace88 100644
--- a/development/plot-benchmarks/src/lib/App.svelte
+++ b/development/plot-benchmarks/src/lib/App.svelte
@@ -3,9 +3,17 @@
   import { writable } from "svelte/store";
   import type { FileMetadata } from "../types/files.js";
   import Session from "./Session.svelte";
+  import { wrap } from "comlink";
+  import { StatService } from "../workers/service.js";
 
   // Stores
   let entries: Writable<FileMetadata[]> = writable([]);
+  const url = new URL("../workers/worker.ts", import.meta.url);
+  const service = wrap<StatService>(
+    new Worker(url, {
+      type: "module",
+    })
+  );
 
   function onFilesChanged(event) {
     const detail: FileMetadata[] = event.detail;
@@ -22,5 +30,5 @@
 </details>
 
 <div class="container">
-  <Session fileEntries={$entries} on:entries={onFilesChanged} />
+  <Session fileEntries={$entries} {service} on:entries={onFilesChanged} />
 </div>
diff --git a/development/plot-benchmarks/src/lib/Chart.svelte b/development/plot-benchmarks/src/lib/Chart.svelte
index 7a063a8..9a224e2 100644
--- a/development/plot-benchmarks/src/lib/Chart.svelte
+++ b/development/plot-benchmarks/src/lib/Chart.svelte
@@ -10,6 +10,7 @@
 
   export let data: Data;
   export let chartType: ChartType = "line";
+  export let isExperimental: boolean = false;
 
   $: {
     if ($chart) {
@@ -27,7 +28,9 @@
   onMount(() => {
     const onUpdate = (chart: Chart) => {
       $chart = chart;
-      $items = chart.options.plugins.legend.labels.generateLabels(chart);
+      // Bad typings.
+      const legend = chart.options.plugins.legend as any;
+      $items = legend.labels.generateLabels(chart);
     };
     const plugins = {
       legend: {
@@ -65,7 +68,14 @@

     </button>
   </div>
-  <canvas id="chart" class="chart" bind:this={element} />
+  <canvas class="chart" bind:this={element} />
+  {#if isExperimental}
+    <footer class="slim">
+      <section class="experimental">
+        <kbd>Experimental</kbd>
+      </section>
+    </footer>
+  {/if}
 </article>
 
 {#if $items}
@@ -89,4 +99,17 @@
     border: none;
     padding: 5px;
   }
+
+  .slim {
+    margin-bottom: 0px;
+    padding: 0;
+  }
+
+  .experimental {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    justify-content: center;
+    margin-bottom: 0px;
+  }
 </style>
diff --git a/development/plot-benchmarks/src/lib/Dataset.svelte b/development/plot-benchmarks/src/lib/Dataset.svelte
index a19eff0..6e81cc0 100644
--- a/development/plot-benchmarks/src/lib/Dataset.svelte
+++ b/development/plot-benchmarks/src/lib/Dataset.svelte
@@ -1,14 +1,23 @@
 <script lang="ts">
   import { createEventDispatcher } from "svelte";
   import { Session, type IndexedWrapper } from "../wrappers/session.js";
-  import type { SelectionEvent, Selection } from "../types/events.js";
+  import type {
+    SelectionEvent,
+    Selection,
+    StatEvent,
+    StatInfo,
+    StatType,
+  } from "../types/events.js";
 
   export let name: string;
   export let datasetGroup: IndexedWrapper[];
 
+  // Dispatchers
+  let selectionDispatcher = createEventDispatcher<SelectionEvent>();
+  let statDispatcher = createEventDispatcher<StatEvent>();
   // State
-  let dispatcher = createEventDispatcher<SelectionEvent>();
   let selected: boolean = true;
+  let compute: boolean = false;
   let sources: Set<string>;
   let sampledMetrics: Set<string>;
   let metrics: Set<string>;
@@ -22,7 +31,21 @@
       name: name,
       enabled: selected,
     };
-    dispatcher("selections", [selection]);
+    selectionDispatcher("selections", [selection]);
+  };
+
+  let stat = function (type: StatType) {
+    return function (event: Event) {
+      event.stopPropagation();
+      const target = event.target as HTMLInputElement;
+      compute = target.checked;
+      const stat: StatInfo = {
+        name: name,
+        type: type,
+        enabled: compute
+      };
+      statDispatcher("info", [stat]);
+    };
   };
 
   $: {
@@ -45,16 +68,32 @@
   <hgroup>
     <div class="section">
       <span class="item">{name}</span>
-      <fieldset class="item">
-        <label for="switch">
-          <input
-            type="checkbox"
-            role="switch"
-            checked={selected}
-            on:change={selection}
-          />
-        </label>
-      </fieldset>
+      <div class="item actions">
+        <fieldset>
+          <label for="switch">
+            Show
+            <input
+              type="checkbox"
+              role="switch"
+              checked={selected}
+              on:change={selection}
+            />
+          </label>
+        </fieldset>
+        {#if sources.size > 1}
+          <fieldset>
+            <label for="switch">
+              P
+              <input
+                type="checkbox"
+                role="switch"
+                checked={compute}
+                on:change={stat("p")}
+              />
+            </label>
+          </fieldset>
+        {/if}
+      </div>
     </div>
     <div class="details">
       <div class="sources">
@@ -103,4 +142,14 @@
   .section .item {
     margin: 0px 10px;
   }
+
+  .actions {
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-end;
+  }
+
+  .actions fieldset {
+    margin-left: 5px;
+  }
 </style>
diff --git a/development/plot-benchmarks/src/lib/Group.svelte b/development/plot-benchmarks/src/lib/Group.svelte
index e538e68..6dcee76 100644
--- a/development/plot-benchmarks/src/lib/Group.svelte
+++ b/development/plot-benchmarks/src/lib/Group.svelte
@@ -1,18 +1,27 @@
 <script lang="ts">
   import { createEventDispatcher } from "svelte";
-  import type { Selection, SelectionEvent } from "../types/events.js";
+  import type {
+    Selection,
+    SelectionEvent,
+    StatEvent,
+    StatInfo,
+  } from "../types/events.js";
   import { Session, type IndexedWrapper } from "../wrappers/session.js";
   import Dataset from "./Dataset.svelte";
 
   export let className: string;
   export let datasetGroup: IndexedWrapper[];
 
-  let dispatcher = createEventDispatcher<SelectionEvent>();
+  let selectionDispatcher = createEventDispatcher<SelectionEvent>();
+  let statDispatcher = createEventDispatcher<StatEvent>();
   let datasetNames: Set<string>;
 
+  // Forward events.
   let selection = function (event: CustomEvent<Selection[]>) {
-    // Forward events.
-    dispatcher("selections", event.detail);
+    selectionDispatcher("selections", event.detail);
+  };
+  let stat = function (event: CustomEvent<StatInfo[]>) {
+    statDispatcher("info", event.detail);
   };
 
   $: {
@@ -24,7 +33,7 @@
   <summary>{className}</summary>
   <div class="details">
     {#each datasetNames as name (name)}
-      <Dataset {datasetGroup} {name} on:selections={selection} />
+      <Dataset {datasetGroup} {name} on:selections={selection} on:info={stat} />
     {/each}
   </div>
 </details>
diff --git a/development/plot-benchmarks/src/lib/Session.svelte b/development/plot-benchmarks/src/lib/Session.svelte
index b751f67..3a0a062 100644
--- a/development/plot-benchmarks/src/lib/Session.svelte
+++ b/development/plot-benchmarks/src/lib/Session.svelte
@@ -1,19 +1,31 @@
 <script lang="ts">
   import { createEventDispatcher } from "svelte";
-  import { writable, type Writable } from "svelte/store";
+  import {
+    writable,
+    type Readable,
+    type Writable,
+    derived,
+  } from "svelte/store";
   import { readBenchmarks } from "../files.js";
   import { ChartDataTransforms } from "../transforms/data-transforms.js";
   import { Transforms } from "../transforms/metric-transforms.js";
   import { STANDARD_MAPPER } from "../transforms/standard-mappers.js";
   import type { Data, Series } from "../types/chart.js";
   import type { Metrics } from "../types/data.js";
-  import type { FileMetadataEvent, Selection } from "../types/events.js";
+  import type {
+    FileMetadataEvent,
+    Selection,
+    StatInfo,
+  } from "../types/events.js";
   import type { FileMetadata } from "../types/files.js";
   import { Session, type IndexedWrapper } from "../wrappers/session.js";
   import Chart from "./Chart.svelte";
   import Group from "./Group.svelte";
+  import type { StatService } from "../workers/service.js";
+  import type { Remote } from "comlink";
 
   export let fileEntries: FileMetadata[];
+  export let service: Remote<StatService>;
 
   // State
   let eventDispatcher = createEventDispatcher<FileMetadataEvent>();
@@ -23,13 +35,23 @@
   let chartData: Data;
   let classGroups: Record<string, IndexedWrapper[]>;
   let size: number;
+  let activeSeries: Promise<Series[]>;
 
   // Stores
   let activeDragDrop: Writable<boolean> = writable(false);
   let suppressed: Writable<Set<string>> = writable(new Set());
+  let activeStats: Writable<StatInfo[]> = writable([]);
+  let active: Readable<Set<string>> = derived(activeStats, ($activeStats) => {
+    const datasets = [];
+    for (let i = 0; i < $activeStats.length; i += 1) {
+      const activeStat = $activeStats[i];
+      datasets.push(activeStat.name);
+    }
+    return new Set(datasets);
+  });
 
   // Events
-  let handler = function (event: CustomEvent<Selection[]>) {
+  let selectionHandler = function (event: CustomEvent<Selection[]>) {
     const selections: Selection[] = event.detail;
     for (let i = 0; i < selections.length; i += 1) {
       const selection = selections[i];
@@ -42,9 +64,28 @@
     $suppressed = $suppressed;
   };
 
+  let statHandler = function (event: CustomEvent<StatInfo[]>) {
+    const statistics = event.detail;
+    for (let i = 0; i < statistics.length; i += 1) {
+      const statInfo = statistics[i];
+      if (!statInfo.enabled) {
+        const index = $activeStats.findIndex(
+          (entry) => entry.name == statInfo.name && entry.type == statInfo.type
+        );
+        if (index >= 0) {
+          $activeStats.splice(index, 1);
+        }
+      } else {
+        $activeStats.push(statInfo);
+      }
+      $activeStats = $activeStats;
+    }
+  };
+
   $: {
     session = new Session(fileEntries);
     metrics = Transforms.buildMetrics(session, $suppressed);
+    activeSeries = service.pSeries(metrics, $active);
     series = ChartDataTransforms.mapToSeries(metrics, STANDARD_MAPPER);
     chartData = ChartDataTransforms.mapToDataset(series);
     classGroups = session.classGroups;
@@ -115,13 +156,26 @@
   >
     <h5>Benchmarks</h5>
     {#each Object.entries(classGroups) as [className, wrappers]}
-      <Group {className} datasetGroup={wrappers} on:selections={handler} />
+      <Group
+        {className}
+        datasetGroup={wrappers}
+        on:selections={selectionHandler}
+        on:info={statHandler}
+      />
     {/each}
   </article>
 
   {#if series.length > 0}
     <Chart data={chartData} />
   {/if}
+
+  {#await activeSeries}
+    <article aria-busy="true" />
+  {:then chartData}
+    {#if chartData.length > 0}
+      <Chart data={ChartDataTransforms.mapToDataset(chartData)} isExperimental={true} />
+    {/if}
+  {/await}
 {/if}
 
 <style>