Sia Partners · BID Sandbox

Live survey-mode demo — drop a CSV, BID renders a deliverable in your browser.

Everything runs client-side. No upload to a server. Parsing, profiling, and rendering all happen in this browser tab; the resulting deliverable opens in a new tab with the same editable layer + chart controls as the static showcase.
← Back to showcase

01 · Upload

Need a sample? Download the inaugural B2B AI Survey CSV (n=200), drop it back in, see the deliverable render. Or upload your own — schema check below tells you what's missing.

Parsed CSV summary

Rows
Columns
Schema match

Couldn't parse the CSV

02 · What gets rendered

A nine-section client-ready deliverable: at-a-glance summary · maturity distribution · deployment funnel · top focus areas · spend-by-company-size · industry view · cloud platform mix · Sia's read (3-paragraph narrative) · methodology. Every prose block is editable. Every chart has top-N / sort / type controls. The browser tab opens once you click Render deliverable.

`; const CHART_CSS = ` `; const CHART_JS = ` + (n/1e9).toFixed(2) + 'B'; if (Math.abs(n) >= 1e6) return '; /* ─── CSV parsing + profiling (mirror of render-survey.ts) ─── */ function splitRow(line) { const out = []; let cur = ''; let inQ = false; for (const ch of line) { if (ch === '"') { inQ = !inQ; continue; } if (ch === ',' && !inQ) { out.push(cur); cur = ''; continue; } cur += ch; } out.push(cur); return out; } function parseCsv(text) { const lines = text.replace(/\r/g, '').trim().split('\n'); if (lines.length < 2) throw new Error('CSV has fewer than 2 lines (no data rows).'); const headers = splitRow(lines[0]); const rows = lines.slice(1).map(line => { const cells = splitRow(line); const o = {}; headers.forEach((h, i) => { o[h] = (cells[i] || '').trim(); }); return o; }); return { headers, rows }; } function tally(rows, col) { const m = new Map(); for (const r of rows) { const v = (r[col] || '').trim() || '(blank)'; m.set(v, (m.get(v) || 0) + 1); } return [...m.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count); } function numericCol(rows, col) { return rows.map(r => parseFloat(r[col])).filter(n => Number.isFinite(n)); } function meanOf(xs) { return xs.length ? xs.reduce((s, n) => s + n, 0) / xs.length : 0; } function medianOf(xs) { if (!xs.length) return 0; const s = [...xs].sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 === 0 ? (s[m - 1] + s[m]) / 2 : s[m]; } function fmtUsd(v) { if (v >= 1e9) return '$' + (v / 1e9).toFixed(2) + 'B'; if (v >= 1e6) return '$' + (v / 1e6).toFixed(1) + 'M'; if (v >= 1e3) return '$' + (v / 1e3).toFixed(0) + 'K'; return '$' + v.toFixed(0); } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function dataAttrEsc(s) { return String(s).replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } const EXPECTED_COLS = [ 'Company_ID','Company_Name','Industry','Region','Company_Size','Employees', 'Annual_Revenue_USD_M','Primary_AI_Focus','AI_Maturity_1_5','Data_Readiness_1_5', 'Executive_Sponsorship_1_5','AI_Governance_Maturity_1_5','Deployment_Stage', 'AI_Use_Case_Count','Dedicated_AI_Team_Size','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Estimated_Annual_Value_Created_USD','Expected_Payback_Months','Implementation_Risk_1_5', 'Budget_Growth_Next_12M_Pct','Top_AI_Tools_Used','Primary_Cloud_Platform', 'Primary_Buyer','Procurement_Model' ]; const REQUIRED_COLS = [ 'Industry','Region','Company_Size','Primary_AI_Focus','AI_Maturity_1_5', 'Deployment_Stage','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Implementation_Risk_1_5','Budget_Growth_Next_12M_Pct','Primary_Cloud_Platform' ]; /* ─── Editable wrapper (matches makeEditable from editable-layer.ts) ─── */ function makeEditable(blockId, sectionId, template, content) { return '' + content + ''; } /* ─── Chart wrapper (matches wrapChart from chart-controls.ts) ─── */ let __chartCounter = 0; function nextChartId(prefix) { __chartCounter += 1; return prefix + '-' + __chartCounter; } function wrapChart(spec, initialSvg, titleHtml, subtitleHtml) { const specB64 = btoa(unescape(encodeURIComponent(JSON.stringify(spec)))); return '
' + '
' + titleHtml + '
' + '
' + subtitleHtml + '
' + '
' + '
' + initialSvg + '
' + '
'; } /* ─── Horizontal-bars renderer (mirror of render-survey.ts) ─── */ function horizontalBarsHtml(opts) { const VB_W = 800, BAR_H = 24, BAR_GAP = 6, BAR_X_START = 220, BAR_X_END = 660; const BAR_AREA_TOP = 28; const BAR_AREA_BOTTOM = BAR_AREA_TOP + opts.rows.length * BAR_H + (opts.rows.length - 1) * BAR_GAP; const X_AXIS_Y = BAR_AREA_BOTTOM + 22, AXIS_TITLE_Y = X_AXIS_Y + 20, VB_H = AXIS_TITLE_Y + 10; const rawMax = Math.max(...opts.rows.map(r => r.value), 1) * 1.15; const mag = Math.pow(10, Math.floor(Math.log10(rawMax))); const xMax = Math.ceil(rawMax / mag) * mag; const xScale = v => BAR_X_START + (v / xMax) * (BAR_X_END - BAR_X_START); const bars = opts.rows.map((r, i) => { const y = BAR_AREA_TOP + i * (BAR_H + BAR_GAP); const w = Math.max(xScale(r.value) - BAR_X_START, 1); const labelY = y + BAR_H / 2 + 4; return '' + esc(r.label) + ': ' + esc(opts.fmtValue(r.value)) + '' + '' + esc(opts.fmtValue(r.value)) + (r.sublabel ? ' · ' + esc(r.sublabel) : '') + '' + '' + esc(r.label) + ''; }).join(''); const ticks = [0, xMax / 2, xMax].map(t => { const x = xScale(t); return '' + esc(opts.fmtValue(t)) + ''; }).join(''); const initialSvg = '' + bars + '' + ticks + '' + esc(opts.axisLabel) + '' + ''; const titleHtml = makeEditable(opts.blockIdBase + '-title', opts.sectionId, 'subheading', opts.title); const subtitleHtml = makeEditable(opts.blockIdBase + '-subtitle', opts.sectionId, 'caveat-band', opts.subtitle); const spec = { chartId: nextChartId(opts.blockIdBase), type: 'horizontal-bars', rows: opts.rows.map(r => ({ label: r.label, value: r.value, sublabel: r.sublabel })), valueFormat: opts.valueFormat, axisLabel: opts.axisLabel, color: '#00A2A3' }; return wrapChart(spec, initialSvg, titleHtml, subtitleHtml); } /* ─── Section builders ────────────────────────────────────── */ function buildCover(title, subline) { return '
' + '
Sia Partners · BID Framework · Survey Read · Sandbox-rendered
' + '

' + makeEditable('cover-title', '00-cover', 'headline', esc(title)) + '

' + '
' + makeEditable('cover-subline', '00-cover', 'subheading', esc(subline)) + '
' + '
'; } function buildAtAGlance(rows) { const n = rows.length; const industries = tally(rows, 'Industry').length; const regions = tally(rows, 'Region').length; const spend = numericCol(rows, 'Annual_AI_Spend_USD'); const maturity = numericCol(rows, 'AI_Maturity_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const inner = 'Surveyed ' + n + ' respondents across ' + industries + ' industries and ' + regions + ' regions. Median AI maturity is ' + medianOf(maturity).toFixed(1) + ' / 5; median annual AI spend is ' + fmtUsd(medianOf(spend)) + '; respondents project +' + medianOf(budgetGrowth).toFixed(0) + '% budget growth over the next 12 months.'; return '
' + '
01 · At a glance
' + '
' + makeEditable('sv-at-a-glance', '01-at-a-glance', 'at-a-glance', inner) + '
'; } function buildSection(num, title, body) { return '
' + '
' + num + ' · ' + esc(title) + '
' + body + '
'; } function buildMaturity(rows) { const counts = new Map([['1', 0], ['2', 0], ['3', 0], ['4', 0], ['5', 0]]); for (const r of rows) { const v = (r['AI_Maturity_1_5'] || '').trim(); if (counts.has(v)) counts.set(v, counts.get(v) + 1); } const bars = [...counts.entries()].map(([k, v]) => ({ label: 'Level ' + k, value: v, sublabel: ((v / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'AI maturity distribution (1 = nascent · 5 = optimized)', subtitle: 'Self-reported maturity level across all respondents', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '02-maturity', blockIdBase: 'sv-maturity' }); } function buildDeploymentFunnel(rows) { const order = ['Pilot','Departmental Rollout','Enterprise Rollout','Scaled / Optimized']; const counts = tally(rows, 'Deployment_Stage'); const ordered = order.map(k => counts.find(c => c.key === k) || { key: k, count: 0 }); const bars = ordered.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Where are respondents in the deployment journey?', subtitle: 'Self-reported stage · ordered Pilot → Scaled', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '03-deployment', blockIdBase: 'sv-deploy' }); } function buildFocusAreas(rows) { const counts = tally(rows, 'Primary_AI_Focus'); const bars = counts.slice(0, 10).map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Top primary AI focus areas', subtitle: 'Each respondent selects one primary focus · top 10 shown', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '04-focus', blockIdBase: 'sv-focus' }); } function buildSpendBySize(rows) { const sizes = ['Mid-Market','Enterprise','Large Enterprise','Global Enterprise']; const bars = sizes.map(size => { const sub = rows.filter(r => r['Company_Size'] === size); const spend = numericCol(sub, 'Annual_AI_Spend_USD'); return { label: size, value: medianOf(spend), sublabel: 'n=' + sub.length + ' · mean ' + fmtUsd(meanOf(spend)) }; }); return horizontalBarsHtml({ title: 'Median annual AI spend by company size', subtitle: 'Median used to dampen the long-tail of large programs · means shown alongside for context', rows: bars, fmtValue: fmtUsd, valueFormat: 'usd', axisLabel: 'Median annual AI spend ($)', sectionId: '05-spend', blockIdBase: 'sv-spend' }); } function buildIndustryView(rows) { const top = tally(rows, 'Industry').slice(0, 8); const bars = top.map(c => { const sub = rows.filter(r => r['Industry'] === c.key); return { label: c.key, value: meanOf(numericCol(sub, 'AI_Maturity_1_5')), sublabel: 'n=' + c.count + ' · median spend ' + fmtUsd(medianOf(numericCol(sub, 'Annual_AI_Spend_USD'))) }; }).sort((a, b) => b.value - a.value); return horizontalBarsHtml({ title: 'Industry view — mean AI maturity by industry', subtitle: 'Top 8 industries by respondent count · ordered by mean maturity (1–5 scale)', rows: bars, fmtValue: v => v.toFixed(1) + ' / 5', valueFormat: 'ratio_5', axisLabel: 'Mean AI maturity (1–5)', sectionId: '06-industry', blockIdBase: 'sv-industry' }); } function buildCloudPlatforms(rows) { const counts = tally(rows, 'Primary_Cloud_Platform'); const bars = counts.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Primary cloud platform mix', subtitle: 'Single platform per respondent · hybrid called out as its own category', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '07-cloud', blockIdBase: 'sv-cloud' }); } function buildSoWhat(rows) { const n = rows.length; const ent = rows.filter(r => ['Enterprise Rollout','Scaled / Optimized'].includes(r['Deployment_Stage'] || '')).length; const entShare = (ent / n) * 100; const roi = numericCol(rows, 'Predicted_ROI_Pct'); const risk = numericCol(rows, 'Implementation_Risk_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const azure = (tally(rows, 'Primary_Cloud_Platform').find(c => c.key === 'Microsoft Azure') || { count: 0 }).count; const azureShare = (azure / n) * 100; const topFocus = tally(rows, 'Primary_AI_Focus')[0] || { key: '', count: 0 }; const para = (id, body) => '

' + makeEditable(id, '08-sia-read', 'paragraph', body) + '

'; return '
' + '
08 · Sia\\'s read
' + '
' + para('sv-sowhat-p1', 'The market is past pilot. ' + entShare.toFixed(0) + '% of respondents report being at Enterprise Rollout or Scaled / Optimized. AI investment is no longer experimental at the scale represented here.') + para('sv-sowhat-p2', 'Self-reported confidence is high. Median predicted ROI is ' + medianOf(roi).toFixed(0) + '%; median implementation risk score is ' + medianOf(risk).toFixed(1) + ' / 5; respondents project budget growth of +' + medianOf(budgetGrowth).toFixed(0) + '% over the next 12 months. Honest caveat: these are self-reported — read as intent and posture, not as audited outcomes.') + para('sv-sowhat-p3', 'The shape of the market. ' + esc(topFocus.key) + ' leads primary focus (' + topFocus.count + ' respondents). Microsoft Azure is the dominant platform at ' + azureShare.toFixed(0) + '% share. Buyer profile skews to CIO / CTO and Chief Data Officer; business-unit-led purchasing is a strong third.') + '
'; } function buildMethodology(rows) { const row = (id, label, body) => '
' + '
' + esc(label) + '
' + '
' + makeEditable(id, '09-methodology', 'methodology-note', body) + '
' + '
'; return '
' + '
09 · Methodology
' + '
' + '

' + makeEditable('sv-meth-intro', '09-methodology', 'methodology-note', 'Survey-mode deliverable rendered live in your browser by the BID sandbox. n=' + rows.length + ' respondents. All distributions, medians, and means computed directly from the uploaded CSV — no proprietary calibration or external anchors applied.') + '

' + row('sv-meth-source', 'Source', 'Uploaded CSV (client-side, no server round-trip). Single response per respondent. No weighting or post-stratification.') + row('sv-meth-counts', 'Counts', 'Industry / region / company-size / focus-area / cloud-platform tallies are raw response counts. Percentages shown alongside use n=' + rows.length + ' as the denominator.') + row('sv-meth-numeric', 'Numeric stats', 'Median used as the central tendency for $-denominated and skewed fields. Mean shown alongside. Risk and maturity scores use 1–5 Likert; mean is appropriate.') + row('sv-meth-bias', 'Read', 'Self-reported survey — ROI, payback, and risk scores reflect respondent intent and confidence, not audited outcomes.') + '
'; } function buildFooter() { return ''; } function composeDeliverable(rows, title) { __chartCounter = 0; const subline = 'n=' + rows.length + ' respondents · live render from uploaded CSV'; const body = '
' + buildCover(title, subline) + buildAtAGlance(rows) + buildSection('02', 'Adoption maturity', buildMaturity(rows)) + buildSection('03', 'Deployment journey', buildDeploymentFunnel(rows)) + buildSection('04', 'What they\\'re building', buildFocusAreas(rows)) + buildSection('05', 'Investment by company size', buildSpendBySize(rows)) + buildSection('06', 'Industry view', buildIndustryView(rows)) + buildSection('07', 'Cloud platform mix', buildCloudPlatforms(rows)) + buildSoWhat(rows) + buildMethodology(rows) + buildFooter() + '
'; return '' + '' + esc(title) + '' + '' + '' + EDITABLE_CSS + CHART_CSS + '' + EDITABLE_TOOLBAR + body + EDITABLE_JS + CHART_JS + ''; } /* ─── Sandbox UI wiring ────────────────────────────────────── */ let parsedRows = null; const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const panel = document.getElementById('panel-summary'); const errPanel = document.getElementById('panel-error'); const renderBtn = document.getElementById('btn-render'); const clearBtn = document.getElementById('btn-clear'); function reset() { parsedRows = null; panel.classList.remove('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); renderBtn.disabled = true; fileInput.value = ''; } function showError(msg) { reset(); document.getElementById('error-detail').textContent = msg; errPanel.classList.add('visible'); dropzone.classList.add('error'); } function showSummary(parsed) { parsedRows = parsed.rows; document.getElementById('stat-rows').textContent = String(parsed.rows.length); document.getElementById('stat-cols').textContent = String(parsed.headers.length); const found = new Set(parsed.headers); const missing = REQUIRED_COLS.filter(c => !found.has(c)); const extras = parsed.headers.filter(h => !EXPECTED_COLS.includes(h)); const schemaEl = document.getElementById('stat-schema'); const detailEl = document.getElementById('schema-check'); if (missing.length === 0) { schemaEl.innerHTML = '✓ Match'; detailEl.innerHTML = extras.length > 0 ? '
Extra columns (ignored): ' + extras.map(esc).join(', ') + '
' : '
All required columns present.
'; renderBtn.disabled = false; } else { schemaEl.innerHTML = '' + missing.length + ' missing'; detailEl.innerHTML = '
Required columns missing:
'; renderBtn.disabled = true; } panel.classList.add('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); } function handleFile(file) { if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const parsed = parseCsv(String(reader.result)); showSummary(parsed); } catch (e) { showError(e.message || String(e)); } }; reader.onerror = () => showError('Could not read file.'); reader.readAsText(file); } dropzone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', e => handleFile(e.target.files[0])); dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('hover'); }); dropzone.addEventListener('dragleave', () => dropzone.classList.remove('hover')); dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('hover'); handleFile(e.dataTransfer.files[0]); }); clearBtn.addEventListener('click', reset); renderBtn.addEventListener('click', () => { if (!parsedRows) return; const title = 'Survey deliverable · live render · n=' + parsedRows.length; const html = composeDeliverable(parsedRows, title); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); }); /* Sample CSV download — fetches the inaugural B2B AI survey from * the bundle and lets the user save it. */ document.getElementById('download-sample').addEventListener('click', async e => { e.preventDefault(); try { const r = await fetch('b2b-enterprise-ai-survey-200.csv'); if (!r.ok) throw new Error('sample CSV not bundled (' + r.status + ')'); const text = await r.text(); const blob = new Blob([text], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'b2b-enterprise-ai-survey-200.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { alert('Could not load sample CSV: ' + err.message); } }); + (n/1e6).toFixed(1) + 'M'; if (Math.abs(n) >= 1e3) return '; /* ─── CSV parsing + profiling (mirror of render-survey.ts) ─── */ function splitRow(line) { const out = []; let cur = ''; let inQ = false; for (const ch of line) { if (ch === '"') { inQ = !inQ; continue; } if (ch === ',' && !inQ) { out.push(cur); cur = ''; continue; } cur += ch; } out.push(cur); return out; } function parseCsv(text) { const lines = text.replace(/\r/g, '').trim().split('\n'); if (lines.length < 2) throw new Error('CSV has fewer than 2 lines (no data rows).'); const headers = splitRow(lines[0]); const rows = lines.slice(1).map(line => { const cells = splitRow(line); const o = {}; headers.forEach((h, i) => { o[h] = (cells[i] || '').trim(); }); return o; }); return { headers, rows }; } function tally(rows, col) { const m = new Map(); for (const r of rows) { const v = (r[col] || '').trim() || '(blank)'; m.set(v, (m.get(v) || 0) + 1); } return [...m.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count); } function numericCol(rows, col) { return rows.map(r => parseFloat(r[col])).filter(n => Number.isFinite(n)); } function meanOf(xs) { return xs.length ? xs.reduce((s, n) => s + n, 0) / xs.length : 0; } function medianOf(xs) { if (!xs.length) return 0; const s = [...xs].sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 === 0 ? (s[m - 1] + s[m]) / 2 : s[m]; } function fmtUsd(v) { if (v >= 1e9) return '$' + (v / 1e9).toFixed(2) + 'B'; if (v >= 1e6) return '$' + (v / 1e6).toFixed(1) + 'M'; if (v >= 1e3) return '$' + (v / 1e3).toFixed(0) + 'K'; return '$' + v.toFixed(0); } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function dataAttrEsc(s) { return String(s).replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } const EXPECTED_COLS = [ 'Company_ID','Company_Name','Industry','Region','Company_Size','Employees', 'Annual_Revenue_USD_M','Primary_AI_Focus','AI_Maturity_1_5','Data_Readiness_1_5', 'Executive_Sponsorship_1_5','AI_Governance_Maturity_1_5','Deployment_Stage', 'AI_Use_Case_Count','Dedicated_AI_Team_Size','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Estimated_Annual_Value_Created_USD','Expected_Payback_Months','Implementation_Risk_1_5', 'Budget_Growth_Next_12M_Pct','Top_AI_Tools_Used','Primary_Cloud_Platform', 'Primary_Buyer','Procurement_Model' ]; const REQUIRED_COLS = [ 'Industry','Region','Company_Size','Primary_AI_Focus','AI_Maturity_1_5', 'Deployment_Stage','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Implementation_Risk_1_5','Budget_Growth_Next_12M_Pct','Primary_Cloud_Platform' ]; /* ─── Editable wrapper (matches makeEditable from editable-layer.ts) ─── */ function makeEditable(blockId, sectionId, template, content) { return '' + content + ''; } /* ─── Chart wrapper (matches wrapChart from chart-controls.ts) ─── */ let __chartCounter = 0; function nextChartId(prefix) { __chartCounter += 1; return prefix + '-' + __chartCounter; } function wrapChart(spec, initialSvg, titleHtml, subtitleHtml) { const specB64 = btoa(unescape(encodeURIComponent(JSON.stringify(spec)))); return '
' + '
' + titleHtml + '
' + '
' + subtitleHtml + '
' + '
' + '
' + initialSvg + '
' + '
'; } /* ─── Horizontal-bars renderer (mirror of render-survey.ts) ─── */ function horizontalBarsHtml(opts) { const VB_W = 800, BAR_H = 24, BAR_GAP = 6, BAR_X_START = 220, BAR_X_END = 660; const BAR_AREA_TOP = 28; const BAR_AREA_BOTTOM = BAR_AREA_TOP + opts.rows.length * BAR_H + (opts.rows.length - 1) * BAR_GAP; const X_AXIS_Y = BAR_AREA_BOTTOM + 22, AXIS_TITLE_Y = X_AXIS_Y + 20, VB_H = AXIS_TITLE_Y + 10; const rawMax = Math.max(...opts.rows.map(r => r.value), 1) * 1.15; const mag = Math.pow(10, Math.floor(Math.log10(rawMax))); const xMax = Math.ceil(rawMax / mag) * mag; const xScale = v => BAR_X_START + (v / xMax) * (BAR_X_END - BAR_X_START); const bars = opts.rows.map((r, i) => { const y = BAR_AREA_TOP + i * (BAR_H + BAR_GAP); const w = Math.max(xScale(r.value) - BAR_X_START, 1); const labelY = y + BAR_H / 2 + 4; return '' + esc(r.label) + ': ' + esc(opts.fmtValue(r.value)) + '' + '' + esc(opts.fmtValue(r.value)) + (r.sublabel ? ' · ' + esc(r.sublabel) : '') + '' + '' + esc(r.label) + ''; }).join(''); const ticks = [0, xMax / 2, xMax].map(t => { const x = xScale(t); return '' + esc(opts.fmtValue(t)) + ''; }).join(''); const initialSvg = '' + bars + '' + ticks + '' + esc(opts.axisLabel) + '' + ''; const titleHtml = makeEditable(opts.blockIdBase + '-title', opts.sectionId, 'subheading', opts.title); const subtitleHtml = makeEditable(opts.blockIdBase + '-subtitle', opts.sectionId, 'caveat-band', opts.subtitle); const spec = { chartId: nextChartId(opts.blockIdBase), type: 'horizontal-bars', rows: opts.rows.map(r => ({ label: r.label, value: r.value, sublabel: r.sublabel })), valueFormat: opts.valueFormat, axisLabel: opts.axisLabel, color: '#00A2A3' }; return wrapChart(spec, initialSvg, titleHtml, subtitleHtml); } /* ─── Section builders ────────────────────────────────────── */ function buildCover(title, subline) { return '
' + '
Sia Partners · BID Framework · Survey Read · Sandbox-rendered
' + '

' + makeEditable('cover-title', '00-cover', 'headline', esc(title)) + '

' + '
' + makeEditable('cover-subline', '00-cover', 'subheading', esc(subline)) + '
' + '
'; } function buildAtAGlance(rows) { const n = rows.length; const industries = tally(rows, 'Industry').length; const regions = tally(rows, 'Region').length; const spend = numericCol(rows, 'Annual_AI_Spend_USD'); const maturity = numericCol(rows, 'AI_Maturity_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const inner = 'Surveyed ' + n + ' respondents across ' + industries + ' industries and ' + regions + ' regions. Median AI maturity is ' + medianOf(maturity).toFixed(1) + ' / 5; median annual AI spend is ' + fmtUsd(medianOf(spend)) + '; respondents project +' + medianOf(budgetGrowth).toFixed(0) + '% budget growth over the next 12 months.'; return '
' + '
01 · At a glance
' + '
' + makeEditable('sv-at-a-glance', '01-at-a-glance', 'at-a-glance', inner) + '
'; } function buildSection(num, title, body) { return '
' + '
' + num + ' · ' + esc(title) + '
' + body + '
'; } function buildMaturity(rows) { const counts = new Map([['1', 0], ['2', 0], ['3', 0], ['4', 0], ['5', 0]]); for (const r of rows) { const v = (r['AI_Maturity_1_5'] || '').trim(); if (counts.has(v)) counts.set(v, counts.get(v) + 1); } const bars = [...counts.entries()].map(([k, v]) => ({ label: 'Level ' + k, value: v, sublabel: ((v / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'AI maturity distribution (1 = nascent · 5 = optimized)', subtitle: 'Self-reported maturity level across all respondents', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '02-maturity', blockIdBase: 'sv-maturity' }); } function buildDeploymentFunnel(rows) { const order = ['Pilot','Departmental Rollout','Enterprise Rollout','Scaled / Optimized']; const counts = tally(rows, 'Deployment_Stage'); const ordered = order.map(k => counts.find(c => c.key === k) || { key: k, count: 0 }); const bars = ordered.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Where are respondents in the deployment journey?', subtitle: 'Self-reported stage · ordered Pilot → Scaled', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '03-deployment', blockIdBase: 'sv-deploy' }); } function buildFocusAreas(rows) { const counts = tally(rows, 'Primary_AI_Focus'); const bars = counts.slice(0, 10).map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Top primary AI focus areas', subtitle: 'Each respondent selects one primary focus · top 10 shown', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '04-focus', blockIdBase: 'sv-focus' }); } function buildSpendBySize(rows) { const sizes = ['Mid-Market','Enterprise','Large Enterprise','Global Enterprise']; const bars = sizes.map(size => { const sub = rows.filter(r => r['Company_Size'] === size); const spend = numericCol(sub, 'Annual_AI_Spend_USD'); return { label: size, value: medianOf(spend), sublabel: 'n=' + sub.length + ' · mean ' + fmtUsd(meanOf(spend)) }; }); return horizontalBarsHtml({ title: 'Median annual AI spend by company size', subtitle: 'Median used to dampen the long-tail of large programs · means shown alongside for context', rows: bars, fmtValue: fmtUsd, valueFormat: 'usd', axisLabel: 'Median annual AI spend ($)', sectionId: '05-spend', blockIdBase: 'sv-spend' }); } function buildIndustryView(rows) { const top = tally(rows, 'Industry').slice(0, 8); const bars = top.map(c => { const sub = rows.filter(r => r['Industry'] === c.key); return { label: c.key, value: meanOf(numericCol(sub, 'AI_Maturity_1_5')), sublabel: 'n=' + c.count + ' · median spend ' + fmtUsd(medianOf(numericCol(sub, 'Annual_AI_Spend_USD'))) }; }).sort((a, b) => b.value - a.value); return horizontalBarsHtml({ title: 'Industry view — mean AI maturity by industry', subtitle: 'Top 8 industries by respondent count · ordered by mean maturity (1–5 scale)', rows: bars, fmtValue: v => v.toFixed(1) + ' / 5', valueFormat: 'ratio_5', axisLabel: 'Mean AI maturity (1–5)', sectionId: '06-industry', blockIdBase: 'sv-industry' }); } function buildCloudPlatforms(rows) { const counts = tally(rows, 'Primary_Cloud_Platform'); const bars = counts.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Primary cloud platform mix', subtitle: 'Single platform per respondent · hybrid called out as its own category', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '07-cloud', blockIdBase: 'sv-cloud' }); } function buildSoWhat(rows) { const n = rows.length; const ent = rows.filter(r => ['Enterprise Rollout','Scaled / Optimized'].includes(r['Deployment_Stage'] || '')).length; const entShare = (ent / n) * 100; const roi = numericCol(rows, 'Predicted_ROI_Pct'); const risk = numericCol(rows, 'Implementation_Risk_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const azure = (tally(rows, 'Primary_Cloud_Platform').find(c => c.key === 'Microsoft Azure') || { count: 0 }).count; const azureShare = (azure / n) * 100; const topFocus = tally(rows, 'Primary_AI_Focus')[0] || { key: '', count: 0 }; const para = (id, body) => '

' + makeEditable(id, '08-sia-read', 'paragraph', body) + '

'; return '
' + '
08 · Sia\\'s read
' + '
' + para('sv-sowhat-p1', 'The market is past pilot. ' + entShare.toFixed(0) + '% of respondents report being at Enterprise Rollout or Scaled / Optimized. AI investment is no longer experimental at the scale represented here.') + para('sv-sowhat-p2', 'Self-reported confidence is high. Median predicted ROI is ' + medianOf(roi).toFixed(0) + '%; median implementation risk score is ' + medianOf(risk).toFixed(1) + ' / 5; respondents project budget growth of +' + medianOf(budgetGrowth).toFixed(0) + '% over the next 12 months. Honest caveat: these are self-reported — read as intent and posture, not as audited outcomes.') + para('sv-sowhat-p3', 'The shape of the market. ' + esc(topFocus.key) + ' leads primary focus (' + topFocus.count + ' respondents). Microsoft Azure is the dominant platform at ' + azureShare.toFixed(0) + '% share. Buyer profile skews to CIO / CTO and Chief Data Officer; business-unit-led purchasing is a strong third.') + '
'; } function buildMethodology(rows) { const row = (id, label, body) => '
' + '
' + esc(label) + '
' + '
' + makeEditable(id, '09-methodology', 'methodology-note', body) + '
' + '
'; return '
' + '
09 · Methodology
' + '
' + '

' + makeEditable('sv-meth-intro', '09-methodology', 'methodology-note', 'Survey-mode deliverable rendered live in your browser by the BID sandbox. n=' + rows.length + ' respondents. All distributions, medians, and means computed directly from the uploaded CSV — no proprietary calibration or external anchors applied.') + '

' + row('sv-meth-source', 'Source', 'Uploaded CSV (client-side, no server round-trip). Single response per respondent. No weighting or post-stratification.') + row('sv-meth-counts', 'Counts', 'Industry / region / company-size / focus-area / cloud-platform tallies are raw response counts. Percentages shown alongside use n=' + rows.length + ' as the denominator.') + row('sv-meth-numeric', 'Numeric stats', 'Median used as the central tendency for $-denominated and skewed fields. Mean shown alongside. Risk and maturity scores use 1–5 Likert; mean is appropriate.') + row('sv-meth-bias', 'Read', 'Self-reported survey — ROI, payback, and risk scores reflect respondent intent and confidence, not audited outcomes.') + '
'; } function buildFooter() { return '
' + '
BID Framework · Survey-mode deliverable · rendered live in browser via the sandbox
' + '
All sections editable — hover to format, click ✎ to refine, hover charts for controls.
' + '
'; } function composeDeliverable(rows, title) { __chartCounter = 0; const subline = 'n=' + rows.length + ' respondents · live render from uploaded CSV'; const body = '
' + buildCover(title, subline) + buildAtAGlance(rows) + buildSection('02', 'Adoption maturity', buildMaturity(rows)) + buildSection('03', 'Deployment journey', buildDeploymentFunnel(rows)) + buildSection('04', 'What they\\'re building', buildFocusAreas(rows)) + buildSection('05', 'Investment by company size', buildSpendBySize(rows)) + buildSection('06', 'Industry view', buildIndustryView(rows)) + buildSection('07', 'Cloud platform mix', buildCloudPlatforms(rows)) + buildSoWhat(rows) + buildMethodology(rows) + buildFooter() + '
'; return '' + '' + esc(title) + '' + '' + '' + EDITABLE_CSS + CHART_CSS + '' + EDITABLE_TOOLBAR + body + EDITABLE_JS + CHART_JS + ''; } /* ─── Sandbox UI wiring ────────────────────────────────────── */ let parsedRows = null; const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const panel = document.getElementById('panel-summary'); const errPanel = document.getElementById('panel-error'); const renderBtn = document.getElementById('btn-render'); const clearBtn = document.getElementById('btn-clear'); function reset() { parsedRows = null; panel.classList.remove('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); renderBtn.disabled = true; fileInput.value = ''; } function showError(msg) { reset(); document.getElementById('error-detail').textContent = msg; errPanel.classList.add('visible'); dropzone.classList.add('error'); } function showSummary(parsed) { parsedRows = parsed.rows; document.getElementById('stat-rows').textContent = String(parsed.rows.length); document.getElementById('stat-cols').textContent = String(parsed.headers.length); const found = new Set(parsed.headers); const missing = REQUIRED_COLS.filter(c => !found.has(c)); const extras = parsed.headers.filter(h => !EXPECTED_COLS.includes(h)); const schemaEl = document.getElementById('stat-schema'); const detailEl = document.getElementById('schema-check'); if (missing.length === 0) { schemaEl.innerHTML = '✓ Match'; detailEl.innerHTML = extras.length > 0 ? '
Extra columns (ignored): ' + extras.map(esc).join(', ') + '
' : '
All required columns present.
'; renderBtn.disabled = false; } else { schemaEl.innerHTML = '' + missing.length + ' missing'; detailEl.innerHTML = '
Required columns missing:
'; renderBtn.disabled = true; } panel.classList.add('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); } function handleFile(file) { if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const parsed = parseCsv(String(reader.result)); showSummary(parsed); } catch (e) { showError(e.message || String(e)); } }; reader.onerror = () => showError('Could not read file.'); reader.readAsText(file); } dropzone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', e => handleFile(e.target.files[0])); dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('hover'); }); dropzone.addEventListener('dragleave', () => dropzone.classList.remove('hover')); dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('hover'); handleFile(e.dataTransfer.files[0]); }); clearBtn.addEventListener('click', reset); renderBtn.addEventListener('click', () => { if (!parsedRows) return; const title = 'Survey deliverable · live render · n=' + parsedRows.length; const html = composeDeliverable(parsedRows, title); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); }); /* Sample CSV download — fetches the inaugural B2B AI survey from * the bundle and lets the user save it. */ document.getElementById('download-sample').addEventListener('click', async e => { e.preventDefault(); try { const r = await fetch('b2b-enterprise-ai-survey-200.csv'); if (!r.ok) throw new Error('sample CSV not bundled (' + r.status + ')'); const text = await r.text(); const blob = new Blob([text], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'b2b-enterprise-ai-survey-200.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { alert('Could not load sample CSV: ' + err.message); } }); + Math.round(n/1e3) + 'K'; return '; /* ─── CSV parsing + profiling (mirror of render-survey.ts) ─── */ function splitRow(line) { const out = []; let cur = ''; let inQ = false; for (const ch of line) { if (ch === '"') { inQ = !inQ; continue; } if (ch === ',' && !inQ) { out.push(cur); cur = ''; continue; } cur += ch; } out.push(cur); return out; } function parseCsv(text) { const lines = text.replace(/\r/g, '').trim().split('\n'); if (lines.length < 2) throw new Error('CSV has fewer than 2 lines (no data rows).'); const headers = splitRow(lines[0]); const rows = lines.slice(1).map(line => { const cells = splitRow(line); const o = {}; headers.forEach((h, i) => { o[h] = (cells[i] || '').trim(); }); return o; }); return { headers, rows }; } function tally(rows, col) { const m = new Map(); for (const r of rows) { const v = (r[col] || '').trim() || '(blank)'; m.set(v, (m.get(v) || 0) + 1); } return [...m.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count); } function numericCol(rows, col) { return rows.map(r => parseFloat(r[col])).filter(n => Number.isFinite(n)); } function meanOf(xs) { return xs.length ? xs.reduce((s, n) => s + n, 0) / xs.length : 0; } function medianOf(xs) { if (!xs.length) return 0; const s = [...xs].sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 === 0 ? (s[m - 1] + s[m]) / 2 : s[m]; } function fmtUsd(v) { if (v >= 1e9) return '$' + (v / 1e9).toFixed(2) + 'B'; if (v >= 1e6) return '$' + (v / 1e6).toFixed(1) + 'M'; if (v >= 1e3) return '$' + (v / 1e3).toFixed(0) + 'K'; return '$' + v.toFixed(0); } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function dataAttrEsc(s) { return String(s).replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } const EXPECTED_COLS = [ 'Company_ID','Company_Name','Industry','Region','Company_Size','Employees', 'Annual_Revenue_USD_M','Primary_AI_Focus','AI_Maturity_1_5','Data_Readiness_1_5', 'Executive_Sponsorship_1_5','AI_Governance_Maturity_1_5','Deployment_Stage', 'AI_Use_Case_Count','Dedicated_AI_Team_Size','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Estimated_Annual_Value_Created_USD','Expected_Payback_Months','Implementation_Risk_1_5', 'Budget_Growth_Next_12M_Pct','Top_AI_Tools_Used','Primary_Cloud_Platform', 'Primary_Buyer','Procurement_Model' ]; const REQUIRED_COLS = [ 'Industry','Region','Company_Size','Primary_AI_Focus','AI_Maturity_1_5', 'Deployment_Stage','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Implementation_Risk_1_5','Budget_Growth_Next_12M_Pct','Primary_Cloud_Platform' ]; /* ─── Editable wrapper (matches makeEditable from editable-layer.ts) ─── */ function makeEditable(blockId, sectionId, template, content) { return '' + content + ''; } /* ─── Chart wrapper (matches wrapChart from chart-controls.ts) ─── */ let __chartCounter = 0; function nextChartId(prefix) { __chartCounter += 1; return prefix + '-' + __chartCounter; } function wrapChart(spec, initialSvg, titleHtml, subtitleHtml) { const specB64 = btoa(unescape(encodeURIComponent(JSON.stringify(spec)))); return '
' + '
' + titleHtml + '
' + '
' + subtitleHtml + '
' + '
' + '
' + initialSvg + '
' + '
'; } /* ─── Horizontal-bars renderer (mirror of render-survey.ts) ─── */ function horizontalBarsHtml(opts) { const VB_W = 800, BAR_H = 24, BAR_GAP = 6, BAR_X_START = 220, BAR_X_END = 660; const BAR_AREA_TOP = 28; const BAR_AREA_BOTTOM = BAR_AREA_TOP + opts.rows.length * BAR_H + (opts.rows.length - 1) * BAR_GAP; const X_AXIS_Y = BAR_AREA_BOTTOM + 22, AXIS_TITLE_Y = X_AXIS_Y + 20, VB_H = AXIS_TITLE_Y + 10; const rawMax = Math.max(...opts.rows.map(r => r.value), 1) * 1.15; const mag = Math.pow(10, Math.floor(Math.log10(rawMax))); const xMax = Math.ceil(rawMax / mag) * mag; const xScale = v => BAR_X_START + (v / xMax) * (BAR_X_END - BAR_X_START); const bars = opts.rows.map((r, i) => { const y = BAR_AREA_TOP + i * (BAR_H + BAR_GAP); const w = Math.max(xScale(r.value) - BAR_X_START, 1); const labelY = y + BAR_H / 2 + 4; return '' + esc(r.label) + ': ' + esc(opts.fmtValue(r.value)) + '' + '' + esc(opts.fmtValue(r.value)) + (r.sublabel ? ' · ' + esc(r.sublabel) : '') + '' + '' + esc(r.label) + ''; }).join(''); const ticks = [0, xMax / 2, xMax].map(t => { const x = xScale(t); return '' + esc(opts.fmtValue(t)) + ''; }).join(''); const initialSvg = '' + bars + '' + ticks + '' + esc(opts.axisLabel) + '' + ''; const titleHtml = makeEditable(opts.blockIdBase + '-title', opts.sectionId, 'subheading', opts.title); const subtitleHtml = makeEditable(opts.blockIdBase + '-subtitle', opts.sectionId, 'caveat-band', opts.subtitle); const spec = { chartId: nextChartId(opts.blockIdBase), type: 'horizontal-bars', rows: opts.rows.map(r => ({ label: r.label, value: r.value, sublabel: r.sublabel })), valueFormat: opts.valueFormat, axisLabel: opts.axisLabel, color: '#00A2A3' }; return wrapChart(spec, initialSvg, titleHtml, subtitleHtml); } /* ─── Section builders ────────────────────────────────────── */ function buildCover(title, subline) { return '
' + '
Sia Partners · BID Framework · Survey Read · Sandbox-rendered
' + '

