/* ============================================================
ConcreteQuantity Calculator V5.2
- Working steps shown under every result
- Input validation (blank / zero / negative / unreasonable)
- Custom waste % beside 5 / 10 / 15
- Unit toggle converts entered values (incl. yd3 and price)
- Metric mode: m³ inputs, 25 kg / 20 kg bags, 8 m³ mixers, litres
- Currency toggle $ / £
- Mix dry-volume factor 1.54 (stated in UI)
- Rebar assumptions stated (material estimate, laps not counted)
============================================================ */
(function(){
"use strict";
/* ---- calculation engine ---- */
var CC = (function(){
var BAG = { b40: 0.30, b60: 0.45, b80: 0.60 }; // ft3 per bag, QUIKRETE spec
var BAGKG = { k25: 0.0117, k20: 0.0094 }; // m3 per bag, UK 25kg/20kg
function num(v){ v = parseFloat(v); return (isFinite(v) && v > 0) ? v : 0; }
function slabFt3(l,w,d){ l=num(l);w=num(w);d=num(d); return l*w*(d/12); }
function rectFt3(l,w,h){ l=num(l);w=num(w);h=num(h); return l*w*h; }
function wallFt3(l,h,t){ l=num(l);h=num(h);t=num(t); return l*h*(t/12); }
function curbFt3(l,w,h){ l=num(l);w=num(w);h=num(h); return l*(w/12)*(h/12); }
function cylFt3(d,h){ d=num(d);h=num(h); var r=(d/12)/2; return Math.PI*r*r*h; }
function circleSlabFt3(d,t){ d=num(d);t=num(t); var r=d/2; return Math.PI*r*r*(t/12); }
function stairsFt3(s,tr,ri,w){ s=num(s);tr=num(tr);ri=num(ri);w=num(w);
return (tr/12)*(ri/12)*(s*(s+1)/2)*w; }
/* V5.2: epsilon guards true-integer float artifacts (e.g. 22.000000000004 -> 22) */
function bagsNeeded(ft3){ ft3=num(ft3); return {
b40: Math.ceil(ft3/BAG.b40 - 1e-9)||0, b60: Math.ceil(ft3/BAG.b60 - 1e-9)||0, b80: Math.ceil(ft3/BAG.b80 - 1e-9)||0 }; }
function bagsNeededM3(m3){ m3=num(m3); return {
k25: Math.ceil(m3/BAGKG.k25 - 1e-9)||0, k20: Math.ceil(m3/BAGKG.k20 - 1e-9)||0 }; }
function mixForFt3(wet){ wet=num(wet); var dry=wet*1.54;
return { dryFt3:dry, cementFt3:dry/7, sandFt3:dry*2/7, aggFt3:dry*4/7 }; }
function rebarForSlab(l,w,sp){ l=num(l);w=num(w);sp=num(sp);
if(!l||!w||!sp) return { totalLF:0, bars20:0, nW:0, nL:0 };
var aW=Math.floor((w*12)/sp)+1, aL=Math.floor((l*12)/sp)+1;
var lf=aW*l+aL*w; return { totalLF:lf, bars20:Math.ceil(lf/20), nW:aW, nL:aL }; }
return {
num:num, slabFt3:slabFt3, rectFt3:rectFt3, wallFt3:wallFt3, curbFt3:curbFt3,
cylFt3:cylFt3, circleSlabFt3:circleSlabFt3, stairsFt3:stairsFt3,
bagsNeeded:bagsNeeded, bagsNeededM3:bagsNeededM3,
mixForFt3:mixForFt3, rebarForSlab:rebarForSlab,
yd3: function(f){return f/27;}, m3: function(f){return f*0.0283168;},
trucks: function(y){ y=num(y); return Math.ceil(y/10)||0; },
trucksM3: function(m){ m=num(m); return Math.ceil(m/8)||0; },
coverageFt2: function(y,t){ y=num(y);t=num(t); return t? (y*27)/(t/12):0; },
coverageM2: function(m,t){ m=num(m);t=num(t); return t? m/(t/100):0; }
};
})();
/* ---- app state ---- */
var UNITS = 'imp'; // 'imp' | 'met'
var CUR = '$'; // '$' | '£'
var ULEN = { imp:'ft', met:'m' }, USM = { imp:'in', met:'cm' };
function toImpLen(v){ return UNITS==='met' ? v*3.28084 : v; } // m -> ft
function toImpSm(v){ return UNITS==='met' ? v/2.54 : v; } // cm -> in
/* ---- V5 supplemental styles (self-contained, no Customizer edit needed) ---- */
(function(){
if(document.getElementById('ccV5style')) return;
var s=document.createElement('style'); s.id='ccV5style';
s.textContent='.cc-working{background:#f8fafc;border:1px dashed #94a3b8;border-radius:10px;'
+'padding:10px 14px;margin:12px 0 4px;font-size:13.5px;line-height:1.65;color:#334155;overflow-wrap:anywhere}'
+'.cc-working b{color:#1e40af}'
+'.cc-error{background:#fef2f2;border:1px solid #fca5a5;color:#b91c1c;border-radius:10px;'
+'padding:10px 14px;margin:12px 0 4px;font-size:14px;line-height:1.5}'
+'.cc-wasterow{display:flex;gap:8px}.cc-wasterow .wasteSel{flex:1 1 auto;min-width:0}'
+'.cc-wasterow .wasteCustom{flex:0 0 104px;width:104px}'
+'#uCur{border:1px solid #e2e8f0;background:#ffffff;border-radius:10px;padding:9px 14px;'
+'font-size:14px;font-weight:600;cursor:pointer;color:#0f172a;margin-left:6px;white-space:nowrap}'
+'#uCur:hover{background:#f1f5f9}';
document.head.appendChild(s);
})();
/* ---- labeled shape diagrams (like textbook figures) ---- */
var SERIF = 'font-style="italic" font-family="Georgia,serif" font-size="16" fill="#1e40af"';
/* ---- inline stroke icons (no external library) ---- */
var ICONS = {
slab: ' ',
footing: ' ',
wall: ' ',
column: ' ',
posthole: ' ',
circle: ' ',
curb: ' ',
stairs: ' ',
bags: ' ',
mix: ' ',
rebar: ' ',
cost: ' ',
cube: ' ',
copy: ' ',
print: ' ',
share: ' ',
};
function boxSVG(a,b,c,ia,ib,ic){
function dim(t,id,x,y){ return ''+t+' '; }
return ' '
+' '
+' '
+' '
+dim(a,ia,75,104)+dim(b,ib,142,54)+dim(c,ic,92,13)+' ';
}
function tallBoxSVG(a,b,c,ia,ib,ic){
function dim(t,id,x,y){ return ''+t+' '; }
return ' '
+' '
+' '
+' '
+dim(a,ia,81,104)+dim(b,ib,128,56)+dim(c,ic,90,8)+' ';
}
function cylSVG(dLbl,hLbl,di,hi){
function dim(t,id,x,y){ return ''+t+' '; }
return ''
+' '
+' '
+' '
+' '
+' '
+' '
+' '
+dim(dLbl,di,68,18)+dim(hLbl,hi,122,60)+' ';
}
function discSVG(dLbl,tLbl,di,ti){
function dim(t,id,x,y){ return ''+t+' '; }
return ''
+' '
+' '
+' '
+' '
+' '
+' '
+dim(dLbl,di,68,30)+dim(tLbl,ti,126,58)+' ';
}
function stairsSVG(trId,riId){
return ' '
+'tread '
+'riser ';
}
function bagSVG(){
return ' '
+' '
+'80 lb ';
}
function mixSVG(){
return ''
+' '
+' '
+' '
+'cement '
+'sand '
+'aggregate '
+'1 : 2 : 4 ';
}
function rebarSVG(sId){
return ''
+' '
+' '
+'s ';
}
function costSVG(){
return ' '
+'$ '
+' '
+'ft² ';
}
/* ---- mode definitions ----
kind: 'len' (ft/m), 'sm' (in/cm), 'num' (plain number),
'yd3' (yd3/m3), 'usd' (price, $/£) */
var MODES = [
{ id:'slab', tab:'Slab', icon:'slab', title:'Concrete Slab Calculator',
formula:'Volume = Length × Width × Thickness ÷ 27 (cubic yards)',
diagram: boxSVG('l','h','w','l','d','w'),
fields:[ {id:'l',label:'Length',kind:'len',ph:'10'}, {id:'w',label:'Width',kind:'len',ph:'10'},
{id:'d',label:'Thickness',kind:'sm',ph:'4'} ],
vol: function(v){ return CC.slabFt3(toImpLen(v.l), toImpLen(v.w), toImpSm(v.d)); },
work: function(v){ var L=+v.l||0, W=+v.w||0, T=+v.d||0;
if(UNITS==='met'){ var m3=L*W*(T/100);
return fmt(L)+' × '+fmt(W)+' × '+fmt(T/100,3)+' m = '+fmt(m3,3)+' m³ '; }
var f=L*W*(T/12);
return fmt(L)+' × '+fmt(W)+' × ('+fmt(T)+' ÷ 12) = '+fmt(f,1)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'footing', tab:'Footing', icon:'footing', title:'Concrete Footing Calculator',
formula:'Volume = Length × Width × Depth ÷ 27 (cubic yards)',
diagram: boxSVG('l','d','w','l','d','w'),
fields:[ {id:'l',label:'Length',kind:'len',ph:'20'}, {id:'w',label:'Width',kind:'len',ph:'2'},
{id:'d',label:'Depth',kind:'sm',ph:'12'} ],
vol: function(v){ return CC.rectFt3(toImpLen(v.l), toImpLen(v.w), toImpSm(v.d)/12); },
work: function(v){ var L=+v.l||0, W=+v.w||0, D=+v.d||0;
if(UNITS==='met'){ var m3=L*W*(D/100);
return fmt(L)+' × '+fmt(W)+' × '+fmt(D/100,3)+' m = '+fmt(m3,3)+' m³ '; }
var f=L*W*(D/12);
return fmt(L)+' × '+fmt(W)+' × ('+fmt(D)+' ÷ 12) = '+fmt(f,1)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'wall', tab:'Wall', icon:'wall', title:'Concrete Wall Calculator',
formula:'Volume = Length × Height × Thickness ÷ 27 (cubic yards)',
diagram: tallBoxSVG('l','h','t','l','h','t'),
fields:[ {id:'l',label:'Length',kind:'len',ph:'20'}, {id:'h',label:'Height',kind:'len',ph:'8'},
{id:'t',label:'Thickness',kind:'sm',ph:'8'} ],
vol: function(v){ return CC.wallFt3(toImpLen(v.l), toImpLen(v.h), toImpSm(v.t)); },
work: function(v){ var L=+v.l||0, H=+v.h||0, T=+v.t||0;
if(UNITS==='met'){ var m3=L*H*(T/100);
return fmt(L)+' × '+fmt(H)+' × '+fmt(T/100,3)+' m = '+fmt(m3,3)+' m³ '; }
var f=L*H*(T/12);
return fmt(L)+' × '+fmt(H)+' × ('+fmt(T)+' ÷ 12) = '+fmt(f,1)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'column', tab:'Column', icon:'column', title:'Concrete Column / Pier Calculator',
formula:'Volume = π × r² × Height (cylinder)',
diagram: cylSVG('d','h','d','h'),
fields:[ {id:'d',label:'Diameter',kind:'sm',ph:'12'}, {id:'h',label:'Height',kind:'len',ph:'4'},
{id:'n',label:'How many',kind:'num',ph:'1'} ],
vol: function(v){ var n=CC.num(v.n)||1; return CC.cylFt3(toImpSm(v.d), toImpLen(v.h))*n; },
work: function(v){ var D=+v.d||0, H=+v.h||0, N=Math.round(+v.n)||1;
if(UNITS==='met'){ var r=(D/100)/2, m3=Math.PI*r*r*H*N;
return 'π × '+fmt(r,3)+'² × '+fmt(H)+' × '+N+' = '+fmt(m3,3)+' m³ '; }
var r2=(D/12)/2, f=Math.PI*r2*r2*H*N;
return 'π × '+fmt(r2,3)+'² × '+fmt(H)+' × '+N+' = '+fmt(f,2)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'posthole', tab:'Post Holes', icon:'posthole', title:'Post Hole Concrete Calculator',
formula:'Each hole = π × r² × Depth. Great for fences & decks.',
diagram: cylSVG('d','h','d','h'),
fields:[ {id:'d',label:'Hole diameter',kind:'sm',ph:'10'}, {id:'h',label:'Hole depth',kind:'sm',ph:'36'},
{id:'n',label:'Number of holes',kind:'num',ph:'8'} ],
vol: function(v){ var n=CC.num(v.n)||1; return CC.cylFt3(toImpSm(v.d), toImpSm(v.h)/12)*n; },
work: function(v){ var D=+v.d||0, H=+v.h||0, N=Math.round(+v.n)||1;
if(UNITS==='met'){ var r=(D/100)/2, m3=Math.PI*r*r*(H/100)*N;
return 'π × '+fmt(r,3)+'² × '+fmt(H/100,3)+' × '+N+' holes = '+fmt(m3,3)+' m³ '; }
var r2=(D/12)/2, f=Math.PI*r2*r2*(H/12)*N;
return 'π × '+fmt(r2,3)+'² × ('+fmt(H)+' ÷ 12) × '+N+' holes = '+fmt(f,2)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'circle', tab:'Circle Slab', icon:'circle', title:'Circular Slab Calculator',
formula:'Volume = π × r² × Thickness ÷ 27 (cubic yards)',
diagram: discSVG('d','t','d','t'),
fields:[ {id:'d',label:'Diameter',kind:'len',ph:'10'}, {id:'t',label:'Thickness',kind:'sm',ph:'4'} ],
vol: function(v){ return CC.circleSlabFt3(toImpLen(v.d), toImpSm(v.t)); },
work: function(v){ var D=+v.d||0, T=+v.t||0;
if(UNITS==='met'){ var r=D/2, m3=Math.PI*r*r*(T/100);
return 'π × '+fmt(r)+'² × '+fmt(T/100,3)+' = '+fmt(m3,3)+' m³ '; }
var r2=D/2, f=Math.PI*r2*r2*(T/12);
return 'π × '+fmt(r2)+'² × ('+fmt(T)+' ÷ 12) = '+fmt(f,1)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'curb', tab:'Curb', icon:'curb', title:'Concrete Curb Calculator',
formula:'Volume = Length × Width × Height ÷ 27 (cubic yards)',
diagram: boxSVG('l','h','w','l','h','w'),
fields:[ {id:'l',label:'Length',kind:'len',ph:'100'}, {id:'w',label:'Width',kind:'sm',ph:'6'},
{id:'h',label:'Height',kind:'sm',ph:'6'} ],
vol: function(v){ return CC.curbFt3(toImpLen(v.l), toImpSm(v.w), toImpSm(v.h)); },
work: function(v){ var L=+v.l||0, W=+v.w||0, H=+v.h||0;
if(UNITS==='met'){ var m3=L*(W/100)*(H/100);
return fmt(L)+' × '+fmt(W/100,3)+' × '+fmt(H/100,3)+' m = '+fmt(m3,3)+' m³ '; }
var f=L*(W/12)*(H/12);
return fmt(L)+' × ('+fmt(W)+' ÷ 12) × ('+fmt(H)+' ÷ 12) = '+fmt(f,1)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ '; } },
{ id:'stairs', tab:'Stairs', icon:'stairs', title:'Concrete Stairs Calculator (Solid Steps)',
formula:'Solid steps = tread × riser × n(n+1)/2 × width',
diagram: stairsSVG('tr','ri'),
fields:[ {id:'s',label:'Steps',kind:'num',ph:'5'}, {id:'tr',label:'Tread depth',kind:'sm',ph:'11'},
{id:'ri',label:'Riser height',kind:'sm',ph:'7'}, {id:'w',label:'Width',kind:'len',ph:'4'} ],
vol: function(v){ return CC.stairsFt3(CC.num(v.s), toImpSm(v.tr), toImpSm(v.ri), toImpLen(v.w)); },
work: function(v){ var S=+v.s||0, TR=+v.tr||0, RI=+v.ri||0, W=+v.w||0;
if(UNITS==='met'){ var m3=(TR/100)*(RI/100)*(S*(S+1)/2)*W;
return 'Tread × riser × '+S+'×'+(S+1)+'/2 × width = '+fmt(m3,3)+' m³ (solid steps)'; }
var f=(TR/12)*(RI/12)*(S*(S+1)/2)*W;
return 'Tread × riser × '+S+'×'+(S+1)+'/2 × width = '+fmt(f,1)+' ft³ , then ÷ 27 = '+fmt(f/27)+' yd³ (solid steps)'; } },
{ id:'bags', tab:'Bags', icon:'bags', title:'How Many Bags of Concrete?',
formula:'Bags = volume ÷ bag yield (80 lb ≈ 0.60 ft³, 60 lb ≈ 0.45 ft³, 40 lb ≈ 0.30 ft³; metric: 25 kg ≈ 0.0117 m³, 20 kg ≈ 0.0094 m³)',
diagram: bagSVG(),
custom:'bags', empty:'Enter the volume above.',
fields:[ {id:'y',label:'Concrete needed',kind:'yd3',ph:'1.25'} ] },
{ id:'mix', tab:'Mix Ratio', icon:'mix', title:'On-Site Mix Calculator (1:2:4)',
formula:'For site-mixed concrete: 1 part cement : 2 sand : 4 aggregate (by volume). Dry-volume factor 1.54.',
diagram: mixSVG(),
custom:'mix', empty:'Enter the volume above.',
fields:[ {id:'y',label:'Concrete needed',kind:'yd3',ph:'1'} ] },
{ id:'rebar', tab:'Rebar', icon:'rebar', title:'Rebar Calculator for Slabs',
formula:'Bars each way = ⌊dimension ÷ spacing⌋ + 1. Material estimate only, not structural design. Standard bar length 20 ft.',
diagram: rebarSVG('s'),
custom:'rebar', empty:'Enter slab size and spacing above.',
fields:[ {id:'l',label:'Slab length',kind:'len',ph:'20'}, {id:'w',label:'Slab width',kind:'len',ph:'20'},
{id:'s',label:'Bar spacing',kind:'sm',ph:'18'} ] },
{ id:'cost', tab:'Cost & Area', icon:'cost', title:'Cost Estimator & Coverage',
formula:'Cost = volume × price. Coverage: area = volume ÷ thickness.',
diagram: costSVG(),
custom:'cost', empty:'Enter volume above (plus price or thickness).',
optional:['p','t'],
fields:[ {id:'y',label:'Concrete needed',kind:'yd3',ph:'5'},
{id:'p',label:'Price per yd³ ',kind:'usd',ph:'150'},
{id:'t',label:'Thickness (for coverage)',kind:'sm',ph:'4'} ] }
];
/* ---- rendering ---- */
function fmt(v, d){ if(!isFinite(v)) return '0';
var s = v.toFixed(d===undefined?2:d);
if(s.indexOf('.')>=0) s = s.replace(/\.?0+$/,'');
return s; }
function unitFor(kind){
if(kind==='len') return ULEN[UNITS];
if(kind==='sm') return USM[UNITS];
if(kind==='yd3') return UNITS==='met' ? 'm³' : 'yd³';
return '';
}
function fieldHTML(f){
var u = unitFor(f.kind);
var suffix = '';
if(f.kind==='usd') suffix = ''+CUR+' ';
else if(u) suffix = ''+u+' ';
return '
';
}
function volExtrasHTML(){
return '';
}
function bagsHTML(b){
if(UNITS==='met')
return ''+b.k25+' 25 kg bags
'+
'
'+b.k20+' 20 kg bags
';
return ''+b.b80+' 80 lb bags
'+
'
'+b.b60+' 60 lb bags
'+
'
'+b.b40+' 40 lb bags
';
}
function volResultsHTML(ft3, waste, price, mode, panel, vals){
if(!(ft3>0)) return 'Enter your measurements above to see results instantly.
';
var met = UNITS==='met', w = 1+waste/100, f2 = ft3*w;
var yd3 = f2/27, m3 = f2*0.0283168;
var bags = met ? CC.bagsNeededM3(m3) : CC.bagsNeeded(ft3*w);
var loads = met ? CC.trucksM3(m3) : CC.trucks(yd3);
var bagN = met ? bags.k25 : bags.b80;
var bagLbl = met ? '25 kg Bags' : '80 lb Bags';
var loadLbl = met ? '8 m³ mixer load' : 'truck load';
var cost = price>0 ? (met ? m3*price : yd3*price) : 0;
var h = 'You need'+(waste>0?' (incl. '+waste+'% waste) ':'')+' ';
h += '
'
+'
'+ICONS.cube+' '+fmt(yd3)+' Cubic Yards
'
+'
'+ICONS.cube+' '+fmt(m3)+' Cubic Metres
'
+'
'+ICONS.bags+' '+bagN+' '+bagLbl+'
'
+'
';
h += '
Working: '+mode.work(vals)
+(waste>0 ? ' → +'+waste+'% waste allowance' : '')+'
';
h += '
'+fmt(f2,1)+' ft³ '+
''+loads+' '+loadLbl+(loads===1?'':'s')+' ';
if(cost>0) h += ''+CUR+fmt(cost)+' material cost ';
h += '
'+bagsHTML(bags)+actionsHTML()+'
';
panel._summary = mode.title+': '+fmt(yd3)+' cubic yards ('+fmt(f2,1)+' cu ft, '+fmt(m3)+' cu m)'
+(waste>0?' incl. '+waste+'% waste':'')+'. Bags: '+bagN+' x '+(met?'25 kg':'80 lb')
+'. via concretequantity.com/concrete-calculator/';
return h;
}
function actionsHTML(){
return ''
+''+ICONS.copy+'Copy '
+''+ICONS.print+'Print '
+''+ICONS.share+'Share
';
}
function doAction(act, btn, panel){
var txt = panel._summary || 'Concrete Calculator: concretequantity.com/concrete-calculator/';
if(act==='print'){ window.print(); return; }
if(act==='share' && navigator.share){ navigator.share({title:'Concrete Calculator', text:txt}).catch(function(){}); return; }
var done = function(ok){
var sp = btn.querySelector('span'), old = sp.textContent;
btn.classList.add('ok'); sp.textContent = ok ? 'Copied' : 'Copy failed';
setTimeout(function(){ btn.classList.remove('ok'); sp.textContent = old; }, 1600);
};
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(txt).then(function(){ done(true); }, function(){ done(false); });
} else {
var ta = document.createElement('textarea');
ta.value = txt; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try{ done(document.execCommand('copy')); }catch(e){ done(false); }
document.body.removeChild(ta);
}
}
function hlDim(panel, fid, on){
panel.querySelectorAll('.cc-diagram [data-dim]').forEach(function(g){
g.classList.toggle('hl', !!(on && g.getAttribute('data-dim')===fid));
});
}
/* ---- validation ---- */
var VLIMIT = { len:5000, sm:500, num:100000, yd3:100000, usd:1000000 }; // sanity caps, imperial
function validateFields(mode, vals){
var opt = mode.optional||[];
var empty = ''+(mode.empty||'Enter your measurements above to see results instantly.')+'
';
for(var i=0;i=0;
if(raw===''||raw==null){ if(!isOpt) return empty; continue; }
var x=parseFloat(raw);
if(!isFinite(x)) return 'Please enter a number for “'+f.label.replace(/<[^>]*>/g,'')+'”.
';
if(x<0) return '“'+f.label.replace(/<[^>]*>/g,'')+'” cannot be negative.
';
if(x===0){ if(!isOpt) return '“'+f.label.replace(/<[^>]*>/g,'')+'” must be greater than zero.
'; continue; }
var imp=x;
if(f.kind==='len'&&UNITS==='met') imp=x*3.28084;
else if(f.kind==='sm'&&UNITS==='met') imp=x/2.54;
else if(f.kind==='yd3'&&UNITS==='met') imp=x*1.30795;
if(imp>(VLIMIT[f.kind]||100000))
return '“'+f.label.replace(/<[^>]*>/g,'')+'” looks unusually large. Please check the value.
';
}
return null;
}
/* ---- per-panel calculation ---- */
function calcPanel(mode, panel){
var vals = {};
panel.querySelectorAll('input[data-fid]').forEach(function(i){ vals[i.getAttribute('data-fid')] = i.value.trim(); });
var box = panel.querySelector('.cc-out');
var verr = validateFields(mode, vals);
if(verr){ box.innerHTML = verr; panel._summary=''; return; }
var met = UNITS==='met', h = '';
if(mode.custom==='bags'){
var yb=CC.num(vals.y), m3b = met? yb : yb*0.764555, ft3b = met? yb*35.3147 : yb*27;
var bb = met? CC.bagsNeededM3(m3b) : CC.bagsNeeded(ft3b);
var bw = met ? fmt(yb)+' m³ ÷ 0.0117 m³ = '+bb.k25+' × 25 kg bags (÷ 0.0094 = '+bb.k20+' × 20 kg)'
: fmt(yb)+' yd³ × 27 = '+fmt(ft3b,1)+' ft³ ÷ 0.60 ft³ = '+bb.b80+' × 80 lb bags ';
h = 'Bags needed '+bagsHTML(bb)+
'
Working: '+bw+'
'+
'
Based on '+fmt(ft3b,1)+' ft³ ('+fmt(m3b,3)+' m³). Bag yields: '+
(met?'25 kg ≈ 0.0117 m³, 20 kg ≈ 0.0094 m³.':'80 lb ≈ 0.60 ft³, 60 lb ≈ 0.45 ft³, 40 lb ≈ 0.30 ft³.')+'
'+
actionsHTML()+'
';
panel._summary = 'Bags for '+fmt(yb)+(met?' cubic metres':' cubic yards')+': '
+(met? bb.k25+' x 25 kg, '+bb.k20+' x 20 kg' : bb.b80+' x 80 lb, '+bb.b60+' x 60 lb, '+bb.b40+' x 40 lb')
+'. via concretequantity.com/concrete-calculator/';
} else if(mode.custom==='mix'){
var ym=CC.num(vals.y), wetFt3 = met? ym*35.3147 : ym*27;
var mx = CC.mixForFt3(wetFt3), body='', mw='';
if(met){
var cemKg = mx.cementFt3*0.0283168*1440, b25 = Math.ceil(cemKg/25)||0;
var sM3 = mx.sandFt3*0.0283168, aM3 = mx.aggFt3*0.0283168, wL = Math.round(cemKg*0.5);
body = ''+b25+' cement bags (25 kg) '+
''+fmt(sM3)+' m³ sand '+
''+fmt(aM3)+' m³ aggregate '+
''+wL+' litres water (approx) ';
mw = 'Dry volume = '+fmt(ym)+' × 1.54 = '+fmt(mx.dryFt3*0.0283168,2)+' m³. '+
'Cement = ÷ 7 = '+fmt(mx.cementFt3*0.0283168,3)+' m³ ≈ '+fmt(cemKg,0)+' kg ≈ '+b25+' × 25 kg bags .';
panel._summary = 'Site mix (1:2:4) for '+fmt(ym)+' cubic metres: '+b25+' cement bags (25 kg), '
+fmt(sM3)+' m3 sand, '+fmt(aM3)+' m3 aggregate. via concretequantity.com/concrete-calculator/';
} else {
var cb = Math.ceil(mx.cementFt3)||0, sY = mx.sandFt3/27, aY = mx.aggFt3/27, wg = cb*6;
body = ''+cb+' cement bags (94 lb) '+
''+fmt(sY)+' yd³ sand '+
''+fmt(aY)+' yd³ aggregate '+
''+wg+' gal water (approx) ';
mw = 'Dry volume = '+fmt(ym)+' yd³ × 27 × 1.54 = '+fmt(mx.dryFt3,1)+' ft³. '+
'Cement = ÷ 7 = '+fmt(mx.cementFt3,1)+' ft³ ≈ '+cb+' bags (94 lb) (1 ft³ ≈ 1 bag).';
panel._summary = 'Site mix (1:2:4) for '+fmt(ym)+' cubic yards: '+cb
+' cement bags, '+fmt(sY)+' yd3 sand, '+fmt(aY)+' yd3 aggregate. via concretequantity.com/concrete-calculator/';
}
h = 'Site-mix materials (1:2:4) '+body+'
'+
'
Working: '+mw+'
'+actionsHTML()+'
';
} else if(mode.custom==='rebar'){
var rb = CC.rebarForSlab(toImpLen(vals.l), toImpLen(vals.w), toImpSm(vals.s));
if(rb.totalLF>0){
var big, sub, rw;
if(met){
var Lm=+vals.l||0, Wm=+vals.w||0, Scm=+vals.s||0;
big = fmt(rb.totalLF*0.3048,0)+' metres ';
sub = ''+rb.bars20+' × 20-ft bars ';
rw = 'Across '+fmt(Wm)+' m @ '+fmt(Scm)+' cm: ⌊'+fmt(Wm*100,0)+' ÷ '+fmt(Scm)+'⌋ + 1 = '+rb.nW+
' bars × '+fmt(Lm)+' m. Across '+fmt(Lm)+' m: '+rb.nL+' bars × '+fmt(Wm)+
' m = '+fmt(rb.totalLF*0.3048,0)+' m ('+fmt(rb.totalLF,0)+' ft).';
} else {
var Li=+vals.l||0, Wi=+vals.w||0, Si=+vals.s||0;
big = fmt(rb.totalLF,0)+' linear feet ';
sub = ''+rb.bars20+' × 20-ft bars ';
rw = 'Across '+fmt(Wi)+' ft @ '+fmt(Si)+' in: ⌊'+fmt(Wi*12,0)+' ÷ '+fmt(Si)+'⌋ + 1 = '+rb.nW+
' bars × '+fmt(Li)+' ft. Across '+fmt(Li)+' ft: '+rb.nL+' bars × '+fmt(Wi)+
' ft. Total = '+fmt(rb.totalLF,0)+' ft .';
}
h = 'Rebar estimate '+
'
'+big+'
'+
'
'+sub+'
'+
'
Working: '+rw+'
'+
'
Material estimate only, not structural design. Bars run the full slab length both ways; laps are not counted, so add about 10% extra for laps and waste.
'+
actionsHTML()+'
';
panel._summary = 'Rebar estimate: '+fmt(rb.totalLF,0)+' linear feet ('+rb.bars20
+' x 20-ft bars). via concretequantity.com/concrete-calculator/';
} else h = ''+mode.empty+'
';
} else if(mode.custom==='cost'){
var yc=CC.num(vals.y), pc=CC.num(vals.p), tc=vals.t, tImp=toImpSm(tc);
h = 'Estimate ';
var parts=[], okc=false, sumc='';
if(yc>0 && pc>0){
h += 'Material cost: '+CUR+fmt(yc*pc)+' ';
parts.push(fmt(yc)+(met?' m³':' yd³')+' × '+CUR+fmt(pc)+' = '+CUR+fmt(yc*pc)+' ');
sumc = 'cost '+CUR+fmt(yc*pc); okc=true;
}
if(yc>0 && tImp>0){
if(met){ var m2=CC.coverageM2(yc, +tc);
h += 'Covers: '+fmt(m2,0)+' m² at '+fmt(+tc)+' cm thick ';
parts.push('Coverage = '+fmt(yc)+' ÷ ('+fmt(+tc)+' ÷ 100) = '+fmt(m2,0)+' m² ');
sumc += (sumc?', ':'')+'covers '+fmt(m2,0)+' sq m';
} else { var f2c=CC.coverageFt2(yc,tImp);
h += 'Covers: '+fmt(f2c,0)+' ft² at '+fmt(+tc)+' '+USM[UNITS]+' thick ';
parts.push('Coverage = '+fmt(yc)+' × 27 ÷ ('+fmt(+tc)+' ÷ 12) = '+fmt(f2c,0)+' ft² ');
sumc += (sumc?', ':'')+'covers '+fmt(f2c,0)+' sq ft';
}
okc=true;
}
h += '
';
if(okc){
if(parts.length) h += '
Working: '+parts.join(' ')+'
';
h += actionsHTML();
panel._summary = 'Concrete estimate: '+fmt(yc)+(met?' cubic metres':' cubic yards')
+(sumc? ', '+sumc : '')+'. via concretequantity.com/concrete-calculator/';
} else h += '
'+mode.empty+'
';
h += '
';
} else {
var vft3 = mode.vol(vals);
var wsel = panel.querySelector('.wasteSel'), wcus = panel.querySelector('.wasteCustom');
var wv = wcus ? parseFloat(wcus.value) : NaN;
var waste = (isFinite(wv)&&wv>0) ? Math.min(wv,100) : (wsel ? (parseFloat(wsel.value)||0) : 0);
var praw = panel.querySelector('.priceInp').value.trim(), price=0;
if(praw!==''){
var px=parseFloat(praw);
if(!isFinite(px)||px<0){ box.innerHTML='Price must be zero or greater.
'; panel._summary=''; return; }
if(px>1000000){ box.innerHTML='Price looks unusually large. Please check it.
'; panel._summary=''; return; }
price=px;
}
h = volResultsHTML(vft3, waste, price, mode, panel, vals);
}
box.innerHTML = h;
}
/* ---- panel + tabs ---- */
function buildPanel(m, first){
var p = document.createElement('div');
p.className = 'cc-panel'+(first?' on':''); p.id = 'ccp-'+m.id;
var inner = ''
+(m.diagram?'
'+m.diagram+'
':'')+'
';
m.fields.forEach(function(f){ inner += fieldHTML(f); });
inner += '
';
if(!m.custom) inner += volExtrasHTML();
inner += '
';
p.innerHTML = inner;
p.querySelectorAll('input,select').forEach(function(el){
el.addEventListener('input', function(){ calcPanel(m, p); });
});
p.querySelectorAll('input[data-fid]').forEach(function(inp){
inp.addEventListener('focus', function(){ hlDim(p, inp.getAttribute('data-fid'), true); });
inp.addEventListener('blur', function(){ hlDim(p, inp.getAttribute('data-fid'), false); });
});
p.addEventListener('click', function(e){
var b = e.target && e.target.closest ? e.target.closest('.cc-act') : null;
if(b) doAction(b.getAttribute('data-act'), b, p);
});
calcPanel(m, p);
return p;
}
function activateTab(id){
var tabs = document.getElementById('ccTabs'), panels = document.getElementById('ccPanels');
if(!tabs || !panels) return false;
var ok = false;
MODES.forEach(function(m){ if(m.id===id) ok = true; });
if(!ok) return false;
tabs.querySelectorAll('button').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-mid')===id); });
panels.querySelectorAll('.cc-panel').forEach(function(p){ p.classList.toggle('on', p.id==='ccp-'+id); });
return true;
}
function buildTabs(){
var tabs = document.getElementById('ccTabs'), panels = document.getElementById('ccPanels');
if(!tabs || !panels) return;
MODES.forEach(function(m, i){
var tb = document.createElement('button');
tb.type='button'; tb.innerHTML = (ICONS[m.icon]||'')+''+m.tab+' ';
tb.setAttribute('role','tab'); tb.setAttribute('data-mid', m.id);
if(i===0) tb.classList.add('on');
tb.onclick = function(){ activateTab(m.id); try{ history.replaceState(null,'','#'+m.id); }catch(e){} };
tabs.appendChild(tb);
panels.appendChild(buildPanel(m, i===0));
});
var h = (location.hash||'').replace('#','');
if(h) activateTab(h);
window.addEventListener('hashchange', function(){ activateTab((location.hash||'').replace('#','')); });
}
function buildSingle(){
var id = window.CC_SINGLE, m = null;
MODES.forEach(function(x){ if(x.id===id) m = x; });
if(!m) return;
var c = document.getElementById('ccSingle');
if(!c) return;
c.appendChild(buildPanel(m, true));
}
/* ---- units + currency ---- */
function recalcAll(){
MODES.forEach(function(m){ var p = document.getElementById('ccp-'+m.id); if(p) calcPanel(m, p); });
}
function setUnits(u){
if(u===UNITS) return;
document.querySelectorAll('#ccPanels input[data-kind], #ccSingle input[data-kind]').forEach(function(inp){
var v = parseFloat(inp.value); if(!isFinite(v)) return;
var k = inp.getAttribute('data-kind');
if(k==='len') inp.value = fmt(u==='met' ? v*0.3048 : v*3.28084);
else if(k==='sm') inp.value = fmt(u==='met' ? v*2.54 : v/2.54);
else if(k==='yd3') inp.value = fmt(u==='met' ? v*0.764555 : v*1.30795);
else if(k==='usd') inp.value = fmt(u==='met' ? v*1.30795 : v/1.30795);
});
UNITS = u;
var ui=document.getElementById('uImp'), um=document.getElementById('uMet');
if(ui) ui.classList.toggle('on', u==='imp');
if(um) um.classList.toggle('on', u==='met');
document.querySelectorAll('#ccPanels span[data-u], #ccSingle span[data-u]').forEach(function(sp){
sp.textContent = unitFor(sp.getAttribute('data-u'));
});
document.querySelectorAll('#ccPanels .upl, #ccSingle .upl').forEach(function(sp){
sp.textContent = u==='met' ? 'm³' : 'yd³';
});
recalcAll();
}
function setCur(c){
CUR = c;
var b=document.getElementById('uCur');
if(b) b.textContent = (c==='£' ? '£ GBP' : '$ USD');
document.querySelectorAll('.curSym').forEach(function(s){ s.textContent = c; });
recalcAll();
}
function buildCurToggle(){
var u=document.querySelector('.cc-units');
if(!u || document.getElementById('uCur')) return;
var b=document.createElement('button');
b.type='button'; b.id='uCur'; b.textContent='$ USD';
b.setAttribute('aria-label','Toggle currency between US dollars and British pounds');
b.onclick=function(){ setCur(CUR==='$' ? '£' : '$'); };
u.appendChild(b);
}
/* ---- init ---- */
var _uImp=document.getElementById('uImp'), _uMet=document.getElementById('uMet');
if(_uImp) _uImp.onclick = function(){ setUnits('imp'); };
if(_uMet) _uMet.onclick = function(){ setUnits('met'); };
buildCurToggle();
if(window.CC_SINGLE){ buildSingle(); } else { buildTabs(); }
})();