| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
| <meta name="generator" content="Observable Framework v1.13.4"> |
| <title>Fontc crater | Google Fonts QA Dashboard</title> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" crossorigin> |
| <link rel="preload" as="style" href="./_import/style.a52b67cd.css"> |
| <link rel="preload" as="style" href="./_observablehq/stdlib/inputs.f7bf0e12.css"> |
| <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" crossorigin> |
| <link rel="stylesheet" type="text/css" href="./_import/style.a52b67cd.css"> |
| <link rel="stylesheet" type="text/css" href="./_observablehq/stdlib/inputs.f7bf0e12.css"> |
| <link rel="modulepreload" href="./_observablehq/client.329de744.js"> |
| <link rel="modulepreload" href="./_observablehq/runtime.fa9f71c0.js"> |
| <link rel="modulepreload" href="./_observablehq/stdlib.c4209144.js"> |
| <link rel="modulepreload" href="./_import/components/Fontc.6f056196.js"> |
| <link rel="modulepreload" href="./_npm/react@19.3.0/jsx-runtime.27cd96ff.js"> |
| <link rel="modulepreload" href="./_npm/d3@7.9.0/080cf928.js"> |
| <link rel="modulepreload" href="./_npm/htl@1.0.0/11521f02.js"> |
| <link rel="modulepreload" href="./_observablehq/stdlib/inputs.0cc910eb.js"> |
| <link rel="modulepreload" href="./_npm/@observablehq/plot@0.6.17/93ce672e.js"> |
| <link rel="modulepreload" href="./_npm/react-dom@19.3.0/client.5bebe338.js"> |
| <link rel="modulepreload" href="./_npm/isoformat@0.2.1/604cb63b.js"> |
| <link rel="modulepreload" href="./_npm/d3-array@3.2.4/b8e751b6.js"> |
| <link rel="modulepreload" href="./_npm/d3-axis@3.0.0/c662ea05.js"> |
| <link rel="modulepreload" href="./_npm/d3-brush@3.0.0/d162d8c6.js"> |
| <link rel="modulepreload" href="./_npm/d3-chord@3.0.1/5cedd0be.js"> |
| <link rel="modulepreload" href="./_npm/d3-color@3.1.0/65d51007.js"> |
| <link rel="modulepreload" href="./_npm/d3-contour@4.0.2/c576a91a.js"> |
| <link rel="modulepreload" href="./_npm/d3-delaunay@6.0.4/8b43826d.js"> |
| <link rel="modulepreload" href="./_npm/d3-dispatch@3.0.1/3fb06f14.js"> |
| <link rel="modulepreload" href="./_npm/d3-drag@3.0.0/20b76948.js"> |
| <link rel="modulepreload" href="./_npm/d3-dsv@3.0.1/097745a2.js"> |
| <link rel="modulepreload" href="./_npm/d3-ease@3.0.1/8683b012.js"> |
| <link rel="modulepreload" href="./_npm/d3-fetch@3.0.1/5c319046.js"> |
| <link rel="modulepreload" href="./_npm/d3-force@3.0.0/33924f3e.js"> |
| <link rel="modulepreload" href="./_npm/d3-format@3.1.2/8d67a1ae.js"> |
| <link rel="modulepreload" href="./_npm/d3-geo@3.1.1/8037bd0b.js"> |
| <link rel="modulepreload" href="./_npm/d3-hierarchy@3.1.2/a9ba0229.js"> |
| <link rel="modulepreload" href="./_npm/d3-interpolate@3.0.1/e0a47f97.js"> |
| <link rel="modulepreload" href="./_npm/d3-path@3.1.0/f9e9e0ab.js"> |
| <link rel="modulepreload" href="./_npm/d3-polygon@3.0.1/2da8eb3b.js"> |
| <link rel="modulepreload" href="./_npm/d3-quadtree@3.0.1/09002e01.js"> |
| <link rel="modulepreload" href="./_npm/d3-random@3.0.1/5108797c.js"> |
| <link rel="modulepreload" href="./_npm/d3-scale@4.0.2/c9f2571b.js"> |
| <link rel="modulepreload" href="./_npm/d3-scale-chromatic@3.1.0/f3c78e0d.js"> |
| <link rel="modulepreload" href="./_npm/d3-selection@3.0.0/28717d05.js"> |
| <link rel="modulepreload" href="./_npm/d3-shape@3.2.0/7c6e2a50.js"> |
| <link rel="modulepreload" href="./_npm/d3-time@3.1.0/3d1d57c6.js"> |
| <link rel="modulepreload" href="./_npm/d3-time-format@4.1.0/643581a8.js"> |
| <link rel="modulepreload" href="./_npm/d3-timer@3.0.1/e99232db.js"> |
| <link rel="modulepreload" href="./_npm/d3-transition@3.0.1/bd07fd9a.js"> |
| <link rel="modulepreload" href="./_npm/d3-zoom@3.0.0/d09f7cc9.js"> |
| <link rel="modulepreload" href="./_npm/interval-tree-1d@1.0.4/f53f17b2.js"> |
| <link rel="modulepreload" href="./_npm/scheduler@0.28.0/3d5e653e.js"> |
| <link rel="modulepreload" href="./_npm/react@19.3.0/d4006ec2.js"> |
| <link rel="modulepreload" href="./_npm/react-dom@19.3.0/79ce8e4b.js"> |
| <link rel="modulepreload" href="./_npm/internmap@2.0.3/af036d7e.js"> |
| <link rel="modulepreload" href="./_npm/delaunator@5.1.0/7b06abe7.js"> |
| <link rel="modulepreload" href="./_npm/binary-search-bounds@2.0.5/5f640032.js"> |
| <link rel="modulepreload" href="./_npm/robust-predicates@3.0.3/b42f30a7.js"> |
| <script type="module"> |
| |
| import {define} from "./_observablehq/client.329de744.js"; |
| import {registerFile} from "./_observablehq/stdlib.c4209144.js"; |
| |
| registerFile("./data/fontc.json", {"name":"./data/fontc.json","mimeType":"application/json","path":"./_file/data/fontc.075d1180.json","lastModified":1791077445185,"size":2902935}); |
| |
| define({id: "18f886a9", inputs: ["FileAttachment"], outputs: ["fontc","RenderFailures","RenderSuccesses"], body: async (FileAttachment) => { |
| const {RenderFailures, RenderSuccesses} = await import("./_import/components/Fontc.6f056196.js"); |
| const fontc = await FileAttachment("./data/fontc.json").json(); |
| |
| return {fontc,RenderFailures,RenderSuccesses}; |
| }}); |
| |
| define({id: "ee6caa08", inputs: ["fontc","html","display","Inputs"], outputs: ["semiFlattened","prev_identical","updown","flattened"], body: (fontc,html,display,Inputs) => { |
| const semiFlattened = fontc.summary.map((cur) => { |
| // Move the `stats` object to the top level |
| return { |
| date: new Date(cur.began), |
| rev: cur.fontc_rev, |
| targets: cur.stats.total_targets, |
| fontc_failed: cur.stats.fontc_failed, |
| fontmake_failed: cur.stats.fontmake_failed, |
| both_failed: cur.stats.both_failed, |
| other: cur.stats.other_failure, |
| identical: cur.stats.identical, |
| similarity: cur.stats.diff_perc_including_failures, |
| }; |
| }); |
| |
| let prev_identical = undefined; |
| let updown = (v) => { |
| if (v > 0) return html`<span class="up">↑${v}</span>`; |
| if (v < 0) return html`<span class="down">↓${-v}</span>`; |
| return ""; |
| }; |
| |
| semiFlattened.forEach((cur) => { |
| let identical_value = cur.identical; |
| if (prev_identical) { |
| let identical_change = cur.identical - prev_identical; |
| if (identical_change) { |
| cur.identical = html`${cur.identical} ${updown(identical_change)}`; |
| } else { |
| cur.identical = html`${cur.identical}`; |
| } |
| } else { |
| cur.identical = html`${cur.identical}`; |
| } |
| prev_identical = identical_value; |
| }); |
| const flattened = fontc.summary.flatMap((cur) => |
| Object.entries(cur.stats) |
| .filter(([status, count]) => |
| ["both_failed", "fontmake_failed", "identical", "produced_diff"].includes( |
| status |
| ) |
| ) |
| .map(([status, count]) => { |
| return { |
| began: cur.began, |
| status, |
| count, |
| }; |
| }) |
| ); |
| display( |
| Inputs.table(semiFlattened, { |
| format: { |
| identical: (v) => v, |
| date: (v) => v.toLocaleDateString(), |
| similarity: (v) => `${v.toFixed(2)}%`, |
| }, |
| reverse: true, |
| width: { |
| date: 90, |
| }, |
| }) |
| ); |
| return {semiFlattened,prev_identical,updown,flattened}; |
| }}); |
| |
| define({id: "92308120", inputs: ["d3","fontc","display","Plot","flattened","width"], outputs: ["y2"], body: (d3,fontc,display,Plot,flattened,width) => { |
| const y2 = d3.scaleLinear( |
| [0, 100], |
| [0, d3.max(fontc.summary, (d) => d.stats.total_targets)] |
| ); |
| |
| display( |
| Plot.plot({ |
| color: { legend: true }, |
| y: { axis: "left", label: "targets" }, |
| |
| marks: [ |
| Plot.ruleY([0]), |
| Plot.axisY(y2.ticks(), { |
| anchor: "right", |
| label: "%", |
| color: "lightgreen", |
| y: y2, |
| tickFormat: y2.tickFormat(), |
| }), |
| |
| Plot.areaY(flattened, { |
| y: "count", |
| x: (d) => new Date(d.began), |
| fill: "status", |
| }), |
| Plot.lineY( |
| fontc.summary, |
| Plot.mapY((D) => D.map(y2), { |
| x: (d) => new Date(d.began), |
| y: (d) => d.stats.diff_perc_including_failures, |
| stroke: "green", |
| strokeWidth: 2, |
| }) |
| ), |
| Plot.lineY( |
| fontc.summary, |
| Plot.mapY((D) => D.map(y2), { |
| x: (d) => new Date(d.began), |
| y: (d) => d.stats.diff_perc_excluding_failures, |
| stroke: "green", |
| strokeOpacity: 0.2, |
| }) |
| ), |
| ], |
| width, |
| }) |
| ); |
| return {y2}; |
| }}); |
| |
| define({id: "b8146c7e", mode: "jsx", inputs: ["display","RenderSuccesses","fontc"], outputs: ["jsx"], body: async (display,RenderSuccesses,fontc) => { |
| const {jsx} = await import("./_npm/react@19.3.0/jsx-runtime.27cd96ff.js"); |
| display(/* @__PURE__ */ jsx(RenderSuccesses, { successes: fontc.lastRun.success })); |
| return {jsx}; |
| }}); |
| |
| define({id: "d96478a2", mode: "jsx", inputs: ["display","RenderFailures","fontc"], outputs: ["jsx"], body: async (display,RenderFailures,fontc) => { |
| const {jsx} = await import("./_npm/react@19.3.0/jsx-runtime.27cd96ff.js"); |
| display(/* @__PURE__ */ jsx(RenderFailures, { failures: fontc.lastRun.failure })); |
| return {jsx}; |
| }}); |
| |
| </script> |
| </head> |
| <body> |
| <input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar"> |
| <label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label> |
| <nav id="observablehq-sidebar"> |
| <ol> |
| <label id="observablehq-sidebar-close" for="observablehq-sidebar-toggle"></label> |
| <li class="observablehq-link"><a href="./">Google Fonts QA Dashboard</a></li> |
| </ol> |
| <ol> |
| <li class="observablehq-link"><a href="./families.html">Family status</a></li> |
| <li class="observablehq-link observablehq-link-active"><a href="./fontc_crater.html">Fontc crater</a></li> |
| <li class="observablehq-link"><a href="./source_check.html">Fontc crater sources check</a></li> |
| <li class="observablehq-link"><a href="./trends.html">Fontspector QA trends</a></li> |
| <li class="observablehq-link"><a href="./updates.html">Server moves</a></li> |
| </ol> |
| <details open> |
| <summary>Standalone pages</summary> |
| <ol> |
| <li class="observablehq-link"><a href="./meta.html">meta</a></li> |
| <li class="observablehq-link"><a href="./familyexplorer.html">familyexplorer</a></li> |
| <li class="observablehq-link"><a href="./fontdiffai.html">fontdiffai</a></li> |
| <li class="observablehq-link"><a href="./opsz_classifier.html">opsz classifier</a></li> |
| <li class="observablehq-link"><a href="./opsz_classifier_v01.html">opsz classifier v01</a></li> |
| <li class="observablehq-link"><a href="./opsz_classifier_v02.html">opsz classifier v02</a></li> |
| <li class="observablehq-link"><a href="./opsz_labeller.html">opsz labeller</a></li> |
| <li class="observablehq-link"><a href="./opsz_prediction.html">opsz prediction</a></li> |
| <li class="observablehq-link"><a href="./quality-tag-review.html">quality tag review</a></li> |
| <li class="observablehq-link"><a href="./quality_gaps_analysis.html">quality gaps analysis</a></li> |
| <li class="observablehq-link"><a href="./quality_outperforming.html">quality outperforming</a></li> |
| <li class="observablehq-link"><a href="./quality_overview.html">quality overview</a></li> |
| <li class="observablehq-link"><a href="./quality_perception.html">quality perception</a></li> |
| <li class="observablehq-link"><a href="./quality_uplevel_analysis.html">quality uplevel analysis</a></li> |
| <li class="observablehq-link"><a href="./quality_usage.html">quality usage</a></li> |
| <li class="observablehq-link"><a href="./tags.html">tags</a></li> |
| <li class="observablehq-link"><a href="./vf-tag-demo-linear.html">vf tag demo linear</a></li> |
| <li class="observablehq-link"><a href="./vf-tag-demo2.html">vf tag demo2</a></li> |
| </ol> |
| </details> |
| </nav> |
| <script>{const e=document.querySelector("#observablehq-sidebar"),o=document.querySelector("#observablehq-sidebar-toggle"),r=sessionStorage.getItem("observablehq-sidebar");r?o.checked=r==="true":o.indeterminate=!0;for(const t of document.querySelectorAll("#observablehq-sidebar summary")){const s=t.parentElement;switch(sessionStorage.getItem(`observablehq-sidebar:${t.textContent}`)){case"true":s.open=!0;break;case"false":s.classList.contains("observablehq-section-active")||(s.open=!1);break}}addEventListener("beforeunload",()=>sessionStorage.setItem("observablehq-sidebar-scrolly",`${e.scrollTop}`));const a=sessionStorage.getItem("observablehq-sidebar-scrolly");a!=null&&(e.style.cssText="overflow: hidden;",e.scrollTop=+a,e.style.cssText="");}</script> |
| <div id="observablehq-center"> |
| <aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]"> |
| <nav> |
| <div>Contents</div> |
| <ol> |
| <li class="observablehq-secondary-link"><a href="#successes">Successes</a></li> |
| <li class="observablehq-secondary-link"><a href="#failures">Failures</a></li> |
| </ol> |
| </nav> |
| </aside> |
| <main id="observablehq-main" class="observablehq"> |
| <div class="observablehq observablehq--block"><!--:18f886a9:--></div> |
| <div class="observablehq observablehq--block"><!--:ee6caa08:--></div> |
| <div class="observablehq observablehq--block"><!--:92308120:--></div> |
| <h2 id="successes" tabindex="-1"><a class="observablehq-header-anchor" href="#successes">Successes</a></h2> |
| <div class="observablehq observablehq--block"><!--:b8146c7e:--></div> |
| <h2 id="failures" tabindex="-1"><a class="observablehq-header-anchor" href="#failures">Failures</a></h2> |
| <div class="observablehq observablehq--block"><!--:d96478a2:--></div> |
| </main> |
| <footer id="observablehq-footer"> |
| <nav><a rel="prev" href="./families.html"><span>Family status</span></a><a rel="next" href="./source_check.html"><span>Fontc crater sources check</span></a></nav> |
| </footer> |
| </div> |
| </body> |
| </html> |