Customize tooltips for sampled distributions.
* Add the ability to normalize ranges for distributions to make comparisons easier.
Test: Tested locally.
Change-Id: Ib76f57162fed262b3db2a76cd9e169b897a93bb9
diff --git a/development/plot-benchmarks/src/lib/Chart.svelte b/development/plot-benchmarks/src/lib/Chart.svelte
index de2616e..175da99 100644
--- a/development/plot-benchmarks/src/lib/Chart.svelte
+++ b/development/plot-benchmarks/src/lib/Chart.svelte
@@ -1,13 +1,14 @@
<script lang="ts">
- import type { ChartType, LegendItem } from "chart.js";
+ import type { ChartType, LegendItem, Point, TooltipItem } from "chart.js";
import { Chart } from "chart.js/auto";
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 { LegendPlugin } from "../plugins.js";
+ import type { Data } from "../types/chart.js";
import type { Controls, ControlsEvent } from "../types/events.js";
+ import Legend from "./Legend.svelte";
+ import { isSampled } from "../transforms/standard-mappers.js";
export let data: Data;
export let chartType: ChartType = "line";
@@ -37,6 +38,24 @@
$items = legend.labels.generateLabels(chart);
};
const plugins = {
+ tooltip: {
+ callbacks: {
+ label: (context: TooltipItem<typeof chartType>): string | null => {
+ // TODO: Configure Tooltips
+ // https://www.chartjs.org/docs/latest/configuration/tooltip.html
+ const label = context.dataset.label;
+ const rp = context.raw as Point;
+ const frequency = context.parsed.y;
+ if (isSampled(label)) {
+ const fx = rp.x.toFixed(2);
+ return `${label}: ${fx} F(${frequency})`;
+ } else {
+ // Fallback to default behavior
+ return undefined;
+ }
+ },
+ },
+ },
legend: {
display: false,
},
diff --git a/development/plot-benchmarks/src/lib/Session.svelte b/development/plot-benchmarks/src/lib/Session.svelte
index a099cf8..d4307ee 100644
--- a/development/plot-benchmarks/src/lib/Session.svelte
+++ b/development/plot-benchmarks/src/lib/Session.svelte
@@ -2,32 +2,32 @@
import type { Remote } from "comlink";
import { createEventDispatcher } from "svelte";
import {
- derived,
- writable,
- type Readable,
- type Writable,
+ derived,
+ writable,
+ type Readable,
+ type Writable,
} from "svelte/store";
import { readBenchmarks } from "../files.js";
import {
- ChartDataTransforms,
- type Mapper,
+ ChartDataTransforms,
+ type Mapper,
} from "../transforms/data-transforms.js";
import { Transforms } from "../transforms/metric-transforms.js";
+ import { buildMapper } from "../transforms/standard-mappers.js";
import type { Data, Series } from "../types/chart.js";
import type { Metrics } from "../types/data.js";
import type {
- Controls,
- DatasetSelection,
- FileMetadataEvent,
- MetricSelection,
- StatInfo,
+ Controls,
+ DatasetSelection,
+ 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 { buildMapper } from "../transforms/standard-mappers.js";
export let fileEntries: FileMetadata[];
export let service: Remote<StatService>;
@@ -40,12 +40,13 @@
let series: Series[];
let chartData: Data;
let classGroups: Record<string, IndexedWrapper[]>;
- let showHistogramControls: boolean;
+ let showControls: boolean;
let size: number;
let activeSeries: Promise<Series[]>;
// Stores
let buckets: Writable<number> = writable(100);
+ let normalizeMetrics: Writable<boolean> = writable(false);
let activeDragDrop: Writable<boolean> = writable(false);
let suppressed: Writable<Set<string>> = writable(new Set());
let suppressedMetrics: Writable<Set<string>> = writable(new Set());
@@ -113,9 +114,13 @@
session = new Session(fileEntries);
mapper = buildMapper($buckets);
metrics = Transforms.buildMetrics(session, $suppressed, $suppressedMetrics);
- showHistogramControls = metrics.sampled && metrics.sampled.length > 0;
+ showControls = metrics.sampled && metrics.sampled.length > 0;
activeSeries = service.pSeries(metrics, $active);
- series = ChartDataTransforms.mapToSeries(metrics, mapper);
+ series = ChartDataTransforms.mapToSeries(
+ metrics,
+ mapper,
+ $normalizeMetrics
+ );
chartData = ChartDataTransforms.mapToDataset(series);
classGroups = session.classGroups;
size = session.fileNames.size;
@@ -183,6 +188,24 @@
on:dragover={onDragOver}
on:dragleave={onDragLeave}
>
+ {#if showControls}
+ <div class="toolbar">
+ <div class="control">
+ <label for="normalize">
+ <input
+ type="checkbox"
+ id="normalize"
+ name="normalize"
+ data-tooltip="Normalize Metrics"
+ on:change={(_) => {
+ $normalizeMetrics = !$normalizeMetrics;
+ }}
+ />
+ ≃
+ </label>
+ </div>
+ </div>
+ {/if}
<h5>Benchmarks</h5>
{#each Object.entries(classGroups) as [className, wrappers]}
<Group
@@ -199,7 +222,7 @@
{#if series.length > 0}
<Chart
data={chartData}
- {showHistogramControls}
+ showHistogramControls={showControls}
on:controls={controlsHandler}
/>
{/if}
@@ -217,6 +240,13 @@
{/if}
<style>
+ .toolbar {
+ padding: 0;
+ margin: 2rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ }
.active {
outline: beige;
outline-style: dashed;
diff --git a/development/plot-benchmarks/src/transforms/data-transforms.ts b/development/plot-benchmarks/src/transforms/data-transforms.ts
index 41ef149..52b2926 100644
--- a/development/plot-benchmarks/src/transforms/data-transforms.ts
+++ b/development/plot-benchmarks/src/transforms/data-transforms.ts
@@ -14,12 +14,15 @@
*/
export class ChartDataTransforms {
- static mapToSeries(metrics: Metrics<number>, mapper: Mapper<number>): Series[] {
+ static mapToSeries(metrics: Metrics<number>, mapper: Mapper<number>, normalize: boolean = false): Series[] {
const series: Series[] = [];
const standard = metrics.standard;
const sampled = metrics.sampled;
// Builds ranges for distribution.
- const ranges = mapper.sampledRanges(metrics);
+ let ranges: Record<string, Range> = {};
+ if (normalize) {
+ ranges = mapper.sampledRanges(metrics);
+ }
// Builds series.
if (standard) {
for (let i = 0; i < standard.length; i += 1) {
@@ -54,7 +57,7 @@
private static chartDataset<T extends ChartType>(series: Series): ChartDataset {
return {
- label: series.label,
+ label: series.descriptiveLabel,
type: series.type,
data: series.data,
...series.options
diff --git a/development/plot-benchmarks/src/transforms/standard-mappers.ts b/development/plot-benchmarks/src/transforms/standard-mappers.ts
index 9db0cd1..df74b8f9 100644
--- a/development/plot-benchmarks/src/transforms/standard-mappers.ts
+++ b/development/plot-benchmarks/src/transforms/standard-mappers.ts
@@ -3,6 +3,8 @@
import type { ChartData, Metric, Metrics, Range } from "../types/data.js";
import type { Mapper } from "./data-transforms.js";
+const SAMPLED_SUFFIX = '(S)';
+
function sampledRanges(metrics: Metrics<number>): Record<string, Range> {
const ranges: Record<string, Range> = {};
const sampled = metrics.sampled;
@@ -43,10 +45,10 @@
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 label = labelFor(metric, source, true);
const [points, _, __] = histogramPoints(chartData.values, buckets, /* target */ undefined, range);
series.push({
- label: label,
+ descriptiveLabel: label,
type: "line",
data: points,
options: {
@@ -63,10 +65,10 @@
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 label = labelFor(metric, source, false);
const points = singlePoints(chartData.values);
series.push({
- label: label,
+ descriptiveLabel: label,
type: "line",
data: points,
options: {
@@ -104,9 +106,13 @@
let pMin: number = 0;
let pMax: number = 0;
let maxFreq: number = 0;
- const histogram = new Array(buckets).fill(0);
+ const histogram: Point[] = new Array(buckets).fill(null);
// The actual number of slots in the histogram
const slots = buckets - 1;
+ for (let i = 0; i < buckets; i += 1) {
+ const interpolated = interpolate(i / slots, min, max);
+ histogram[i] = { x: interpolated, y: 0 };
+ }
for (let i = 0; i < flattened.length; i += 1) {
const value = flattened[i];
if (target && value < target) {
@@ -117,9 +123,9 @@
}
const n = normalize(value, min, max);
const index = Math.ceil(n * slots);
- histogram[index] = histogram[index] + 1;
- if (maxFreq < histogram[index]) {
- maxFreq = histogram[index];
+ histogram[index].y = histogram[index].y + 1;
+ if (maxFreq < histogram[index].y) {
+ maxFreq = histogram[index].y;
}
}
if (target) {
@@ -129,7 +135,7 @@
}
// Pay attention to both sides of the normal distribution.
let p = Math.min(pMin / flattened.length, pMax / flattened.length);
- return [singlePoints(histogram), targetPoints, p];
+ return [histogram, targetPoints, p];
}
function selectPoints(buckets: number, index: number, target: number) {
@@ -168,7 +174,7 @@
return (n - min) / ((max - min) + 1e-9);
}
-function interpolate(normalized: number, min: number, max: number) {
+function interpolate(normalized: number, min: number, max: number): number {
const range = max - min;
const value = normalized * range;
return value + min;
@@ -177,8 +183,9 @@
/**
* Generates a series label.
*/
-function labelFor<T>(metric: Metric<T>, source: string): string {
- return `${source} {${metric.class} ${metric.benchmark}} - ${metric.label}`;
+function labelFor<T>(metric: Metric<T>, source: string, sampled: boolean): string {
+ const suffix = sampled ? SAMPLED_SUFFIX : '';
+ return `${source} {${metric.class} ${metric.benchmark}} - ${metric.label} ${suffix}`;
}
export function datasetName(metric: Metric<any>): string {
@@ -190,7 +197,7 @@
* comparing equal distributions.
*/
function rangeLabel(metric: Metric<unknown>): string {
- return `${metric.benchmark}>${metric.label}`;
+ return `${metric.label}`;
}
/**
@@ -223,3 +230,7 @@
export function buildMapper(buckets: number): Mapper<number> {
return new StandardMapper(buckets);
}
+
+export function isSampled(label: string | null | undefined): boolean {
+ return label && label.indexOf(SAMPLED_SUFFIX) >= 0;
+}
diff --git a/development/plot-benchmarks/src/types/chart.ts b/development/plot-benchmarks/src/types/chart.ts
index e90f8d2..c5f8cb5 100644
--- a/development/plot-benchmarks/src/types/chart.ts
+++ b/development/plot-benchmarks/src/types/chart.ts
@@ -18,7 +18,7 @@
* Used by a Mapper for data transformations.
*/
export interface Series {
- label: string;
+ descriptiveLabel: string;
type: ChartType;
data: Point[];
// Additional series options
diff --git a/development/plot-benchmarks/src/workers/service.ts b/development/plot-benchmarks/src/workers/service.ts
index 33c52f2..3373353 100644
--- a/development/plot-benchmarks/src/workers/service.ts
+++ b/development/plot-benchmarks/src/workers/service.ts
@@ -29,7 +29,7 @@
const [delta, distribution] = this.buildDistribution(reference, target);
const [points, pPlots, p] = histogramPoints([distribution], /* buckets */ 100, /* target */ delta);
series.push({
- label: `${name} { ${metric.label} } - Likelihood`,
+ descriptiveLabel: `${name} { ${metric.label} } - Likelihood`,
type: "line",
data: points,
options: {
@@ -38,7 +38,7 @@
});
if (pPlots && pPlots.length > 0) {
series.push({
- label: `${name} { ${metric.label} } - { P = ${p} }`,
+ descriptiveLabel: `${name} { ${metric.label} } - { P = ${p} }`,
type: "bar",
data: pPlots,
options: {
@@ -69,7 +69,7 @@
const [delta, distribution] = this.buildStandardDistribution(reference, target);
const [points, pPlots, p] = histogramPoints([distribution], /* buckets */ 100, /* target */ delta);
series.push({
- label: `${name} { ${metric.label} } - Likelihood`,
+ descriptiveLabel: `${name} { ${metric.label} } - Likelihood`,
type: "line",
data: points,
options: {
@@ -78,7 +78,7 @@
});
if (pPlots && pPlots.length > 0) {
series.push({
- label: `${name} { ${metric.label} } - { P = ${p} }`,
+ descriptiveLabel: `${name} { ${metric.label} } - { P = ${p} }`,
type: "bar",
data: pPlots,
options: {