' + makeEditable('cover-title', '00-cover', 'headline', esc(title)) + '

' + '
' + makeEditable('cover-subline', '00-cover', 'subheading', esc(subline)) + '
' + '
'; } function buildAtAGlance(rows) { const n = rows.length; const industries = tally(rows, 'Industry').length; const regions = tally(rows, 'Region').length; const spend = numericCol(rows, 'Annual_AI_Spend_USD'); const maturity = numericCol(rows, 'AI_Maturity_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const inner = 'Surveyed ' + n + ' respondents across ' + industries + ' industries and ' + regions + ' regions. Median AI maturity is ' + medianOf(maturity).toFixed(1) + ' / 5; median annual AI spend is ' + fmtUsd(medianOf(spend)) + '; respondents project +' + medianOf(budgetGrowth).toFixed(0) + '% budget growth over the next 12 months.'; return '
' + '
01 · At a glance
' + '
' + makeEditable('sv-at-a-glance', '01-at-a-glance', 'at-a-glance', inner) + '
'; } function buildSection(num, title, body) { return '
' + '
' + num + ' · ' + esc(title) + '
' + body + '
'; } function buildMaturity(rows) { const counts = new Map([['1', 0], ['2', 0], ['3', 0], ['4', 0], ['5', 0]]); for (const r of rows) { const v = (r['AI_Maturity_1_5'] || '').trim(); if (counts.has(v)) counts.set(v, counts.get(v) + 1); } const bars = [...counts.entries()].map(([k, v]) => ({ label: 'Level ' + k, value: v, sublabel: ((v / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'AI maturity distribution (1 = nascent · 5 = optimized)', subtitle: 'Self-reported maturity level across all respondents', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '02-maturity', blockIdBase: 'sv-maturity' }); } function buildDeploymentFunnel(rows) { const order = ['Pilot','Departmental Rollout','Enterprise Rollout','Scaled / Optimized']; const counts = tally(rows, 'Deployment_Stage'); const ordered = order.map(k => counts.find(c => c.key === k) || { key: k, count: 0 }); const bars = ordered.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Where are respondents in the deployment journey?', subtitle: 'Self-reported stage · ordered Pilot → Scaled', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '03-deployment', blockIdBase: 'sv-deploy' }); } function buildFocusAreas(rows) { const counts = tally(rows, 'Primary_AI_Focus'); const bars = counts.slice(0, 10).map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Top primary AI focus areas', subtitle: 'Each respondent selects one primary focus · top 10 shown', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '04-focus', blockIdBase: 'sv-focus' }); } function buildSpendBySize(rows) { const sizes = ['Mid-Market','Enterprise','Large Enterprise','Global Enterprise']; const bars = sizes.map(size => { const sub = rows.filter(r => r['Company_Size'] === size); const spend = numericCol(sub, 'Annual_AI_Spend_USD'); return { label: size, value: medianOf(spend), sublabel: 'n=' + sub.length + ' · mean ' + fmtUsd(meanOf(spend)) }; }); return horizontalBarsHtml({ title: 'Median annual AI spend by company size', subtitle: 'Median used to dampen the long-tail of large programs · means shown alongside for context', rows: bars, fmtValue: fmtUsd, valueFormat: 'usd', axisLabel: 'Median annual AI spend ($)', sectionId: '05-spend', blockIdBase: 'sv-spend' }); } function buildIndustryView(rows) { const top = tally(rows, 'Industry').slice(0, 8); const bars = top.map(c => { const sub = rows.filter(r => r['Industry'] === c.key); return { label: c.key, value: meanOf(numericCol(sub, 'AI_Maturity_1_5')), sublabel: 'n=' + c.count + ' · median spend ' + fmtUsd(medianOf(numericCol(sub, 'Annual_AI_Spend_USD'))) }; }).sort((a, b) => b.value - a.value); return horizontalBarsHtml({ title: 'Industry view — mean AI maturity by industry', subtitle: 'Top 8 industries by respondent count · ordered by mean maturity (1–5 scale)', rows: bars, fmtValue: v => v.toFixed(1) + ' / 5', valueFormat: 'ratio_5', axisLabel: 'Mean AI maturity (1–5)', sectionId: '06-industry', blockIdBase: 'sv-industry' }); } function buildCloudPlatforms(rows) { const counts = tally(rows, 'Primary_Cloud_Platform'); const bars = counts.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Primary cloud platform mix', subtitle: 'Single platform per respondent · hybrid called out as its own category', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '07-cloud', blockIdBase: 'sv-cloud' }); } function buildSoWhat(rows) { const n = rows.length; const ent = rows.filter(r => ['Enterprise Rollout','Scaled / Optimized'].includes(r['Deployment_Stage'] || '')).length; const entShare = (ent / n) * 100; const roi = numericCol(rows, 'Predicted_ROI_Pct'); const risk = numericCol(rows, 'Implementation_Risk_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const azure = (tally(rows, 'Primary_Cloud_Platform').find(c => c.key === 'Microsoft Azure') || { count: 0 }).count; const azureShare = (azure / n) * 100; const topFocus = tally(rows, 'Primary_AI_Focus')[0] || { key: '', count: 0 }; const para = (id, body) => '

' + makeEditable(id, '08-sia-read', 'paragraph', body) + '

'; return '
' + '
08 · Sia\\'s read
' + '
' + para('sv-sowhat-p1', 'The market is past pilot. ' + entShare.toFixed(0) + '% of respondents report being at Enterprise Rollout or Scaled / Optimized. AI investment is no longer experimental at the scale represented here.') + para('sv-sowhat-p2', 'Self-reported confidence is high. Median predicted ROI is ' + medianOf(roi).toFixed(0) + '%; median implementation risk score is ' + medianOf(risk).toFixed(1) + ' / 5; respondents project budget growth of +' + medianOf(budgetGrowth).toFixed(0) + '% over the next 12 months. Honest caveat: these are self-reported — read as intent and posture, not as audited outcomes.') + para('sv-sowhat-p3', 'The shape of the market. ' + esc(topFocus.key) + ' leads primary focus (' + topFocus.count + ' respondents). Microsoft Azure is the dominant platform at ' + azureShare.toFixed(0) + '% share. Buyer profile skews to CIO / CTO and Chief Data Officer; business-unit-led purchasing is a strong third.') + '
'; } function buildMethodology(rows) { const row = (id, label, body) => '
' + '
' + esc(label) + '
' + '
' + makeEditable(id, '09-methodology', 'methodology-note', body) + '
' + '
'; return '
' + '
09 · Methodology
' + '
' + '

' + makeEditable('sv-meth-intro', '09-methodology', 'methodology-note', 'Survey-mode deliverable rendered live in your browser by the BID sandbox. n=' + rows.length + ' respondents. All distributions, medians, and means computed directly from the uploaded CSV — no proprietary calibration or external anchors applied.') + '

' + row('sv-meth-source', 'Source', 'Uploaded CSV (client-side, no server round-trip). Single response per respondent. No weighting or post-stratification.') + row('sv-meth-counts', 'Counts', 'Industry / region / company-size / focus-area / cloud-platform tallies are raw response counts. Percentages shown alongside use n=' + rows.length + ' as the denominator.') + row('sv-meth-numeric', 'Numeric stats', 'Median used as the central tendency for $-denominated and skewed fields. Mean shown alongside. Risk and maturity scores use 1–5 Likert; mean is appropriate.') + row('sv-meth-bias', 'Read', 'Self-reported survey — ROI, payback, and risk scores reflect respondent intent and confidence, not audited outcomes.') + '
'; } function buildFooter() { return '
' + '
BID Framework · Survey-mode deliverable · rendered live in browser via the sandbox
' + '
All sections editable — hover to format, click ✎ to refine, hover charts for controls.
' + '
'; } function composeDeliverable(rows, title) { __chartCounter = 0; const subline = 'n=' + rows.length + ' respondents · live render from uploaded CSV'; const body = '
' + buildCover(title, subline) + buildAtAGlance(rows) + buildSection('02', 'Adoption maturity', buildMaturity(rows)) + buildSection('03', 'Deployment journey', buildDeploymentFunnel(rows)) + buildSection('04', 'What they\\'re building', buildFocusAreas(rows)) + buildSection('05', 'Investment by company size', buildSpendBySize(rows)) + buildSection('06', 'Industry view', buildIndustryView(rows)) + buildSection('07', 'Cloud platform mix', buildCloudPlatforms(rows)) + buildSoWhat(rows) + buildMethodology(rows) + buildFooter() + '
'; return '' + '' + esc(title) + '' + '' + '' + EDITABLE_CSS + CHART_CSS + '' + EDITABLE_TOOLBAR + body + EDITABLE_JS + CHART_JS + ''; } /* ─── Sandbox UI wiring ────────────────────────────────────── */ let parsedRows = null; const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const panel = document.getElementById('panel-summary'); const errPanel = document.getElementById('panel-error'); const renderBtn = document.getElementById('btn-render'); const clearBtn = document.getElementById('btn-clear'); function reset() { parsedRows = null; panel.classList.remove('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); renderBtn.disabled = true; fileInput.value = ''; } function showError(msg) { reset(); document.getElementById('error-detail').textContent = msg; errPanel.classList.add('visible'); dropzone.classList.add('error'); } function showSummary(parsed) { parsedRows = parsed.rows; document.getElementById('stat-rows').textContent = String(parsed.rows.length); document.getElementById('stat-cols').textContent = String(parsed.headers.length); const found = new Set(parsed.headers); const missing = REQUIRED_COLS.filter(c => !found.has(c)); const extras = parsed.headers.filter(h => !EXPECTED_COLS.includes(h)); const schemaEl = document.getElementById('stat-schema'); const detailEl = document.getElementById('schema-check'); if (missing.length === 0) { schemaEl.innerHTML = '✓ Match'; detailEl.innerHTML = extras.length > 0 ? '
Extra columns (ignored): ' + extras.map(esc).join(', ') + '
' : '
All required columns present.
'; renderBtn.disabled = false; } else { schemaEl.innerHTML = '' + missing.length + ' missing'; detailEl.innerHTML = '
Required columns missing:
    ' + missing.map(c => '
  • ' + esc(c) + '
  • ').join('') + '
'; renderBtn.disabled = true; } panel.classList.add('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); } function handleFile(file) { if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const parsed = parseCsv(String(reader.result)); showSummary(parsed); } catch (e) { showError(e.message || String(e)); } }; reader.onerror = () => showError('Could not read file.'); reader.readAsText(file); } dropzone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', e => handleFile(e.target.files[0])); dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('hover'); }); dropzone.addEventListener('dragleave', () => dropzone.classList.remove('hover')); dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('hover'); handleFile(e.dataTransfer.files[0]); }); clearBtn.addEventListener('click', reset); renderBtn.addEventListener('click', () => { if (!parsedRows) return; const title = 'Survey deliverable · live render · n=' + parsedRows.length; const html = composeDeliverable(parsedRows, title); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); }); /* Sample CSV download — fetches the inaugural B2B AI survey from * the bundle and lets the user save it. */ document.getElementById('download-sample').addEventListener('click', async e => { e.preventDefault(); try { const r = await fetch('b2b-enterprise-ai-survey-200.csv'); if (!r.ok) throw new Error('sample CSV not bundled (' + r.status + ')'); const text = await r.text(); const blob = new Blob([text], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'b2b-enterprise-ai-survey-200.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { alert('Could not load sample CSV: ' + err.message); } }); + Math.round(n).toString(); case 'ratio_5': return n.toFixed(1) + ' / 5'; case 'pct': return n.toFixed(0) + '%'; case 'count': default: return Math.round(n).toString(); } } function niceMax(rawMax) { if (rawMax <= 0) return 1; var mag = Math.pow(10, Math.floor(Math.log10(rawMax))); return Math.ceil(rawMax / mag) * mag; } function escXml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function svgNS() { return 'http://www.w3.org/2000/svg'; } function renderHorizontalBars(spec) { var VB_W = 800; var BAR_H = 24; var BAR_GAP = 6; var BAR_X_START = 220; var BAR_X_END = 660; var BAR_AREA_TOP = 28; var BAR_AREA_BOTTOM = BAR_AREA_TOP + spec.rows.length * BAR_H + (spec.rows.length - 1) * BAR_GAP; var X_AXIS_Y = BAR_AREA_BOTTOM + 22; var AXIS_TITLE_Y = X_AXIS_Y + 20; var VB_H = AXIS_TITLE_Y + 10; var rawMax = Math.max.apply(null, spec.rows.map(function(r) { return r.value; }).concat([1])) * 1.15; var xMax = niceMax(rawMax); var xScale = function(v) { return BAR_X_START + (v / xMax) * (BAR_X_END - BAR_X_START); }; var color = spec.color || BRAND.teal; var bars = spec.rows.map(function(r, i) { var y = BAR_AREA_TOP + i * (BAR_H + BAR_GAP); var w = Math.max(xScale(r.value) - BAR_X_START, 1); var labelY = y + BAR_H / 2 + 4; var valueText = fmtValue(r.value, spec.valueFormat); return '' + '' + escXml(r.label) + ': ' + escXml(valueText) + '' + '' + escXml(valueText) + (r.sublabel ? (' · ' + escXml(r.sublabel)) : '') + '' + '' + escXml(r.label) + ''; }).join(''); var ticks = [0, xMax / 2, xMax].map(function(t) { var x = xScale(t); return '' + escXml(fmtValue(t, spec.valueFormat)) + ''; }).join(''); return '' + '' + bars + '' + ticks + '' + escXml(spec.axisLabel) + '' + ''; } function renderVerticalBars(spec) { var VB_W = 800; var VB_H = 360; var n = spec.rows.length; var X_START = 60; var X_END = 760; var Y_TOP = 30; var Y_BASE = 270; var X_LABEL_AREA = 80; var availW = X_END - X_START; var BAR_W = Math.min(60, (availW - (n - 1) * 14) / Math.max(n, 1)); var BAR_GAP = (availW - n * BAR_W) / Math.max(n - 1, 1); var rawMax = Math.max.apply(null, spec.rows.map(function(r) { return r.value; }).concat([1])) * 1.15; var yMax = niceMax(rawMax); var hScale = function(v) { return (v / yMax) * (Y_BASE - Y_TOP); }; var color = spec.color || BRAND.teal; var bars = spec.rows.map(function(r, i) { var x = X_START + i * (BAR_W + BAR_GAP); var h = hScale(r.value); var y = Y_BASE - h; var cx = x + BAR_W / 2; var valueText = fmtValue(r.value, spec.valueFormat); /* Tilt label if narrow */ var rotateLabel = n > 6; var labelOut = rotateLabel ? ('' + escXml(r.label) + '') : ('' + escXml(r.label) + ''); return '' + '' + escXml(r.label) + ': ' + escXml(valueText) + '' + '' + escXml(valueText) + '' + labelOut; }).join(''); var ticks = [0, yMax / 2, yMax].map(function(t) { var y = Y_BASE - hScale(t); return '' + '' + '' + escXml(fmtValue(t, spec.valueFormat)) + ''; }).join(''); return '' + '' + ticks + bars + '' + '' + escXml(spec.axisLabel) + '' + ''; } function renderSvg(spec) { if (spec.type === 'vertical-bars') return renderVerticalBars(spec); return renderHorizontalBars(spec); } function sortRows(rows, mode) { var copy = rows.slice(); if (mode === 'value-desc') copy.sort(function(a,b) { return b.value - a.value; }); else if (mode === 'value-asc') copy.sort(function(a,b) { return a.value - b.value; }); else if (mode === 'label-asc') copy.sort(function(a,b) { return a.label.localeCompare(b.label); }); /* 'original' = no-op */ return copy; } function applySpec(chartEl, originalSpec, mut) { var rows = sortRows(originalSpec.rows, mut.sort); if (mut.topN && mut.topN < rows.length) rows = rows.slice(0, mut.topN); var spec = Object.assign({}, originalSpec, { rows: rows, type: mut.type }); var area = chartEl.querySelector('.bid-chart-area'); if (area) area.innerHTML = renderSvg(spec); window.__bidChartEdits.push({ chart_id: originalSpec.chartId, mutation: mut, timestamp: Date.now() }); } function buildControls(chartEl) { var specB64 = chartEl.getAttribute('data-bid-chart-spec'); if (!specB64) return; var originalSpec; try { originalSpec = JSON.parse(atob(specB64)); } catch (e) { return; } if (!originalSpec || !originalSpec.rows) return; var controls = chartEl.querySelector('.bid-chart-controls'); if (!controls) return; var mut = { sort: 'original', topN: originalSpec.rows.length, type: originalSpec.type }; /* Top-N (only if >3 rows) */ if (originalSpec.rows.length > 3) { var topLabel = document.createElement('label'); topLabel.textContent = 'Top'; controls.appendChild(topLabel); var slider = document.createElement('input'); slider.type = 'range'; slider.min = String(Math.min(3, originalSpec.rows.length)); slider.max = String(originalSpec.rows.length); slider.value = String(originalSpec.rows.length); controls.appendChild(slider); var topVal = document.createElement('span'); topVal.className = 'bid-chart-topn-val'; topVal.textContent = String(originalSpec.rows.length); controls.appendChild(topVal); slider.addEventListener('input', function() { mut.topN = parseInt(slider.value, 10); topVal.textContent = slider.value; applySpec(chartEl, originalSpec, mut); }); } var sep1 = document.createElement('span'); sep1.className = 'bid-chart-sep'; controls.appendChild(sep1); /* Sort */ var sortLabel = document.createElement('label'); sortLabel.textContent = 'Sort'; controls.appendChild(sortLabel); var sortSel = document.createElement('select'); [ { v: 'original', l: 'Original' }, { v: 'value-desc', l: 'Value ↓' }, { v: 'value-asc', l: 'Value ↑' }, { v: 'label-asc', l: 'A → Z' } ].forEach(function(o) { var opt = document.createElement('option'); opt.value = o.v; opt.textContent = o.l; sortSel.appendChild(opt); }); controls.appendChild(sortSel); sortSel.addEventListener('change', function() { mut.sort = sortSel.value; applySpec(chartEl, originalSpec, mut); }); var sep2 = document.createElement('span'); sep2.className = 'bid-chart-sep'; controls.appendChild(sep2); /* Chart type swap */ var typeLabel = document.createElement('label'); typeLabel.textContent = 'Type'; controls.appendChild(typeLabel); var typeSel = document.createElement('select'); [ { v: 'horizontal-bars', l: 'Horizontal' }, { v: 'vertical-bars', l: 'Vertical' } ].forEach(function(o) { var opt = document.createElement('option'); opt.value = o.v; opt.textContent = o.l; if (o.v === originalSpec.type) opt.selected = true; typeSel.appendChild(opt); }); controls.appendChild(typeSel); typeSel.addEventListener('change', function() { mut.type = typeSel.value; applySpec(chartEl, originalSpec, mut); }); var sep3 = document.createElement('span'); sep3.className = 'bid-chart-sep'; controls.appendChild(sep3); /* Reset */ var reset = document.createElement('button'); reset.type = 'button'; reset.className = 'bid-chart-reset'; reset.textContent = 'Reset'; controls.appendChild(reset); reset.addEventListener('click', function() { mut = { sort: 'original', topN: originalSpec.rows.length, type: originalSpec.type }; if (slider) { slider.value = String(originalSpec.rows.length); topVal.textContent = slider.value; } sortSel.value = 'original'; typeSel.value = originalSpec.type; applySpec(chartEl, originalSpec, mut); }); } function attach() { document.querySelectorAll('.bid-chart').forEach(function(el) { if (el.dataset.bidChartWired === '1') return; el.dataset.bidChartWired = '1'; buildControls(el); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', attach); } else { attach(); } })(); `; /* ─── CSV parsing + profiling (mirror of render-survey.ts) ─── */ function splitRow(line) { const out = []; let cur = ''; let inQ = false; for (const ch of line) { if (ch === '"') { inQ = !inQ; continue; } if (ch === ',' && !inQ) { out.push(cur); cur = ''; continue; } cur += ch; } out.push(cur); return out; } function parseCsv(text) { const lines = text.replace(/\r/g, '').trim().split('\n'); if (lines.length < 2) throw new Error('CSV has fewer than 2 lines (no data rows).'); const headers = splitRow(lines[0]); const rows = lines.slice(1).map(line => { const cells = splitRow(line); const o = {}; headers.forEach((h, i) => { o[h] = (cells[i] || '').trim(); }); return o; }); return { headers, rows }; } function tally(rows, col) { const m = new Map(); for (const r of rows) { const v = (r[col] || '').trim() || '(blank)'; m.set(v, (m.get(v) || 0) + 1); } return [...m.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count); } function numericCol(rows, col) { return rows.map(r => parseFloat(r[col])).filter(n => Number.isFinite(n)); } function meanOf(xs) { return xs.length ? xs.reduce((s, n) => s + n, 0) / xs.length : 0; } function medianOf(xs) { if (!xs.length) return 0; const s = [...xs].sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 === 0 ? (s[m - 1] + s[m]) / 2 : s[m]; } function fmtUsd(v) { if (v >= 1e9) return '$' + (v / 1e9).toFixed(2) + 'B'; if (v >= 1e6) return '$' + (v / 1e6).toFixed(1) + 'M'; if (v >= 1e3) return '$' + (v / 1e3).toFixed(0) + 'K'; return '$' + v.toFixed(0); } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function dataAttrEsc(s) { return String(s).replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } const EXPECTED_COLS = [ 'Company_ID','Company_Name','Industry','Region','Company_Size','Employees', 'Annual_Revenue_USD_M','Primary_AI_Focus','AI_Maturity_1_5','Data_Readiness_1_5', 'Executive_Sponsorship_1_5','AI_Governance_Maturity_1_5','Deployment_Stage', 'AI_Use_Case_Count','Dedicated_AI_Team_Size','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Estimated_Annual_Value_Created_USD','Expected_Payback_Months','Implementation_Risk_1_5', 'Budget_Growth_Next_12M_Pct','Top_AI_Tools_Used','Primary_Cloud_Platform', 'Primary_Buyer','Procurement_Model' ]; const REQUIRED_COLS = [ 'Industry','Region','Company_Size','Primary_AI_Focus','AI_Maturity_1_5', 'Deployment_Stage','Annual_AI_Spend_USD','Predicted_ROI_Pct', 'Implementation_Risk_1_5','Budget_Growth_Next_12M_Pct','Primary_Cloud_Platform' ]; /* ─── Editable wrapper (matches makeEditable from editable-layer.ts) ─── */ function makeEditable(blockId, sectionId, template, content) { return '' + content + ''; } /* ─── Chart wrapper (matches wrapChart from chart-controls.ts) ─── */ let __chartCounter = 0; function nextChartId(prefix) { __chartCounter += 1; return prefix + '-' + __chartCounter; } function wrapChart(spec, initialSvg, titleHtml, subtitleHtml) { const specB64 = btoa(unescape(encodeURIComponent(JSON.stringify(spec)))); return '
' + '
' + titleHtml + '
' + '
' + subtitleHtml + '
' + '
' + '
' + initialSvg + '
' + '
'; } /* ─── Horizontal-bars renderer (mirror of render-survey.ts) ─── */ function horizontalBarsHtml(opts) { const VB_W = 800, BAR_H = 24, BAR_GAP = 6, BAR_X_START = 220, BAR_X_END = 660; const BAR_AREA_TOP = 28; const BAR_AREA_BOTTOM = BAR_AREA_TOP + opts.rows.length * BAR_H + (opts.rows.length - 1) * BAR_GAP; const X_AXIS_Y = BAR_AREA_BOTTOM + 22, AXIS_TITLE_Y = X_AXIS_Y + 20, VB_H = AXIS_TITLE_Y + 10; const rawMax = Math.max(...opts.rows.map(r => r.value), 1) * 1.15; const mag = Math.pow(10, Math.floor(Math.log10(rawMax))); const xMax = Math.ceil(rawMax / mag) * mag; const xScale = v => BAR_X_START + (v / xMax) * (BAR_X_END - BAR_X_START); const bars = opts.rows.map((r, i) => { const y = BAR_AREA_TOP + i * (BAR_H + BAR_GAP); const w = Math.max(xScale(r.value) - BAR_X_START, 1); const labelY = y + BAR_H / 2 + 4; return '' + esc(r.label) + ': ' + esc(opts.fmtValue(r.value)) + '' + '' + esc(opts.fmtValue(r.value)) + (r.sublabel ? ' · ' + esc(r.sublabel) : '') + '' + '' + esc(r.label) + ''; }).join(''); const ticks = [0, xMax / 2, xMax].map(t => { const x = xScale(t); return '' + esc(opts.fmtValue(t)) + ''; }).join(''); const initialSvg = '' + bars + '' + ticks + '' + esc(opts.axisLabel) + '' + ''; const titleHtml = makeEditable(opts.blockIdBase + '-title', opts.sectionId, 'subheading', opts.title); const subtitleHtml = makeEditable(opts.blockIdBase + '-subtitle', opts.sectionId, 'caveat-band', opts.subtitle); const spec = { chartId: nextChartId(opts.blockIdBase), type: 'horizontal-bars', rows: opts.rows.map(r => ({ label: r.label, value: r.value, sublabel: r.sublabel })), valueFormat: opts.valueFormat, axisLabel: opts.axisLabel, color: '#00A2A3' }; return wrapChart(spec, initialSvg, titleHtml, subtitleHtml); } /* ─── Section builders ────────────────────────────────────── */ function buildCover(title, subline) { return '
' + '
Sia Partners · BID Framework · Survey Read · Sandbox-rendered
' + '

' + makeEditable('cover-title', '00-cover', 'headline', esc(title)) + '

' + '
' + makeEditable('cover-subline', '00-cover', 'subheading', esc(subline)) + '
' + '
'; } function buildAtAGlance(rows) { const n = rows.length; const industries = tally(rows, 'Industry').length; const regions = tally(rows, 'Region').length; const spend = numericCol(rows, 'Annual_AI_Spend_USD'); const maturity = numericCol(rows, 'AI_Maturity_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const inner = 'Surveyed ' + n + ' respondents across ' + industries + ' industries and ' + regions + ' regions. Median AI maturity is ' + medianOf(maturity).toFixed(1) + ' / 5; median annual AI spend is ' + fmtUsd(medianOf(spend)) + '; respondents project +' + medianOf(budgetGrowth).toFixed(0) + '% budget growth over the next 12 months.'; return '
' + '
01 · At a glance
' + '
' + makeEditable('sv-at-a-glance', '01-at-a-glance', 'at-a-glance', inner) + '
'; } function buildSection(num, title, body) { return '
' + '
' + num + ' · ' + esc(title) + '
' + body + '
'; } function buildMaturity(rows) { const counts = new Map([['1', 0], ['2', 0], ['3', 0], ['4', 0], ['5', 0]]); for (const r of rows) { const v = (r['AI_Maturity_1_5'] || '').trim(); if (counts.has(v)) counts.set(v, counts.get(v) + 1); } const bars = [...counts.entries()].map(([k, v]) => ({ label: 'Level ' + k, value: v, sublabel: ((v / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'AI maturity distribution (1 = nascent · 5 = optimized)', subtitle: 'Self-reported maturity level across all respondents', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '02-maturity', blockIdBase: 'sv-maturity' }); } function buildDeploymentFunnel(rows) { const order = ['Pilot','Departmental Rollout','Enterprise Rollout','Scaled / Optimized']; const counts = tally(rows, 'Deployment_Stage'); const ordered = order.map(k => counts.find(c => c.key === k) || { key: k, count: 0 }); const bars = ordered.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Where are respondents in the deployment journey?', subtitle: 'Self-reported stage · ordered Pilot → Scaled', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '03-deployment', blockIdBase: 'sv-deploy' }); } function buildFocusAreas(rows) { const counts = tally(rows, 'Primary_AI_Focus'); const bars = counts.slice(0, 10).map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Top primary AI focus areas', subtitle: 'Each respondent selects one primary focus · top 10 shown', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '04-focus', blockIdBase: 'sv-focus' }); } function buildSpendBySize(rows) { const sizes = ['Mid-Market','Enterprise','Large Enterprise','Global Enterprise']; const bars = sizes.map(size => { const sub = rows.filter(r => r['Company_Size'] === size); const spend = numericCol(sub, 'Annual_AI_Spend_USD'); return { label: size, value: medianOf(spend), sublabel: 'n=' + sub.length + ' · mean ' + fmtUsd(meanOf(spend)) }; }); return horizontalBarsHtml({ title: 'Median annual AI spend by company size', subtitle: 'Median used to dampen the long-tail of large programs · means shown alongside for context', rows: bars, fmtValue: fmtUsd, valueFormat: 'usd', axisLabel: 'Median annual AI spend ($)', sectionId: '05-spend', blockIdBase: 'sv-spend' }); } function buildIndustryView(rows) { const top = tally(rows, 'Industry').slice(0, 8); const bars = top.map(c => { const sub = rows.filter(r => r['Industry'] === c.key); return { label: c.key, value: meanOf(numericCol(sub, 'AI_Maturity_1_5')), sublabel: 'n=' + c.count + ' · median spend ' + fmtUsd(medianOf(numericCol(sub, 'Annual_AI_Spend_USD'))) }; }).sort((a, b) => b.value - a.value); return horizontalBarsHtml({ title: 'Industry view — mean AI maturity by industry', subtitle: 'Top 8 industries by respondent count · ordered by mean maturity (1–5 scale)', rows: bars, fmtValue: v => v.toFixed(1) + ' / 5', valueFormat: 'ratio_5', axisLabel: 'Mean AI maturity (1–5)', sectionId: '06-industry', blockIdBase: 'sv-industry' }); } function buildCloudPlatforms(rows) { const counts = tally(rows, 'Primary_Cloud_Platform'); const bars = counts.map(c => ({ label: c.key, value: c.count, sublabel: ((c.count / rows.length) * 100).toFixed(0) + '%' })); return horizontalBarsHtml({ title: 'Primary cloud platform mix', subtitle: 'Single platform per respondent · hybrid called out as its own category', rows: bars, fmtValue: v => '' + v, valueFormat: 'count', axisLabel: 'Number of respondents', sectionId: '07-cloud', blockIdBase: 'sv-cloud' }); } function buildSoWhat(rows) { const n = rows.length; const ent = rows.filter(r => ['Enterprise Rollout','Scaled / Optimized'].includes(r['Deployment_Stage'] || '')).length; const entShare = (ent / n) * 100; const roi = numericCol(rows, 'Predicted_ROI_Pct'); const risk = numericCol(rows, 'Implementation_Risk_1_5'); const budgetGrowth = numericCol(rows, 'Budget_Growth_Next_12M_Pct'); const azure = (tally(rows, 'Primary_Cloud_Platform').find(c => c.key === 'Microsoft Azure') || { count: 0 }).count; const azureShare = (azure / n) * 100; const topFocus = tally(rows, 'Primary_AI_Focus')[0] || { key: '', count: 0 }; const para = (id, body) => '

' + makeEditable(id, '08-sia-read', 'paragraph', body) + '

'; return '
' + '
08 · Sia\\'s read
' + '
' + para('sv-sowhat-p1', 'The market is past pilot. ' + entShare.toFixed(0) + '% of respondents report being at Enterprise Rollout or Scaled / Optimized. AI investment is no longer experimental at the scale represented here.') + para('sv-sowhat-p2', 'Self-reported confidence is high. Median predicted ROI is ' + medianOf(roi).toFixed(0) + '%; median implementation risk score is ' + medianOf(risk).toFixed(1) + ' / 5; respondents project budget growth of +' + medianOf(budgetGrowth).toFixed(0) + '% over the next 12 months. Honest caveat: these are self-reported — read as intent and posture, not as audited outcomes.') + para('sv-sowhat-p3', 'The shape of the market. ' + esc(topFocus.key) + ' leads primary focus (' + topFocus.count + ' respondents). Microsoft Azure is the dominant platform at ' + azureShare.toFixed(0) + '% share. Buyer profile skews to CIO / CTO and Chief Data Officer; business-unit-led purchasing is a strong third.') + '
'; } function buildMethodology(rows) { const row = (id, label, body) => '
' + '
' + esc(label) + '
' + '
' + makeEditable(id, '09-methodology', 'methodology-note', body) + '
' + '
'; return '
' + '
09 · Methodology
' + '
' + '

' + makeEditable('sv-meth-intro', '09-methodology', 'methodology-note', 'Survey-mode deliverable rendered live in your browser by the BID sandbox. n=' + rows.length + ' respondents. All distributions, medians, and means computed directly from the uploaded CSV — no proprietary calibration or external anchors applied.') + '

' + row('sv-meth-source', 'Source', 'Uploaded CSV (client-side, no server round-trip). Single response per respondent. No weighting or post-stratification.') + row('sv-meth-counts', 'Counts', 'Industry / region / company-size / focus-area / cloud-platform tallies are raw response counts. Percentages shown alongside use n=' + rows.length + ' as the denominator.') + row('sv-meth-numeric', 'Numeric stats', 'Median used as the central tendency for $-denominated and skewed fields. Mean shown alongside. Risk and maturity scores use 1–5 Likert; mean is appropriate.') + row('sv-meth-bias', 'Read', 'Self-reported survey — ROI, payback, and risk scores reflect respondent intent and confidence, not audited outcomes.') + '
'; } function buildFooter() { return '
' + '
BID Framework · Survey-mode deliverable · rendered live in browser via the sandbox
' + '
All sections editable — hover to format, click ✎ to refine, hover charts for controls.
' + '
'; } function composeDeliverable(rows, title) { __chartCounter = 0; const subline = 'n=' + rows.length + ' respondents · live render from uploaded CSV'; const body = '
' + buildCover(title, subline) + buildAtAGlance(rows) + buildSection('02', 'Adoption maturity', buildMaturity(rows)) + buildSection('03', 'Deployment journey', buildDeploymentFunnel(rows)) + buildSection('04', 'What they\\'re building', buildFocusAreas(rows)) + buildSection('05', 'Investment by company size', buildSpendBySize(rows)) + buildSection('06', 'Industry view', buildIndustryView(rows)) + buildSection('07', 'Cloud platform mix', buildCloudPlatforms(rows)) + buildSoWhat(rows) + buildMethodology(rows) + buildFooter() + '
'; return '' + '' + esc(title) + '' + '' + '' + EDITABLE_CSS + CHART_CSS + '' + EDITABLE_TOOLBAR + body + EDITABLE_JS + CHART_JS + ''; } /* ─── Sandbox UI wiring ────────────────────────────────────── */ let parsedRows = null; const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const panel = document.getElementById('panel-summary'); const errPanel = document.getElementById('panel-error'); const renderBtn = document.getElementById('btn-render'); const clearBtn = document.getElementById('btn-clear'); function reset() { parsedRows = null; panel.classList.remove('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); renderBtn.disabled = true; fileInput.value = ''; } function showError(msg) { reset(); document.getElementById('error-detail').textContent = msg; errPanel.classList.add('visible'); dropzone.classList.add('error'); } function showSummary(parsed) { parsedRows = parsed.rows; document.getElementById('stat-rows').textContent = String(parsed.rows.length); document.getElementById('stat-cols').textContent = String(parsed.headers.length); const found = new Set(parsed.headers); const missing = REQUIRED_COLS.filter(c => !found.has(c)); const extras = parsed.headers.filter(h => !EXPECTED_COLS.includes(h)); const schemaEl = document.getElementById('stat-schema'); const detailEl = document.getElementById('schema-check'); if (missing.length === 0) { schemaEl.innerHTML = '✓ Match'; detailEl.innerHTML = extras.length > 0 ? '
Extra columns (ignored): ' + extras.map(esc).join(', ') + '
' : '
All required columns present.
'; renderBtn.disabled = false; } else { schemaEl.innerHTML = '' + missing.length + ' missing'; detailEl.innerHTML = '
Required columns missing:
    ' + missing.map(c => '
  • ' + esc(c) + '
  • ').join('') + '
'; renderBtn.disabled = true; } panel.classList.add('visible'); errPanel.classList.remove('visible'); dropzone.classList.remove('error'); } function handleFile(file) { if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const parsed = parseCsv(String(reader.result)); showSummary(parsed); } catch (e) { showError(e.message || String(e)); } }; reader.onerror = () => showError('Could not read file.'); reader.readAsText(file); } dropzone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', e => handleFile(e.target.files[0])); dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('hover'); }); dropzone.addEventListener('dragleave', () => dropzone.classList.remove('hover')); dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('hover'); handleFile(e.dataTransfer.files[0]); }); clearBtn.addEventListener('click', reset); renderBtn.addEventListener('click', () => { if (!parsedRows) return; const title = 'Survey deliverable · live render · n=' + parsedRows.length; const html = composeDeliverable(parsedRows, title); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); }); /* Sample CSV download — fetches the inaugural B2B AI survey from * the bundle and lets the user save it. */ document.getElementById('download-sample').addEventListener('click', async e => { e.preventDefault(); try { const r = await fetch('b2b-enterprise-ai-survey-200.csv'); if (!r.ok) throw new Error('sample CSV not bundled (' + r.status + ')'); const text = await r.text(); const blob = new Blob([text], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'b2b-enterprise-ai-survey-200.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { alert('Could not load sample CSV: ' + err.message); } });