Merge "Add the ability to pick the number of buckets in the histogram." into androidx-main
diff --git a/development/plot-benchmarks/src/lib/Chart.svelte b/development/plot-benchmarks/src/lib/Chart.svelte
index 9a224e2..de2616e 100644
--- a/development/plot-benchmarks/src/lib/Chart.svelte
+++ b/development/plot-benchmarks/src/lib/Chart.svelte
@@ -1,16 +1,18 @@
 <script lang="ts">
   import type { ChartType, LegendItem } from "chart.js";
   import { Chart } from "chart.js/auto";
-  import { onMount } from "svelte";
+  import { createEventDispatcher, onMount } from "svelte";
   import { writable, type Writable } from "svelte/store";
   import type { Data } from "../types/chart.js";
   import { LegendPlugin } from "../plugins.js";
   import Legend from "./Legend.svelte";
   import { saveToClipboard as save } from "../clipboard.js";
+  import type { Controls, ControlsEvent } from "../types/events.js";
 
   export let data: Data;
   export let chartType: ChartType = "line";
   export let isExperimental: boolean = false;
+  export let showHistogramControls: boolean = false;
 
   $: {
     if ($chart) {
@@ -20,7 +22,9 @@
   }
 
   // State
+  let controlsDispatcher = createEventDispatcher<ControlsEvent>();
   let element: HTMLCanvasElement;
+  let buckets: Writable<number> = writable(100);
   let chart: Writable<Chart | null> = writable(null);
   let items: Writable<LegendItem[] | null> = writable(null);
 
@@ -56,6 +60,18 @@
       await save($chart);
     }
   }
+
+  function onHistogramChanged(event: Event) {
+    const element = event.target as EventTarget & HTMLInputElement;
+    const oldValue = $buckets;
+    $buckets = parseInt(element.value, 10);
+    if (oldValue != $buckets) {
+      let controls: Controls = {
+        buckets: $buckets,
+      };
+      controlsDispatcher("controls", controls);
+    }
+  }
 </script>
 
 <article>
@@ -69,6 +85,24 @@
     </button>
   </div>
   <canvas class="chart" bind:this={element} />
+  {#if showHistogramControls}
+    <div class="controls">
+      <label for="buckets">
+        Histogram
+        <input
+          type="range"
+          data-tooltip={$buckets}
+          data-placement="right"
+          min="10"
+          max="250"
+          value={$buckets}
+          id="buckets"
+          name="buckets"
+          on:change={onHistogramChanged}
+        />
+      </label>
+    </div>
+  {/if}
   {#if isExperimental}
     <footer class="slim">
       <section class="experimental">
@@ -92,19 +126,20 @@
     flex-direction: row;
     justify-content: flex-end;
   }
-
   .toolbar .btn {
     width: auto;
     height: auto;
     border: none;
     padding: 5px;
   }
-
+  .controls {
+    margin-top: 20px;
+    width: 100%;
+  }
   .slim {
     margin-bottom: 0px;
     padding: 0;
   }
-
   .experimental {
     display: flex;
     flex-direction: row;
diff --git a/development/plot-benchmarks/src/lib/Session.svelte b/development/plot-benchmarks/src/lib/Session.svelte
index e70f88c..a099cf8 100644
--- a/development/plot-benchmarks/src/lib/Session.svelte
+++ b/development/plot-benchmarks/src/lib/Session.svelte
@@ -1,29 +1,33 @@
 <script lang="ts">
+  import type { Remote } from "comlink";
   import { createEventDispatcher } from "svelte";
   import {
+    derived,
     writable,
     type Readable,
     type Writable,
-    derived,
   } from "svelte/store";
   import { readBenchmarks } from "../files.js";
-  import { ChartDataTransforms } from "../transforms/data-transforms.js";
+  import {
+    ChartDataTransforms,
+    type Mapper,
+  } 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,
+    Controls,
     DatasetSelection,
-    StatInfo,
+    FileMetadataEvent,
     MetricSelection,
+    StatInfo,
   } from "../types/events.js";
   import type { FileMetadata } from "../types/files.js";
+  import type { StatService } from "../workers/service.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";
+  import { buildMapper } from "../transforms/standard-mappers.js";
 
   export let fileEntries: FileMetadata[];
   export let service: Remote<StatService>;
@@ -31,14 +35,17 @@
   // State
   let eventDispatcher = createEventDispatcher<FileMetadataEvent>();
   let session: Session;
+  let mapper: Mapper<number>;
   let metrics: Metrics<number>;
   let series: Series[];
   let chartData: Data;
   let classGroups: Record<string, IndexedWrapper[]>;
+  let showHistogramControls: boolean;
   let size: number;
   let activeSeries: Promise<Series[]>;
 
   // Stores
+  let buckets: Writable<number> = writable(100);
   let activeDragDrop: Writable<boolean> = writable(false);
   let suppressed: Writable<Set<string>> = writable(new Set());
   let suppressedMetrics: Writable<Set<string>> = writable(new Set());
@@ -97,11 +104,18 @@
     }
   };
 
