Marketing and Conversion Analytics Generator

Single-file simulated SQL analytics
Ready

1. Marketing ROI Analysis

/*
  Query 1 — Marketing ROI Analysis

  Required canonical query:

  SELECT mc.name,
         COUNT(DISTINCT wv.visitor_id) AS visitors,
         COUNT(DISTINCT c.conversion_id) AS conversions,
         SUM(c.value) AS total_revenue,
         mc.budget_spent,
         (SUM(c.value) - mc.budget_spent) / mc.budget_spent * 100 AS roi_percent
  FROM marketing_campaigns mc
  LEFT JOIN website_visitors wv
    ON DATE(wv.session_start) BETWEEN mc.start_date AND mc.end_date
  LEFT JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  GROUP BY mc.campaign_id;

  Date-range-filtered version used in this app:

  SELECT mc.name,
         COUNT(DISTINCT wv.visitor_id) AS visitors,
         COUNT(DISTINCT c.conversion_id) AS conversions,
         SUM(c.value) AS total_revenue,
         mc.budget_spent,
         CASE
           WHEN mc.budget_spent = 0 THEN NULL
           ELSE (SUM(c.value) - mc.budget_spent) / mc.budget_spent * 100
         END AS roi_percent
  FROM marketing_campaigns mc
  LEFT JOIN website_visitors wv
    ON DATE(wv.session_start) BETWEEN mc.start_date AND mc.end_date
  LEFT JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  WHERE mc.start_date <= @end_date
    AND mc.end_date >= @start_date
    AND (wv.session_start IS NULL OR DATE(wv.session_start) BETWEEN @start_date AND @end_date)
    AND (c.conversion_date IS NULL OR DATE(c.conversion_date) BETWEEN @start_date AND @end_date)
  GROUP BY mc.campaign_id;
*/

2. Funnel Conversion Rates

/*
  Query 2 — Funnel Conversion Rates

  Required canonical query:

  SELECT source,
         COUNT(DISTINCT visitor_id) AS total_visitors,
         SUM(CASE WHEN conversion_type = 'signup' THEN 1 ELSE 0 END) AS signups,
         SUM(CASE WHEN conversion_type = 'purchase' THEN 1 ELSE 0 END) AS purchases,
         (SUM(CASE WHEN conversion_type = 'purchase' THEN 1 ELSE 0 END) * 1.0 / COUNT(DISTINCT visitor_id)) * 100 AS conversion_rate
  FROM website_visitors wv
  LEFT JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  WHERE session_start >= DATE_SUB(NOW(), INTERVAL 30 DAY)
  GROUP BY source;

  Date-range-filtered version used in this app:

  SELECT wv.source,
         COUNT(DISTINCT wv.visitor_id) AS total_visitors,
         SUM(CASE WHEN c.conversion_type = 'signup' THEN 1 ELSE 0 END) AS signups,
         SUM(CASE WHEN c.conversion_type = 'purchase' THEN 1 ELSE 0 END) AS purchases,
         CASE
           WHEN COUNT(DISTINCT wv.visitor_id) = 0 THEN 0
           ELSE (SUM(CASE WHEN c.conversion_type = 'purchase' THEN 1 ELSE 0 END) * 1.0 / COUNT(DISTINCT wv.visitor_id)) * 100
         END AS conversion_rate
  FROM website_visitors wv
  LEFT JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  WHERE DATE(wv.session_start) BETWEEN @start_date AND @end_date
    AND (c.conversion_date IS NULL OR DATE(c.conversion_date) BETWEEN @start_date AND @end_date)
  GROUP BY wv.source;
*/

3. Cohort Analysis + Heat Map

