智能体
github.blog ·
@Erin Havens
密钥保护必须与软件规模同步扩展
GitHub 称目前每三个 PR 就有一个涉及 AI Agent,一年前不足十分之一;公开代码中约每两秒出现一个新密钥,三年内逐年翻倍。其九季度数据显示,开发者并非更粗心,而是被开发速度甩开:推送路径拦截被开发者覆盖的比例从 6.63% 降至 3.93%。GitHub 与 Microsoft Applied Sciences 合作构建微调分类器,将推送保护扩展到非结构化密钥,称可在两毫秒内评估一批候选密钥,并有望使可拦截的密钥数量翻倍以上。
AI推荐理由 GitHub 官方博客给出九季度数据:GitHub 上每三个 PR 就有一个涉及 AI Agent(一年前不足十分之一),并称公开代码中约每两秒出现一个新密钥、三年内逐年翻倍;同时推出与 Microsoft Applied Sciences 合作微调的分类器,将推送保护扩展到非结构化密钥,称可在两毫秒内评估候选密钥并有望使可拦截密钥数量翻倍以上。数据与模型细节具体,对 AI 编码安全有实际价值;但正文片段混入无关代码,且效果为官方自述。
76
正文
Today, one in three pull requests on GitHub involves an AI agent. A year ago, that number was fewer than one in 10. If that pace holds, within the next two years, most of the code pushed to GitHub could be written by an agent. Much of it may never be fully read by a human. If developers and agents move faster, we have a responsibility to ensure protection keeps up with the accelerated rate of code creation. That means preventing more leaks before they happen and making the response to exposures that remain less dependent on manual human effort. This is a pivotal point for leaked secrets. Developers aren’t becoming more careless; they’re being outpaced. The tools that let developers create more software should also take on more of the work of protecting it. In this essay, I share the nine quarters of data behind that claim. I also introduce the fine-tuned classifier we built with Microsoft Applied Sciences to extend push protection to unstructured secrets. The model assesses a whole set of candidate secrets in less than two milliseconds and could more than double the number of secrets that we can prevent. Outpaced, not careless A new secret appears in publicly visible code about once every two seconds, doubling yearly for the past three years. Public discourse is quick to jump to the idea that AI made developers careless. Between Q2 2024 and Q2 2026, screened pushes grew 2.84 times while pushes carrying credentials grew 2.59 times. Across nine complete quarters of data, we found no statistically detectable trend regarding per-push prevalence. At the same time, we found data suggesting that, more than ever, developers understand the risk of accidental exposures and are less willing to accept that risk. Over the same period, the share of push-path blocks overridden by developers fell linearly from 6.63% to 3.93%. These figures challenge the common claim that agents are causing developers to become more careless. hi ? hi : v; } /* ---- Color ramp ---------------------------------------------------- * The whole palette for a series is derived from ONE base hex, so a single tunable color drives an entire faint->dark scale. This is the convention behind the contribution-heatmap shading and the accent colors on badges/stat numbers. */ var WHITE = [255, 255, 255], BLACK = [0, 0, 0]; function hexToRgb(h) { h = String(h).replace("#", ""); if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; } function rgbToHex(c) { return "#" + c.map(function (x) { x = Math.max(0, Math.min(255, Math.round(x))); return (x level 4 (dark), from one base hex. // Ratios are the project standard — keep them identical across charts. function rampFrom(baseHex) { var b = hexToRgb(baseHex); return [ rgbToHex(mix(b, WHITE, 0.80)), rgbToHex(mix(b, WHITE, 0.52)), rgbToHex(mix(b, WHITE, 0.20)), baseHex, rgbToHex(mix(b, BLACK, 0.30)) ]; } // The "strong" step (level 4) — use it for badges, stat numbers, accents. function strongOf(baseHex) { return rampFrom(baseHex)[4]; } /* ---- Number formatting -------------------------------------------- */ // 1234567 -> "1,234,567" function fmtInt(n) { return Math.round(n).toLocaleString("en-US"); } // Compact absolute counts: 6400000 -> "6.4M", 2000000000 -> "2B", 39000 -> "39K". function fmtCompact(n) { var v = Math.abs(n), s = n = 1e9) return s + trim((v / 1e9).toFixed(1)) + "B"; if (v >= 1e6) return s + trim((v / 1e6).toFixed(1)) + "M"; if (v >= 1e3) return s + Math.round(v / 1e3) + "K"; return s + String(Math.round(v)); } /* ---- Tooltip ------------------------------------------------------- * One shared tooltip per chart. `root` must be position:relative (the .ghchart wrapper is). Returns { el, show, hide }. var tip = createTooltip(root); marker.addEventListener("pointermove", function () { tip.show(marker, "Mon · May 11, 2026", [ "10.4M pushes/day", "May · 321.9M scanned" ]); }); marker.addEventListener("pointerleave", tip.hide); Motion contract (matches chart.css): • fade in/out at 0.12s; • when moving between marks, ease left/top at 0.1s; • on the FIRST appearance the position ease is suppressed so the card fades in place instead of sliding in from the previous mark; • flip below the anchor when there's no room above; • clamp horizontally so it never overflows the chart width; • prefers-reduced-motion disables the eases (handled in chart.css). */ function createTooltip(root, opts) { opts = opts || {}; var tip = html("div", opts.className || "ghchart__tooltip"); tip.setAttribute("role", "status"); var titleEl = html("div", "ghchart__tt-title"); var bodyEl = html("div", "ghchart__tt-body"); tip.appendChild(titleEl); tip.appendChild(bodyEl); root.appendChild(tip); var mq = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null; var visible = false; function hide() { visible = false; tip.setAttribute("data-visible", "false"); } function show(anchorEl, titleText, lines) { var firstAppear = !visible; visible = true; titleEl.textContent = titleText; bodyEl.textContent = ""; (lines || []).forEach(function (ln) { var row = html("div", "ghchart__tt-line"); row.innerHTML = ln; // lines may contain … bodyEl.appendChild(row); }); var a = anchorEl.getBoundingClientRect(); var r = root.getBoundingClientRect(); var left = a.left + a.width / 2 - r.left; var top = a.top - r.top - 12; // Suppress the position ease the first time so it fades in place. var suppress = firstAppear && !(mq && mq.matches); if (suppress) tip.style.transition = "opacity 0.12s ease"; tip.setAttribute("data-visible", "true"); var halfW = tip.offsetWidth / 2; left = clamp(left, halfW + 2, root.clientWidth - halfW - 2); if (top - tip.offsetHeight flip below top = a.bottom - r.top + 12; tip.style.transform = "translate(-50%, 0)"; } else { tip.style.transform = "translate(-50%, -100%)"; } tip.style.left = left + "px"; tip.style.top = top + "px"; if (suppress) { void tip.offsetWidth; tip.style.transition = ""; } return tip; } return { el: tip, show: show, hide: hide }; } var Kit = { el: el, html: html, clamp: clamp, hexToRgb: hexToRgb, rgbToHex: rgbToHex, mix: mix, rampFrom: rampFrom, strongOf: strongOf, fmtInt: fmtInt, fmtCompact: fmtCompact, createTooltip: createTooltip }; function pointsToPath(points, tension) { if (!points.length) return ""; if (points.length === 1) return "M" + points[0].x + " " + points[0].y; var d = "M" + points[0].x + " " + points[0].y; var t = tension || 0; for (var i = 0; i 0) { var cp1x = p1.x + (p2.x - p0.x) * t / 6; var cp1y = p1.y + (p2.y - p0.y) * t / 6; var cp2x = p2.x - (p3.x - p1.x) * t / 6; var cp2y = p2.y - (p3.y - p1.y) * t / 6; d += " C" + cp1x + " " + cp1y + " " + cp2x + " " + cp2y + " " + p2.x + " " + p2.y; } else { d += " L" + p2.x + " " + p2.y; } } return d; } function fmtPlain(v, decimals) { var n = Number(v); if (!isFinite(n)) return String(v); if (decimals == null) return String(v); return n.toFixed(decimals).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1"); } function createText(txt, attrs) { var t = el("text", attrs); t.textContent = txt; return t; } /* ---- Axis scales ---------------------------------------------------- * Maps a data value onto a pixel coordinate. `p0` is the pixel for `min` and `p1` the pixel for `max`, so a y-axis passes (y0, y1) — bottom first — and an x-axis passes (x0, x1). Log axes clamp at `min` rather than returning -Infinity, because several figures carry legitimate zeros that must sit on the floor instead of vanishing. */ function makeScale(min, max, p0, p1, isLog) { if (isLog) { var lo = Math.log(Math.max(min, 1e-12)) / Math.LN10; var span = (Math.log(Math.max(max, 1e-12)) / Math.LN10) - lo || 1; return function (v) { var n = Number(v); if (!isFinite(n) || n [10, 100, ... 100000]. function logTicks(min, max) { var out = []; for (var e = Math.ceil(Math.log(min) / Math.LN10 - 1e-9); Math.pow(10, e) spec.max) return; var yy = yFn(tv); f.svg.appendChild(el("line", { "class": "ghchart__gridline", x1: f.x0, x2: f.x1, y1: yy, y2: yy })); f.svg.appendChild(createText(tickLabel(tv, spec), { "class": "ghchart__axislabel", x: f.x0 - 18, y: yy + 7, "text-anchor": "end" })); }); } function axisTitles(f, config, xLabelY) { if (config.y && config.y.title) { f.svg.appendChild(createText(config.y.title, { "class": "ghchart__axislabel", x: -(f.y1 + f.y0) / 2, y: config.y.titleOffset || 30, transform: "rotate(-90)", "text-anchor": "middle" })); } if (config.x && config.x.title) { f.svg.appendChild(createText(config.x.title, { "class": "ghchart__axislabel", x: (f.x0 + f.x1) / 2, y: xLabelY == null ? f.y0 + 86 : xLabelY, "text-anchor": "middle" })); } } // Always-on comparison labels and the double-headed gap arrow, shared by // every renderer. `xFn`/`yFn` translate data space; `px`/`py` bypass them. function decorate(f, config, xFn, yFn) { function annText(txt, ax, ay, opts) { var t = createText(txt, { "class": "ghchart__ann", x: ax, y: ay, "text-anchor": opts.anchor || "start" }); t.style.fontSize = (opts.size || 23) + "px"; t.style.fontWeight = opts.weight || 400; t.style.fill = opts.color || "#656d76"; t.setAttribute("dominant-baseline", "central"); return t; } (config.annotations || []).forEach(function (a) { var px = (a.px != null ? a.px : xFn(a.x)) + (a.dx || 0); var py = (a.py != null ? a.py : yFn(a.y)) + (a.dy || 0); var anchor = a.align === "right" ? "end" : a.align === "middle" ? "middle" : "start"; String(a.text).split("\n").forEach(function (ln, i) { f.svg.appendChild(annText(ln, px, py + i * (a.lh || 30), { anchor: anchor, size: a.size, weight: a.weight, color: a.color })); }); }); var gaps = config.gaps || (config.gap ? [config.gap] : []); gaps.forEach(function (g) { var gx = (g.px != null ? g.px : xFn(g.x)) + (g.dx || 0), yT = yFn(g.top), yB = yFn(g.bottom); var col = g.color || "#8250df"; f.svg.appendChild(el("line", { x1: gx, x2: gx, y1: yT + 9, y2: yB - 9, stroke: col, "stroke-width": g.width || 2.3 })); [[yT, 1], [yB, -1]].forEach(function (h) { f.svg.appendChild(el("polygon", { points: gx + "," + h[0] + " " + (gx - 6.6) + "," + (h[0] + h[1] * 12) + " " + (gx + 6.6) + "," + (h[0] + h[1] * 12), fill: col })); }); var mid = (yT + yB) / 2; f.svg.appendChild(annText(g.t1, gx + 20, mid - 15, { size: g.size1 || 26, weight: 700, color: col })); f.svg.appendChild(annText(g.t2, gx + 20, mid + 15, { size: g.size2 || 21, color: col })); }); return annText; } // Shared chrome: resets the mount, writes optional title/subtitle, and // returns false when the mount is missing or already rendered. function openRoot(root, config) { root = typeof root === "string" ? document.getElementById(root) : root; if (!root || root.getAttribute("data-ghchart-rendered") === "true") return null; root.setAttribute("data-ghchart-rendered", "true"); root.classList.add("ghchart"); root.textContent = ""; if (config.title) { var title = html("span", "ghchart__title"); title.textContent = config.title; root.appendChild(title); } if (config.subtitle) { var sub = html("p", "ghchart__subtitle"); sub.textContent = config.subtitle; root.appendChild(sub); } return root; } function closeRoot(root, f, config) { root.appendChild(f.svg); if (config.caption) { var cap = html("div", "ghchart__caption"); cap.innerHTML = config.caption; root.appendChild(cap); } return root; } function renderLine(root, config) { root = openRoot(root, config); if (!root) return root; var labels = config.labels || []; var series = (config.series || []).map(function (s) { var ramp = rampFrom(s.base || config.base || "#3094ff"); return { name: s.name, data: s.data || [], xData: s.xData, raw: s.raw || [], base: s.base || config.base || "#3094ff", ramp: ramp, color: ramp[s.strokeStep == null ? 3 : s.strokeStep], width: s.width || 4, dash: s.dash || "", fill: !!s.fill, fillOpacity: s.fillOpacity == null ? 0.72 : s.fillOpacity, pointRadius: s.pointRadius == null ? 6 : s.pointRadius, pointRadiusLast: s.pointRadiusLast == null ? (s.pointRadius == null ? 7 : s.pointRadius) : s.pointRadiusLast, ringWidth: s.ringWidth == null ? 2.8 : s.ringWidth, valueSuffix: s.valueSuffix || "", rawSuffix: s.rawSuffix || "", rawDecimals: s.rawDecimals }; }); var allVals = series.reduce(function (a, s) { return a.concat(s.data.filter(function (v) { return v != null; })); }, []); var ySpec = Object.assign({}, config.y); if (ySpec.min == null) ySpec.min = Math.min.apply(null, allVals); if (ySpec.max == null) ySpec.max = Math.max.apply(null, allVals); var f = frame(config, { right: 60 }); var x0 = f.x0, x1 = f.x1, y0 = f.y0, y1 = f.y1, W = f.W, H = f.H, svg = f.svg; svg.setAttribute("class", "ghchart__plot ghchart__plot--line"); svg.setAttribute("aria-label", config.ariaLabel || (config.title || "Line chart") + ": " + series.map(function (s) { return s.name; }).join(" and ")); var x = function (i) { return labels.length ySpec.max) return; var ry = y(r.value); svg.appendChild(el("line", { x1: x0, x2: x1, y1: ry, y2: ry, stroke: r.color || "#afb8c1", "stroke-width": r.width == null ? 2.2 : r.width, "stroke-dasharray": r.dash == null ? "8 7" : r.dash })); if (r.label) { var rt = createText(r.label, { "class": "ghchart__ann", x: (r.x == null ? x0 : (x0 + (x1 - x0) * r.x)), y: ry + (r.dy == null ? -12 : r.dy), "text-anchor": r.align === "right" ? "end" : r.align === "middle" ? "middle" : "start" }); rt.style.fontSize = (r.size == null ? 21 : r.size) + "px"; rt.style.fontWeight = r.weight == null ? 400 : r.weight; rt.style.fill = r.labelColor || "#818b98"; svg.appendChild(rt); } }); axisTitles(f, config, y0 + (config.xTitleOffset || 86)); if (xs && xs.ticks) { xs.ticks.forEach(function (tv) { svg.appendChild(createText(tickLabel(tv, xs), { "class": "ghchart__axislabel ghchart__axislabel--x", x: xv(tv), y: y0 + (config.xLabelOffset || 42), "text-anchor": "middle" })); if (xs.grid) svg.appendChild(el("line", { "class": "ghchart__gridline", x1: xv(tv), x2: xv(tv), y1: y0, y2: y1 })); }); } /* Dense daily axes (Figure 9 carries 49 days) will happily overprint "May 25" on "May 26". `xLabelMinGap` drops any label that would land within N viewBox units of the last one drawn, keeping the first. */ var lastLabelX = -Infinity, minGap = config.xLabelMinGap || 0; labels.forEach(function (label, i) { if (label === "") return; var lx = x(i); if (lx - lastLabelX 1) { var fillD = pointsToPath(pts, config.tension || 0) + " L" + pts[pts.length - 1].x + " " + y0 + " L" + pts[0].x + " " + y0 + " Z"; svg.appendChild(el("path", { d: fillD, fill: s.ramp[0], opacity: s.fillOpacity })); } svg.appendChild(el("path", { "class": "ghchart__line", d: pointsToPath(pts, config.tension || 0), stroke: s.color, "stroke-width": s.width, "stroke-dasharray": s.dash })); if (s.pointRadius > 0) { pts.forEach(function (p, i) { var r = i === pts.length - 1 ? s.pointRadiusLast : s.pointRadius; var dot = el("circle", { "class": "ghchart__dot", cx: p.x, cy: p.y, r: r, fill: s.color, stroke: "#ffffff", "stroke-width": s.ringWidth }); dot.setAttribute("data-r", r); dot.setAttribute("data-i", p.index); svg.appendChild(dot); plotted[plotted.length - 1].dots.push(dot); }); } }); /* Always-on comparison labels. SKILL §4 bars values on marks but states an exception for deliberate comparison labels; this report also ships as a print PDF, where hover does not exist, so the exception is the rule here. */ decorate(f, config, xs ? xv : x, y); var guide = el("line", { "class": "ghchart__guide", x1: x0, x2: x0, y1: y1, y2: y0 }); svg.appendChild(guide); var tip = createTooltip(root); function tooltipLines(i) { return series.filter(function (s) { return s.data[i] != null; }).map(function (s) { var bits = [s.name + ": " + fmtPlain(s.data[i], config.valueDecimals == null ? 1 : config.valueDecimals) + "" + (s.valueSuffix || "")]; if (s.raw[i] != null) bits.push("" + fmtPlain(s.raw[i], s.rawDecimals) + "" + (s.rawSuffix || "")); return bits.join(" · "); }); } /* Hover geometry follows whichever x-axis the figure uses. On a value axis the hit bands are uneven, which is correct: day 90 owns a wider band than day 30 because it covers more of the plot. */ var hx = function (i) { return xs && series[0] && series[0].xData ? xv(series[0].xData[i]) : x(i); }; function activate(i, anchor) { guide.setAttribute("x1", hx(i)); guide.setAttribute("x2", hx(i)); guide.style.opacity = "1"; plotted.forEach(function (p) { p.dots.forEach(function (dot) { var r = Number(dot.getAttribute("data-r")); dot.setAttribute("r", Number(dot.getAttribute("data-i")) === i ? r + 3 : r); }); }); tip.show(anchor, labels[i], tooltipLines(i)); } function deactivate() { guide.style.opacity = "0"; plotted.forEach(function (p) { p.dots.forEach(function (dot) { dot.setAttribute("r", dot.getAttribute("data-r")); }); }); tip.hide(); } if (config.hover !== false) { labels.forEach(function (_label, i) { var left = i === 0 ? x0 : (hx(i - 1) + hx(i)) / 2; var right = i === labels.length - 1 ? x1 : (hx(i) + hx(i + 1)) / 2; var hit = el("rect", { "class": "ghchart__hit", x: left, y: y1, width: right - left, height: y0 - y1 }); hit.style.cursor = "pointer"; hit.addEventListener("pointermove", function () { activate(i, hit); }); hit.addEventListener("pointerleave", deactivate); svg.appendChild(hit); }); } root.appendChild(svg); if (config.caption) { var cap = html("div", "ghchart__caption"); cap.innerHTML = config.caption; root.appendChild(cap); } return root; } /* ---- Scatter -------------------------------------------------------- * Both axes are free (linear or log), which is what the distribution figures need: repository spread against reuse, CCDF tails, public against private rates. Optional `curves` draw underneath every marker so a reference contour never sits on top of the data it explains. */ function renderScatter(root, config) { root = openRoot(root, config); if (!root) return root; var xs = Object.assign({ scale: "linear" }, config.x); var ys = Object.assign({ scale: "linear" }, config.y); var f = frame(config, { right: 60, bottom: 104 }); var svg = f.svg; svg.setAttribute("class", "ghchart__plot ghchart__plot--scatter"); var x = makeScale(xs.min, xs.max, f.x0, f.x1, xs.scale === "log"); var y = makeScale(ys.min, ys.max, f.y0, f.y1, ys.scale === "log"); gridY(f, ys, y); (xs.ticks || (xs.scale === "log" ? logTicks(xs.min, xs.max) : [])).forEach(function (tv) { if (tv xs.max) return; var xx = x(tv); if (xs.grid !== false) svg.appendChild(el("line", { "class": "ghchart__gridline", x1: xx, x2: xx, y1: f.y0, y2: f.y1 })); svg.appendChild(createText(tickLabel(tv, xs), { "class": "ghchart__axislabel ghchart__axislabel--x", x: xx, y: f.y0 + (config.xLabelOffset || 42), "text-anchor": "middle" })); }); axisTitles(f, config, f.y0 + (config.xTitleOffset || 86)); // Reference contours, drawn first so markers always sit above them. if (config.reference) { var rf = config.reference, rfx = x(rf.value); svg.appendChild(el("line", { x1: rfx, x2: rfx, y1: f.y1, y2: f.y0, stroke: rf.color || "#b9c0c8", "stroke-width": rf.width || 2.8, "stroke-dasharray": rf.dash || "9 7" })); if (rf.label) { var rft = createText(rf.label, { "class": "ghchart__ann", x: rfx, y: f.y1 - 14, "text-anchor": "middle" }); rft.style.fontSize = (rf.size || 23) + "px"; rft.style.fill = rf.labelColor || "#9aa4ae"; svg.appendChild(rft); } } var clipId = "ghc-clip-" + Math.random().toString(36).slice(2, 9); var defs = el("defs", {}); var cp = el("clipPath", { id: clipId }); cp.appendChild(el("rect", { x: f.x0, y: f.y1, width: f.x1 - f.x0, height: f.y0 - f.y1 })); defs.appendChild(cp); svg.appendChild(defs); (config.curves || []).forEach(function (c) { var d = c.points.map(function (p, i) { return (i ? "L" : "M") + x(p[0]) + " " + y(p[1]); }).join(" "); svg.appendChild(el("path", { "class": "ghchart__curve", d: d, fill: "none", "clip-path": "url(#" + clipId + ")", stroke: c.color || "#d8dde3", "stroke-width": c.width || 2, "stroke-dasharray": c.dash || "" })); if (c.label) { var lp = c.labelAt || c.points[c.points.length - 1]; var lt = createText(c.label, { "class": "ghchart__ann", x: x(lp[0]) + (c.dx || 0), y: y(lp[1]) + (c.dy || 0), "text-anchor": c.align === "right" ? "end" : "start" }); lt.style.fontSize = (c.size || 21) + "px"; lt.style.fill = c.labelColor || "#8c959f"; lt.setAttribute("dominant-baseline", "central"); svg.appendChild(lt); } }); var tip = createTooltip(root); (config.points || []).forEach(function (p) { var base = p.color || config.base || "#0969da"; var cx = x(p.x), cy = y(p.y); var dot = el("circle", { "class": "ghchart__dot", cx: cx, cy: cy, r: p.r || config.pointRadius || 12, fill: base, stroke: "#ffffff", "stroke-width": p.ringWidth || config.ringWidth || 2.8 }); if (p.opacity != null) dot.setAttribute("opacity", p.opacity); dot.setAttribute("data-r", p.r || config.pointRadius || 12); dot.style.cursor = "pointer"; if (p.label || p.lines) { dot.addEventListener("pointermove", function () { dot.setAttribute("r", Number(dot.getAttribute("data-r")) + 3); tip.show(dot, p.label || "", p.lines || []); }); dot.addEventListener("pointerleave", function () { dot.setAttribute("r", dot.getAttribute("data-r")); tip.hide(); }); } svg.appendChild(dot); }); decorate(f, config, x, y); return closeRoot(root, f, config); } /* ---- Bar ------------------------------------------------------------ * Vertical bars on a categorical x-axis, with optional always-on value labels above each bar — the same print-first reasoning as the line annotations, since the PDF has no hover. */ function renderBar(root, config) { root = openRoot(root, config); if (!root) return root; var labels = config.labels || []; var ys = Object.assign({ scale: "linear" }, config.y); var vals = (config.values || []).filter(function (v) { return v != null; }); if (ys.min == null) ys.min = ys.scale === "log" ? Math.min.apply(null, vals) : 0; if (ys.max == null) ys.max = Math.max.apply(null, vals); var f = frame(config, { right: 60 }); var svg = f.svg; svg.setAttribute("class", "ghchart__plot ghchart__plot--bar"); var y = makeScale(ys.min, ys.max, f.y0, f.y1, ys.scale === "log"); gridY(f, ys, y); axisTitles(f, config, f.y0 + (config.xTitleOffset || 86)); var n = labels.length || 1; var slot = (f.x1 - f.x0) / n; var bw = slot * (config.barWidth == null ? 0.62 : config.barWidth); var tip = createTooltip(root); labels.forEach(function (label, i) { var cx = f.x0 + slot * (i + 0.5); var v = (config.values || [])[i]; svg.appendChild(createText(label, { "class": "ghchart__axislabel ghchart__axislabel--x", x: cx, y: f.y0 + (config.xLabelOffset || 42), "text-anchor": "middle" })); if (v == null) return; var top = y(v), base = (config.colors && config.colors[i]) || config.base || "#0969da"; var rect = el("rect", { "class": "ghchart__bar", x: cx - bw / 2, y: Math.min(top, f.y0), width: bw, height: Math.max(Math.abs(f.y0 - top), 1), fill: base, rx: config.radius == null ? 4 : config.radius }); if (config.strokeColors && config.strokeColors[i] != null) rect.setAttribute("stroke", config.strokeColors[i]); if (config.strokeWidths && config.strokeWidths[i] != null) rect.setAttribute("stroke-width", config.strokeWidths[i]); if (config.strokeDasharrays && config.strokeDasharrays[i] != null) rect.setAttribute("stroke-dasharray", config.strokeDasharrays[i]); rect.style.cursor = "pointer"; var lines = (config.tooltips && config.tooltips[i]) || null; if (lines) { rect.addEventListener("pointermove", function () { tip.show(rect, label, lines); }); rect.addEventListener("pointerleave", tip.hide); } svg.appendChild(rect); if (config.valueLabels) { var vl = createText(config.valueLabels[i], { "class": "ghchart__ann", x: cx, y: top - (config.valueLabelOffset || 22), "text-anchor": "middle" }); vl.style.fontSize = (config.valueLabelSize || 23) + "px"; vl.style.fontWeight = 600; vl.style.fill = config.valueLabelColor || "#424a53"; vl.setAttribute("dominant-baseline", "central"); svg.appendChild(vl); } }); decorate(f, config, function (i) { return f.x0 + slot * (i + 0.5); }, y); return closeRoot(root, f, config); } /* ---- Horizontal bar -------------------------------------------------- * Categories on the y-axis, values running left to right. Used where the category names are phrases rather than tokens ("HashiCorp Vault · secrets mgr"), which a vertical axis cannot carry without rotating type. `reference` draws a vertical comparison line, e.g. a base rate. */ function renderBarH(root, config) { root = openRoot(root, config); if (!root) return root; var labels = config.labels || []; var values = config.values || []; var xs = Object.assign({ scale: "linear", min: 0 }, config.x); if (xs.max == null) xs.max = Math.max.apply(null, values); var f = frame(config, { top: 40, right: 230, bottom: 104, left: 420 }); var svg = f.svg; svg.setAttribute("class", "ghchart__plot ghchart__plot--barh"); var x = makeScale(xs.min, xs.max, f.x0, f.x1, xs.scale === "log"); (xs.ticks || []).forEach(function (tv) { var xx = x(tv); svg.appendChild(el("line", { "class": "ghchart__gridline", x1: xx, x2: xx, y1: f.y0, y2: f.y1 })); svg.appendChild(createText(tickLabel(tv, xs), { "class": "ghchart__axislabel ghchart__axislabel--x", x: xx, y: f.y0 + (config.xLabelOffset || 42), "text-anchor": "middle" })); }); axisTitles(f, config, f.y0 + (config.xTitleOffset || 86)); var n = labels.length || 1; var slot = (f.y0 - f.y1) / n; var bh = slot * (config.barWidth == null ? 0.6 : config.barWidth); var tip = createTooltip(root); // Reference line first, so bars and their labels always sit above it. if (config.reference) { var r = config.reference, rx = x(r.value); svg.appendChild(el("line", { x1: rx, x2: rx, y1: f.y1, y2: f.y0, stroke: r.color || "#b9c0c8", "stroke-width": r.width || 2.8, "stroke-dasharray": r.dash || "9 7" })); if (r.label) { // Above the plot, not inside it: at the foot the label sat underneath // the shortest bar, which is exactly where a base rate cannot hide. var rt = createText(r.label, { "class": "ghchart__ann", x: rx + (r.dx == null ? 14 : r.dx), y: f.y1 + (r.dy == null ? -14 : r.dy), "text-anchor": r.align === "right" ? "end" : r.align === "middle" ? "middle" : "start" }); rt.style.fontSize = (r.size == null ? 21 : r.size) + "px"; rt.style.fontWeight = r.weight == null ? 500 : r.weight; rt.style.fill = r.labelColor || "#9aa4ae"; svg.appendChild(rt); } } labels.forEach(function (label, i) { var cy = f.y1 + slot * (i + 0.5); var v = values[i]; var lt = createText(label, { "class": "ghchart__axislabel", x: f.x0 - 20, y: cy, "text-anchor": "end" }); lt.setAttribute("dominant-baseline", "central"); if (config.categorySize) lt.style.fontSize = config.categorySize + "px"; if (config.categoryColor) lt.style.fill = config.categoryColor; svg.appendChild(lt); if (v == null) return; var bar = el("rect", { "class": "ghchart__bar", x: f.x0, y: cy - bh / 2, width: Math.max(x(v) - f.x0, 1), height: bh, fill: (config.colors && config.colors[i]) || config.base || "#0969da", rx: config.radius == null ? 4 : config.radius }); bar.style.cursor = "pointer"; var lines = (config.tooltips && config.tooltips[i]) || null; if (lines) { bar.addEventListener("pointermove", function () { tip.show(bar, label, lines); }); bar.addEventListener("pointerleave", tip.hide); } svg.appendChild(bar); var valueLabelX = x(v) + (config.valueLabelOffset == null ? 13 : config.valueLabelOffset); if (config.wilsonIntervals) { var nSource = config.ns && config.ns[i] != null ? config.ns[i] : (config.valueLabels && String(config.valueLabels[i]).match(/n=([0-9,]+)/)); var sampleN = Array.isArray(nSource) ? Number(String(nSource[1]).replace(/,/g, "")) : Number(nSource); if (sampleN > 0) { var z = config.wilsonZ == null ? 1.959964 : config.wilsonZ; var p = Number(v) / 100; var denom = 1 + z * z / sampleN; var center = (p + z * z / (2 * sampleN)) / denom; var half = z / denom * Math.sqrt(p * (1 - p) / sampleN + z * z / (4 * sampleN * sampleN)); var lo = Math.max(xs.min, Math.min(xs.max, (center - half) * 100)); var hi = Math.max(xs.min, Math.min(xs.max, (center + half) * 100)); var xLo = x(lo), xHi = x(hi), cap = bh * 0.31; valueLabelX = xHi + (config.valueLabelOffset == null ? 13 : config.valueLabelOffset); svg.appendChild(el("line", { x1: xLo, x2: xHi, y1: cy, y2: cy, stroke: "#ffffff", "stroke-width": config.intervalHaloWidth == null ? 6 : config.intervalHaloWidth, "stroke-linecap": "round" })); svg.appendChild(el("line", { x1: xLo, x2: xLo, y1: cy - cap, y2: cy + cap, stroke: "#ffffff", "stroke-width": config.intervalHaloWidth == null ? 6 : config.intervalHaloWidth, "stroke-linecap": "round" })); svg.appendChild(el("line", { x1: xHi, x2: xHi, y1: cy - cap, y2: cy + cap, stroke: "#ffffff", "stroke-width": config.intervalHaloWidth == null ? 6 : config.intervalHaloWidth, "stroke-linecap": "round" })); svg.appendChild(el("line", { x1: xLo, x2: xHi, y1: cy, y2: cy, stroke: config.intervalColor || "#24292f", "stroke-width": config.intervalWidth == null ? 2.2 : config.intervalWidth, "stroke-linecap": "round" })); svg.appendChild(el("line", { x1: xLo, x2: xLo, y1: cy - cap, y2: cy + cap, stroke: config.intervalColor || "#24292f", "stroke-width": config.intervalWidth == null ? 2.2 : config.intervalWidth, "stroke-linecap": "round" })); svg.appendChild(el("line", { x1: xHi, x2: xHi, y1: cy - cap, y2: cy + cap, stroke: config.intervalColor || "#24292f", "stroke-width": config.intervalWidth == null ? 2.2 : config.intervalWidth, "stroke-linecap": "round" })); } } if (config.valueLabels) { var vl = createText(config.valueLabels[i], { "class": "ghchart__ann", x: valueLabelX, y: cy }); vl.style.fontSize = (config.valueLabelSize || 21) + "px"; vl.style.fontWeight = 600; vl.style.fill = config.valueLabelColor || "#59636e"; vl.setAttribute("dominant-baseline", "central"); svg.appendChild(vl); } }); decorate(f, config, x, function (i) { return f.y1 + slot * (i + 0.5); }); return closeRoot(root, f, config); } // Square co-occurrence matrix. Cell colour is a sequential ramp derived from // one base hex, extended two steps past the five-level convention so seven // bands fit while the shared levels keep the skill's exact mix ratios. function matrixRamp(baseHex) { var b = hexToRgb(baseHex); return [ rgbToHex(mix(b, WHITE, 0.92)), rgbToHex(mix(b, WHITE, 0.80)), rgbToHex(mix(b, WHITE, 0.52)), rgbToHex(mix(b, WHITE, 0.20)), baseHex, rgbToHex(mix(b, BLACK, 0.30)), rgbToHex(mix(b, BLACK, 0.55)) ]; } function renderMatrix(root, config) { root = openRoot(root, config); if (!root) return root; var f = frame(config, { top: 190, right: 30, bottom: 118, left: 200 }); var svg = f.svg; var labels = config.labels || []; var n = labels.length; var groups = config.groups || []; var groupColor = config.groupColor || {}; var m = config.matrix || []; var breaks = config.breaks || []; var ramp = matrixRamp(config.base || "#3094ff"); var noData = config.noDataColor || "#ebedf0"; var availW = f.x1 - f.x0, availH = f.y0 - f.y1; var cell = Math.min(availW / n, availH / n); var gw = cell * n; var gl = f.x0 + Math.max(0, (availW - gw) / 2); var gt = f.y1; function band(v) { for (var i = 0; i existing.version) { existing.version = VERSION; existing.render = render; existing.primitives = Kit; } global.GHChartKit = global.GHChartKit || Kit; })(typeof globalThis !== "undefined" ? globalThis : this); ;(function () { "use strict"; function enhance(root, config) { if (root.getAttribute("data-ghchart-rendered") === "true") return; var K = window.GHChartEngine.primitives; var quarters = config.quarters; var slots = Array.prototype.slice.call(root.querySelectorAll(".ghpr__slot")); var labels = Array.prototype.slice.call(root.querySelectorAll(".ghpr__x-label")); var guide = root.querySelector(".ghpr__guide"); var readout = root.querySelector(".ghpr__readout"); var readoutQuarter = root.querySelector(".ghpr__readout-quarter"); var readoutVolume = root.querySelector(".ghpr__readout-volume"); var readoutShare = root.querySelector(".ghpr__readout-share"); if (slots.length !== quarters.length || labels.length !== quarters.length || !guide || !readout || !readoutQuarter || !readoutVolume || !readoutShare) { throw new Error("The bar-and-line chart requires matching chart.html, chart.css, and chart.js."); } function formatCount(value) { return K.fmtCompact(Math.round(value / 1000000) * 1000000); } function updateReadout(q) { readoutQuarter.textContent = q.label; readoutVolume.textContent = formatCount(q.pushes); readoutShare.textContent = (100 * q.finds / q.pushes).toFixed(2) + "%"; } root.style.setProperty("--ghpr-volume-ink", config.volumeBase); root.style.setProperty("--ghpr-volume-text", K.strongOf(config.volumeBase)); root.style.setProperty("--ghpr-share-ink", config.rateBase); root.style.setProperty("--ghpr-share-text", K.strongOf(config.rateBase)); root.style.setProperty("--ghpr-bar-width", config.barWidth); readout.setAttribute("role", "status"); readout.setAttribute("aria-live", "polite"); readout.setAttribute("aria-atomic", "true"); function reset() { slots.forEach(function (slot) { slot.setAttribute("data-active", "false"); }); labels.forEach(function (label) { label.setAttribute("data-active", "false"); }); root.setAttribute("data-ghpr-active", "false"); updateReadout(quarters[quarters.length - 1]); readout.setAttribute("data-active", "false"); } function activate(i) { var q = quarters[i]; slots.forEach(function (slot, index) { slot.setAttribute("data-active", index === i ? "true" : "false"); }); labels.forEach(function (label, index) { label.setAttribute("data-active", index === i ? "true" : "false"); }); root.style.setProperty("--ghpr-active-position", (i + 0.5) / quarters.length * 100 + "%"); root.setAttribute("data-ghpr-active", "true"); updateReadout(q); readout.setAttribute("data-active", "true"); } slots.forEach(function (slot, i) { var q = quarters[i]; slot.setAttribute("role", "button"); slot.setAttribute("tabindex", "0"); slot.setAttribute("aria-label", q.label + ": " + formatCount(q.pushes) + " public pushes screened; " + (100 * q.finds / q.pushes).toFixed(2) + " percent push prevalence, meaning pushes containing a detected provider secret. Arrow keys move between quarters."); slot.addEventListener("pointerenter", function () { activate(i); }); slot.addEventListener("pointerdown", function () { slot.focus(); activate(i); }); slot.addEventListener("pointerleave", function () { if (document.activeElement !== slot) reset(); }); slot.addEventListener("focus", function () { activate(i); }); slot.addEventListener("blur", reset); slot.addEventListener("keydown", function (event) { var next; if (event.key === "ArrowRight") next = Math.min(slots.length - 1, i + 1); else if (event.key === "ArrowLeft") next = Math.max(0, i - 1); else if (event.key === "Home") next = 0; else if (event.key === "End") next = slots.length - 1; else if (event.key === "Escape") { reset(); return; } else if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(i); return; } else return; event.preventDefault(); slots[next].focus(); }); }); root.setAttribute("data-ghchart-rendered", "true"); } var CONFIG = { "title": "More pushes, no clear rise in push prevalence", "subtitle": "2024 Q2\u20132026 Q2", "ariaLabel": "Nine quarters, 2024 Q2 through 2026 Q2. Equal-width green bars show screened public pushes on the left axis, in millions. The blue line and quarterly points show push prevalence on the right axis, in percent: the share of screened public pushes containing a detected provider secret. The axes have different units; compare each series over time, not bar/line crossings. Public push volume grew 2.8 times, while the quarterly share varied from 0.47 to 0.71 percent without a clear upward trend. Supported provider patterns include GitHub's own tokens. Blocked and bypassed attempts are included.", "volumeBase": "#08872b", "rateBase": "#0377ff", "quarters": [ { "label": "2024 Q2", "pushes": 202241213, "finds": 1046224 }, { "label": "2024 Q3", "pushes": 195041780, "finds": 1056582 }, { "label": "2024 Q4", "pushes": 223196423, "finds": 1107237 }, { "label": "2025 Q1", "pushes": 247866195, "finds": 1292931 }, { "label": "2025 Q2", "pushes": 258457360, "finds": 1830765 }, { "label": "2025 Q3", "pushes": 304013079, "finds": 1933975 }, { "label": "2025 Q4", "pushes": 347602577, "finds": 2228616 }, { "label": "2026 Q1", "pushes": 428519511, "finds": 2626324 }, { "label": "2026 Q2", "pushes": 574206558, "finds": 2712225 } ], "barWidth": 0.6, "volumeY": { "min": 0, "max": 600000000, "ticks": [ 0, 300000000, 600000000 ] }, "rateY": { "min": 0, "max": 1, "ticks": [ 0, 0.5, 1 ] }, "scope": { "traffic": "Public push traffic", "patterns": "Supported provider patterns, including GitHub's own tokens", "source": "Blog author" }, "analysis": { "model": "Exploratory OLS on quarterly rate percentages versus quarter index", "quarters": 9, "slopePerYear": 0.029215870393486706, "ciLowPerYear": -0.07200002336113129, "ciHighPerYear": 0.1304317641481047, "pValue": 0.5168367091504764, "rSquared": 0.062399929619299986 }, "captionHtml": " Public pushes, Q2 2024\u2013Q2 2026. Push prevalence is the share with a detected secret. Covers supported provider patterns, including GitHub\u2019s own tokens. " }; document.querySelectorAll("[data-ghc-push-leak]").forEach(function (root, i) { if (root.getAttribute("data-ghchart-rendered") === "true") return; var id = "gh-chart-push-volume-leak-rate-" + i; while (document.getElementById(id) && document.getElementById(id) !== root) { i += 1; id = "gh-chart-push-volume-leak-rate-" + i; } root.id = id; enhance(root, CONFIG); }); })(); .ghchart { /* Theme tokens */ --ghc-text: #1f2328; --ghc-muted: #656d76; --ghc-grid: #d0d7de; --ghc-axis: #59636e; --ghc-card-bg: #ffffff; --ghc-card-border: #d0d7de; --ghc-divider: #b6bfb8; --ghc-guide: #afb8c1; /* GitHub brand font — matches the github.blog stack (Mona Sans, with the blog's metric fallback) so it renders identically in-context. */ font-family: "Mona Sans", "Mona Sans Fallback", -apple-system, "system-ui", "Segoe UI", Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; position: relative; box-sizing: border-box; width: 100%; max-width: 699px; margin: 0 auto; color: var(--ghc-text); line-height: 1.4; } .ghchart *, .ghchart *::before, .ghchart *::after { box-sizing: border-box; } /* ------------------------------------------------------------------ */ /* Header + legend */ /* ------------------------------------------------------------------ */ .ghchart__header { display: flex; justify-content: flex-end; align-items: center; min-height: 24px; margin-bottom: 4px; } .ghchart__legend { display: flex; align-items: center; gap: 20px; } .ghchart__legend-item { display: inline-flex; align-items: center; gap: 8px; font-size: clamp(14px, 1.9vw, 17px); font-weight: 600; color: var(--ghc-muted); white-space: nowrap; } .ghchart__swatch { width: 14px; height: 14px; border-radius: 4px; flex: 0 0 auto; } /* ------------------------------------------------------------------ */ /* Plot (SVG) */ /* ------------------------------------------------------------------ */ .ghchart__plot { display: block; width: 100%; height: auto; overflow: visible; touch-action: pan-y; } .ghchart__gridline { stroke: var(--ghc-grid); stroke-width: 1; stroke-dasharray: 2 6; stroke-linecap: round; opacity: 0.9; } .ghchart__axislabel { fill: var(--ghc-axis); font-size: 22px; font-weight: 500; } .ghchart__axislabel--x { font-weight: 600; } .ghchart__line { fill: none; stroke-linecap: round; stroke-linejoin: round; } .ghchart__dot { transition: r 0.12s ease; } .ghchart__guide { stroke: var(--ghc-guide); stroke-width: 1.5; stroke-dasharray: 4 4; opacity: 0; transition: opacity 0.12s ease; } .ghchart__hit { fill: transparent; } /* ------------------------------------------------------------------ */ /* Title + divider */ /* ------------------------------------------------------------------ */ .ghchart__title { /* Matches the github.blog H2 (.wp-block-heading) exactly */ display: block; font-size: 32px; line-height: 40px; font-weight: 700; letter-spacing: -0.18px; color: var(--ghc-text); } .ghchart__rule { height: 1.5px; border-radius: 1.5px; background: var(--ghc-divider); margin: 10px 0 18px; } /* ------------------------------------------------------------------ */ /* Bar chart: subtitle, panels, bars, caption */ /* ------------------------------------------------------------------ */ .ghchart__subtitle { margin: -6px 0 20px; font-size: clamp(13px, 1.6vw, 15px); line-height: 1.5; color: var(--ghc-muted); } .ghchart__panel { margin-top: 30px; } .ghchart__panel:first-of-type { margin-top: 6px; } .ghchart__panel-title { margin: 0 0 6px; font-size: clamp(15px, 1.9vw, 18px); font-weight: 700; letter-spacing: -0.01em; color: var(--ghc-text); } .ghchart__plot--bar { touch-action: pan-y; } .ghchart__axisline { stroke: var(--ghc-axis); stroke-width: 1.25; opacity: 0.35; } .ghchart__bar { transition: opacity 0.12s ease; } .ghchart__barhit { fill: transparent; } /* Always-on value labels above selected bars. */ .ghchart__barvalue { fill: var(--ghc-text); font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; pointer-events: none; } /* Anchor annotation: dotted reference line + floating pill. */ .ghchart__anno-line { stroke: #8b949e; stroke-width: 1.5; stroke-dasharray: 4 3; stroke-linecap: round; pointer-events: none; } .ghchart__anno-chip { fill: #ffebe9; stroke: #ffcecb; stroke-width: 1; pointer-events: none; } .ghchart__anno-chip-text { fill: #a0111f; font-weight: 700; pointer-events: none; } .ghchart__caption { margin-top: 22px; padding-top: 12px; border-top: 1px solid var(--ghc-card-border); font-size: 12.5px; line-height: 1.55; color: var(--ghc-muted); } .ghchart__caption b { color: var(--ghc-text); font-weight: 700; } .ghchart__caption code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 0.92em; padding: 1px 5px; background: #f6f8fa; border: 1px solid var(--ghc-card-border); border-radius: 5px; } .ghchart__tag { display: inline-block; font-size: 10px; font-weight: 650; letter-spacing: 0.03em; text-transform: uppercase; padding: 2px 7px; border-radius: 999px; border: 1px solid #aceebb; color: #0a5223; background: #dafbe1; white-space: nowrap; vertical-align: middle; } /* ------------------------------------------------------------------ */ /* Calendar heatmap (GitHub contribution-graph style) */ /* ------------------------------------------------------------------ */ .ghheat__eyebrow { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; } .ghheat__eyedot { width: 9px; height: 9px; border-radius: 50%; background: #cf222e; box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.14); flex: 0 0 auto; } .ghheat__eyetext { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #cf222e; } /* Subtitle here is a prominent lede, not the muted variant. */ .ghheat__subtitle { color: var(--ghc-text); font-size: clamp(14px, 1.7vw, 16px); margin: 2px 0 8px; } .ghheat__subtitle b { font-weight: 700; } .ghheat__panel { margin-top: 36px; } .ghheat__phead { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; margin-bottom: 16px; } .ghheat__badge { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; color: #fff; font-size: 12px; font-weight: 700; flex: 0 0 auto; align-self: center; } .ghheat__ptitle { font-size: clamp(16px, 2vw, 19px); font-weight: 700; letter-spacing: -0.01em; color: var(--ghc-text); } .ghheat__pdesc { font-size: 14px; color: var(--ghc-muted); } .ghheat__body { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; } .ghheat__gridwrap { flex: 1 1 340px; min-width: 260px; } .ghheat__grid { display: block; width: 100%; height: auto; overflow: visible; } .ghheat__cell { stroke: rgba(27, 31, 36, 0.06); stroke-width: 1; transition: stroke 0.1s ease; } .ghheat__cell:hover { stroke: #24292f; stroke-width: 1.4; } .ghheat__cell--pad { opacity: 0; pointer-events: none; } .ghheat__wd, .ghheat__month { fill: var(--ghc-muted); font-size: 11px; font-weight: 600; } .ghheat__side { flex: 1 1 250px; min-width: 230px; } .ghheat__stat { display: flex; align-items: baseline; gap: 7px; } .ghheat__statbig { font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em; line-height: 1; } .ghheat__statsmall { font-size: 15px; font-weight: 600; color: var(--ghc-muted); } .ghheat__note { margin: 10px 0 16px; font-size: 14px; line-height: 1.55; color: var(--ghc-muted); } .ghheat__note b { color: var(--ghc-text); font-weight: 700; } .ghheat__legend { display: flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--ghc-muted); flex-wrap: wrap; } .ghheat__sw { width: 12px; height: 12px; border-radius: 3px; display: inline-block; flex: 0 0 auto; } .ghheat__sw--empty { border: 1px solid #d0d7de; } .ghheat__legunit { margin-left: 2px; } .ghheat__extra { display: flex; align-items: flex-start; gap: 7px; margin-top: 14px; font-size: 12px; line-height: 1.5; color: var(--ghc-muted); max-width: 330px; } .ghheat__extra .ghheat__sw { margin-top: 2px; } /* ------------------------------------------------------------------ */ /* Tooltip */ /* ------------------------------------------------------------------ */ .ghchart__tooltip { position: absolute; z-index: 5; top: 0; left: 0; min-width: 128px; max-width: 260px; padding: 10px 12px; background: var(--ghc-card-bg); border: 1px solid var(--ghc-card-border); border-radius: 12px; /* Layered drop shadow from Primer Brand's ActionMenu (.ActionMenu__menu) */ box-shadow: 0px 100px 80px rgba(0, 0, 0, 0.01), 0px 41px 33px rgba(0, 0, 0, 0.02), 0px 22px 17px rgba(0, 0, 0, 0.02), 0px 12px 10px rgba(0, 0, 0, 0.03), 0px 6px 5px rgba(0, 0, 0, 0.04), 0px 2px 2px rgba(0, 0, 0, 0.07); pointer-events: none; opacity: 0; transform: translate(-50%, -100%); /* Fade in on show and out on hide; quickly ease left/top when tracking from one point to the next. The position ease is suppressed on the first appearance (see chart.js) so the tooltip fades in place rather than sliding in from its previous spot. */ transition: opacity 0.12s ease, left 0.1s cubic-bezier(0.4, 0, 0.2, 1), top 0.1s cubic-bezier(0.4, 0, 0.2, 1); will-change: opacity, transform, left, top; } .ghchart__tooltip[data-visible="true"] { opacity: 1; } .ghchart__tt-title { font-size: 13px; font-weight: 700; color: var(--ghc-text); margin-bottom: 6px; } .ghchart__tt-row { display: flex; align-items: center; gap: 8px; font-size: 13px; line-height: 1.6; color: var(--ghc-muted); } .ghchart__tt-swatch { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; } .ghchart__tt-name { flex: 1 1 auto; } .ghchart__tt-value { font-weight: 700; color: var(--ghc-text); font-variant-numeric: tabular-nums; } /* Bar tooltip body: a title line + free-form text lines (no swatches). */ .ghchart__tt-line { font-size: 13px; line-height: 1.5; color: var(--ghc-muted); } .ghchart__tt-line:first-child { color: var(--ghc-text); font-weight: 600; } .ghchart__tt-line + .ghchart__tt-line { margin-top: 2px; } @media (prefers-reduced-motion: reduce) { .ghchart__dot, .ghchart__guide, .ghchart__bar, .ghheat__cell, .ghchart__tooltip { transition: none; } } /* ------------------------------------------------------------------ */ /* Co-occurrence matrix */ /* ------------------------------------------------------------------ */ .ghchart__matrixrule { stroke: #aeb6bf; stroke-width: 2.4; } .ghchart__matrixframe { stroke: var(--ghc-card-border); stroke-width: 1.8; } .ghchart__matrixswatch { stroke: var(--ghc-card-border); stroke-width: 1.4; } [data-ghc-push-leak] { --ghpr-column-gap: 6px; --ghpr-frame-inset: 16px; --ghpr-plot-left: calc(42px + var(--ghpr-frame-inset)); --ghpr-plot-right: calc(36px + var(--ghpr-frame-inset)); --ghpr-border: #e4ebe6; --ghc-text: #000000; --ghc-muted: #58635b; --ghc-axis: #58635b; --ghc-grid: #e4ebe6; --ghc-divider: #b6bfb8; font-family: "Mona Sans", "MonaSansFallback", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; padding: 0; border: 0; border-radius: 0; background: var(--ghc-card-bg); box-shadow: none; overflow: hidden; } [data-ghc-push-leak] .ghpr__header { padding: 0 0 14px; border-bottom: 0; } [data-ghc-push-leak] .ghchart__title { display: block; max-width: 620px; margin-bottom: 0; font-size: 20px; line-height: 26px; font-weight: 600; letter-spacing: -0.01em; text-wrap: balance; } [data-ghc-push-leak] .ghpr__body { padding: 0; } [data-ghc-push-leak] .ghpr__legend { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 8px; } [data-ghc-push-leak] .ghpr__legend-item:last-child { text-align: right; } [data-ghc-push-leak] .ghpr__legend-item:last-child .ghpr__legend-name { justify-content: flex-end; } [data-ghc-push-leak] .ghpr__legend-name { display: flex; height: 18px; align-items: center; gap: 6px; font-size: 13px; line-height: 18px; font-weight: 600; } [data-ghc-push-leak] .ghpr__swatch { display: inline-block; flex: 0 0 auto; } [data-ghc-push-leak] .ghpr__swatch--volume { width: 10px; height: 12px; border-radius: 1px 1px 0 0; background: var(--ghpr-volume-ink); } [data-ghc-push-leak] .ghpr__swatch--prevalence { position: relative; width: 18px; height: 2px; background: var(--ghpr-share-ink); } [data-ghc-push-leak] .ghpr__swatch--prevalence::after { content: ""; position: absolute; left: 6px; top: -2px; width: 6px; height: 6px; border-radius: 50%; background: var(--ghpr-share-ink); } [data-ghc-push-leak] .ghpr__frame { position: relative; box-sizing: border-box; height: 260px; border: 1px solid var(--ghpr-border); border-radius: 8px; } [data-ghc-push-leak] .ghpr__plot { position: absolute; left: var(--ghpr-plot-left); right: var(--ghpr-plot-right); top: 24px; bottom: 56px; } [data-ghc-push-leak] .ghpr__grid, [data-ghc-push-leak] .ghpr__columns { position: absolute; inset: 0; } [data-ghc-push-leak] .ghpr__grid { pointer-events: none; } [data-ghc-push-leak] .ghpr__gridline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--ghc-grid); opacity: 1; } [data-ghc-push-leak] .ghpr__gridline--baseline { border-top-co