+  let controlsHandler = function (event: CustomEvent<Controls>) {
+    const controls: Controls = event.detail;
+    $buckets = controls.buckets;
+  };
+
   $: {
     session = new Session(fileEntries);
+    mapper = buildMapper($buckets);
     metrics = Transforms.buildMetrics(session, $suppressed, $suppressedMetrics);
+    showHistogramControls = metrics.sampled && metrics.sampled.length > 0;
     activeSeries = service.pSeries(metrics, $active);
-    series = ChartDataTransforms.mapToSeries(metrics, STANDARD_MAPPER);
+    series = ChartDataTransforms.mapToSeries(metrics, mapper);
     chartData = ChartDataTransforms.mapToDataset(series);
     classGroups = session.classGroups;
     size = session.fileNames.size;
@@ -183,7 +197,11 @@
   </article>
 
   {#if series.length > 0}
-    <Chart data={chartData} />
+    <Chart
+      data={chartData}
+      {showHistogramControls}
+      on:controls={controlsHandler}
+    />
   {/if}
 
   {#await activeSeries}
diff --git a/development/plot-benchmarks/src/transforms/data-transforms.ts b/development/plot-benchmarks/src/transforms/data-transforms.ts
index c176da9..41ef149 100644
--- a/development/plot-benchmarks/src/transforms/data-transforms.ts
+++ b/development/plot-benchmarks/src/transforms/data-transforms.ts
@@ -18,8 +18,9 @@
     const series: Series[] = [];
     const standard = metrics.standard;
     const sampled = metrics.sampled;
-    // Builds ranges for distribution
+    // Builds ranges for distribution.
     const ranges = mapper.sampledRanges(metrics);
+    // Builds series.
     if (standard) {
       for (let i = 0; i < standard.length; i += 1) {
         const metric = standard[i];
diff --git a/development/plot-benchmarks/src/transforms/standard-mappers.ts b/development/plot-benchmarks/src/transforms/standard-mappers.ts
index 11baf19..9db0cd1 100644
--- a/development/plot-benchmarks/src/transforms/standard-mappers.ts
+++ b/development/plot-benchmarks/src/transforms/standard-mappers.ts
@@ -37,14 +37,14 @@
   return ranges;
 }
 
-function sampledMapper(metric: Metric<number[]>, range: Range | null): Series[] {
+function sampledMapper(metric: Metric<number[]>, buckets: number, range: Range | null): Series[] {
   const series: Series[] = [];
   const data: Record<string, ChartData<number[]>> = metric.data;
   const entries = Object.entries(data);
   for (let i = 0; i < entries.length; i += 1) {
     const [source, chartData] = entries[i];
     const label = labelFor(metric, source);
-    const [points, _, __] = histogramPoints(chartData.values, /* buckets */ undefined, /* target */ undefined, range);
+    const [points, _, __] = histogramPoints(chartData.values, buckets, /* target */ undefined, range);
     series.push({
       label: label,
       type: "line",
@@ -194,11 +194,32 @@
 }
 
 /**
- * The standard mapper.
+ * The Standard Mapper.
  */
-export const STANDARD_MAPPER: Mapper = {
-  rangeLabel: rangeLabel,
-  standard: standardMapper,
-  sampled: sampledMapper,
-  sampledRanges: sampledRanges
-};
+class StandardMapper {
+  constructor(private buckets: number) {
+    // Does nothing.
+  }
+  // Delegate
+  rangeLabel(metric: Metric<unknown>): string {
+    return rangeLabel(metric);
+  }
+  standard(metric: Metric<number>): Series[] {
+    return standardMapper(metric);
+  }
+  sampled(metric: Metric<number[]>, range: Range | null): Series[] {
+    return sampledMapper(metric, this.buckets, range);
+  }
+  sampledRanges(metrics: Metrics<number>): Record<string, Range> {
+    return sampledRanges(metrics);
+  }
+}
+
+/**
+ * Builds a Standard mapper.
+ * @param buckets are the number of buckets in the histogram to use.
+ * @return an instance of `Mapper`.
+ */
+export function buildMapper(buckets: number): Mapper<number> {
+  return new StandardMapper(buckets);
+}
diff --git a/development/plot-benchmarks/src/types/chart.ts b/development/plot-benchmarks/src/types/chart.ts
index 0d77b63..e90f8d2 100644
--- a/development/plot-benchmarks/src/types/chart.ts
+++ b/development/plot-benchmarks/src/types/chart.ts
@@ -20,7 +20,7 @@
 export interface Series {
   label: string;
   type: ChartType;
-  data: Array<Point>;
+  data: Point[];
   // Additional series options
   // For e.g. https://www.chartjs.org/docs/latest/charts/line.html
   options: object;
diff --git a/development/plot-benchmarks/src/types/events.ts b/development/plot-benchmarks/src/types/events.ts
index 72890d4..f98f9ed 100644
--- a/development/plot-benchmarks/src/types/events.ts
+++ b/development/plot-benchmarks/src/types/events.ts
@@ -31,3 +31,11 @@
 export interface StatEvent {
   info: StatInfo[];
 }
+
+export interface ControlsEvent {
+  controls: Controls;
+}
+
+export interface Controls {
+  buckets: number;
+}
diff --git a/development/plot-benchmarks/src/workers/service.ts b/development/plot-benchmarks/src/workers/service.ts
index 90ff9a5..33c52f2 100644
--- a/development/plot-benchmarks/src/workers/service.ts
+++ b/development/plot-benchmarks/src/workers/service.ts
@@ -27,7 +27,7 @@
                 continue;
               }
               const [delta, distribution] = this.buildDistribution(reference, target);
-              const [points, pPlots, p] = histogramPoints([distribution], 100, delta);
+              const [points, pPlots, p] = histogramPoints([distribution], /* buckets */ 100, /* target */ delta);
               series.push({
                 label: `${name} { ${metric.label} } - Likelihood`,
                 type: "line",
@@ -67,7 +67,7 @@
                 continue;
               }
               const [delta, distribution] = this.buildStandardDistribution(reference, target);
-              const [points, pPlots, p] = histogramPoints([distribution], 100, delta);
+              const [points, pPlots, p] = histogramPoints([distribution], /* buckets */ 100, /* target */ delta);
               series.push({
                 label: `${name} { ${metric.label} } - Likelihood`,
                 type: "line",