/*
  Query 3 — Cohort Analysis

  Required canonical query:

  SELECT DATE_FORMAT(session_start, '%Y-%m') AS cohort_month,
         COUNT(DISTINCT visitor_id) AS visitors,
         AVG(pages_viewed) AS avg_pages_per_session,
         COUNT(DISTINCT conversion_id) / COUNT(DISTINCT visitor_id) * 100 AS conversion_rate_percent
  FROM website_visitors wv
  LEFT JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  GROUP BY DATE_FORMAT(session_start, '%Y-%m')
  ORDER BY cohort_month;

  Date-range-filtered version used in this app:

  SELECT DATE_FORMAT(wv.session_start, '%Y-%m') AS cohort_month,
         COUNT(DISTINCT wv.visitor_id) AS visitors,
         AVG(wv.pages_viewed) AS avg_pages_per_session,
         CASE
           WHEN COUNT(DISTINCT wv.visitor_id) = 0 THEN 0
           ELSE COUNT(DISTINCT c.conversion_id) / COUNT(DISTINCT wv.visitor_id) * 100
         END AS conversion_rate_percent
  FROM website_visitors wv
  LEFT JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  WHERE DATE(wv.session_start) BETWEEN @start_date AND @end_date
    AND (c.conversion_date IS NULL OR DATE(c.conversion_date) BETWEEN @start_date AND @end_date)
  GROUP BY DATE_FORMAT(wv.session_start, '%Y-%m')
  ORDER BY cohort_month;
*/
Low Medium-low Medium High Very high

4. Customer Lifetime Value by Source

