Add primitive charting capabilities

This commit is contained in:
Alexey Milovidov 2022-06-18 18:15:45 +02:00
parent 8c8cd6a21d
commit eba897d4ec

View File

@ -386,6 +386,39 @@
text-align: center;
margin-top: 5em;
}
#chart
{
background-color: var(--element-background-color);
filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
display: none;
height: 70vh;
}
/* This is for charts (uPlot) */
.u-wrap {position: relative;user-select: none;}
.u-over, .u-under, .u-axis {position: absolute;}
.u-under {overflow: hidden;}
.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}
.u-legend {margin: auto;text-align: center; margin-top: 1em; font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, monospace;}
.u-inline {display: block;}
.u-inline * {display: inline-block;}
.u-inline tr {margin-right: 16px;}
.u-legend th {font-weight: 600;}
.u-legend th > * {vertical-align: middle;display: inline-block;}
.u-legend td { min-width: 13em; }
.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}
.u-inline.u-live th::after {content: ":";vertical-align: middle;}
.u-inline:not(.u-live) .u-value {display: none;}
.u-series > * {padding: 4px;}
.u-series th {cursor: pointer;}
.u-legend .u-off > * {opacity: 0.3;}
.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}
.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}
.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}
.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}
.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}
.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
</style>
</head>
@ -410,6 +443,7 @@
<table class="monospace-table shadow" id="data-table"></table>
<pre class="monospace-table shadow" id="data-unparsed"></pre>
</div>
<div id="chart"></div>
<svg id="graph" fill="none"></svg>
<p id="error" class="monospace shadow">
</p>
@ -530,8 +564,13 @@
if (status === 200) {
let json;
try { json = JSON.parse(response); } catch (e) {}
if (json !== undefined && json.statistics !== undefined) {
renderResult(json);
} else if (Array.isArray(json) && json.length == 2 &&
Array.isArray(json[0]) && Array.isArray(json[1]) && json[0].length > 1 && json[0].length == json[1].length) {
/// If user requested FORMAT JSONCompactColumns, we will render it as a chart.
renderChart(json);
} else {
renderUnparsedResult(response);
}
@ -578,30 +617,27 @@
}
}
function clearElement(id)
{
let elem = document.getElementById(id);
while (elem.firstChild) {
elem.removeChild(elem.lastChild);
}
elem.style.display = 'none';
}
function clear()
{
let table = document.getElementById('data-table');
while (table.firstChild) {
table.removeChild(table.lastChild);
}
let graph = document.getElementById('graph');
while (graph.firstChild) {
graph.removeChild(graph.lastChild);
}
graph.style.display = 'none';
document.getElementById('data-unparsed').innerText = '';
document.getElementById('data-unparsed').style.display = 'none';
document.getElementById('error').innerText = '';
document.getElementById('error').style.display = 'none';
clearElement('data-table');
clearElement('graph');
clearElement('chart');
clearElement('data-unparsed');
clearElement('error');
clearElement('hourglass');
document.getElementById('check-mark').innerText = '';
document.getElementById('hourglass').innerText = '';
document.getElementById('stats').innerText = '';
document.getElementById('hourglass').style.display = 'none';
document.getElementById('check-mark').style.display = 'none';
document.getElementById('logo-container').style.display = 'block';
}
@ -738,6 +774,7 @@
}
let table = document.getElementById('data-table');
table.appendChild(tbody);
table.style.display = 'table';
}
function renderTable(response)
@ -792,6 +829,7 @@
let table = document.getElementById('data-table');
table.appendChild(thead);
table.appendChild(tbody);
table.style.display = 'table';
}
/// A function to render raw data when non-default format is specified.
@ -873,16 +911,80 @@
svg.style.height = graph.graph().height;
}
function setColorTheme(theme) {
window.localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
let load_uplot_promise;
function loadUplot() {
if (load_uplot_promise) { return load_uplot_promise; }
load_uplot_promise = loadJS('https://cdn.jsdelivr.net/npm/uplot@1.6.21/dist/uPlot.iife.min.js',
'sha384-TwdJPnTsKP6pnvFZZKda0WJCXpjcHCa7MYHmjrYDu6rsEsb/UnFdoL0phS5ODqTA');
return load_uplot_promise;
}
let uplot;
async function renderChart(json)
{
await loadUplot();
clear();
let chart = document.getElementById('chart');
chart.style.display = 'block';
let paths = uPlot.paths.stepped({align: 1});
const [line_color, fill_color, grid_color, axes_color] = theme == 'light'
? ["#F80", "#FED", "#c7d0d9", "#2c3235"]
: ["#888", "#045", "#2c3235", "#c7d0d9"];
const opts = {
width: chart.clientWidth,
height: chart.clientHeight,
scales: { x: { time: json[0][0] > 1000000000 && json[0][0] < 2000000000 } },
axes: [ { stroke: axes_color,
grid: { width: 1 / devicePixelRatio, stroke: grid_color },
ticks: { width: 1 / devicePixelRatio, stroke: grid_color } },
{ stroke: axes_color,
grid: { width: 1 / devicePixelRatio, stroke: grid_color },
ticks: { width: 1 / devicePixelRatio, stroke: grid_color } } ],
series: [ { label: "x" },
{ label: "y", stroke: line_color, fill: fill_color,
drawStyle: 0, lineInterpolation: 1, paths } ],
padding: [ null, null, null, (Math.ceil(Math.log10(Math.max(...json[1]))) + Math.floor(Math.log10(Math.max(...json[1])) / 3)) * 6 ],
};
uplot = new uPlot(opts, json, chart);
}
function resizeChart() {
if (uplot) {
let chart = document.getElementById('chart');
uplot.setSize({ width: chart.clientWidth, height: chart.clientHeight });
}
}
function redrawChart() {
if (uplot && document.getElementById('chart').style.display == 'block') {
renderChart(uplot.data);
}
}
new ResizeObserver(resizeChart).observe(document.getElementById('chart'));
/// First we check if theme is set via the 'theme' GET parameter, if not, we check localStorage, otherwise we check OS preference.
let theme = current_url.searchParams.get('theme');
if (['dark', 'light'].indexOf(theme) === -1) {
theme = window.localStorage.getItem('theme');
}
if (!theme) {
theme = 'light';
}
function setColorTheme(new_theme, update_preference) {
theme = new_theme;
if (update_preference) {
window.localStorage.setItem('theme', theme);
}
document.documentElement.setAttribute('data-theme', theme);
redrawChart();
}
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
@ -890,26 +992,21 @@
/// Obtain system-level user preference
const media_query_list = window.matchMedia('(prefers-color-scheme: dark)');
if (media_query_list.matches) {
/// Set without saving to localstorage
document.documentElement.setAttribute('data-theme', 'dark');
setColorTheme('dark');
}
/// There is a rumor that on some computers, the theme is changing automatically on day/night.
media_query_list.addEventListener('change', function(e) {
if (e.matches) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
setColorTheme(e.matches ? 'dark' : 'light');
});
}
document.getElementById('toggle-light').onclick = function() {
setColorTheme('light');
setColorTheme('light', true);
}
document.getElementById('toggle-dark').onclick = function() {
setColorTheme('dark');
setColorTheme('dark', true);
}
</script>
</html>