Staafgrafiek met tabel New
Accessible SVG bar chart (savings per year) built from a plain HTML table: bars grow when in view (IntersectionObserver), per-bar tooltip on hover and focus, arrow keys between bars and a toggle to the table view. Without JS or with reduced motion you keep the table or a static chart.
Zonnepanelen · fam. De Vries, Zwolle
Besparing per jaar
| Jaar | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 |
|---|---|---|---|---|---|---|---|
| Besparing | € 312 | € 498 | € 611 | € 702 | € 655 | € 840 | € 918 |
| t.o.v. vorig jaar | nvt | +60% | +23% | +15% | -7% | +28% | +9% |
How to use it
- Paste the HTML where the numbers & charts should appear.
- Add the CSS to your stylesheet. All classes are prefixed, so nothing clashes with your own styles.
- Add the JavaScript and call init() with the element that contains the HTML.
<!-- Staafgrafiek met tabel · NedDev · eigen werk. -->
<div class="jouw-container">
<figure class="sbg"><div class="sbg__hd"><div><p class="sbg__k">Zonnepanelen · fam. De Vries, Zwolle</p><p class="sbg__t">Besparing per jaar</p></div><div class="sbg__sw" role="group" aria-label="Weergave"><button type="button" class="sbg__b" aria-pressed="true" data-v="g">Grafiek</button><button type="button" class="sbg__b" aria-pressed="false" data-v="t">Tabel</button></div></div>
<div class="sbg__plot"><div class="sbg__tip" hidden></div></div>
<table class="sbg__tbl"><caption class="sbg__sr">Besparing per jaar in euro's, zonnepanelen familie De Vries, Zwolle</caption><thead><tr><th scope="row">Jaar</th><th scope="col">2019</th><th scope="col">2020</th><th scope="col">2021</th><th scope="col">2022</th><th scope="col">2023</th><th scope="col">2024</th><th scope="col">2025</th></tr></thead><tbody><tr><th scope="row">Besparing</th><td data-v="312">€ 312</td><td data-v="498">€ 498</td><td data-v="611">€ 611</td><td data-v="702">€ 702</td><td data-v="655">€ 655</td><td data-v="840">€ 840</td><td data-v="918">€ 918</td></tr><tr><th scope="row">t.o.v. vorig jaar</th><td>nvt</td><td>+60%</td><td>+23%</td><td>+15%</td><td>-7%</td><td>+28%</td><td>+9%</td></tr></tbody></table>
<figcaption class="sbg__f">Bron: omvormer-data · stroomprijs € 0,28 per kWh</figcaption></figure>
</div>/* Staafgrafiek met tabel · NedDev · eigen werk. */
.sbg{position:absolute;inset:0;margin:0;box-sizing:border-box;padding:14px 16px 10px;background:#fff;color:#0a0a0a;font-family:'Switzer',system-ui,sans-serif;display:flex;flex-direction:column;gap:6px}
.sbg__hd{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.sbg__k{margin:0;font:500 10px/1.3 'JetBrains Mono',monospace;letter-spacing:.04em;color:#5f5e5a}
.sbg__t{margin:2px 0 0;font-size:15px;font-weight:650;line-height:1.2}
.sbg__sw{display:flex;border:1px solid rgba(10,10,10,.18);border-radius:999px;padding:2px;flex:none}
.sbg__b{font:600 11px 'Switzer',system-ui,sans-serif;border:0;background:none;color:#5f5e5a;padding:4px 10px;border-radius:999px;cursor:pointer;transition:background .15s,color .15s}
.sbg__b:hover{color:#0a0a0a}
.sbg__b[aria-pressed="true"]{background:#0a0a0a;color:#fff}
.sbg__b:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.sbg__plot{position:relative;flex:1;min-height:0}
.sbg__svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.sbg__grid{stroke:rgba(10,10,10,.08);stroke-width:1}
.sbg__base{stroke:rgba(10,10,10,.35);stroke-width:1}
.sbg__ax{font:500 10px 'JetBrains Mono',monospace;fill:#6b6a66}
.sbg__val{font:700 11px 'Switzer',system-ui,sans-serif;fill:#0a0a0a;opacity:0;transition:opacity .3s .9s}
.sbg__col{outline:none;cursor:default}
.sbg__hit{fill:transparent;stroke:none}
.sbg__col:focus-visible .sbg__hit{fill:rgba(10,10,10,.04);stroke:#0a0a0a;stroke-width:1.5}
.sbg__bar{fill:#F5480F;transform-box:fill-box;transform-origin:50% 100%;transform:scaleY(0);transition:transform .7s cubic-bezier(.2,.7,.2,1) calc(var(--i)*70ms),opacity .15s}
.sbg.is-in .sbg__bar{transform:none}
.sbg.is-in .sbg__val{opacity:1}
.sbg.is-hover .sbg__col:not(.is-on) .sbg__bar{opacity:.35}
.sbg.is-hover .sbg__val{opacity:0;transition-delay:0s}
.sbg__tip{position:absolute;z-index:2;pointer-events:none;transform:translate(-50%,-100%);background:#0a0a0a;color:#fff;border-radius:4px;padding:5px 8px;font:500 11px/1.35 'Switzer',system-ui,sans-serif;white-space:nowrap;box-shadow:0 4px 12px rgba(10,10,10,.18)}
.sbg__tip strong{display:block;font-size:13px;font-variant-numeric:tabular-nums}
.sbg__tip span{color:#cfcdc9}
.sbg__sr,.sbg__tbl{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
.sbg.is-tbl .sbg__plot{display:none}
.sbg.is-tbl .sbg__tbl{position:static;width:100%;height:auto;clip-path:none;white-space:normal;overflow:visible;border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums;margin:auto 0}
.sbg__tbl th,.sbg__tbl td{padding:6px 3px;border-bottom:1px solid rgba(10,10,10,.1);text-align:right}
.sbg__tbl th[scope="row"]{text-align:left;font-weight:600;color:#5f5e5a;font-size:11px}
.sbg__tbl thead th{font:600 11px 'JetBrains Mono',monospace}
.sbg__f{font:500 9.5px 'JetBrains Mono',monospace;color:#6b6a66}
@media(max-width:420px){.sbg{padding:10px 12px 8px}.sbg__f{display:none}.sbg__tbl th[scope="row"]{font-size:10px}.sbg.is-tbl .sbg__tbl{font-size:10.5px}}
@media(prefers-reduced-motion:reduce){.sbg__bar,.sbg__val{transition:none}}// Staafgrafiek met tabel · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.sbg');if(!w)return;var NS='http://www.w3.org/2000/svg';
function el(t,a){var e=document.createElementNS(NS,t);for(var k in a)e.setAttribute(k,a[k]);return e;}
function eur(v){return '€ '+v.toLocaleString('nl-NL');}
var heads=w.querySelectorAll('.sbg__tbl thead th[scope="col"]'),vals=w.querySelectorAll('.sbg__tbl tbody tr:first-child td');
var rows=[].map.call(heads,function(h,i){return {l:h.textContent.trim(),v:+vals[i].dataset.v};});
var W=380,H=176,L=38,R=8,T=16,B=22,max=1000,pw=W-L-R,ph=H-T-B,n=rows.length,step=pw/n,bw=Math.min(30,step*.5);
function delta(i){if(!i)return '';var p=Math.round((rows[i].v-rows[i-1].v)/rows[i-1].v*100);return (p>0?'+':'')+p+'% t.o.v. '+rows[i-1].l;}
var plot=w.querySelector('.sbg__plot'),tip=w.querySelector('.sbg__tip');
var svg=el('svg',{viewBox:'0 0 '+W+' '+H,'class':'sbg__svg',role:'group','aria-label':'Staafgrafiek, pijltjestoetsen lezen per jaar'});
[0,250,500,750,1000].forEach(function(t){var y=T+ph-t/max*ph;svg.appendChild(el('line',{x1:L,x2:W-R,y1:y,y2:y,'class':t?'sbg__grid':'sbg__base'}));var tx=el('text',{x:L-6,y:y+3,'class':'sbg__ax','text-anchor':'end'});tx.textContent=t.toLocaleString('nl-NL');svg.appendChild(tx);});
var cols=rows.map(function(d,i){var cx=L+step*i+step/2,h=d.v/max*ph,x=cx-bw/2,y=T+ph-h,r=Math.min(4,h);
var g=el('g',{'class':'sbg__col',tabindex:'0',role:'img','aria-label':d.l+': '+eur(d.v)+(i?', '+delta(i):'')});g.style.setProperty('--i',i);
g.appendChild(el('rect',{x:cx-step/2+1,y:T-4,width:step-2,height:ph+4,rx:3,'class':'sbg__hit'}));
g.appendChild(el('path',{'class':'sbg__bar',d:'M'+x+','+(T+ph)+'V'+(y+r)+'Q'+x+','+y+' '+(x+r)+','+y+'H'+(x+bw-r)+'Q'+(x+bw)+','+y+' '+(x+bw)+','+(y+r)+'V'+(T+ph)+'Z'}));
var lb=el('text',{x:cx,y:H-6,'class':'sbg__ax','text-anchor':'middle'});lb.textContent=d.l;g.appendChild(lb);
if(i===n-1){var vl=el('text',{x:cx,y:y-6,'class':'sbg__val','text-anchor':'middle'});vl.textContent=eur(d.v);g.appendChild(vl);}
svg.appendChild(g);return {g:g,cx:cx,y:y,d:d,i:i};});
plot.insertBefore(svg,tip);
function show(c){w.classList.add('is-hover');cols.forEach(function(x){x.g.classList.toggle('is-on',x===c);});
tip.innerHTML='<strong>'+eur(c.d.v)+'</strong><span>'+c.d.l+(c.i?' · '+delta(c.i):'')+'</span>';tip.hidden=false;
var m=svg.getScreenCTM(),pr=plot.getBoundingClientRect(),pt=svg.createSVGPoint();pt.x=c.cx;pt.y=c.y;var p=pt.matrixTransform(m);
var tw=tip.offsetWidth,left=Math.max(tw/2,Math.min(pr.width-tw/2,p.x-pr.left)),top=p.y-pr.top-8;tip.style.left=left+'px';tip.style.top=Math.max(tip.offsetHeight,top)+'px';}
function hide(){w.classList.remove('is-hover');tip.hidden=true;cols.forEach(function(x){x.g.classList.remove('is-on');});}
cols.forEach(function(c){c.g.addEventListener('pointerenter',function(){show(c);});c.g.addEventListener('focus',function(){show(c);});c.g.addEventListener('blur',hide);
c.g.addEventListener('keydown',function(e){var k=e.key,j=k==='ArrowRight'?c.i+1:k==='ArrowLeft'?c.i-1:k==='Home'?0:k==='End'?n-1:-2;if(k==='Escape'){hide();return;}if(j>-2){e.preventDefault();cols[Math.max(0,Math.min(n-1,j))].g.focus();}});});
svg.addEventListener('pointerleave',hide);
w.querySelectorAll('.sbg__b').forEach(function(b){b.addEventListener('click',function(){w.querySelectorAll('.sbg__b').forEach(function(x){x.setAttribute('aria-pressed',x===b?'true':'false');});hide();w.classList.toggle('is-tbl',b.dataset.v==='t');});});
if(matchMedia('(prefers-reduced-motion:reduce)').matches||!('IntersectionObserver' in window)){w.classList.add('is-in');}else{var io=new IntersectionObserver(function(es){if(es[0].isIntersecting){w.classList.add('is-in');io.disconnect();}},{threshold:.35});io.observe(w);}
}
init(document.querySelector('.jouw-container'));
More numbers & charts
Seen something you like?
Add it to your selection and send it over. We build your site around it, in your brand.
Get in touch →