智能体
github.blog ·
@Brian Celenza
GitHub 为 agent 规模开发重建 Git 基础设施
GitHub 官方博客介绍其正在不停机重建 Git 基础设施,以支撑 agent 规模的软件开发:开发者与 AI 代理在日均数百万次提交的仓库中并发工作,对 Git 架构提出新要求。文章给出设计原则与 2026 年 8 月仓库活跃度分布,但正文片段夹杂大量样式代码,关键性能数据不完整。
AI推荐理由 GitHub 官方博客介绍为 agent 规模开发重建 Git 基础设施,明确提到开发者与代理在日均百万级提交的仓库中并发工作,属于编码代理规模化对底层架构影响的一手工程信息;正文片段含架构设计原则与 2026 年 8 月仓库活跃度分布,但被大量 CSS 代码干扰,具体性能数据未完整呈现。
78
正文
Every day on GitHub, millions of developers build the products their customers rely on, contribute to open source, and pursue personal projects. GitHub’s architecture has changed steadily over the years to support that work and the growing demands of the developers and organizations who depend on it. Agentic software development is driving the next architectural shift. With developers and agents working concurrently in repositories that receive millions of commits a day, these workloads demand a different Git architecture. We’re rebuilding GitHub’s Git infrastructure to support them. This post explores the demands shaping that work and the design principles behind it. Today’s highest-volume workloads show the scale we’re building for. The gap between a typical repository and the busiest ones is wider than most people expect. Here’s a rough picture of the monthly repository activity distribution on GitHub from August 2026: after summary card. */ .ghchart__tl-stat { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px 24px; padding: 16px 22px; border: 1px solid var(--ghc-card-border); border-radius: 12px; /* Greenish grey to match the AI gateway diagram canvas (--ghd-canvas). */ background: #f2f5f3; margin: 0 0 18px; } .ghchart__tl-stat-main { display: flex; align-items: center; gap: 20px; } .ghchart__tl-stat-col { display: flex; flex-direction: column; gap: 2px; } .ghchart__tl-stat-k { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ghc-muted); } .ghchart__tl-stat-v { font-size: clamp(28px, 4.4vw, 40px); font-weight: 800; letter-spacing: -0.02em; line-height: 1; color: var(--ghc-text); } .ghchart__tl-stat-v--muted { color: var(--ghc-muted); font-weight: 700; } .ghchart__tl-stat-arrow { font-size: 26px; color: var(--ghc-muted); } .ghchart__tl-stat-delta { text-align: right; } .ghchart__tl-stat-delta-v { font-size: clamp(26px, 4vw, 38px); font-weight: 800; letter-spacing: -0.02em; line-height: 1; color: #1a7f37; } .ghchart__tl-stat-delta-k { font-size: 13px; color: var(--ghc-muted); margin-top: 4px; } /* Stage legend (2-up, monospace-free, stacks on mobile). */ .ghchart__tl-legend { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 28px; margin: 0 0 20px; } .ghchart__tl-legend-item { display: inline-flex; align-items: center; gap: 10px; min-width: 0; } .ghchart__tl-swatch { width: 13px; height: 13px; border-radius: 3px; flex: 0 0 auto; } .ghchart__tl-legend-text { font-size: clamp(13px, 1.7vw, 15px); color: var(--ghc-text); font-variant-numeric: tabular-nums; } .ghchart__tl-rule { height: 1.5px; border-radius: 1.5px; background: var(--ghc-divider); margin: 4px 0 18px; } /* SVG plot text. */ .ghchart__tl-rowlabel { fill: var(--ghc-text); font-size: 22px; font-weight: 700; letter-spacing: 0.02em; } .ghchart__tl-rowsub { fill: var(--ghc-muted); font-size: 18px; font-weight: 500; } .ghchart__tl-seg-label { fill: #ffffff; font-size: 23px; font-weight: 700; letter-spacing: -0.01em; } .ghchart__tl-callout-line { stroke: var(--ghc-guide); stroke-width: 1.5; fill: none; } .ghchart__tl-callout-label { font-size: 21px; font-weight: 600; letter-spacing: -0.01em; } /* Segments + ribbons ease their opacity for the before/after hover emphasis. */ .ghchart__tl-seg { transition: opacity 0.14s ease; } .ghchart__tl-seg[data-stage] { cursor: pointer; } .ghchart__tl-seg[data-stage]:focus-visible { outline: 2px solid var(--ghc-axis); outline-offset: 1px; } .ghchart__tl-flow { opacity: 0.16; transition: opacity 0.14s ease; } /* Timeline millisecond gridlines: thicker + larger dashes so the axis marks read clearly behind the bars (scoped so scatter gridlines are unaffected). */ .ghchart__plot--timeline .ghchart__gridline--v { stroke-width: 1.5; stroke-dasharray: 5 5; opacity: 1; } /* Timeline bracket labels read larger than the trend bracket note (they are structural spans, not a small secondary annotation). */ .ghchart__tl-bracket-label { font-size: 20px; } /* Wrapper so DOM labels can sit over the SVG at viewBox-proportional %s. */ .ghchart__tl-plotwrap { position: relative; } /* Right-bracket label as a DOM node: a fixed, readable size that does NOT scale down with the plot on small windows (unlike SVG text). */ .ghchart__tl-bracket-label-dom { position: absolute; transform: translateY(-50%); font-size: clamp(12px, 1.5vw, 14px); font-weight: 700; letter-spacing: 0.02em; color: var(--ghc-muted); white-space: nowrap; pointer-events: none; } .ghchart__tl-tick { fill: var(--ghc-axis); } /* Axis title rendered as a DOM caption below the plot; matches the legend labels' size/weight/color instead of scaling with the SVG. */ .ghchart__tl-axis-caption { font-size: clamp(13px, 1.7vw, 15px); font-weight: 400; color: var(--ghc-text); text-align: center; margin-top: 8px; } @media (max-width: 560px) { .ghchart__tl-legend { grid-template-columns: 1fr; } .ghchart__tl-stat { justify-content: flex-start; } .ghchart__tl-stat-delta { text-align: left; } } /* ------------------------------------------------------------------ */ /* 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); /* 24px top gap matches the github.blog H2 → paragraph block spacing. */ margin: 24px 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; /* `width: max-content` keeps the box sized to its own content instead of the browser's absolute-position "shrink-to-fit" formula, which derives available width from how far `left` sits from the containing block's edge — near the right edge that shrinks toward zero and forces the row text to wrap awkwardly even though the box is later clamped into view. max-content is unaffected by `left`, so wrapping only happens if content truly exceeds max-width. */ width: max-content; 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; } /* Optional "partial data" note, shown only for a hovered point past a line's config.partial.fromIndex (see .ghchart__line--dashed). */ .ghchart__tt-note { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--ghc-card-border); font-size: 12px; font-style: italic; color: var(--ghc-muted); } /* 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; } /* Timeline before/after tooltip: only the title is bold; both value lines stay regular weight and the same color (no emphasised first line). */ .ghchart__tt-body--plain .ghchart__tt-line, .ghchart__tt-body--plain .ghchart__tt-line:first-child { font-weight: 400; color: var(--ghc-text); } @media (prefers-reduced-motion: reduce) { .ghchart__dot, .ghchart__guide, .ghchart__bar, .ghchart__pt, .ghchart__square, .ghheat__cell, .ghchart__tl-seg, .ghchart__tl-flow, .ghchart__tooltip { transition: none; } } /* Shared chart engine — reusable renderer for the blog-charts library. ------------------------------------------------------------------ Exposes window.GHChartEngine = { renderers, render(config, mountEl?), destroy(mountEl) }. Renderers are keyed by config.type (currently "line"); add a new viz type by registering GHChartEngine.renderers[] = function (root, config) {…}. This same file is embedded verbatim at the top of each exported WordPress chart.js, followed by that chart's CONFIG + a small auto-boot. It has no external dependencies and is safe to include more than once on a page. */ (function (global) { "use strict"; var SVGNS = "http://www.w3.org/2000/svg"; function el(tag, attrs) { var node = document.createElementNS(SVGNS, tag); if (attrs) { for (var k in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, k)) { node.setAttribute(k, attrs[k]); } } } return node; } function html(tag, cls) { var node = document.createElement(tag); if (cls) node.className = cls; return node; } function clamp(v, lo, hi) { return v hi ? hi : v; } 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 = 4.5) return "#1f2328"; return 1.05 / (background + 0.05) >= 4.5 ? "#ffffff" : "#000000"; } var renderers = {}; var renderCleanups = (global.GHChartEngine && global.GHChartEngine._renderCleanups) || new WeakMap(); renderers.comparison = function (root, config) { var axis = config.xAxis, series = config.series, rows = config.rows; if (!axis || !Number.isFinite(axis.min) || !Number.isFinite(axis.max) || axis.min > 0 || axis.max axis.max; }) || rows.some(function (row) { return !Array.isArray(row.values) || row.values.length !== series.length || row.values.some(function (v) { return !Number.isFinite(v) || v axis.max; }) || !Array.isArray(row.decimals) || row.decimals.length !== series.length || row.decimals.some(function (n) { return !Number.isInteger(n) || n 10; }); })) throw new Error("Comparison chart requires a zero-inclusive axis, hex series colors, and matching finite values/decimal precision for each series."); root.textContent = ""; root.setAttribute("role", "figure"); root.setAttribute("aria-label", config.title); var heading = html("div", "ghchart__headline"); heading.textContent = config.title; root.appendChild(heading); if (config.subtitle) { var subtitle = html("p", "ghchart__subtitle"); subtitle.textContent = config.subtitle; root.appendChild(subtitle); } var legend = html("div", "ghchart__legend ghchart__legend--trend"); series.forEach(function (s) { var item = html("span", "ghchart__legend-item"); var swatch = html("span", "ghchart__swatch"); swatch.style.background = s.color; item.appendChild(swatch); item.appendChild(document.createTextNode(s.name)); legend.appendChild(item); }); root.appendChild(legend); var marks = []; function at(value) { return (value - axis.min) / (axis.max - axis.min) * 100; } function signed(value, decimals) { return (value > 0 ? "+" : "") + value.toFixed(decimals) + "%"; } rows.forEach(function (row) { var group = html("div", "ghchart__comparison-row"); group.setAttribute("role", "group"); group.setAttribute("aria-label", row.label); var label = html("div", "ghchart__comparison-label"); label.textContent = row.label; label.setAttribute("aria-hidden", "true"); group.appendChild(label); var plot = html("div", "ghchart__comparison-plot"); axis.ticks.forEach(function (value) { var line = html("span", "ghchart__comparison-grid"); line.style.left = at(value) + "%"; line.setAttribute("aria-hidden", "true"); plot.appendChild(line); }); var zero = html("span", "ghchart__comparison-grid ghchart__comparison-zero"); zero.style.left = at(0) + "%"; zero.setAttribute("aria-hidden", "true"); plot.appendChild(zero); series.forEach(function (s, index) { var value = row.values[index], formatted = signed(value, row.decimals[index]); var track = html("div", "ghchart__comparison-track"); track.setAttribute("role", "group"); track.setAttribute("aria-label", s.name); var bar = html("span", "ghchart__comparison-bar"); // Anchor the zero-facing edge, without fractional transform snapping. if (value = textWidth + 8) { positionLabel(mark, negative ? "right" : "left", mark.value, "outside"); } else if (Math.abs(end - zero) >= textWidth + 16) { positionLabel(mark, negative ? "left" : "right", mark.value, "inside"); } else if (opposite >= textWidth + 8) { positionLabel(mark, negative ? "left" : "right", 0, "opposite"); } else { // Very narrow plots or long precision strings get a separate label line. mark.track.classList.add("ghchart__comparison-track--stacked"); mark.label.setAttribute("data-placement", "stacked"); var top = mark.label.offsetHeight + 6; mark.bar.style.top = top + "px"; mark.track.style.height = top + 28 + "px"; } }); } function schedule() { if (!active) return; cancelAnimationFrame(frame); frame = requestAnimationFrame(layoutValues); } layoutValues(); if (global.ResizeObserver) { observer = new ResizeObserver(schedule); observer.observe(root); } else global.addEventListener("resize", schedule); if (document.fonts) { document.fonts.ready.then(schedule); document.fonts.addEventListener("loadingdone", schedule); } return function () { active = false; cancelAnimationFrame(frame); if (observer) observer.disconnect(); global.removeEventListener("resize", schedule); if (document.fonts) document.fonts.removeEventListener("loadingdone", schedule); }; }; /* ================================================================ Line chart renderer ================================================================ */ renderers.line = function (root, config) { // viewBox geometry (design units; scales fluidly to any width). var VB = { w: 1000, h: 560 }; var M = { top: 30, right: 34, bottom: 56, left: 86 }; // Inset the plotted points slightly from the axis so the first/last // months (e.g. "Jan") don't crowd the y-axis labels (e.g. "50"). var PAD_X = 18; // Reset (in case of re-render) and use a unique id prefix so multiple // charts on one page never share gradient ids. root.textContent = ""; var uid = config.mountId; var n = config.labels.length; var yMin = config.yAxis.min; var yMax = config.yAxis.max; var step = config.yAxis.step; // Optional log y (yAxis.scale: "log") for data spanning many decades; // ticks are then explicit (yAxis.ticks), e.g. 1, 10, 100 … 1B. var isLog = config.yAxis.scale === "log"; var yTicks = config.yAxis.ticks; if (!yTicks) { yTicks = []; for (var tv0 = yMin; tv0 we can't measure before layout, so estimate from the // glyph mix at the 22-unit axis font size. var AXIS_FONT = 22; var Y_LABEL_GAP = 22; function estimateTextW(s) { var w = 0; for (var i = 0; i cubic Bezier) path builder. // config.curve: "linear" draws straight segments — for discrete // categories (e.g. percentiles) where a curve would imply values // between the points. function smooth(points) { if (!points.length) return ""; if (points.length 0 && segLen / PERIOD partialIdx) ? "" : "none"; } // Anchor above the highest point in this column. var topSi = 0, topY = Infinity; config.series.forEach(function (s, si) { var yy = yAt(s.values[i]); if (yy { title, lines:[…] } } No printed value labels — values surface on hover (consistent with the line chart). Bars use a single tunable color with rounded tops. ================================================================ */ renderers.bar = function (root, config) { root.textContent = ""; var barColor = (config.series && config.series[0] && config.series[0].color) || "#ff7b72"; var barRadius = config.barRadius != null ? config.barRadius : 8; // Per-bar fill: a bar may point at a different series (e.g. a muted // "preliminary" color) via panel.barSeries[index]; otherwise series[0]. function barColorFor(panel, j) { var si = (panel.barSeries && panel.barSeries[j] != null) ? panel.barSeries[j] : 0; var s = config.series && config.series[si]; return (s && s.color) || barColor; } var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null; function reduceMotion() { return !!(motionMQ && motionMQ.matches); } /* ---- Title + divider (+ optional subtitle) ------------------ */ var title = html("span", "ghchart__title"); title.textContent = config.title; root.appendChild(title); root.appendChild(html("div", "ghchart__rule")); if (config.subtitle) { var sub = html("p", "ghchart__subtitle"); sub.textContent = config.subtitle; root.appendChild(sub); } /* ---- One shared tooltip for all panels ---------------------- */ var tip = html("div", "ghchart__tooltip"); tip.setAttribute("role", "status"); var tipTitle = html("div", "ghchart__tt-title"); var tipBody = html("div", "ghchart__tt-body"); tip.appendChild(tipTitle); tip.appendChild(tipBody); root.appendChild(tip); var allBars = []; // { el, pi, bi } var activeKey = null; // "pi:bi" of the hovered bar function hideTip() { activeKey = null; tip.setAttribute("data-visible", "false"); allBars.forEach(function (b) { b.el.style.opacity = "1"; }); } // Hoisted: referenced by the per-column pointer handlers below. function showTip(pi, bi, panel, panelBars) { var reappearing = activeKey === null; activeKey = pi + ":" + bi; allBars.forEach(function (b) { b.el.style.opacity = (b.pi === pi && b.bi === bi) ? "1" : "0.42"; }); var content = panel.tooltip ? panel.tooltip(bi, panel) : { title: (panel.labels || [])[bi], lines: [ (panel.formatValue || String)(panel.values[bi]) ] }; tipTitle.textContent = content.title != null ? content.title : ""; tipBody.textContent = ""; (content.lines || []).forEach(function (ln) { var row = html("div", "ghchart__tt-line"); row.textContent = ln; tipBody.appendChild(row); }); var anchor = panelBars[bi].el.getBoundingClientRect(); var rootRect = root.getBoundingClientRect(); var left = anchor.left + anchor.width / 2 - rootRect.left; var top = anchor.top - rootRect.top - 12; var suppress = reappearing && !reduceMotion(); 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 0) { for (var v = yMin; v dark ramp is derived from a single tunable base color. ================================================================ */ renderers.heatmap = function (root, config) { root.textContent = ""; function trim0(s) { return s.replace(/\.0$/, ""); } function fmtM(v) { if (v >= 1000) { var g = v / 1000; return (g >= 10 ? Math.round(g) : trim0(g.toFixed(1))) + "B"; } if (v >= 1) return trim0(v.toFixed(1)) + "M"; return Math.round(v * 1000) + "K"; } var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; var MON_FULL = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var WD = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; var year = config.year || 2026; var startM = config.startMonth != null ? config.startMonth : 0; var endM = config.endMonth != null ? config.endMonth : 5; var emptyColor = config.emptyColor || "#ebedf0"; function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); } function parseISO(s) { var p = s.split("-"); return new Date(+p[0], +p[1] - 1, +p[2]); } function dayStart(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); } /* ---- Eyebrow + title + subtitle ----------------------------- */ if (config.eyebrow) { var eb = html("div", "ghheat__eyebrow"); eb.appendChild(html("span", "ghheat__eyedot")); var ebt = html("span", "ghheat__eyetext"); ebt.textContent = config.eyebrow; eb.appendChild(ebt); root.appendChild(eb); } var title = html("span", "ghchart__title"); title.textContent = config.title; root.appendChild(title); if (config.subtitle) { var sub = html("p", "ghchart__subtitle ghheat__subtitle"); sub.innerHTML = config.subtitle; root.appendChild(sub); } /* ---- One shared tooltip -------------------------------------- */ var tip = html("div", "ghchart__tooltip"); tip.setAttribute("role", "status"); var tipTitle = html("div", "ghchart__tt-title"); var tipBody = html("div", "ghchart__tt-body"); tip.appendChild(tipTitle); tip.appendChild(tipBody); root.appendChild(tip); var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null; function reduceMotion() { return !!(motionMQ && motionMQ.matches); } var activeCell = null; function hideTip() { activeCell = null; tip.setAttribute("data-visible", "false"); } function showTip(rectEl, titleText, lines) { var reappearing = activeCell === null; activeCell = rectEl; tipTitle.textContent = titleText; tipBody.textContent = ""; lines.forEach(function (ln) { var r = html("div", "ghchart__tt-line"); r.innerHTML = ln; tipBody.appendChild(r); }); var a = rectEl.getBoundingClientRect(), rr = root.getBoundingClientRect(); var left = a.left + a.width / 2 - rr.left, top = a.top - rr.top - 12; var suppress = reappearing && !reduceMotion(); 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 = startM && m = afS && ds = 0 ? ramp[lvl] : emptyColor }); if (inRange) { (function (dt, mm, has) { var dateStr = WD[dt.getDay()] + " \u00b7 " + MON[mm] + " " + dt.getDate() + ", " + year; var lines; if (has) { var daily = totals[mm] / daysInMonth(year, mm); lines = [ "" + fmtM(daily) + " " + panel.legendLabel, MON_FULL[mm] + " \u00b7 " + fmtM(totals[mm]) + " " + panel.unitTotal ]; } else { lines = [panel.emptyNote || "No vetted data"]; } cellEl.addEventListener("pointermove", function () { showTip(cellEl, dateStr, lines); }); cellEl.addEventListener("pointerdown", function () { showTip(cellEl, dateStr, lines); }); cellEl.addEventListener("pointerleave", hideTip); })(new Date(d), m, hasData); } else { cellEl.setAttribute("class", "ghheat__cell ghheat__cell--pad"); } svg.appendChild(cellEl); d.setDate(d.getDate() + 1); } var gridwrap = html("div", "ghheat__gridwrap"); gridwrap.appendChild(svg); body.appendChild(gridwrap); /* ---- Side column: stat + note + legend + extra ---- */ var side = html("div", "ghheat__side"); if (panel.stat) { var stat = html("div", "ghheat__stat"); var big = html("span", "ghheat__statbig"); big.textContent = panel.stat.big; big.style.color = strong; stat.appendChild(big); if (panel.stat.small) { var small = html("span", "ghheat__statsmall"); small.textContent = panel.stat.small; stat.appendChild(small); } side.appendChild(stat); } if (panel.note) { var note = html("p", "ghheat__note"); note.innerHTML = panel.note; side.appendChild(note); } var leg = html("div", "ghheat__legend"); var lw1 = html("span", "ghheat__legword"); lw1.textContent = "Fewer"; leg.appendChild(lw1); ramp.forEach(function (c) { var s = html("span", "ghheat__sw"); s.style.background = c; leg.appendChild(s); }); var lw2 = html("span", "ghheat__legword"); lw2.textContent = "More"; leg.appendChild(lw2); if (panel.legendLabel) { var lu = html("span", "ghheat__legunit"); lu.textContent = "\u00b7 " + panel.legendLabel; leg.appendChild(lu); } side.appendChild(leg); if (panel.extraNote) { var ex = html("div", "ghheat__extra"); var exsw = html("span", "ghheat__sw ghheat__sw--empty"); exsw.style.background = emptyColor; ex.appendChild(exsw); var ext = html("span", "ghheat__extratext"); ext.innerHTML = panel.extraNote; ex.appendChild(ext); side.appendChild(ex); } body.appendChild(side); wrap.appendChild(body); root.appendChild(wrap); }); if (config.caption) { var cap = html("div", "ghchart__caption"); cap.innerHTML = config.caption; root.appendChild(cap); } }; /* ================================================================ Scatter renderer — X/Y numeric plot with categorical points. Used for cost-vs-quality benchmark charts: a highlighted "primary" point (glow halo), a "reference" point with a dotted crosshair, and muted "other" points. Values never print on the marks — a point's name shows for identity and the exact (y, x) reveals on hover. ================================================================ */ renderers.scatter = function (root, config) { root.textContent = ""; var uid = config.mountId; var VB = { w: 1000, h: 600 }; var M = { top: 40, right: 30, bottom: 74, left: 96 }; var innerW = VB.w - M.left - M.right; var innerH = VB.h - M.top - M.bottom; var xa = config.xAxis, ya = config.yAxis; var fmtX = xa.format || function (v) { return String(v); }; var fmtY = ya.format || function (v) { return String(v); }; var tipSuffix = config.tipSuffix || ""; function xAt(v) { return M.left + innerW * (v - xa.min) / (xa.max - xa.min); } function yAt(v) { return M.top + innerH * (1 - (v - ya.min) / (ya.max - ya.min)); } var baseY = yAt(ya.min); var motionMQ = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null; function reduceMotion() { return !!(motionMQ && motionMQ.matches); } function colorOf(pt) { var s = config.series && config.series[pt.series || 0]; return (s && s.color) || "#8c959f"; } /* ---- Title (compact narrative headline, no rule) ------------ */ var title = html("div", "ghchart__headline"); title.textContent = config.title; root.appendChild(title); /* ---- Header + legend (from series) -------------------------- */ if (!config.hideLegend) { var header = html("div", "ghchart__header"); var legend = html("div", "ghchart__legend"); (config.series || []).forEach(function (s) { var item = html("span", "ghchart__legend-item"); var sw = html("span", "ghchart__swatch ghchart__swatch--dot"); sw.style.background = s.color; item.appendChild(sw); item.appendChild(document.createTextNode(s.name)); legend.appendChild(item); }); header.appendChild(legend); root.appendChild(header); } /* ---- SVG plot ----------------------------------------------- */ var svg = el("svg", { "class": "ghchart__plot ghchart__plot--scatter", viewBox: "0 0 " + VB.w + " " + VB.h, preserveAspectRatio: "xMidYMid meet", role: "img", "aria-label": config.title + " scatter plot" }); // Gridlines (both directions) + tick labels. var eps = 1e-6; for (var yv = ya.min; yv 0) { svg.appendChild(el("line", { "class": "ghchart__phase-sep", x1: x0, y1: M.top, x2: x0, y2: baseY })); } var pl = el("text", { "class": "ghchart__phase-label", x: (x0 + x1) / 2, y: M.top - 40, "text-anchor": "middle" }); pl.textContent = ph.label; svg.appendChild(pl); if (ph.avg) { var pa = el("text", { "class": "ghchart__phase-avg", x: (x0 + x1) / 2, y: M.top + 24, "text-anchor": "middle", fill: cPoint }); pa.textContent = ph.avg; svg.appendChild(pa); } }); // Horizontal gridlines + y tick labels (skip a gridline at the reference y). var refY = config.reference ? config.reference.y : null; (ya.ticks || []).forEach(function (tv) { var gy = yAt(tv); if (refY === null || Math.abs(tv - refY) > 1e-9) { svg.appendChild(el("line", { "class": "ghchart__gridline", x1: M.left, y1: gy, x2: VB.w - M.right, y2: gy })); } var yl = el("text", { "class": "ghchart__axislabel ghchart__axislabel--y", x: M.left - 16, y: gy, dy: "0.32em", "text-anchor": "end" }); yl.textContent = fmtY(tv); svg.appendChild(yl); }); // X tick labels (dated). (xa.ticks || []).forEach(function (t) { var gx = xAt(t.at); var first = Math.abs(t.at - xa.min) " + v + ""; return "" + v + "" + (p ? " (" + p + ")" : ""); } /* ---- Headline + subtitle ------------------------------------- */ if (config.title) { var head = html("div", "ghchart__headline"); head.textContent = config.title; root.appendChild(head); } if (config.subtitle) { var sub = html("p", "ghchart__subtitle"); sub.textContent = config.subtitle; root.appendChild(sub); } /* ---- Segmented "mix" bar -------------------------------------- */ var seg = config.segments; if (seg && seg.items && seg.items.length) { var segSec = html("div", "ghchart__dist-section"); if (seg.title) { var segTitle = html("div", "ghchart__panel-title"); segTitle.textContent = seg.title; segSec.appendChild(segTitle); } var segTotal = seg.items.reduce(function (a, b) { return a + b.value; }, 0) || 1; var bar = html("div", "ghchart__seg"); var notes = html("div", "ghchart__seg-note-row"); seg.items.forEach(function (it, i) { var basis = (it.value / segTotal) * 100; var col = (series[i + 1] && series[i + 1].color) || it.color || barColor; var cell = html("div", "ghchart__seg-cell"); cell.style.flex = "0 0 " + basis + "%"; cell.style.background = col; cell.textContent = it.label; bar.appendChild(cell); var note = html("div", "ghchart__seg-note"); note.style.flex = "0 0 " + basis + "%"; note.innerHTML = valueHTML(it.value); notes.appendChild(note); }); segSec.appendChild(bar); segSec.appendChild(notes); root.appendChild(segSec); } /* ---- Ranked bar panels (grid: 2-up, stacking on narrow) ------ */ var panels = config.panels || []; if (panels.length) { var cols = html("div", "ghchart__dist-cols"); if (config.columns === 1 || panels.length === 1) { cols.classList.add("ghchart__dist-cols--stack"); } panels.forEach(function (panel) { var sec = html("div", "ghchart__dist-section"); if (panel.title) { var pt = html("div", "ghchart__panel-title"); pt.textContent = panel.title; sec.appendChild(pt); } if (panel.subtitle) { var descriptor = html("p", "ghchart__subtitle"); descriptor.textContent = panel.subtitle; sec.appendChild(descriptor); } if (panel.stats) { var stats = html("dl", "ghchart__dist-stats"); panel.stats.forEach(function (stat) { var pair = html("div", "ghchart__dist-stat"); var term = html("dt"); term.textContent = stat.label; var value = html("dd"); value.textContent = stat.value; value.style.color = strongOf((series[panel.series || 0] || series[0]).color); pair.appendChild(term); pair.appendChild(value); stats.appendChild(pair); }); sec.appendChild(stats); cols.appendChild(sec); return; } var list = html("div", "ghchart__dist-list"); var pmax = (panel.items || []).reduce(function (m, it) { return it.value > m ? it.value : m; }, 0) || 1; (panel.items || []).forEach(function (it) { var row = html("div", "ghchart__dist-row"); var cat = html("div", "ghchart__dist-cat"); cat.textContent = it.label; row.appendChild(cat); var track = html("div", "ghchart__dist-track"); var b = html("div", "ghchart__dist-bar"); b.style.width = Math.max((it.value / pmax) * 100, 1.2) + "%"; b.style.background = it.color || barColor; track.appendChild(b); row.appendChild(track); var val = html("div", "ghchart__dist-val"); val.innerHTML = valueHTML(it.value); row.appendChild(val); list.appendChild(row); }); sec.appendChild(list); cols.appendChild(sec); }); root.appendChild(cols); } /* ---- Caption / footnote -------------------------------------- */ if (config.caption) { var cap = html("div", "ghchart__caption"); cap.innerHTML = config.caption; root.appendChild(cap); } }; /* ================================================================ Timeline / staged-duration renderer ---------------------------------------------------------------- A horizontal time axis broken into labeled stage segments. Composes only the pieces the data needs (like the trend renderer): • eyebrow kicker + narrative headline + optional subtitle; • an optional before/after stat card and a stage legend; • one or more rows of segmented duration bars on a shared time scale — a row is either sequential (segments end to end) or parallel (a full-height "gate" segment then concurrent lanes); • optional flow ribbons linking matching stages between the first two rows (the sequential -> parallel "collapse"); • optional elbow-leader callouts above narrow segments; • optional span brackets (below a row, or right of a parallel group); • an optional numeric axis (ticks + gridlines + title). No hover — every label is printed, so it reads on paper and to screen readers. Stage colors come from config.series so the harness tunes them live: each entry is { id, name, color }. ================================================================ */ renderers.timeline = function (root, config) { root.textContent = ""; root.setAttribute("role", "img"); if (config.title) root.setAttribute("aria-label", config.title); var stages = config.series || []; var stageById = {}; stages.forEach(function (s) { if (s && s.id) stageById[s.id] = s; }); function colorOf(seg) { return seg.color || (stageById[seg.id] && stageById[seg.id].color) || "#8250df"; } function nameOf(seg) { if (seg.label != null) return seg.label; return seg.name || (stageById[seg.id] && stageById[seg.id].name) || ""; } /* ---- DOM header: eyebrow + headline + subtitle --------------- */ if (config.eyebrow) { var eb = html("div", "ghchart__tl-eyebrow"); eb.textContent = config.eyebrow; root.appendChild(eb); } if (config.title) { var hl = html("div", "ghchart__headline ghchart__tl-headline"); hl.textContent = config.title; root.appendChild(hl); } if (config.subtitle) { var sbt = html("p", "ghchart__subtitle"); sbt.textContent = config.subtitle; root.appendChild(sbt); } /* ---- Stat card (before -> after + delta) -------------------- */ if (config.stat) { var st = config.stat; var card = html("div", "ghchart__tl-stat"); var main = html("div", "ghchart__tl-stat-main"); function statCol(k, v, muted) { var col = html("div", "ghchart__tl-stat-col"); var kk = html("div", "ghchart__tl-stat-k"); kk.textContent = k; col.appendChild(kk); var vv = html("div", "ghchart__tl-stat-v" + (muted ? " ghchart__tl-stat-v--muted" : "")); vv.textContent = v; col.appendChild(vv); return col; } main.appendChild(statCol(st.beforeLabel, st.beforeValue, true)); var arrow = html("div", "ghchart__tl-stat-arrow"); arrow.textContent = "\u2192"; main.appendChild(arrow); main.appendChild(statCol(st.afterLabel, st.afterValue, false)); card.appendChild(main); if (st.delta) { var dl = html("div", "ghchart__tl-stat-delta"); var dv = html("div", "ghchart__tl-stat-delta-v"); dv.textContent = st.delta; dl.appendChild(dv); if (st.deltaLabel) { var dk = html("div", "ghchart__tl-stat-delta-k"); dk.textContent = st.deltaLabel; dl.appendChild(dk); } card.appendChild(dl); } root.appendChild(card); } /* ---- Legend -------------------------------------------------- */ if (config.legend && config.legend.length) { var lg = html("div", "ghchart__tl-legend"); config.legend.forEach(function (item) { var s = stageById[item.id] || {}; var it = html("span", "ghchart__tl-legend-item"); var sw = html("span", "ghchart__tl-swatch"); sw.style.background = item.color || s.color || "#8250df"; it.appendChild(sw); var tx = html("span", "ghchart__tl-legend-text"); tx.textContent = (item.name || s.name || "") + (item.value ? " \u00b7 " + item.value : ""); it.appendChild(tx); lg.appendChild(it); }); root.appendChild(lg); } if (config.rule) root.appendChild(html("div", "ghchart__tl-rule")); /* ---- Plot geometry ------------------------------------------- */ var rows = config.rows || []; function rowTotal(r) { if (r.parallel) { var g = r.gate ? r.gate.value : 0, mx = 0; r.parallel.forEach(function (p) { if (p.value > mx) mx = p.value; }); return g + mx; } var t = 0; (r.segments || []).forEach(function (s) { t += s.value; }); return t; } var maxTotal = 0; rows.forEach(function (r) { var t = rowTotal(r); if (t > maxTotal) maxTotal = t; }); var axis = config.axis; var scaleMax = config.scaleMax || (axis && axis.max) || maxTotal || 1; var VB_W = 1000; var hasRowLabels = rows.some(function (r) { return r.label; }); var M = { left: hasRowLabels ? 150 : 44, right: 46 }; var innerW = VB_W - M.left - M.right; function xAt(v) { return M.left + innerW * v / scaleMax; } function wAt(v) { return innerW * v / scaleMax; } var SEQ_H = 56, LANE_H = 13, LANE_GAP = 6, SEG_R = 4; function rowBandH(r) { if (r.parallel) { var n = r.parallel.length; return Math.max(SEQ_H, n * LANE_H + (n - 1) * LANE_GAP); } return SEQ_H; } var flows = !!config.flows && rows.length >= 2; var FLOW_GAP = config.flowGap != null ? config.flowGap : 250, ROW_GAP = 48; // Reserve vertical space for above-callouts (tallest tier wins). var anyAbove = rows.some(function (r) { return (r.segments || []).some(function (s) { return s.labelPos === "above"; }); }); var maxTier = 0; rows.forEach(function (r) { (r.segments || []).forEach(function (s) { if (s.labelPos === "above" && (s.tier || 0) > maxTier) maxTier = s.tier || 0; }); }); var CALLOUT_TIER_H = 40; var topPad = anyAbove ? (18 + (maxTier + 1) * CALLOUT_TIER_H) : 16; // Assign each row a vertical band. var cursor = topPad; var rowLayout = rows.map(function (r, i) { var top = cursor, h = rowBandH(r); cursor = top + h; if (i row1), matched by stage id (drawn under bars). if (flows) { var r0 = rows[0], l0 = rowLayout[0], r1 = rows[1], l1 = rowLayout[1]; var beforeSpan = {}, acc0 = 0; (r0.segments || []).forEach(function (s) { beforeSpan[s.id] = { x0: xAt(acc0), x1: xAt(acc0 + s.value) }; acc0 += s.value; }); var afterSpan = {}, gv1 = r1.gate ? r1.gate.value : 0; if (r1.gate) afterSpan[r1.gate.id] = { x0: xAt(0), x1: xAt(gv1), y: l1.top }; (r1.parallel || []).forEach(function (p, k) { // Land each ribbon on its companion lane's actual depth, not the row top. afterSpan[p.id] = { x0: xAt(gv1), x1: xAt(gv1 + p.value), y: l1.top + k * (LANE_H + LANE_GAP) }; }); var by = l0.bottom; Object.keys(beforeSpan).forEach(function (id) { var b = beforeSpan[id], a = afterSpan[id]; if (!a) return; var ay = a.y, ddy = (ay - by) * 0.5; var d = "M " + b.x0 + " " + by + " C " + b.x0 + " " + (by + ddy) + " " + a.x0 + " " + (ay - ddy) + " " + a.x0 + " " + ay + " L " + a.x1 + " " + ay + " C " + a.x1 + " " + (ay - ddy) + " " + b.x1 + " " + (by + ddy) + " " + b.x1 + " " + by + " Z"; var flowPath = el("path", { "class": "ghchart__tl-flow", d: d, fill: colorOf({ id: id }), "data-stage": id }); svg.appendChild(flowPath); hoverSlot(id).flowEl = flowPath; }); } // Rows: segmented bars. rows.forEach(function (r, ri) { var L = rowLayout[ri]; if (r.label) { var rl = el("text", { "class": "ghchart__tl-rowlabel", x: M.left - 20, y: L.top + L.h / 2 - 3, "text-anchor": "end" }); rl.textContent = r.label; svg.appendChild(rl); if (r.sublabel) { var rs = el("text", { "class": "ghchart__tl-rowsub", x: M.left - 20, y: L.top + L.h / 2 + 18, "text-anchor": "end" }); rs.textContent = r.sublabel; svg.appendChild(rs); } } if (r.parallel) { var gv = r.gate ? r.gate.value : 0; if (r.gate) { var gateEl = el("rect", { "class": "ghchart__tl-seg", x: xAt(0), y: L.top, width: Math.max(0, wAt(gv)), height: L.h, rx: SEG_R, fill: colorOf(r.gate), "data-stage": r.gate.id }); svg.appendChild(gateEl); var gslot = hoverSlot(r.gate.id); gslot.afterEl = gateEl; gslot.after = r.gate.value; gslot.name = gslot.name || nameOf(r.gate); } r.parallel.forEach(function (p, k) { var ly = L.top + k * (LANE_H + LANE_GAP); var laneEl = el("rect", { "class": "ghchart__tl-seg", x: xAt(gv), y: ly, width: Math.max(1, wAt(p.value)), height: LANE_H, rx: 3, fill: colorOf(p), "data-stage": p.id }); svg.appendChild(laneEl); var pslot = hoverSlot(p.id); pslot.afterEl = laneEl; pslot.after = p.value; pslot.name = pslot.name || nameOf(p); }); } else { var acc = 0; (r.segments || []).forEach(function (s) { var x = xAt(acc), w = Math.max(1, wAt(s.value)); var segEl = el("rect", { "class": "ghchart__tl-seg", x: x, y: L.top, width: w, height: L.h, rx: SEG_R, fill: colorOf(s), "data-stage": s.id }); svg.appendChild(segEl); var slot = hoverSlot(s.id); slot.name = slot.name || nameOf(s); if (ri === 0) { slot.beforeEl = segEl; slot.before = s.value; } else { slot.afterEl = segEl; slot.after = s.value; } if (s.labelPos === "inside") { var ilAttrs = { "class": "ghchart__tl-seg-label", x: x + w / 2, y: L.top + L.h / 2, dy: "0.34em", "text-anchor": "middle" }; // Inline style (not a fill attribute) so it overrides the class's // default white fill — lets a bright segment take dark label text. if (s.labelColor) ilAttrs.style = "fill:" + s.labelColor; var il = el("text", ilAttrs); il.textContent = nameOf(s); svg.appendChild(il); } acc += s.value; }); } }); // Elbow-leader callouts above narrow segments. rows.forEach(function (r, ri) { if (r.parallel) return; var L = rowLayout[ri], acc = 0; (r.segments || []).forEach(function (s) { var x = xAt(acc), w = wAt(s.value), cx = x + w / 2; if (s.labelPos === "above") { var tier = s.tier || 0; var tierY = topPad - 14 - tier * CALLOUT_TIER_H; // labelX pins the label to an absolute data-x (so callouts can be // left-aligned into a stack); labelDX is a relative nudge otherwise. var lx = s.labelX != null ? xAt(s.labelX) : cx + (s.labelDX || 0); var anchor = s.labelAnchor || "start"; svg.appendChild(el("path", { "class": "ghchart__tl-callout-line", d: "M " + cx + " " + L.top + " L " + cx + " " + tierY + " L " + lx + " " + tierY, fill: "none" })); var pad = anchor === "start" ? 6 : (anchor === "end" ? -6 : 0); var cl = el("text", { "class": "ghchart__tl-callout-label", x: lx + pad, y: tierY, dy: "0.34em", "text-anchor": anchor, fill: s.labelColor || colorOf(s) }); cl.textContent = nameOf(s); svg.appendChild(cl); } acc += s.value; }); }); // Below brackets (span with up-ticks + label beneath). var belowY = (rowLayout.length ? rowLayout[rowLayout.length - 1].bottom : topPad) + 14; belowBrackets.forEach(function (b, bi) { var x0 = xAt(b.from), x1 = xAt(b.to), yy = belowY + bi * 46; svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: yy, x2: x1, y2: yy })); svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x0, y1: yy, x2: x0, y2: yy - 7 })); svg.appendChild(el("line", { "class": "ghchart__bracket", x1: x1, y1: yy, x2: x1, y2: yy - 7 })); var bl = el("text", { "class": "ghchart__bracket-label ghchart__tl-bracket-label", x: (x0 + x1) / 2, y: yy + 22, "text-anchor": "middle" }); bl.textContent = b.text; svg.appendChild(bl); }); // Right brackets (vertical span beside a parallel group + label). rightBrackets.forEach(function (b) { var L = rowLayout[b.row || 0]; var bx = xAt(b.at != null ? b.at : rowTotal(rows[b.row || 0])) + 14; var y0 = L.top, y1 = L.bottom; svg.appendChild(el("line", { "class": "ghchart__bracket", x1: bx, y1: y0, x2: bx, y2: y1 })); svg.appendChild(el("line", { "class": "ghchart__bracket", x1: bx, y1: y0, x2: bx - 7, y2: y0 })); svg.appendChild(el("line", { "class": "ghchart__bracket", x1: bx, y1: y1, x2: bx - 7, y2: y1 })); // Label as a DOM node (not SVG text) so it keeps a fixed, readable size // instead of scaling down with the plot on small windows. var rlabel = html("div", "ghchart__tl-bracket-label-dom"); rlabel.textContent = b.text; rlabel.style.left = ((bx + 12) / VB_W * 100) + "%"; rlabel.style.top = (((y0 + y1) / 2) / VB_H * 100) + "%"; plotwrap.appendChild(rlabel); }); // Numeric axis: tick labels + title. if (axis && axis.ticks) { axis.ticks.forEach(function (tv, i) { var gx = xAt(tv); var first = i === 0, last = i === axis.ticks.length - 1; var xl = el("text", { "class": "ghchart__axislabel ghchart__axislabel--x ghchart__tl-tick", x: gx, y: axisY + 26, "text-anchor": first ? "start" : (last ? "end" : "middle") }); xl.textContent = axis.format ? axis.format(tv) : String(tv); svg.appendChild(xl); }); } /* ---- Hover interaction (before/after chart) ------------------ Emphasise a stage's whole connection (its before block, after block and ribbon) and show a tooltip with the before/after values. Only when there are flows to connect and hover isn't disabled. */ if (flows && config.hover !== false) { var unit = config.unit ? " " + config.unit : ""; function fmtHover(v) { return v == null ? "\u2013" : v + unit; } var tip = html("div", "ghchart__tooltip"); tip.setAttribute("role", "status"); var tipTitle = html("div", "ghchart__tt-title"); var tipBody = html("div", "ghchart__tt-body ghchart__tt-body--plain"); tip.appendChild(tipTitle); tip.appendChild(tipBody); root.appendChild(tip); var focused = null; function focusStage(id) { focused = id; Object.keys(stageHover).forEach(function (k) { var s = stageHover[k], on = (k === id); if (s.beforeEl) s.beforeEl.style.opacity = on ? "1" : "0.16"; if (s.afterEl) s.afterEl.style.opacity = on ? "1" : "0.16"; if (s.flowEl) s.flowEl.style.opacity = on ? "0.5" : "0.04"; }); var st = stageHover[id]; tipTitle.textContent = st.name || id; tipBody.textContent = ""; var lb = html("div", "ghchart__tt-line"); lb.textContent = "Before " + fmtHover(st.before); var la = html("div", "ghchart__tt-line"); la.textContent = "After " + fmtHover(st.after); tipBody.appendChild(lb); tipBody.appendChild(la); } function clearStage() { focused = null; Object.keys(stageHover).forEach(function (k) { var s = stageHover[k]; if (s.beforeEl) s.beforeEl.style.opacity = ""; if (s.afterEl) s.afterEl.style.opacity = ""; if (s.flowEl) s.flowEl.style.opacity = ""; }); tip.setAttribute("data-visible", "false"); } function placeTip(clientX, clientY) { var rr = root.getBoundingClientRect(); var left = clientX - rr.left, top = clientY - rr.top - 14; tip.setAttribute("data-visible", "true"); var halfW = tip.offsetWidth / 2; left = clamp(left, halfW + 2, root.clientWidth - halfW - 2); if (top - tip.offsetHeight existing.version) { /* This bundle is newer. Every export embeds the COMPLETE engine, so this bundle's `renderers` is a self-contained, up-to-date set. Adopt it as the shared registry (upgrading older same-named renderers in place), while defensively preserving any renderer types only the older engine happened to have. Then take over render(). */ for (var ek in existing.renderers) { if (Object.prototype.hasOwnProperty.call(existing.renderers, ek) && !renderers[ek]) { renderers[ek] = existing.renderers[ek]; } } existing.renderers = renderers; existing.render = render; existing.destroy = destroy; existing.version = ENGINE_VERSION; existing._renderCleanups = renderCleanups; } else { /* Same or older version already present: only contribute renderer types the shared registry is missing (don't clobber an equal/newer engine). */ for (var rk in renderers) { if (Object.prototype.hasOwnProperty.call(renderers, rk) && !existing.renderers[rk]) { existing.renderers[rk] = renderers[rk]; } } } } else { /* No engine yet, or a pre-versioning (old-style) engine loaded first. Take over, preserving any renderers the old-style engine registered. */ if (existing && existing.renderers) { for (var ok in existing.renderers) { if (Object.prototype.hasOwnProperty.call(existing.renderers, ok) && !renderers[ok]) { renderers[ok] = existing.renderers[ok]; } } } global.GHChartEngine = { renderers: renderers, render: render, destroy: destroy, version: ENGINE_VERSION, _renderCleanups: renderCleanups }; } })(typeof window !== "undefined" ? window : this); ;(function () { "use strict"; var CONFIG = { mountId: "gh-chart-git-events-by-percentile", type: "line", title: "Git events per repository",