/* Waterfall Chart — frontend.js (Chart.js floating bars) */ (function () { 'use strict'; var CHARTJS_CDN = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js'; function loadChartJs(cb) { if (window.Chart) { cb(); return; } var s = document.createElement('script'); s.src = CHARTJS_CDN; s.onload = cb; document.head.appendChild(s); } function hexToRgb(hex) { var r = parseInt((hex || '#000000').slice(1, 3), 16); var g = parseInt((hex || '#000000').slice(3, 5), 16); var b = parseInt((hex || '#000000').slice(5, 7), 16); return r + ',' + g + ',' + b; } function abbr(n, prefix, suffix, doAbbr) { var abs = Math.abs(n); var sign = n < 0 ? '-' : ''; var str; if (!doAbbr || abs < 1000) { str = abs.toLocaleString(); } else if (abs < 1000000) { str = (abs / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; } else if (abs < 1000000000) { str = (abs / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; } else { str = (abs / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B'; } return sign + prefix + str + suffix; } function typeColor(type, o) { if (type === 'start') return o.startColor; if (type === 'increase') return o.increaseColor; if (type === 'decrease') return o.decreaseColor; return o.totalColor; } /* Compute [base, top] for each bar */ function computeFloats(items, o) { var running = 0; return items.map(function (item) { var v = parseFloat(item.value) || 0; var type = item.type || 'increase'; var base, top; if (type === 'start') { base = 0; top = v; running = v; } else if (type === 'total') { base = 0; top = running; } else if (type === 'increase') { base = running; top = running + v; running = top; } else { /* decrease */ top = running; base = running + v; running = base; } return { label: item.label, type: type, floatY: [Math.min(base, top), Math.max(base, top)], displayValue: v, barColor: typeColor(type, o), running: running }; }); } function initChart(appEl, o) { var wrap = document.createElement('div'); wrap.className = 'bkbg-wfc-wrap'; wrap.style.background = o.bgColor; wrap.style.borderRadius = '10px'; wrap.style.padding = (o.paddingTop || 24) + 'px 24px ' + (o.paddingBottom || 24) + 'px'; wrap.style.maxWidth = o.maxWidth + 'px'; wrap.style.margin = '0 auto'; if (o.showTitle && o.title) { var titleEl = document.createElement('div'); titleEl.className = 'bkbg-wfc-title'; titleEl.textContent = o.title; titleEl.style.color = o.titleColor; wrap.appendChild(titleEl); } var canvasWrap = document.createElement('div'); canvasWrap.className = 'bkbg-wfc-canvas-wrap'; canvasWrap.style.height = o.height + 'px'; var canvas = document.createElement('canvas'); canvas.style.width = '100%'; canvas.style.height = '100%'; canvasWrap.appendChild(canvas); wrap.appendChild(canvasWrap); /* tooltip */ var tooltip = document.createElement('div'); tooltip.className = 'bkbg-wfc-tooltip'; canvasWrap.appendChild(tooltip); /* legend */ if (o.showLegend) { var legend = document.createElement('div'); legend.className = 'bkbg-wfc-legend'; var legendItems = [ { type: 'start', label: 'Start', color: o.startColor }, { type: 'increase', label: 'Increase', color: o.increaseColor }, { type: 'decrease', label: 'Decrease', color: o.decreaseColor }, { type: 'total', label: 'Total', color: o.totalColor } ]; legendItems.forEach(function (li) { var item = document.createElement('div'); item.className = 'bkbg-wfc-legend-item'; item.style.color = o.labelColor; var dot = document.createElement('span'); dot.className = 'bkbg-wfc-legend-dot'; dot.style.background = li.color; var lbl = document.createElement('span'); lbl.textContent = li.label; item.appendChild(dot); item.appendChild(lbl); legend.appendChild(item); }); wrap.appendChild(legend); } appEl.parentNode.insertBefore(wrap, appEl); appEl.style.display = 'none'; loadChartJs(function () { var segs = computeFloats(o.items || [], o); var labels = segs.map(function (s) { return s.label; }); var floatData = segs.map(function (s) { return s.floatY; }); var barColors = segs.map(function (s) { return 'rgba(' + hexToRgb(s.barColor) + ',0.88)'; }); var barColorsBorder = segs.map(function (s) { return s.barColor; }); var displayValues = segs.map(function (s) { return s.displayValue; }); /* Connector lines via custom plugin */ var bR = o.borderRadius || 0; var connectorPlugin = { id: 'bkbg-wfc-connectors', afterDatasetsDraw: function (chart) { if (!o.showConnectors) return; var ctx = chart.ctx; var meta = chart.getDatasetMeta(0); ctx.save(); ctx.strokeStyle = o.connectorColor || '#9ca3af'; ctx.lineWidth = 1.5; ctx.setLineDash([5, 4]); for (var i = 0; i < meta.data.length - 1; i++) { var curr = meta.data[i]; var next = meta.data[i + 1]; if (!curr || !next) continue; var seg = segs[i]; /* connect from top of current to top of next start */ var connY = seg.type === 'decrease' ? chart.scales.y.getPixelForValue(seg.floatY[1]) : chart.scales.y.getPixelForValue(seg.floatY[1]); ctx.beginPath(); ctx.moveTo(curr.x + curr.width / 2, connY); ctx.lineTo(next.x - next.width / 2, connY); ctx.stroke(); } ctx.restore(); } }; new window.Chart(canvas, { type: 'bar', plugins: [connectorPlugin], data: { labels: labels, datasets: [{ data: floatData, backgroundColor: barColors, borderColor: barColorsBorder, borderWidth: 0, borderRadius: bR, borderSkipped: false, barThickness: o.barThickness || 48 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: false, external: function (context) { var tooltipModel = context.tooltip; if (!tooltipModel || tooltipModel.opacity === 0) { tooltip.classList.remove('is-visible'); return; } var idx = tooltipModel.dataPoints[0].dataIndex; var seg = segs[idx]; tooltip.innerHTML = '