Create metric selector via JS
This commit is contained in:
parent
8c7399725d
commit
d82804e209
9 changed files with 187 additions and 192 deletions
|
|
@ -1,3 +1,6 @@
|
|||
import * as metrics from "./graph/metrics.js";
|
||||
import { Requests } from "./graph/requests.js";
|
||||
|
||||
import uPlot from "./uPlot.js";
|
||||
|
||||
/*
|
||||
|
|
@ -116,76 +119,19 @@ const COLORS = [
|
|||
"#000000", // Black
|
||||
];
|
||||
|
||||
interface GraphData {
|
||||
hashes: string[];
|
||||
times: number[];
|
||||
measurements: { [key: string]: (number | null)[]; };
|
||||
}
|
||||
|
||||
function update_plot_with_data(data: GraphData) {
|
||||
let series: uPlot.Series[] = [{}];
|
||||
let values: uPlot.AlignedData = [data.times];
|
||||
|
||||
for (const [i, metric] of Object.keys(data.measurements).sort().entries()) {
|
||||
series.push({
|
||||
label: metric,
|
||||
spanGaps: true,
|
||||
stroke: COLORS[i % COLORS.length],
|
||||
});
|
||||
values.push(data.measurements[metric]!);
|
||||
}
|
||||
|
||||
const opts: uPlot.Options = {
|
||||
title: "Measurements",
|
||||
width: 600,
|
||||
height: 400,
|
||||
series,
|
||||
};
|
||||
|
||||
plot?.destroy();
|
||||
plot = new uPlot(opts, values, plot_div);
|
||||
}
|
||||
|
||||
async function update_plot_with_metrics(metrics: string[]) {
|
||||
const url = "data?" + new URLSearchParams(metrics.map(m => ["metric", m]));
|
||||
const response = await fetch(url);
|
||||
const data: GraphData = await response.json();
|
||||
update_plot_with_data(data);
|
||||
}
|
||||
|
||||
function find_selected_metrics(): string[] {
|
||||
const inputs = metrics_div.querySelectorAll<HTMLInputElement>('input[type="checkbox"]');
|
||||
|
||||
let metrics: string[] = [];
|
||||
for (const input of inputs) {
|
||||
if (input.checked) {
|
||||
metrics.push(input.name);
|
||||
}
|
||||
}
|
||||
return metrics;
|
||||
};
|
||||
|
||||
async function update_plot() {
|
||||
const metrics = find_selected_metrics();
|
||||
if (metrics.length > 0) {
|
||||
await update_plot_with_metrics(metrics);
|
||||
} else {
|
||||
update_plot_with_data({
|
||||
hashes: [],
|
||||
times: [],
|
||||
measurements: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialization
|
||||
|
||||
const plot_div = document.getElementById("plot")!;
|
||||
const metrics_div = document.getElementById("metrics")!;
|
||||
let plot: uPlot | null = null;
|
||||
|
||||
for (const input of metrics_div.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')) {
|
||||
input.addEventListener("change", update_plot);
|
||||
}
|
||||
let requests = new Requests();
|
||||
|
||||
update_plot();
|
||||
async function init() {
|
||||
let response = await requests.get_metrics();
|
||||
console.log("Metrics:", response);
|
||||
if (response !== null) {
|
||||
metrics.update(metrics_div, response.metrics);
|
||||
}
|
||||
}
|
||||
init();
|
||||
|
|
|
|||
68
scripts/graph/metrics.ts
Normal file
68
scripts/graph/metrics.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { el } from "./util.js";
|
||||
|
||||
class Folder {
|
||||
metric: string | null = null;
|
||||
children: Map<string, Folder> = new Map();
|
||||
|
||||
getOrCreateChild(name: string): Folder {
|
||||
let child = this.children.get(name);
|
||||
if (child === undefined) {
|
||||
child = new Folder();
|
||||
this.children.set(name, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
add(metric: string) {
|
||||
let current: Folder = this;
|
||||
for (let segment of metric.split("/")) {
|
||||
current = current.getOrCreateChild(segment);
|
||||
}
|
||||
current.metric = metric;
|
||||
}
|
||||
|
||||
toHtmlElement(name: string): HTMLElement {
|
||||
if (this.children.size > 0) { // Folder
|
||||
name = `${name}/`;
|
||||
if (this.metric === null) { // Folder without metric
|
||||
return el("details", { "class": "no-metric" },
|
||||
el("summary", {}, name),
|
||||
this.childrenToHtmlElements(),
|
||||
);
|
||||
} else { // Folder with metric
|
||||
return el("details", {},
|
||||
el("summary", {},
|
||||
el("input", { "type": "checkbox", "name": this.metric }),
|
||||
" ", name,
|
||||
),
|
||||
this.childrenToHtmlElements(),
|
||||
);
|
||||
}
|
||||
} else if (this.metric !== null) { // Normal metric
|
||||
return el("label", {},
|
||||
el("input", { "type": "checkbox", "name": this.metric }),
|
||||
" ", name,
|
||||
);
|
||||
} else { // Metric without metric, should never happen
|
||||
return el("label", {}, name);
|
||||
}
|
||||
}
|
||||
|
||||
childrenToHtmlElements(): HTMLElement {
|
||||
let result: HTMLElement = el("ul", {});
|
||||
for (let [name, folder] of this.children.entries()) {
|
||||
result.append(el("li", {}, folder.toHtmlElement(name)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function update(div: HTMLElement, metrics: string[]) {
|
||||
let folder = new Folder();
|
||||
for (let metric of metrics) {
|
||||
folder.add(metric);
|
||||
}
|
||||
|
||||
div.textContent = ""; // Remove children
|
||||
div.append(folder.childrenToHtmlElements());
|
||||
}
|
||||
71
scripts/graph/requests.ts
Normal file
71
scripts/graph/requests.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* `/graph/metrics` response data.
|
||||
*/
|
||||
export type MetricsResponse = {
|
||||
// data_id: number; // TODO Uncomment
|
||||
|
||||
metrics: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* `/graph/commits` response data.
|
||||
*/
|
||||
export type CommitsResponse = {
|
||||
// graph_id: number; // TODO Uncomment
|
||||
|
||||
hash_by_hash: string[];
|
||||
author_by_hash: number[];
|
||||
committer_date_by_hash: string[];
|
||||
message_by_hash: string[];
|
||||
parents: [string, string][];
|
||||
};
|
||||
|
||||
/**
|
||||
* `/graph/measurements` response data.
|
||||
*/
|
||||
export type MeasurementsResponse = {
|
||||
// graph_id: number; // TODO Uncomment
|
||||
// data_id: number; // TODO Uncomment
|
||||
|
||||
measurements: { [key: string]: (number | null)[]; };
|
||||
};
|
||||
|
||||
/**
|
||||
* Request different kinds of data from the server.
|
||||
*
|
||||
* This class has two main purposes:
|
||||
*
|
||||
* 1. Providing a nice interface for requesting data from the server
|
||||
* 2. Preventing sending the same request again while still waiting for the server
|
||||
*/
|
||||
export class Requests {
|
||||
#requesting_metrics: Promise<MetricsResponse> | null = null;
|
||||
#requesting_commits: Promise<CommitsResponse> | null = null;
|
||||
#requesting_measurements: Map<string, Promise<MeasurementsResponse>> = new Map();
|
||||
|
||||
async #request_data<R>(url: string): Promise<R> {
|
||||
let response = await fetch(url);
|
||||
let data: R = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async get_metrics(): Promise<MetricsResponse | null> {
|
||||
if (this.#requesting_metrics !== null) {
|
||||
try {
|
||||
return await this.#requesting_metrics;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
this.#requesting_metrics = this.#request_data<MetricsResponse>("metrics");
|
||||
try {
|
||||
return await this.#requesting_metrics;
|
||||
} catch (error) {
|
||||
console.error("Could not get metrics:", error);
|
||||
return null;
|
||||
} finally {
|
||||
this.#requesting_metrics = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
scripts/graph/util.ts
Normal file
11
scripts/graph/util.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Create an {@link HTMLElement}.
|
||||
*/
|
||||
export function el(name: string, attributes: { [key: string]: string; }, ...children: (string | Node)[]) {
|
||||
let element = document.createElement(name);
|
||||
for (let [name, value] of Object.entries(attributes)) {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
element.append(...children);
|
||||
return element;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue