diff --git a/scripts/graph.ts b/scripts/graph.ts index 4cb290c..d2756fb 100644 --- a/scripts/graph.ts +++ b/scripts/graph.ts @@ -1,3 +1,6 @@ +import { Metrics } from "./graph/metrics.js"; +import { State } from "./graph/state.js"; + import uPlot from "./uPlot.js"; /* @@ -120,4 +123,10 @@ const COLORS = [ const plotDiv = document.getElementById("plot")!; const metricsDiv = document.getElementById("metrics")!; -let plot: uPlot | null = null; + +const metrics = new Metrics(metricsDiv); +const state = new State(metrics); +state.update(); + +// For debugging +(window as any).state = state; diff --git a/scripts/graph/metrics.ts b/scripts/graph/metrics.ts index de190cf..7df0e1a 100644 --- a/scripts/graph/metrics.ts +++ b/scripts/graph/metrics.ts @@ -1,3 +1,4 @@ +import { MetricsResponse } from "./requests.js"; import { el } from "./util.js"; class Folder { @@ -57,12 +58,50 @@ class Folder { } } -export function updateMetricsDiv(div: HTMLElement, metrics: string[]) { - let folder = new Folder(); - for (let metric of metrics) { - folder.add(metric); +export class Metrics { + #div: HTMLElement; + #dataId: number | null = null; + + constructor(div: HTMLElement) { + this.#div = div; } - div.textContent = ""; // Remove children - div.append(folder.childrenToHtmlElements()); + getSelected(): Set { + const selected = new Set(); + + const checkedInputs = this.#div.querySelectorAll("input:checked"); + for (const input of checkedInputs) { + selected.add(input.name); + } + + return selected; + } + + requiresUpdate(dataId: number): boolean { + // At the moment, updating the metrics results in all
tags + // closing again. To prevent this (as it can be frustrating if you've + // navigated deep into the metrics hierarchy), we never require updates + // after the initial update. + return this.#dataId === null; + // return this.#dataId === null || this.#dataId < dataId; + } + + update(response: MetricsResponse) { + const selected = this.getSelected(); + + const folder = new Folder(); + for (const metric of response.metrics) { + folder.add(metric); + } + + this.#div.textContent = ""; // Remove children + this.#div.append(folder.childrenToHtmlElements()); + + const inputs = this.#div.querySelectorAll("input"); + for (const input of inputs) { + input.checked = selected.has(input.name); + } + + this.#dataId = response.dataId; + } } diff --git a/scripts/graph/state.ts b/scripts/graph/state.ts index 01a9363..ee6cdd0 100644 --- a/scripts/graph/state.ts +++ b/scripts/graph/state.ts @@ -1,40 +1,67 @@ -import { updateMetricsDiv } from "./metrics.js"; -import { MetricsResponse, getMetrics } from "./requests.js"; +import { Metrics } from "./metrics.js"; +import { getMetrics } from "./requests.js"; export class State { - #metricsDiv: HTMLElement; + #latestGraphId: number = -Infinity; + #latestDataId: number = -Infinity; - #updating: boolean = false; - #metrics: MetricsResponse | null = null; + #metrics: Metrics; - constructor(metricsDiv: HTMLElement) { - this.#metricsDiv = metricsDiv; + #requestingNewMetrics: boolean = false; + + // commits (with graph id and data id) + // raw measurements (with graph id and data id) + // processed measurements (with graph id and data id) + + constructor(metrics: Metrics) { + this.#metrics = metrics; } /** - * Look at current state and try to change it so that it represents what the - * user wants. + * Update state and plot and request new data if necessary. Tries to match + * the user's wishes as closely as possible. * * This function is idempotent. */ - async update() { - if (this.#updating) { - return; + update() { + // TODO Invalidate and update data + // TODO Update graph + this.#requestDataWhereNecessary(); + } + + ////////////////////////////////// + // Requesting and updating data // + ////////////////////////////////// + + #updateDataId(dataId: number) { + if (dataId > this.#latestDataId) { + this.#latestDataId = dataId; } + } + + #updateGraphId(graphId: number) { + if (graphId > this.#latestGraphId) { + this.#latestGraphId = graphId; + } + } + + #requestDataWhereNecessary() { + if (this.#metrics.requiresUpdate(this.#latestDataId)) { + this.#requestMetrics(); + } + } + + async #requestMetrics() { + if (this.#requestingNewMetrics) return; + console.log("Requesting new metrics"); try { - await this.#update_impl(); + this.#requestingNewMetrics = true; + const response = await getMetrics(); + this.#updateDataId(response.dataId); + this.#metrics.update(response); + this.update(); } finally { - this.#updating = false; + this.#requestingNewMetrics = false; } } - - async #update_impl() { - this.#update_metrics(); - } - - async #update_metrics() { - this.#metrics = await getMetrics(); - if (this.#metrics === null) { return; } - updateMetricsDiv(this.#metricsDiv, this.#metrics.metrics); - } }