(function () { "use strict"; function clamp(value, min, max) { if (value < min) { return min; } if (value > max) { return max; } return value; } function toNumber(value) { var num = Number(value); return Number.isFinite(num) ? num : null; } function escapeHTML(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function formatValue(value, format) { if (value === null || value === undefined || Number.isNaN(value)) { return "-"; } switch (format) { case "int": return String(Math.round(value)); case "float1": return Number(value).toFixed(1); case "float2": return Number(value).toFixed(2); default: return String(value); } } function pickTickIndices(total, desired) { if (total <= 0) { return []; } if (total === 1) { return [0]; } var target = Math.max(2, Math.min(total, desired || 6)); if (target >= total) { var all = []; for (var i = 0; i < total; i++) { all.push(i); } return all; } var indices = [0]; var step = (total - 1) / (target - 1); for (var j = 1; j < target - 1; j++) { indices.push(Math.round(j * step)); } indices.push(total - 1); var seen = {}; var deduped = []; for (var k = 0; k < indices.length; k++) { var idx = indices[k]; if (!seen[idx]) { seen[idx] = true; deduped.push(idx); } } deduped.sort(function (a, b) { return a - b; }); return deduped; } function getPlotBounds(width, height) { return { left: 52, top: 16, right: width - 20, bottom: height - 78, }; } function buildScales(config, plot) { var maxY = 0; for (var i = 0; i < config.series.length; i++) { var values = config.series[i].values || []; for (var j = 0; j < values.length; j++) { var value = toNumber(values[j]); if (value !== null && value > maxY) { maxY = value; } } } if (maxY <= 0) { maxY = 1; } var count = config.labels.length; var xSpan = plot.right - plot.left; var ySpan = plot.bottom - plot.top; return { maxY: maxY, xForIndex: function (index) { if (count <= 1) { return plot.left; } return plot.left + (index / (count - 1)) * xSpan; }, yForValue: function (value) { var numeric = toNumber(value); if (numeric === null) { return null; } return plot.bottom - (numeric / maxY) * ySpan; }, }; } function drawGrid(ctx, plot, config, scales) { ctx.save(); ctx.strokeStyle = "#e2e8f0"; ctx.lineWidth = 1; ctx.setLineDash([2, 4]); var yTickCount = Math.max(2, config.yTicks || 5); for (var i = 0; i < yTickCount; i++) { var yRatio = i / (yTickCount - 1); var y = plot.top + yRatio * (plot.bottom - plot.top); ctx.beginPath(); ctx.moveTo(plot.left, y); ctx.lineTo(plot.right, y); ctx.stroke(); } var xIndices = pickTickIndices(config.labels.length, config.xTicks || 6); for (var j = 0; j < xIndices.length; j++) { var x = scales.xForIndex(xIndices[j]); ctx.beginPath(); ctx.moveTo(x, plot.top); ctx.lineTo(x, plot.bottom); ctx.stroke(); } ctx.restore(); } function drawAxes(ctx, plot) { ctx.save(); ctx.strokeStyle = "#94a3b8"; ctx.lineWidth = 1.5; ctx.setLineDash([]); ctx.beginPath(); ctx.moveTo(plot.left, plot.bottom); ctx.lineTo(plot.right, plot.bottom); ctx.stroke(); ctx.beginPath(); ctx.moveTo(plot.left, plot.top); ctx.lineTo(plot.left, plot.bottom); ctx.stroke(); ctx.restore(); } function drawLabels(ctx, plot, config, scales) { ctx.save(); ctx.fillStyle = "#475569"; ctx.font = "10px sans-serif"; var yTickCount = Math.max(2, config.yTicks || 5); for (var i = 0; i < yTickCount; i++) { var ratio = i / (yTickCount - 1); var y = plot.top + ratio * (plot.bottom - plot.top); var value = scales.maxY * (1 - ratio); ctx.textAlign = "right"; ctx.textBaseline = "middle"; ctx.fillText(formatValue(value, "int"), plot.left - 8, y); } var xIndices = pickTickIndices(config.labels.length, config.xTicks || 6); for (var j = 0; j < xIndices.length; j++) { var idx = xIndices[j]; var tick = (config.tickLabels && config.tickLabels[idx]) || config.labels[idx] || ""; ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillText(tick, scales.xForIndex(idx), plot.bottom + 12); } if (config.yLabel) { ctx.save(); ctx.translate(16, plot.top + (plot.bottom-plot.top)/2); ctx.rotate(-Math.PI / 2); ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.font = "12px sans-serif"; ctx.fillText(config.yLabel, 0, 0); ctx.restore(); } if (config.xLabel) { ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.font = "12px sans-serif"; ctx.fillText(config.xLabel, plot.left + (plot.right-plot.left)/2, plot.bottom + 48); } ctx.restore(); } function drawSeries(ctx, plot, config, scales) { for (var i = 0; i < config.series.length; i++) { var series = config.series[i]; var values = series.values || []; if (!values.length) { continue; } ctx.save(); ctx.strokeStyle = series.color || "#2563eb"; ctx.lineWidth = series.lineWidth || 2.5; ctx.setLineDash(Array.isArray(series.dash) ? series.dash : []); ctx.beginPath(); var moved = false; for (var j = 0; j < values.length; j++) { var y = scales.yForValue(values[j]); if (y === null) { continue; } var x = scales.xForIndex(j); if (!moved) { ctx.moveTo(x, y); moved = true; } else { ctx.lineTo(x, y); } } ctx.stroke(); ctx.restore(); } } function drawLegend(ctx, config, width, height) { var x = 52; var y = height - 32; ctx.save(); ctx.font = "12px sans-serif"; ctx.textBaseline = "middle"; for (var i = 0; i < config.series.length; i++) { var series = config.series[i]; var label = series.name || "Series"; ctx.strokeStyle = series.color || "#2563eb"; ctx.fillStyle = "#475569"; ctx.lineWidth = series.lineWidth || 2.5; ctx.setLineDash(Array.isArray(series.dash) ? series.dash : []); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + 16, y); ctx.stroke(); ctx.setLineDash([]); ctx.fillText(label, x + 22, y); x += 22 + ctx.measureText(label).width + 18; if (x > width - 160) { x = 52; y += 18; } } ctx.restore(); } function updateTooltip(state, config) { if (!state.tooltip) { return; } if (state.hoverIndex === null) { state.tooltip.classList.remove("visible"); state.tooltip.setAttribute("aria-hidden", "true"); return; } var idx = state.hoverIndex; var rows = []; rows.push('
' + escapeHTML(config.labels[idx] || "") + "
"); for (var i = 0; i < config.series.length; i++) { var series = config.series[i]; if (series.tooltipHidden) { continue; } var values = series.values || []; var value = toNumber(values[idx]); var valueLabel = formatValue(value, series.tooltipFormat || "int"); rows.push( '
' + '' + escapeHTML(series.name || "Series") + "" + '' + escapeHTML(valueLabel) + "" + "
" ); } var hoverRows = config.hoverRows || []; for (var j = 0; j < hoverRows.length; j++) { var hover = hoverRows[j]; var values = hover.values || []; var label = values[idx] || "-"; rows.push( '
' + '' + escapeHTML(hover.name || "Value") + "" + '' + escapeHTML(label) + "" + "
" ); } state.tooltip.innerHTML = rows.join(""); state.tooltip.classList.add("visible"); state.tooltip.setAttribute("aria-hidden", "false"); var box = state.wrapper.getBoundingClientRect(); var tooltipBox = state.tooltip.getBoundingClientRect(); var left = clamp(state.mouseX + 14, 4, box.width - tooltipBox.width - 4); var top = clamp(state.mouseY + 14, 4, box.height - tooltipBox.height - 4); state.tooltip.style.left = left + "px"; state.tooltip.style.top = top + "px"; } function drawHover(ctx, plot, config, scales, hoverIndex) { if (hoverIndex === null) { return; } var x = scales.xForIndex(hoverIndex); ctx.save(); ctx.strokeStyle = "#94a3b8"; ctx.lineWidth = 1; ctx.setLineDash([3, 4]); ctx.beginPath(); ctx.moveTo(x, plot.top); ctx.lineTo(x, plot.bottom); ctx.stroke(); ctx.setLineDash([]); for (var i = 0; i < config.series.length; i++) { var series = config.series[i]; var values = series.values || []; var y = scales.yForValue(values[hoverIndex]); if (y === null) { continue; } ctx.fillStyle = "#ffffff"; ctx.strokeStyle = series.color || "#2563eb"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.restore(); } function renderLineChart(options) { if (!options || !options.canvasId || !options.config) { return; } var canvas = document.getElementById(options.canvasId); if (!canvas) { return; } var config = options.config; if (!Array.isArray(config.labels) || config.labels.length === 0 || !Array.isArray(config.series) || config.series.length === 0) { return; } var wrapper = canvas.parentElement; var tooltip = options.tooltipId ? document.getElementById(options.tooltipId) : null; var ctx = canvas.getContext("2d"); if (!ctx) { return; } var state = { canvas: canvas, wrapper: wrapper, tooltip: tooltip, hoverIndex: null, mouseX: 0, mouseY: 0, scales: null, plot: null, cssWidth: 0, cssHeight: config.height || 360, }; function redraw() { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); var dpr = window.devicePixelRatio || 1; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, state.cssWidth, state.cssHeight); drawGrid(ctx, state.plot, config, state.scales); drawAxes(ctx, state.plot); drawSeries(ctx, state.plot, config, state.scales); drawLabels(ctx, state.plot, config, state.scales); drawLegend(ctx, config, state.cssWidth, state.cssHeight); drawHover(ctx, state.plot, config, state.scales, state.hoverIndex); updateTooltip(state, config); } function resize() { var rect = canvas.getBoundingClientRect(); var width = Math.max(320, Math.floor(rect.width)); var height = config.height || 360; var dpr = window.devicePixelRatio || 1; canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr); canvas.style.height = height + "px"; state.cssWidth = width; state.cssHeight = height; state.plot = getPlotBounds(width, height); state.scales = buildScales(config, state.plot); redraw(); } canvas.addEventListener("mousemove", function (event) { var rect = canvas.getBoundingClientRect(); var x = event.clientX - rect.left; var y = event.clientY - rect.top; state.mouseX = x; state.mouseY = y; if (!state.plot || config.labels.length === 0) { return; } if (x < state.plot.left || x > state.plot.right || y < state.plot.top || y > state.plot.bottom) { state.hoverIndex = null; redraw(); return; } var ratio = (x - state.plot.left) / (state.plot.right - state.plot.left); var idx = Math.round(ratio * (config.labels.length - 1)); state.hoverIndex = clamp(idx, 0, config.labels.length - 1); redraw(); }); canvas.addEventListener("mouseleave", function () { state.hoverIndex = null; redraw(); }); window.addEventListener("resize", resize); if (window.ResizeObserver) { var observer = new ResizeObserver(function () { resize(); }); observer.observe(wrapper); } resize(); } function renderFromScript(options) { if (!options || !options.configId) { return; } var configNode = document.getElementById(options.configId); if (!configNode) { return; } var payload = configNode.textContent || ""; if (!payload.trim()) { return; } try { var config = JSON.parse(payload); renderLineChart({ canvasId: options.canvasId, tooltipId: options.tooltipId, config: config, }); } catch (error) { // Leave page functional even when chart config is malformed. } } function renderFromDataset(options) { if (!options || !options.canvasId) { return; } var canvas = document.getElementById(options.canvasId); if (!canvas) { return; } var payload = canvas.dataset.chartConfig || ""; if (!payload.trim()) { return; } try { var config = JSON.parse(payload); renderLineChart({ canvasId: options.canvasId, tooltipId: options.tooltipId, config: config, }); } catch (error) { // Leave page functional even when chart config is malformed. } } window.Web3Charts = { renderLineChart: renderLineChart, renderFromScript: renderFromScript, renderFromDataset: renderFromDataset, }; })();