Files
vctp2/dist/assets/js/web3-charts.js
Nathan Coad a993aedf79
All checks were successful
continuous-integration/drone/push Build is passing
use javascript chart instead of svg
2026-02-06 16:42:48 +11:00

522 lines
13 KiB
JavaScript

(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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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('<div class="web3-chart-tooltip-title">' + escapeHTML(config.labels[idx] || "") + "</div>");
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(
'<div class="web3-chart-tooltip-row">' +
'<span class="web3-chart-tooltip-label"><span class="web3-chart-tooltip-swatch" style="background:' + escapeHTML(series.color || "#2563eb") + '"></span>' +
escapeHTML(series.name || "Series") +
"</span>" +
'<span class="web3-chart-tooltip-value">' + escapeHTML(valueLabel) + "</span>" +
"</div>"
);
}
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(
'<div class="web3-chart-tooltip-row">' +
'<span class="web3-chart-tooltip-label">' + escapeHTML(hover.name || "Value") + "</span>" +
'<span class="web3-chart-tooltip-value">' + escapeHTML(label) + "</span>" +
"</div>"
);
}
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,
};
})();