From 8c0958caed0404fb02102f0a97dc349d039b6da4 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 8 Nov 2026 22:20:17 +0000 Subject: [PATCH] Add a dark mode --- codespeed/static/css/dygraph.css | 8 +- codespeed/static/css/main.css | 164 +++++- codespeed/static/images/asc.gif | Bin 54 -> 0 bytes codespeed/static/images/bg.gif | Bin 64 -> 0 bytes codespeed/static/images/desc.gif | Bin 54 -> 0 bytes codespeed/static/js/codespeed.js | 88 ++++ codespeed/static/js/comparison.js | 14 +- codespeed/static/js/timeline.js | 23 +- codespeed/templates/codespeed/base.html | 18 + codespeed/templates/codespeed/comparison.html | 6 +- .../static/images/pypy-speed-center-dark.svg | 487 ++++++++++++++++++ speed_pypy/templates/admin/base_site.html | 11 - speed_pypy/templates/admin/index.html | 2 +- speed_pypy/templates/codespeed/base_site.html | 4 +- speed_pypy/templates/embed_comparison.html | 36 +- speed_pypy/templates/home.html | 20 +- .../static/images/speed-center-logo-dark.svg | 87 ++++ speed_python/static/js/home_geomean.js | 8 +- speed_python/templates/admin/base_site.html | 11 - .../templates/codespeed/base_site.html | 4 +- 20 files changed, 924 insertions(+), 67 deletions(-) delete mode 100644 codespeed/static/images/asc.gif delete mode 100644 codespeed/static/images/bg.gif delete mode 100644 codespeed/static/images/desc.gif create mode 100644 speed_pypy/static/images/pypy-speed-center-dark.svg create mode 100644 speed_python/static/images/speed-center-logo-dark.svg diff --git a/codespeed/static/css/dygraph.css b/codespeed/static/css/dygraph.css index 3757069d..f5c7e7bc 100644 --- a/codespeed/static/css/dygraph.css +++ b/codespeed/static/css/dygraph.css @@ -13,7 +13,7 @@ top: 0px; right: 2px; */ - background: white; + background: var(--surface, white); line-height: normal; text-align: left; overflow: hidden; @@ -63,8 +63,8 @@ /* This class only applies to annotations without icons */ /* Old class name: .dygraphDefaultAnnotation */ .dygraph-default-annotation { - border: 1px solid black; - background-color: white; + border: 1px solid var(--text, black); + background-color: var(--surface, white); text-align: center; } @@ -74,7 +74,7 @@ z-index: 10; line-height: normal; overflow: hidden; - color: black; /* replaces old axisLabelColor option */ + color: var(--chart-text, black); /* replaces old axisLabelColor option */ } .dygraph-axis-label-x { diff --git a/codespeed/static/css/main.css b/codespeed/static/css/main.css index 589aaa46..3b3700de 100644 --- a/codespeed/static/css/main.css +++ b/codespeed/static/css/main.css @@ -6,7 +6,10 @@ * Visual values live in the custom properties below; change the look there. */ +/* Light theme. */ :root { + color-scheme: light; + --font-sans: "Fira Sans", Helvetica, Arial, sans-serif; --font-mono: "Menlo", "ProFont", "Inconsolata", "Lucida Sans Console", monospace; @@ -23,7 +26,9 @@ --accent: #2563eb; --accent-hover: #1d4ed8; --accent-soft: #e8efff; + --warning: #d9480f; + /* Changes table cells: background and the text on it */ --status-red: #FF5640; --status-red-text: #1f2328; --status-green: #9FD54D; @@ -32,14 +37,69 @@ --status-lightgreen-text: #1f2328; --status-yellow: #FFD843; --status-yellow-text: #1f2328; + /* Home page report summaries: coloured text on the card */ + --summary-red: #FF5640; + --summary-green: #5c9a1b; + --summary-lightgreen: #8bb000; + --summary-yellow: #c9a000; + + /* Read by the chart scripts (Theme.chartColors in codespeed.js) for the + parts of a canvas chart the stylesheet cannot reach */ + --chart-text: #636c76; + --chart-grid: rgba(0, 0, 0, 0.1); --radius: 8px; --radius-sm: 4px; --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08); + --shadow-raised: 0 6px 16px rgba(16, 24, 40, 0.12); + --tooltip-bg: rgba(255, 255, 255, 0.96); + /* The home page thumbnails are screenshots of the light theme */ + --thumb-filter: none; --sidebar-width: 14em; --gap: 1em; } +/* Dark theme. */ +:root[data-theme="dark"] { + color-scheme: dark; + + --page-bg: #0d1117; + --surface: #161b22; + --surface-alt: #1f242c; + --border: #30363d; + --border-strong: #484f58; + + --text: #e6edf3; + --text-muted: #9ea7b3; + --text-faint: #7d8590; + + --accent: #58a6ff; + --accent-hover: #79c0ff; + --accent-soft: #1b2d4a; + --warning: #f0883e; + + --status-red: #9f2f24; + --status-red-text: #fbe3e0; + --status-green: #3b7a28; + --status-green-text: #e6f4de; + --status-lightgreen: #5f8a1e; + --status-lightgreen-text: #eef7da; + --status-yellow: #96760e; + --status-yellow-text: #fcf2d0; + --summary-red: #ff7b72; + --summary-green: #7ccf5a; + --summary-lightgreen: #b8e04c; + --summary-yellow: #e3b341; + + --chart-text: #9ea7b3; + --chart-grid: rgba(255, 255, 255, 0.12); + + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.5); + --shadow-raised: 0 6px 16px rgba(0, 0, 0, 0.55); + --tooltip-bg: rgba(22, 27, 34, 0.97); + --thumb-filter: invert(0.88) hue-rotate(180deg); +} + /* Global styles */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } @@ -110,6 +170,34 @@ ul#links li a:hover, ul#links li a:focus { text-decoration: none; } +/* Theme toggle: shows the icon of the theme a click switches to */ +button#theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2em; + height: 2em; + margin-left: 0.25em; + padding: 0; + color: var(--text-muted); + background: none; + border: 0; + border-radius: var(--radius-sm); + cursor: pointer; +} +button#theme-toggle:hover, button#theme-toggle:focus-visible { + color: var(--text); + background-color: var(--surface-alt); +} +button#theme-toggle svg { width: 1.1em; height: 1.1em; } +button#theme-toggle .icon-sun, +:root[data-theme="dark"] button#theme-toggle .icon-moon { display: none; } +:root[data-theme="dark"] button#theme-toggle .icon-sun { display: block; } + +/* A site can ship a second logo for the dark theme (see base_site.html) */ +:root[data-theme="dark"] div#title img.logo-light, +:root:not([data-theme="dark"]) div#title img.logo-dark { display: none; } + div#wrapper { margin: 0; } /* Section tabs */ @@ -189,25 +277,36 @@ div#presentation div.menubox { } div#presentation div.menubox:hover { border-color: var(--accent); - box-shadow: 0 6px 16px rgba(16, 24, 40, 0.12); + box-shadow: var(--shadow-raised); transform: translateY(-2px); } div#presentation div.menubox h2 { font-size: 1.1em; font-weight: 600; } div#presentation div.menubox p { color: var(--text-muted); } -/* The three icons differ in size (98 to 147px wide); scale them to one height */ +/* The thumbnail sits in a pseudo-element so the dark theme can filter it + without touching the text */ div#presentation div.menubox p { + position: relative; min-height: 4.6em; margin: 0.6em 0 0; padding-left: 122px; text-align: left; +} +div#presentation div.menubox p::before { + content: ''; + position: absolute; + top: 0.2em; + left: 0; + width: 122px; + height: 65px; background-repeat: no-repeat; - background-position: 0 0.2em; + background-position: 0 0; background-size: auto 65px; + filter: var(--thumb-filter); } -div#presentation div#changes p { background-image: url(../images/changes.png); } -div#presentation div#timeline p { background-image: url(../images/timeline.png); } -div#presentation div#comparison p { background-image: url(../images/comparison.png); } +div#presentation div#changes p::before { background-image: url(../images/changes.png); } +div#presentation div#timeline p::before { background-image: url(../images/timeline.png); } +div#presentation div#comparison p::before { background-image: url(../images/comparison.png); } /* Sidebar and content columns */ @@ -364,11 +463,20 @@ table.tablesorter thead tr th { border-right: 1px solid var(--border); } table.tablesorter thead tr th:last-child { border-right: 0; } -table.tablesorter thead tr th.header { - background-image: url(../images/bg.gif); - background-repeat: no-repeat; - background-position: center right; - cursor: pointer; +table.tablesorter thead tr th.header { position: relative; cursor: pointer; } +/* Sort indicator: both arrows on a sortable column, one on the sorted column. + Drawn through a mask so it takes the theme's colours. */ +table.tablesorter thead tr th.header::after { + content: ''; + position: absolute; + top: 50%; + right: 4px; + width: 8px; + height: 10px; + margin-top: -5px; + background-color: var(--text-faint); + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 10'%3E%3Cpath d='M4 0 8 4H0zM0 6h8l-4 4z'/%3E%3C/svg%3E") no-repeat center / contain; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 10'%3E%3Cpath d='M4 0 8 4H0zM0 6h8l-4 4z'/%3E%3C/svg%3E") no-repeat center / contain; } table.tablesorter tfoot tr td { padding: 0.4em 0.5em; @@ -390,12 +498,20 @@ table.tablesorter tbody td { } table.tablesorter tbody td:last-child, table.tablesorter tfoot td:last-child { border-right: 0; } table.tablesorter tbody tr.odd td { background-color: var(--surface-alt); } -table.tablesorter thead tr .headerSortUp { background-image: url(../images/asc.gif); } -table.tablesorter thead tr .headerSortDown { background-image: url(../images/desc.gif); } +table.tablesorter thead tr .headerSortUp::after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 10'%3E%3Cpath d='M4 2.5 8 6.5H0z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 10'%3E%3Cpath d='M4 2.5 8 6.5H0z'/%3E%3C/svg%3E"); +} +table.tablesorter thead tr .headerSortDown::after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 10'%3E%3Cpath d='M0 3.5h8l-4 4z'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 10'%3E%3Cpath d='M0 3.5h8l-4 4z'/%3E%3C/svg%3E"); +} table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp { background-color: var(--accent-soft); color: var(--accent); } +table.tablesorter thead tr .headerSortDown::after, +table.tablesorter thead tr .headerSortUp::after { background-color: var(--accent); } table.tablesorter tbody td:not(:first-child), table.tablesorter tfoot td:not(:first-child) { font-family: var(--font-mono); @@ -406,16 +522,16 @@ table.tablesorter tfoot td:first-child { text-align: left; } table.tablesorter tbody tr td.status-red, td.status-red { background-color: var(--status-red); color: var(--status-red-text); } -.reports tr.status-red td.summary { color: var(--status-red); font-weight: 600; } +.reports tr.status-red td.summary { color: var(--summary-red); font-weight: 600; } table.tablesorter tbody tr td.status-green, td.status-green { background-color: var(--status-green); color: var(--status-green-text); } -tr.status-green td.summary { color: #5c9a1b; font-weight: 600; } +tr.status-green td.summary { color: var(--summary-green); font-weight: 600; } table.tablesorter tbody tr td.status-yellow, td.status-yellow { background-color: var(--status-yellow); color: var(--status-yellow-text); } -tr.status-yellow td.summary { color: #c9a000; font-weight: 600; } +tr.status-yellow td.summary { color: var(--summary-yellow); font-weight: 600; } table.tablesorter tbody tr td.status-lightgreen, td.status-lightgreen { background-color: var(--status-lightgreen); color: var(--status-lightgreen-text); } -tr.status-lightgreen td.summary { color: #8bb000; font-weight: 600; } +tr.status-lightgreen td.summary { color: var(--summary-lightgreen); font-weight: 600; } table.tablesorter tbody tr.highlight td, tr.highlight td { background-color: var(--accent-soft) !important; @@ -468,10 +584,8 @@ a#permalink, a#exportcsv { gap: 0.25em; font-size: 0.9em; } +/* The download glyph's viewBox carries a margin, so both look the same size */ a#permalink svg, a#exportcsv svg { width: 1.1em; height: 1.1em; } -/* The download glyph fills its 16px grid, the link glyph does not; - shrink it slightly so the two look the same size. */ -a#exportcsv svg { width: 0.95em; height: 0.95em; } a#exportcsv + a#permalink { margin-left: 1em; } /* Plot styles */ @@ -493,7 +607,7 @@ div.miniplot:hover { border-color: var(--accent); background-color: var(--surfac #dygraph-tooltip { display: none; position: absolute; - background: rgba(255, 255, 255, 0.96); + background: var(--tooltip-bg); color: var(--text); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); @@ -502,7 +616,7 @@ div.miniplot:hover { border-color: var(--accent); background-color: var(--surfac pointer-events: none; z-index: 100; white-space: nowrap; - box-shadow: 0 4px 12px rgba(16, 24, 40, 0.14); + box-shadow: var(--shadow-raised); } div.compplot { @@ -536,8 +650,8 @@ p.note, p.warning { border: 1px solid var(--border); border-radius: var(--radius-sm); } -p.warning { margin-left: 10px; border-left: 3px solid #d9480f; } -p.warning::before { content: '⚠ '; color: #d9480f; } +p.warning { margin-left: 10px; border-left: 3px solid var(--warning); } +p.warning::before { content: '⚠ '; color: var(--warning); } p.note { margin: 20px 2.4em 0; padding-left: 55px; @@ -591,5 +705,5 @@ div.footer a:hover { color: var(--accent); text-decoration: underline; } } /* speed.pypy.org additions */ -.envlinecolor { float: right; font-size: 12px; line-height: 18px; color: #555; } +.envlinecolor { float: right; font-size: 12px; line-height: 18px; color: var(--text-muted); } a#exportcsv:hover { text-decoration: underline; } diff --git a/codespeed/static/images/asc.gif b/codespeed/static/images/asc.gif deleted file mode 100644 index 74157867f25acbc146704d43399d6c3605ba7724..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54 zcmZ?wbhEHb6lGvxXkcJa);0M5|G(l-7DfgJMg|=QAOOiQF!A=tFW`Q0{?_dDi`go= G4AuZ#-wosd diff --git a/codespeed/static/images/bg.gif b/codespeed/static/images/bg.gif deleted file mode 100644 index fac668fcf42af844a3af0a239fa638ddbc08443c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 zcmZ?wbhEHb6lLIKXkcJa);0M5|G(l-7DfgJMg|=QAOOiQFp2l{H=O3Yl~fU8)V1~= QTew|n!uOuePzDBT00piR0RR91 diff --git a/codespeed/static/images/desc.gif b/codespeed/static/images/desc.gif deleted file mode 100644 index 3b30b3c58eabdb47a1c420ad03c8e30b966cc858..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54 zcmZ?wbhEHb6lGvxXkcJa);0M5|G(l-7DfgJMg|=QAOOiQF!A>EGoD<#VNP?1QCB1* GgEatI(+xQQ diff --git a/codespeed/static/js/codespeed.js b/codespeed/static/js/codespeed.js index 01739526..a31111d6 100644 --- a/codespeed/static/js/codespeed.js +++ b/codespeed/static/js/codespeed.js @@ -56,3 +56,91 @@ $(function() { }); }); }); + +/* Colour theme. + * + * base.html sets data-theme on before first paint. This keeps it in + * step with the header toggle, the choice stored in localStorage, the OS + * setting (followed until the user picks one) and other tabs, and tells the + * chart scripts when to redraw: canvas charts cannot read the stylesheet. + */ +var Theme = (function() { + var STORAGE_KEY = 'codespeed-theme'; + var root = document.documentElement; + var listeners = []; + var osDark = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null; + + function current() { + return root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; + } + + function stored() { + var value = null; + try { value = localStorage.getItem(STORAGE_KEY); } catch (e) {} + return (value === 'light' || value === 'dark') ? value : null; + } + + function updateButton() { + var label = 'Switch to ' + (current() === 'dark' ? 'light' : 'dark') + ' theme'; + $('#theme-toggle').attr({'aria-label': label, title: label}); + } + + function apply(theme) { + if (theme === current()) { return; } + root.setAttribute('data-theme', theme); + updateButton(); + listeners.forEach(function(fn) { fn(theme); }); + } + + // The colours a canvas chart needs, resolved from the stylesheet's tokens + function chartColors() { + var style = getComputedStyle(root); + function token(name, fallback) { + return style.getPropertyValue(name).trim() || fallback; + } + return { + text: token('--chart-text', '#666'), + grid: token('--chart-grid', 'rgba(0, 0, 0, 0.1)'), + faint: token('--text-faint', '#aaa'), + surface: token('--surface', '#fff') + }; + } + + // Dygraphs options for the parts it draws on the canvas. Hovering a + // series fades the others by painting the plot with the highlight + // background, which defaults to white, so it must match the surface. + function dygraphOptions() { + var colors = chartColors(); + return { + axisLineColor: colors.text, + gridLineColor: colors.grid, + gridLineWidth: 1, + highlightSeriesBackgroundColor: colors.surface + }; + } + + $(function() { + updateButton(); + $('#theme-toggle').click(function() { + var next = current() === 'dark' ? 'light' : 'dark'; + try { localStorage.setItem(STORAGE_KEY, next); } catch (e) {} + apply(next); + }); + if (osDark && osDark.addEventListener) { + osDark.addEventListener('change', function(e) { + if (!stored()) { apply(e.matches ? 'dark' : 'light'); } + }); + } + // A toggle in another tab of the site + window.addEventListener('storage', function(e) { + if (e.key === STORAGE_KEY && stored()) { apply(stored()); } + }); + }); + + return { + current: current, + chartColors: chartColors, + dygraphOptions: dygraphOptions, + onChange: function(fn) { listeners.push(fn); } + }; +})(); diff --git a/codespeed/static/js/comparison.js b/codespeed/static/js/comparison.js index abec6ef0..72016f5c 100644 --- a/codespeed/static/js/comparison.js +++ b/codespeed/static/js/comparison.js @@ -17,6 +17,13 @@ var COLORS = [ ]; function getColor(i) { return COLORS[i % COLORS.length]; } +// Chart.js draws its own axis text and grid lines, so hand it the theme colours +function applyChartTheme() { + var colors = Theme.chartColors(); + Chart.defaults.color = colors.text; + Chart.defaults.borderColor = colors.grid; +} + function getExeLabel(key) { return $("input[name='executables']") .filter(function() { return $(this).val() === key; }) @@ -421,7 +428,7 @@ function renderComparisonPlot(plotid, unit, benchmarks, exes, enviros, baseline, if (ds && ds.allNull) { // Apply unicode combining strikethrough to each character item.text = item.text.split('').join('\u0336') + '\u0336'; - item.fontColor = '#aaa'; + item.fontColor = Theme.chartColors().faint; } }); return items; @@ -440,6 +447,11 @@ function renderComparisonPlot(plotid, unit, benchmarks, exes, enviros, baseline, } function init(defaults) { + applyChartTheme(); + Theme.onChange(function() { + applyChartTheme(); + if (compdata) { refreshContent(); } + }); bench_units = defaults.bench_units; // Set default values diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index 5ed88ada..0642a0dc 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -275,7 +275,7 @@ function renderPlot(data) { var header = '
' + (dk ? dk.slice(0, 16) : ''); if (commitInfo) { - header += ' commit: ' + commitInfo.commit; + header += ' commit: ' + commitInfo.commit; if (commitInfo.tag) { header += '  tag: ' + commitInfo.tag; } if (versionBoundaries.length > 0 && commitInfo.suite_version) { header += '  suite: ' + commitInfo.suite_version; } header += ''; @@ -287,7 +287,7 @@ function renderPlot(data) { if (!s.isVisible || s.y === undefined) { return; } var seriesKey = labelToSeriesKey[s.labelHTML] || labelToSeriesKey[s.name]; if (!seriesKey) { return; } // skip baseline - var color = colorMap[s.labelHTML] || colorMap[s.name] || '#333'; + var color = colorMap[s.labelHTML] || colorMap[s.name] || 'var(--text)'; html += '
' + '' + s.labelHTML + ': ' + s.y.toPrecision(4) + ''; if (dk && commitMap[dk] && commitMap[dk][seriesKey]) { @@ -346,6 +346,7 @@ function renderPlot(data) { } } + var theme = Theme.dygraphOptions(); plotInstance = new Dygraph( document.getElementById('plot'), built.data, @@ -354,6 +355,10 @@ function renderPlot(data) { titleHeight: 24, labels: built.labels, colors: built.colors, + axisLineColor: theme.axisLineColor, + gridLineColor: theme.gridLineColor, + gridLineWidth: theme.gridLineWidth, + highlightSeriesBackgroundColor: theme.highlightSeriesBackgroundColor, customBars: true, series: seriesOpts, legend: 'never', @@ -366,6 +371,8 @@ function renderPlot(data) { dateWindow: singlePointWindow, connectSeparatedPoints: true, underlayCallback: function(canvas, area, g) { + // Read the colour at draw time: a theme switch redraws the plot + var textColor = Theme.chartColors().text; versionBoundaries.forEach(function(b) { var xval = equidistant ? b.idx : dateIndex[b.dk]; var cx = g.toDomXCoord(xval); @@ -373,12 +380,14 @@ function renderPlot(data) { canvas.beginPath(); canvas.moveTo(cx, area.y); canvas.lineTo(cx, area.y + area.h); - canvas.strokeStyle = 'rgba(120, 120, 120, 0.6)'; + canvas.strokeStyle = textColor; + canvas.globalAlpha = 0.6; canvas.lineWidth = 1.5; canvas.setLineDash([4, 3]); canvas.stroke(); canvas.setLineDash([]); - canvas.fillStyle = 'rgba(80, 80, 80, 0.75)'; + canvas.fillStyle = textColor; + canvas.globalAlpha = 0.85; canvas.font = '10px sans-serif'; canvas.fillText(b.label, cx + 3, area.y + 12); canvas.restore(); @@ -456,7 +465,7 @@ function renderPlot(data) { var absX = chartOffset.left + mx; var onRight = absX > $(window).width() / 2; $tip.html(legendFormatter({x: lastHighlightX, series: lastHighlightPoints.map(function(pt) { - return {color: colorMap[pt.name] || '#333', + return {color: colorMap[pt.name] || 'var(--text)', labelHTML: pt.name, name: pt.name, y: pt.yval, isVisible: true}; })})); @@ -682,6 +691,10 @@ function init(def) { defaults.environments = defaults.environment ? [defaults.environment] : []; } $.ajaxSetup({cache: false}); + // The miniplots draw no axes or grid, so only the main plot needs redrawing + Theme.onChange(function() { + if (plotInstance) { plotInstance.updateOptions(Theme.dygraphOptions()); } + }); initializeSite(); } diff --git a/codespeed/templates/codespeed/base.html b/codespeed/templates/codespeed/base.html index 1d9b9bb4..d59ba08e 100644 --- a/codespeed/templates/codespeed/base.html +++ b/codespeed/templates/codespeed/base.html @@ -10,6 +10,19 @@ {# Fira Sans, regular and bold, subset to printable ASCII #} + {% comment %}Pick the colour theme before first paint so the page never flashes + the other one. The toggle button and the stored choice are handled in + codespeed.js, which uses the same storage key.{% endcomment %} + {% block favicon %}{% endblock %} {% block extra_head %}{% endblock %} @@ -31,6 +44,11 @@

{% block page_title %}Speed Center{% endblock page_title %}

{% endblock top_nav %} {% endblock page_header %} + {# Bootstrap Icons "moon" and "sun" (MIT, twbs/icons); the stylesheet shows one per theme #} +
diff --git a/codespeed/templates/codespeed/comparison.html b/codespeed/templates/codespeed/comparison.html index 02c85424..31d1bcea 100644 --- a/codespeed/templates/codespeed/comparison.html +++ b/codespeed/templates/codespeed/comparison.html @@ -78,8 +78,10 @@ - {% comment %}Bootstrap Icons "download" glyph (MIT, twbs/icons).{% endcomment %} - Export CSV + {% comment %}Bootstrap Icons "download" glyph (MIT, twbs/icons). The tray touches the + edges of the 16 unit grid, so the viewBox adds a margin: an edge on a fractional + pixel would otherwise be clipped.{% endcomment %} + Export CSV {% include "codespeed/permalink.html" %}
diff --git a/speed_pypy/static/images/pypy-speed-center-dark.svg b/speed_pypy/static/images/pypy-speed-center-dark.svg new file mode 100644 index 00000000..8967f3a0 --- /dev/null +++ b/speed_pypy/static/images/pypy-speed-center-dark.svg @@ -0,0 +1,487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/speed_pypy/templates/admin/base_site.html b/speed_pypy/templates/admin/base_site.html index 21043865..d55ff3cb 100644 --- a/speed_pypy/templates/admin/base_site.html +++ b/speed_pypy/templates/admin/base_site.html @@ -3,17 +3,6 @@ {% block title %}{{ title }} | {% trans 'Codespeed admin' %}{% endblock %} -{% block extrastyle %} - -{% endblock %} - - {% block branding %}

{% trans 'Codespeed administration' %}

{% endblock %} diff --git a/speed_pypy/templates/admin/index.html b/speed_pypy/templates/admin/index.html index 540c9303..e4c70d9d 100644 --- a/speed_pypy/templates/admin/index.html +++ b/speed_pypy/templates/admin/index.html @@ -21,7 +21,7 @@ Download - + diff --git a/speed_pypy/templates/codespeed/base_site.html b/speed_pypy/templates/codespeed/base_site.html index 0e8782e3..3205a853 100644 --- a/speed_pypy/templates/codespeed/base_site.html +++ b/speed_pypy/templates/codespeed/base_site.html @@ -7,8 +7,8 @@ {% endblock favicon %} -{# The logo carries the site name, so the text title is left empty #} {% block logo %} - PyPy Speed Center + PyPy Speed Center + PyPy Speed Center {% endblock logo %} {% block page_title %}{% endblock page_title %} diff --git a/speed_pypy/templates/embed_comparison.html b/speed_pypy/templates/embed_comparison.html index 08ae49af..13acb984 100644 --- a/speed_pypy/templates/embed_comparison.html +++ b/speed_pypy/templates/embed_comparison.html @@ -7,6 +7,11 @@ PyPy vs CPython benchmark comparison @@ -27,6 +33,34 @@ return {x: eventPosition.x, y: eventPosition.y}; }; + // Colour theme, ?theme=light|dark set by the embedding page wins, then the + // theme of a same-origin parent, else light. + var comparisonChart = null; + function resolveTheme() { + var fromQuery = /[?&]theme=(light|dark)(&|$)/.exec(window.location.search); + if (fromQuery) { return fromQuery[1]; } + try { + if (window.parent !== window) { + var parentTheme = window.parent.document.documentElement.getAttribute('data-theme'); + return parentTheme === 'dark' ? 'dark' : 'light'; + } + } catch (e) { /* cross-origin parent */ } + return 'light'; + } + function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + var style = getComputedStyle(document.documentElement); + Chart.defaults.color = style.getPropertyValue('--chart-text').trim(); + Chart.defaults.borderColor = style.getPropertyValue('--chart-grid').trim(); + if (comparisonChart) { comparisonChart.update(); } + } + window.addEventListener('message', function(e) { + if (e.origin === window.location.origin && e.data && e.data.codespeedTheme) { + applyTheme(e.data.codespeedTheme); + } + }); + applyTheme(resolveTheme()); + function renderplot(data) { var wrap = document.getElementById('baseline-comparison-plot'); if (data === null || data.length === 0 || @@ -65,7 +99,7 @@ var canvas1 = document.createElement('canvas'); wrap.appendChild(canvas1); - new Chart(canvas1, { + comparisonChart = new Chart(canvas1, { type: 'bar', data: { labels: benchmarks, diff --git a/speed_pypy/templates/home.html b/speed_pypy/templates/home.html index 22e1de77..9b3fc228 100644 --- a/speed_pypy/templates/home.html +++ b/speed_pypy/templates/home.html @@ -68,6 +68,24 @@

How has PyPy performance evolved over time?

return {x: eventPosition.x, y: eventPosition.y}; }; + // Chart.js draws its own axis text and grid lines, so hand it the theme colours + var historicalChart = null; + function applyChartTheme() { + var colors = Theme.chartColors(); + Chart.defaults.color = colors.text; + Chart.defaults.borderColor = colors.grid; + } + applyChartTheme(); + Theme.onChange(function(theme) { + applyChartTheme(); + if (historicalChart) { historicalChart.update(); } + // Plot 1 lives in the iframe, which cannot see this page's theme change + var frame = document.querySelector('#historical iframe'); + if (frame && frame.contentWindow) { + frame.contentWindow.postMessage({codespeedTheme: theme}, window.location.origin); + } + }); + function renderplot(data) { if (data === null || data.length === 0) { $("#historical-plot").html(getLoadText('Error retrieving data', 0)); @@ -140,7 +158,7 @@

How has PyPy performance evolved over time?

var wrap2 = document.getElementById('historical-plot'); var canvas2 = document.createElement('canvas'); wrap2.appendChild(canvas2); - new Chart(canvas2, { + historicalChart = new Chart(canvas2, { type: 'bar', data: { labels: ticks2, diff --git a/speed_python/static/images/speed-center-logo-dark.svg b/speed_python/static/images/speed-center-logo-dark.svg new file mode 100644 index 00000000..2665b851 --- /dev/null +++ b/speed_python/static/images/speed-center-logo-dark.svg @@ -0,0 +1,87 @@ + + + + diff --git a/speed_python/static/js/home_geomean.js b/speed_python/static/js/home_geomean.js index df67bca2..7636df1f 100644 --- a/speed_python/static/js/home_geomean.js +++ b/speed_python/static/js/home_geomean.js @@ -47,10 +47,15 @@ } var labels = ['Date'].concat(series.map(function(s) { return s.label; })); + var theme = Theme.dygraphOptions(); - new Dygraph(container, rows, { + var graph = new Dygraph(container, rows, { labels: labels, colors: COLORS.slice(0, series.length), + axisLineColor: theme.axisLineColor, + gridLineColor: theme.gridLineColor, + gridLineWidth: theme.gridLineWidth, + highlightSeriesBackgroundColor: theme.highlightSeriesBackgroundColor, connectSeparatedPoints: true, drawPoints: true, pointSize: 2, @@ -80,6 +85,7 @@ '&exe=' + s.executable + '&env=' + s.environment; } }); + Theme.onChange(function() { graph.updateOptions(Theme.dygraphOptions()); }); } document.addEventListener('DOMContentLoaded', function() { diff --git a/speed_python/templates/admin/base_site.html b/speed_python/templates/admin/base_site.html index 21043865..d55ff3cb 100644 --- a/speed_python/templates/admin/base_site.html +++ b/speed_python/templates/admin/base_site.html @@ -3,17 +3,6 @@ {% block title %}{{ title }} | {% trans 'Codespeed admin' %}{% endblock %} -{% block extrastyle %} - -{% endblock %} - - {% block branding %}

{% trans 'Codespeed administration' %}

{% endblock %} diff --git a/speed_python/templates/codespeed/base_site.html b/speed_python/templates/codespeed/base_site.html index ed92b363..8f149cc2 100644 --- a/speed_python/templates/codespeed/base_site.html +++ b/speed_python/templates/codespeed/base_site.html @@ -7,8 +7,8 @@ {% endblock favicon %} -{# The logo carries the site name, so the text title is left empty #} {% block logo %} - Python Speed Center + Python Speed Center + Python Speed Center {% endblock logo %} {% block page_title %}{% endblock page_title %}