| /** |
| * @license |
| * Copyright 2023 The Go Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style |
| * license that can be found in the LICENSE file. |
| */ |
| |
| import * as Plot from "@observablehq/plot"; |
| |
| import "../shared/base"; |
| |
| declare global { |
| interface Page { |
| Charts: ChartData; |
| } |
| |
| interface ChartData { |
| Programs: Program[]; |
| DateRange: [string, string]; |
| } |
| |
| interface Program { |
| ID: string; |
| Name: string; |
| Counters: Counter[]; |
| Active: boolean; |
| } |
| |
| interface Counter { |
| ID: string; |
| Name: string; |
| Data: Datum[]; |
| } |
| |
| interface Datum { |
| [key: string]: any; |
| Week: string; |
| Program: string; |
| Version: string; |
| GOARCH: string; |
| GOOS: string; |
| GoVersion: string; |
| Key: string; |
| Value: number; |
| } |
| const Page: Page; |
| } |
| |
| window.onload = function () { |
| drawCharts(); |
| configSelector(); |
| breadcrumbController(); |
| sectionController(); |
| }; |
| |
| // sectionController adds event listeners to the section headers |
| // to toggle them open and closed. |
| function sectionController() { |
| const html = document.querySelector("html")!; |
| for (const e of document.querySelectorAll("h2")) { |
| e.addEventListener("click", function () { |
| let closed = localStorage.getItem("closed-sections")?.split(","); |
| if (closed?.includes(this.id)) { |
| closed = closed.filter((v) => v !== this.id); |
| const str = closed.join(","); |
| localStorage.setItem("closed-sections", str); |
| html.setAttribute("data-closed-sections", str); |
| } else { |
| closed = [this.id].concat(closed ?? []); |
| const str = closed.join(","); |
| localStorage.setItem("closed-sections", str); |
| html.setAttribute("data-closed-sections", str); |
| } |
| }); |
| } |
| } |
| |
| // drawCharts draws the charts using @observable/plot. It is called when |
| // the page is first rendered and when a facet is selected. |
| function drawCharts() { |
| for (const program of Page.Charts.Programs ?? []) { |
| for (const counter of program.Counters ?? []) { |
| const rectYOpts: Plot.BinXInputs<Plot.RectYOptions> = { |
| tip: true, |
| x: (d: Datum) => new Date(d.Week), |
| y: (d: Datum) => d.Value, |
| interval: "week", |
| fill: (d: Datum) => { |
| const n = Number(d.Key); |
| return isNaN(n) ? d.Key : n; |
| }, |
| }; |
| |
| const chart = Plot.plot({ |
| nice: true, |
| x: { |
| domain: Page.Charts.DateRange.map((d) => new Date(d)), |
| label: "Week", |
| }, |
| y: { |
| label: "Value", |
| }, |
| color: { |
| type: "ordinal", |
| legend: true, |
| scheme: "Spectral", |
| reverse: true, |
| label: "Counter", |
| }, |
| height: 256, |
| style: "overflow:visible;width:100%;background:transparent", |
| marks: [ |
| Plot.rectY(counter.Data, Plot.binX({ y: "sum" }, rectYOpts)), |
| Plot.ruleY([0]), |
| ], |
| }); |
| document |
| .querySelector(`[data-chart-id="${counter.ID}"]`) |
| ?.replaceChildren(chart); |
| } |
| } |
| } |
| |
| // configSelector adds an event listener that reloads the page when a config |
| // version is selected. |
| function configSelector() { |
| const el = document.querySelector<HTMLButtonElement>(".js-selectConfig"); |
| el?.addEventListener("change", () => { |
| const params = new URLSearchParams(location.search); |
| params.set(el.name, el.value); |
| history.replaceState(null, "", "?" + params.toString()); |
| location.reload(); |
| }); |
| } |
| |
| // breadcrumbController updates the navigation header as the user scrolls |
| // that page displaying information about the content currently in the |
| // viewport. |
| function breadcrumbController() { |
| const headings = |
| document.querySelectorAll<HTMLHeadingElement>("h1, h2, h3, h4"); |
| const callback = debounce(() => { |
| let above: HTMLHeadingElement[] = []; |
| for (const h of headings) { |
| const rect = h.getBoundingClientRect(); |
| if (rect.height && rect.top < 80) { |
| above.unshift(h); |
| } |
| } |
| if (above.length < 2) { |
| above = []; |
| } |
| let threshold = Infinity; |
| const els: HTMLHeadingElement[] = []; |
| for (const h of above) { |
| const level = Number(h.tagName[1]); |
| if (level < threshold) { |
| threshold = level; |
| els.unshift(h); |
| } |
| } |
| const breadcrumb = document.querySelector(".js-breadcrumb ol"); |
| const items = []; |
| for (const h of els) { |
| breadcrumb?.replaceChildren; |
| const li = document.createElement("li"); |
| const a = document.createElement("a"); |
| a.href = `#${h.id}`; |
| a.innerText = h.getAttribute("data-label") ?? h.innerText; |
| li.appendChild(a); |
| items.push(li); |
| } |
| breadcrumb?.replaceChildren(...items); |
| }, 100); |
| |
| const observer = new IntersectionObserver(callback); |
| for (const h of headings) { |
| observer.observe(h); |
| } |
| } |
| |
| function debounce<T extends (...args: unknown[]) => unknown>( |
| callback: T, |
| wait: number |
| ) { |
| let timeout: number; |
| return (...args: unknown[]) => { |
| clearTimeout(timeout); |
| timeout = setTimeout(() => callback(...args), wait); |
| }; |
| } |
| |
| export {}; |