/*
  Query 4 — Customer Lifetime Value by Source

  Required canonical query:

  SELECT wv.source,
         COUNT(DISTINCT wv.visitor_id) AS unique_customers,
         AVG(c.value) AS avg_order_value,
         SUM(c.value) / COUNT(DISTINCT c.conversion_id) AS lifetime_value
  FROM website_visitors wv
  JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  WHERE c.conversion_type = 'purchase'
  GROUP BY wv.source;

  Date-range-filtered version used in this app:

  SELECT wv.source,
         COUNT(DISTINCT wv.visitor_id) AS unique_customers,
         AVG(c.value) AS avg_order_value,
         SUM(c.value) / COUNT(DISTINCT c.conversion_id) AS lifetime_value
  FROM website_visitors wv
  JOIN conversions c
    ON wv.visitor_id = c.visitor_id
  WHERE c.conversion_type = 'purchase'
    AND DATE(wv.session_start) BETWEEN @start_date AND @end_date
    AND DATE(c.conversion_date) BETWEEN @start_date AND @end_date
  GROUP BY wv.source;
*/
`; const win = window.open("", "_blank"); win.document.write(html); win.document.close(); } function drawTextTableInPDF(doc, columns, rows, widths) { const margin = 40; const maxY = doc.internal.pageSize.getHeight() - margin; let y = margin + 42; const headerLines = columns.map((c, i) => doc.splitTextToSize(c.label, widths[i] - 4)); const maxHeaderLines = Math.max(...headerLines.map(l => l.length)); for (let lineIndex = 0; lineIndex < maxHeaderLines; lineIndex++) { let x = margin; for (let i = 0; i < columns.length; i++) { const lines = headerLines[i]; if (lines[lineIndex]) { doc.text(lines[lineIndex], x, y); } x += widths[i]; } y += 12; if (y > maxY) { doc.addPage(); y = margin + 40; } } doc.setFillColor(241, 245, 249); doc.rect(margin, y, widths.reduce((a, b) => a + b, 0), maxHeaderLines * 12, "fill"); for (const row of rows) { const cellLines = columns.map((c, i) => doc.splitTextToSize(rawValue(row[c.key], c.format), widths[i] - 4)); const maxLines = Math.max(...cellLines.map(l => l.length)); if (y + maxLines * 12 > maxY) { doc.addPage(); y = margin + 40; } let x = margin; for (let i = 0; i < columns.length; i++) { const lines = cellLines[i]; for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { if (lines[lineIndex]) { doc.text(lines[lineIndex], x, y + lineIndex * 12); } } x += widths[i]; } y += maxLines * 12 + 6; } } function exportPDF(tableId) { const title = `Analytics Report - ${tableId}`; const range = `${startDateInput.value} to ${endDateInput.value}`; if (!window.jspdf || !window.jspdf.jsPDF) { fallbackPrint(tableId, title); return; } const doc = new window.jspdf.jsPDF({ unit: "pt", format: "a4" }); const margin = 40; const available = doc.internal.pageSize.getWidth() - margin * 2; const columns = TABLE_COLUMNS[tableId]; const rows = current[tableId]; doc.setFontSize(16); doc.text(title, margin, margin); doc.setFontSize(10); doc.text(`Date range: ${range}`, margin, margin + 18); if (!rows.length) { doc.text("No rows returned for this date range.", margin, margin + 40); doc.save(`report_${tableId}.pdf`); return; } const rawWidths = columns.map(c => c.width || 100); const totalRawWidth = rawWidths.reduce((a, b) => a + b, 0); const scaledWidths = rawWidths.map(w => Math.max(60, Math.floor(available * (w / totalRawWidth)))); drawTextTableInPDF(doc, columns, rows, scaledWidths); doc.save(`report_${tableId}.pdf`); } function exportAllCSV() { const sections = ["table1", "table2", "table3", "table4"]; const lines = []; for (const section of sections) { if (!current[section].length) continue; lines.push(`REPORT ${section.toUpperCase()}`); lines.push(`Date range: ${startDateInput.value} to ${endDateInput.value}`); const columns = TABLE_COLUMNS[section]; lines.push(columns.map(c => csvEscape(c.label)).join(",")); for (const row of current[section]) { lines.push(columns.map(c => csvEscape(rawValue(row[c.key], c.format))).join(",")); } lines.push(""); } downloadBlob(lines.join("\n"), "all_analytics_reports.csv", "text/csv;charset=utf-8;"); } function exportAllPDF() { if (!window.jspdf || !window.jspdf.jsPDF) { alert("jsPDF is unavailable. Individual PDF buttons fall back to printable PDF when clicked."); return; } const doc = new window.jspdf.jsPDF({ unit: "pt", format: "a4" }); const margin = 40; const available = doc.internal.pageSize.getWidth() - margin * 2; doc.setFontSize(18); doc.text("Marketing and Conversion Analytics Report", margin, margin); doc.setFontSize(10); doc.text(`Date range: ${startDateInput.value} to ${endDateInput.value}`, margin, margin + 18); let y = margin + 42; const sections = ["table1", "table2", "table3", "table4"]; for (const section of sections) { const columns = TABLE_COLUMNS[section]; const rows = current[section]; doc.setFontSize(13); doc.text(section.toUpperCase(), margin, y); y += 22; if (!rows.length) { doc.setFontSize(10); doc.text("No rows returned.", margin, y); y += 24; if (y > doc.internal.pageSize.getHeight() - margin) { doc.addPage(); y = margin + 40; } continue; } const rawWidths = columns.map(c => c.width || 100); const totalRawWidth = rawWidths.reduce((a, b) => a + b, 0); const scaledWidths = rawWidths.map(w => Math.max(60, Math.floor(available * (w / totalRawWidth)))); drawTextTableInPDF(doc, columns, rows, scaledWidths); y += 32; if (y > doc.internal.pageSize.getHeight() - margin) { doc.addPage(); y = margin + 40; } } doc.save("all_analytics_reports.pdf"); } function debounce(fn, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } function setPreset(days) { const end = new Date(); const start = new Date(); start.setDate(start.getDate() - days); startDateInput.value = isoDate(start); endDateInput.value = isoDate(end); } document.getElementById("runBtn").addEventListener("click", () => { runAllQueries(); }); document.getElementById("exportAllCsvBtn").addEventListener("click", exportAllCSV); document.getElementById("exportAllPdfBtn").addEventListener("click", exportAllPDF); presetSelect.addEventListener("change", () => { setPreset(Number(presetSelect.value)); runAllQueries(); }); startDateInput.addEventListener("change", debounce(runAllQueries, 150)); endDateInput.addEventListener("change", debounce(runAllQueries, 150)); document.querySelectorAll("[data-export]").forEach(button => { button.addEventListener("click", () => { const type = button.dataset.export; const tableId = button.dataset.table; if (type === "csv") { exportCSV(tableId); } else if (type === "pdf") { exportPDF(tableId); } }); }); /* Real-time sidebar update. This recomputes lightweight metrics without re-rendering every chart. */ function updateRealTimeMetrics() { const data = getFilteredData(); renderKPIs(data); } startDateInput.addEventListener("input", debounce(updateRealTimeMetrics, 100)); endDateInput.addEventListener("input", debounce(updateRealTimeMetrics, 100)); setPreset(30); runAllQueries();