]> git.ipfire.org Git - thirdparty/google/fonts.git/commitdiff
Add dashboard source
authorSimon Cozens <simon@simon-cozens.org>
Wed, 26 Mar 2025 10:36:46 +0000 (10:36 +0000)
committerSimon Cozens <simon@simon-cozens.org>
Tue, 1 Apr 2025 08:15:47 +0000 (09:15 +0100)
.ci/fontspector-dashboard/observablehq.config.js [new file with mode: 0644]
.ci/fontspector-dashboard/package.json [new file with mode: 0644]
.ci/fontspector-dashboard/src/.gitignore [new file with mode: 0644]
.ci/fontspector-dashboard/src/index.md [new file with mode: 0644]
.ci/fontspector-dashboard/src/results.json.js [new file with mode: 0644]
.github/workflows/fontspectorall.yaml

diff --git a/.ci/fontspector-dashboard/observablehq.config.js b/.ci/fontspector-dashboard/observablehq.config.js
new file mode 100644 (file)
index 0000000..bbf5a6b
--- /dev/null
@@ -0,0 +1,9 @@
+// See https://observablehq.com/framework/config for documentation.
+export default {
+  title: "Fontspector Dashboard",
+  root: "src",
+  footer: "",
+  sidebar: false, // whether to show the sidebar
+  output: "../../fontspector-dashboard", // path to the output root for build
+  preserveExtension: true
+};
diff --git a/.ci/fontspector-dashboard/package.json b/.ci/fontspector-dashboard/package.json
new file mode 100644 (file)
index 0000000..1610599
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "type": "module",
+  "private": true,
+  "scripts": {
+    "clean": "rimraf src/.observablehq/cache",
+    "build": "observable build",
+    "dev": "observable preview",
+    "deploy": "observable deploy",
+    "observable": "observable"
+  },
+  "dependencies": {
+    "@duckdb/node-api": "^1.2.1-alpha.16",
+    "@observablehq/framework": "^1.13.2",
+    "@observablehq/stdlib": "^5.8.8"
+  },
+  "devDependencies": {
+    "rimraf": "^5.0.5"
+  },
+  "engines": {
+    "node": ">=18"
+  }
+}
diff --git a/.ci/fontspector-dashboard/src/.gitignore b/.ci/fontspector-dashboard/src/.gitignore
new file mode 100644 (file)
index 0000000..1235d15
--- /dev/null
@@ -0,0 +1 @@
+/.observablehq/cache/
diff --git a/.ci/fontspector-dashboard/src/index.md b/.ci/fontspector-dashboard/src/index.md
new file mode 100644 (file)
index 0000000..2e4099d
--- /dev/null
@@ -0,0 +1,187 @@
+---
+toc: false
+---
+
+```js
+const allResults = FileAttachment("./results.json").json();
+
+const categoricals = {
+    type: "categorical",
+    domain: ['INFO', 'WARN', 'FAIL', 'ERROR'],
+    range: ["#2182bf", "#bdae4f", "#cf4f2b", "#ff0000"],
+    legend: true
+  };
+```
+
+<div class="hero">
+  <h1> Google Fonts QA </h1>
+  <h2> WARNs last run: <span class="huge warn">${ allResults.headline.WARN }</h2>
+  <h2> FAILs last run: <span class="huge fail">${ allResults.headline.FAIL }</h2>
+</div>
+
+
+<div class="card">
+
+## Overall failures
+
+```js
+Plot.plot({
+  marks: [
+    Plot.ruleY([0]),
+    Plot.line(
+      allResults.fails_by_run,
+      Plot.stackY2({ y: "count", x: (d) => new Date(d.run), stroke: "status" })
+    ),
+    Plot.dot(
+      allResults.fails_by_run,
+      Plot.stackY2(
+        { y: "count", x: "run", fill: "status", "tip": true }
+      )
+    )
+  ],
+  color: categoricals,
+  width
+})
+```
+
+</div>
+
+
+<div>
+
+<hr>
+
+<div class="runslider">
+<p>Select run:</p>
+
+```js
+const runSlider = view(html`<input type=range step=1 min=0 max=${allResults.allRuns.length-1} value=${allResults.allRuns.length-1}>`)
+```
+
+```js
+const selectedRun = allResults.allRuns[allResults.allRuns.length-(1+runSlider)]
+```
+
+<span class="when">${(new Date(selectedRun)).toISOString().replace("T", " ").replace(/\.\d+Z$/, "") }</span>
+</div>
+
+
+<div class="grid grid-cols-2">
+  <div class="card">
+    <h2>Most failing checks</h2>
+
+```js
+Plot.plot({
+  marginBottom: 90,
+  marginLeft: 90,
+   x: {
+    tickRotate: -30,
+    label: null,
+  },
+  color: categoricals,
+  marks: [
+    Plot.ruleY([0]),
+    Plot.rectY(allResults.most_failing_checks[selectedRun],
+  
+    { y: "count", x: "check_id", sort: {x: "y", reverse: "true"}, tip: true, fill: "status" },
+  )
+]})
+```
+
+  </div>
+
+  <div class="card">
+
+## Most failing families
+
+
+```js
+Plot.plot({
+   x: {
+    tickRotate: -30,
+    label: null,
+
+  },
+  color: categoricals,
+  marks: [
+  
+    Plot.ruleY([0]),
+    Plot.barY(allResults.most_failing_families[selectedRun],
+    { y: "count", x: "family", tip: true, fill: "status", order: "status", sort: {x: "y", reverse: true} },
+  ),
+],
+})
+```
+
+  </div>
+</div>
+
+</div>
+
+
+
+<style>
+
+.card {
+  height: 450px;
+}
+
+.hero {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  font-family: var(--sans-serif);
+  text-wrap: balance;
+  text-align: center;
+}
+
+.runslider {
+  height: 50px;
+}
+
+.runslider div { display: inline-block}
+.runslider div:first-child { display: inline-block; width: 50% ; }
+.runslider p { display: inline-block; font-family: sans-serif; }
+
+.hero h1 {
+  margin: 1rem 0;
+  padding: 1rem 0;
+  max-width: none;
+  font-size: 14vw;
+  font-weight: 900;
+  line-height: 1;
+  background: linear-gradient(30deg, var(--theme-foreground-focus), currentColor);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.hero h2 {
+  margin: 0;
+  max-width: 34em;
+  font-size: 40px;
+  font-style: initial;
+  font-weight: 500;
+  line-height: 1.5;
+  color: var(--theme-foreground-muted);
+  vertical-align: middle; 
+  display: inline-block;
+}
+
+.hero h2 .huge {
+  font-size: 60px;
+  font-weight: 700;
+  vertical-align: middle; 
+}
+
+.warn { color: #bdae4f; }
+.fail { color: #cf4f2b; }
+
+
+@media (min-width: 640px) {
+  .hero h1 {
+    font-size: 90px;
+  }
+}
+
+</style>
diff --git a/.ci/fontspector-dashboard/src/results.json.js b/.ci/fontspector-dashboard/src/results.json.js
new file mode 100644 (file)
index 0000000..3c2a08a
--- /dev/null
@@ -0,0 +1,102 @@
+import { DuckDBInstance } from "@duckdb/node-api";
+
+const instance = await DuckDBInstance.create("./src/fontspector.db");
+const db = await instance.connect();
+
+const reader = await db.runAndReadAll(
+  "SELECT distinct run FROM results ORDER BY run DESC"
+);
+const rows = reader.getRows();
+let allRuns = rows.map((c) => Number(c[0].micros) / 1000);
+let latestRun = allRuns[0];
+
+let headline = await db.runAndReadAll(`
+select status, count(status) as count
+  FROM (SELECT * FROM fontspector.results WHERE epoch_ms(run) == ${latestRun})
+  where status == 'WARN' or status == 'FAIL'
+  group by status`);
+const headlineRows = headline.getRows();
+
+let fails_by_run = (
+  await db.runAndReadAll(`
+select run, status, count(status) as count from fontspector.results
+where status == 'WARN' or status == 'FAIL'
+group by status, run
+order by run, status;
+`)
+).getRows();
+fails_by_run = fails_by_run.map((c) => {
+  return {
+    run: Number(c[0].micros) / 1000,
+    status: c[1],
+    count: Number(c[2]),
+  };
+});
+
+let most_failing_checks = (
+  await db.runAndReadAll(`
+select run, check_id, status, count(status) as count
+  FROM fontspector.results
+  where status == 'FAIL' or status == 'ERROR' or status == 'WARN'
+  group by run, check_id, status order by count desc;
+`)
+).getRows();
+let mfc = {};
+for (var row of most_failing_checks) {
+  let key = Number(row[0].micros) / 1000;
+  if (!mfc[key]) {
+    mfc[key] = [];
+  }
+  if (mfc[key].length < 10) {
+    mfc[key].push({
+      check_id: row[1],
+      status: row[2],
+      count: Number(row[3]),
+    });
+  }
+}
+
+/*
+
+```sql id=most_failing_families
+select directory as family, status, count(status) as count
+  FROM (SELECT * FROM fontspector.results WHERE epoch_ms(run) == ${allRuns[runSlider]})
+  where (status == 'FAIL' or status == 'ERROR' or status == 'WARN')
+  AND family in (SELECT directory as family FROM fontspector.results WHERE status == 'FAIL' or status == 'ERROR' or status == 'WARN' group by family order by count(status) desc limit 20)
+  group by directory, status;
+```
+*/
+
+
+let mff = {};
+for (var run of allRuns) {
+  let most_failing_families = (
+    await db.runAndReadAll(`
+select directory as family, status, count(status) as count
+  FROM (SELECT * FROM fontspector.results WHERE epoch_ms(run) == ${run})
+  where (status == 'FAIL' or status == 'ERROR' or status == 'WARN')
+  AND family in (SELECT directory as family FROM fontspector.results WHERE status == 'FAIL' or status == 'ERROR' or status == 'WARN' group by family order by count(status) desc limit 20)
+  group by directory, status;
+  `)
+  ).getRows();
+  mff[run] = [];
+  for (var row of most_failing_families) {
+    mff[run].push({
+      family: row[0],
+      status: row[1],
+      count: Number(row[2]),
+    });
+  }
+}
+const results = {
+  headline: {
+    [headlineRows[0][0]]: Number(headlineRows[0][1]),
+    [headlineRows[1][0]]: Number(headlineRows[1][1]),
+  },
+  allRuns,
+  fails_by_run,
+  most_failing_checks: mfc,
+  most_failing_families: mff,
+};
+
+process.stdout.write(JSON.stringify(results));
index b19c27b84ce970dc6c9ddcb0e65b59e5b65f586d..8135005c1dcb5e550db92d8676fe4081b2e50367 100644 (file)
@@ -17,8 +17,15 @@ jobs:
       - name: Build fontspector
         run: cargo install --git https://github.com/fonttools/fontspector --features duckdb
       - name: Test all the things
-        run: fontspector --profile googlefonts ofl/*/*{.ttf,.pb,*html,*svg,*jpg,*gif} --skip-network  --succinct --duckdb .ci/fontspector.db || true
+        run: fontspector --profile googlefonts ofl/*/*{.ttf,.pb,*html,*svg,*jpg,*gif} --skip-network --succinct --duckdb .ci/fontspector-dashboard/src/fontspector.db || true
       - name: Upload results
         uses: stefanzweifel/git-auto-commit-action@v5
         with:
-          file_pattern: .ci/fontspector.db
+          file_pattern: .ci/fontspector-dashboard/src/fontspector.db
+      - name: Rebuild dashboard
+        run: rm -rf fontspector-dashboard && cd .ci/fontspector-dashboard && npm install && npm run build
+      - name: Upload results
+        uses: stefanzweifel/git-auto-commit-action@v5
+        with:
+          branch: gh-pages
+          file_pattern: fontspector-dashboard