use javascript chart instead of svg
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
521
dist/assets/js/web3-charts.js
vendored
Normal file
521
dist/assets/js/web3-charts.js
vendored
Normal file
@@ -0,0 +1,521 @@
|
||||
(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, """)
|
||||
.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('<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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user