diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-27 20:27:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-27 20:27:13 +0300 |
| commit | 5cedb6d00367463d68031e1b8b80446dffdbed04 (patch) | |
| tree | af9ac10ee4893bd6ad74babd2c0909f2cfdc1311 /internal/showcase | |
| parent | 027bfd08e367ce520acfccf1b452687fb4bfdabb (diff) | |
showcase: dynamic SVG scaling fills browser window without letterboxing
Replace the fixed viewBox+preserveAspectRatio approach with JS-driven
rescale(). The SVG root now has no viewBox; instead rescale() runs on
load and on every resize event:
- Sets the SVG viewBox to the actual window dimensions.
- Computes a uniform scale s = min(W/CHART_W, H/CHART_H) and a
centering translate so the chart group fills the window edge-to-edge
along the limiting axis with no empty bars.
- All chart elements are wrapped in <g id="chart"> so a single
transform attribute repositions everything together.
The tooltip coordinate conversion switches from svgEl.getScreenCTM()
to chartEl.getScreenCTM() so cursor positions are expressed in chart-
local coordinates (CHART_W × CHART_H space) after the rescale transform.
Similarly, tooltip boundary clamping uses the CHART_W/CHART_H constants
instead of the now-dynamic svgEl.viewBox.baseVal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/showcase')
| -rw-r--r-- | internal/showcase/rank_history_svg.go | 62 |
1 files changed, 44 insertions, 18 deletions
diff --git a/internal/showcase/rank_history_svg.go b/internal/showcase/rank_history_svg.go index b3f6c3a..3b299ba 100644 --- a/internal/showcase/rank_history_svg.go +++ b/internal/showcase/rank_history_svg.go @@ -309,11 +309,11 @@ func GenerateRankHistorySVG(summaries []ProjectSummary) string { // --- Assemble the full SVG --- var svg strings.Builder - // width/height="100%" makes the SVG fill the browser window while the - // viewBox preserves internal coordinate space and aspect ratio. - fmt.Fprintf(&svg, - `<svg xmlns="http://www.w3.org/2000/svg" width="100%%" height="100%%" viewBox="0 0 %d %d" preserveAspectRatio="xMidYMid meet">`, - svgViewWidth, svgViewHeight) + // width/height="100%" makes the SVG fill the browser window. No viewBox + // or preserveAspectRatio here — the JS rescale() function sets the viewBox + // to the window size and applies a translate+scale transform on #chart so + // the graph always fits the window without letterboxing. + svg.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">`) // Embedded CSS – kept compact but readable. svg.WriteString(`<style> @@ -334,9 +334,11 @@ svg{background:#1a1a2e;font-family:monospace} .ttrow{fill:#bbb;font-size:10px;font-family:monospace} </style>`) - // Solid background rect (belt-and-suspenders for SVG viewers that ignore - // the CSS background property). - fmt.Fprintf(&svg, `<rect width="%d" height="%d" fill="#1a1a2e"/>`, svgViewWidth, svgViewHeight) + // Solid background rect covers the whole viewport regardless of how the + // chart group is transformed. #chart is opened next so that rescale() + // can reposition all chart elements together as a single unit. + svg.WriteString(`<rect width="100%" height="100%" fill="#1a1a2e"/>`) + svg.WriteString(`<g id="chart">`) // Title + subtitle. fmt.Fprintf(&svg, `<text class="title" x="%d" y="28" text-anchor="middle">Project Rank History</text>`, svgViewWidth/2) @@ -368,20 +370,43 @@ svg{background:#1a1a2e;font-family:monospace} <g id="ttbd"></g> </g>`) - // Inline JavaScript for interactivity. + // Close the #chart group before the script so all chart elements are + // contained inside it and can be repositioned together by rescale(). + svg.WriteString(`</g>`) + + // Inline JavaScript for interactivity and dynamic scaling. // PROJECTS is the JSON array; each entry has name, color, and points[]. + // CHART_W / CHART_H are the fixed viewBox coordinates used when designing + // the chart; rescale() maps them to the actual window size at runtime. fmt.Fprintf(&svg, `<script><![CDATA[ var PROJECTS=%s; +var CHART_W=%d, CHART_H=%d; var svgEl=document.querySelector('svg'); +var chartEl=document.getElementById('chart'); var tt=document.getElementById('tt'); var ttbg=document.getElementById('ttbg'); var tttl=document.getElementById('tttl'); var ttbd=document.getElementById('ttbd'); // allPG = plot line groups; allLG = legend entry groups (same count, same order). -var allPG=svgEl.querySelectorAll('.pg'); -var allLG=svgEl.querySelectorAll('.lg'); +// Query inside chartEl so the IDs are scoped to the chart group. +var allPG=chartEl.querySelectorAll('.pg'); +var allLG=chartEl.querySelectorAll('.lg'); var activeIdx=-1; +// rescale keeps the chart group centered and fully visible at any window size. +// It sets a viewBox matching the window and applies a uniform scale+translate +// to #chart so that the fixed CHART_W×CHART_H design fits without letterboxing. +function rescale(){ + var W=window.innerWidth||document.documentElement.clientWidth; + var H=window.innerHeight||document.documentElement.clientHeight; + svgEl.setAttribute('viewBox','0 0 '+W+' '+H); + var s=Math.min(W/CHART_W,H/CHART_H); + var tx=(W-CHART_W*s)/2, ty=(H-CHART_H*s)/2; + chartEl.setAttribute('transform','translate('+tx+','+ty+') scale('+s+')'); +} +window.addEventListener('resize',rescale); +rescale(); + // onEnter is called when the cursor enters a project group or legend entry. // It dims all other groups, shows the tooltip, and marks the project active. function onEnter(idx,evt){ @@ -420,7 +445,7 @@ function onEnter(idx,evt){ for(var i=0;i<allPG.length;i++){ allPG[i].style.opacity=(i===idx)?'1':'0.08'; } - // Bold the hovered line. allPG[idx] is already the correct element. + // Bold the hovered line. allPG[idx].querySelector('.pl').style.strokeWidth='3'; // Highlight hovered legend entry; dim all others. @@ -450,21 +475,22 @@ svgEl.addEventListener('mousemove',function(evt){ if(activeIdx>=0)moveTT(evt); }); -// moveTT repositions the tooltip near the cursor, keeping it inside the viewBox. +// moveTT repositions the tooltip near the cursor, keeping it inside the chart +// coordinate space (CHART_W × CHART_H). chartEl.getScreenCTM() accounts for +// the rescale() transform, so the returned point is already in chart coords. function moveTT(evt){ var pt=svgEl.createSVGPoint(); pt.x=evt.clientX; pt.y=evt.clientY; - var sp=pt.matrixTransform(svgEl.getScreenCTM().inverse()); + var sp=pt.matrixTransform(chartEl.getScreenCTM().inverse()); var w=parseFloat(ttbg.getAttribute('width')); var h=parseFloat(ttbg.getAttribute('height')); var tx=sp.x+14, ty=sp.y-10; - var vw=svgEl.viewBox.baseVal.width, vh=svgEl.viewBox.baseVal.height; - if(tx+w>vw-5)tx=sp.x-w-14; - if(ty+h>vh-5)ty=vh-h-5; + if(tx+w>CHART_W-5)tx=sp.x-w-14; + if(ty+h>CHART_H-5)ty=CHART_H-h-5; if(ty<5)ty=5; tt.setAttribute('transform','translate('+tx+','+ty+')'); } -]]></script>`, string(projectsJSON)) +]]></script>`, string(projectsJSON), svgViewWidth, svgViewHeight) svg.WriteString(`</svg>`) return svg.String() |
