Files
speed-data-docker/index.html
2025-11-29 15:24:11 -08:00

475 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Speedtest data</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@700&family=Inter:wght@400;900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
/* highcharts */
.highcharts-figure,
.highcharts-data-table table {
/* min-width: 560px;
max-width: 1000px;*/
margin: 1em auto;
}
.highcharts-data-table table {
font-family: Verdana, sans-serif;
border-collapse: collapse;
border: 1px solid #ebebeb;
margin: 10px auto;
text-align: center;
width: 100%;
/*max-width: 500px;*/
}
.highcharts-data-table caption {
padding: 1em 0;
font-size: 1.2em;
color: #555;
}
.highcharts-data-table th {
font-weight: 600;
padding: 0.5em;
}
.highcharts-data-table td,
.highcharts-data-table th,
.highcharts-data-table caption {
padding: 0.5em;
}
.highcharts-data-table thead tr,
.highcharts-data-table tr:nth-child(even) {
background: #f8f8f8;
}
.highcharts-data-table tr:hover {
background: #f1f7ff;
}
/* General */
body {
font-family: "Inter", sans-serif;
}
h1, h2, h5 {
text-align: center;
}
/* Recent boxes */
#recent_samples {
width: max-content;
margin: auto;
}
#recent .recent_sample {
display: block;
background-color: #eee;
border: 2px solid #eee;
border-radius: 5px;
}
#recent .recent_sample .recent_dn,
#recent .recent_sample .recent_up {
font-family: "Fira Mono", sans-serif;
color: #777;
background-color: #fff;
margin: 5px;
border-radius: 5px;
padding: 0.3em;
}
#recent .recent_sample .recent_dn {
color: #2caffe;
}
#recent .recent_sample .recent_up {
color: #544fc5;
padding-left: 0.3em;
}
#recent .recent_spd {
}
.recent_box .recent_fld {
font-family: "Inter", sans-serif;
display: table-row;
}
.recent_box {
margin: 5px;
text-align: left;
}
.recent_box .recent_fld .recent_fld_name {
color: #777;
font-weight: bold;
}
.recent_box .recent_fld .recent_fld_val {
color: #333;
margin-bottom: 0.3em;
}
.recent_box .recent_fld .recent_fld_name,
.recent_box .recent_fld .recent_fld_val {
display: table-cell;
}
/* Spinner */
#spinner {
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #544fc5;
border-bottom: 16px solid #2caffe;
width: 80px;
height: 80px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
margin: 5em auto;
}
#spinner-desc {
margin: 0 auto;
width: 300px;
text-align: center;
padding-top: 3em;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result_url {
text-decoration: none;
font-weight: bold;
font-size: 0.7em;
}
/* Responsive */
@media only screen and (max-width: 1100px) {
#recent #recent_1 .recent_dn, #recent #recent_1 .recent_up {
font-size: 2em;
}
#recent #recent_2 .recent_dn, #recent #recent_2 .recent_up {
font-size: 1.5em;
}
#recent #recent_3 .recent_dn, #recent #recent_3 .recent_up,
#recent #recent_4 .recent_dn, #recent #recent_4 .recent_up,
#recent #recent_5 .recent_dn, #recent #recent_5 .recent_up {
font-size: 1em;
}
#recent #recent_1 .recent_fld {
font-size: 0.7em;
}
#recent #recent_2 .recent_fld {
font-size: 0.5em;
}
#recent #recent_3 .recent_fld,
#recent #recent_4 .recent_fld,
#recent #recent_5 .recent_fld {
font-size: 0.4em;
}
#recent {
display: flex;
flex-wrap: wrap;
width: min-content;
}
.recent_box {
font-size: larger;
}
#recent .recent_sample {
flex-basis: content;
margin: 5px auto;
}
.sep {
width: 100%;
}
.highcharts-figure {
display: none;
}
}
@media only screen and (min-width: 1100px) {
#recent #recent_1 .recent_dn, #recent #recent_1 .recent_up {
font-size: 3em;
}
#recent #recent_2 .recent_dn, #recent #recent_2 .recent_up {
font-size: 1.5em;
}
#recent #recent_3 .recent_dn, #recent #recent_3 .recent_up,
#recent #recent_4 .recent_dn, #recent #recent_4 .recent_up,
#recent #recent_5 .recent_dn, #recent #recent_5 .recent_up {
font-size: 1.2em;
}
#recent #recent_1 .recent_fld {
font-size: 1em;
}
#recent #recent_2 .recent_fld {
font-size: 0.7em;
}
#recent #recent_3 .recent_fld,
#recent #recent_4 .recent_fld,
#recent #recent_5 .recent_fld {
font-size: 0.5em;
}
#recent {
display: inline-block;
width: max-content;
height: min-content;
}
#recent .recent_sample {
float: left;
margin: 5px;
}
}
</style>
</head>
<body>
<div id="spinner"></div>
<div id="spinner-desc">Preparing data ...</div>
<div id="recent_samples">
<div id="recent">
</div>
</div>
<div style="clear:both"></div>
<figure class="highcharts-figure">
<div id="container"></div>
</figure>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/boost.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script>
var shortContent = ($(window).width() < 1000);
var refDate = new Date;
var isp = "";
function getDate(d) {
var dt = Date.parse(d);
dt = dt - refDate.getTimezoneOffset() * 60 * 1000;
return dt;
}
function getFmtDate(d) {
const dt = new Date(d);
return dt.toDateString()
+ ", " + dt.getHours() + ":" + dt.getMinutes() + ":" + dt.getSeconds();
}
function getDownloadData(d) {
var arr = [];
d.forEach((sd) => {
if (sd.error == undefined) {
var dt = getDate(sd.timestamp);
arr.push([
dt,
sd.download.bandwidth*8/1000000
]);
}
});
return arr;
}
function getUploadData(d) {
var arr = [];
d.forEach((sd) => {
if (sd.error == undefined) {
var dt = getDate(sd.timestamp);
arr.push([
dt,
sd.upload.bandwidth*8/1000000
]);
}
});
return arr;
}
function prepare_chart_data() {
dndata = getDownloadData(speeddata);
updata = getUploadData(speeddata);
}
function show_chart() {
prepare_chart_data();
console.time('line');
Highcharts.chart('container', {
chart: {
type: 'spline',
zoomType: 'x',
height: '40%'
},
title: {
text: 'Trend in last 500 samples'
},
subtitle: {
text: 'Measured hourly by Speedtest CLI'
},
accessibility: {
screenReaderSection: {
beforeChartFormat: '<{headingTagName}>{chartTitle}</{headingTagName}><div>{chartSubtitle}</div><div>{chartLongdesc}</div><div>{xAxisDescription}</div><div>{yAxisDescription}</div>'
}
},
tooltip: {
valueDecimals: 2
},
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: 'Speed (MB/s)'
},
plotBands: [{ // Fast
from: 900,
to: 1000,
color: 'rgba(68, 170, 68, 0.1)',
label: {
text: 'Fast',
style: {
color: '#606060'
}
}
}, { // Slow
from: 300,
to: 499,
color: 'rgba(170, 50, 50, 0.1)',
label: {
text: 'Slow',
style: {
color: '#606060'
}
}
}, { // Crawling
from: 0,
to: 299,
color: 'rgba(200, 50, 50, 0.3)',
label: {
text: 'Crawling',
style: {
color: '#606060'
}
}
}]
},
series: [
{
data: dndata,
lineWidth: 2,
name: 'Download (MB/s)'
},
{
data: updata,
lineWidth: 2,
name: 'Upload (MB/s)'
}
]
});
console.timeEnd('line');
}
function get_speed(num) {
return (num*8/1000000).toFixed(2);
}
function get_up_dn_icon(dt, dtprev, isdn) {
var icon = "&bull;";
var colr = "#0a0";
var titl = "Upward trend";
if (isdn) {
if (dt.download.bandwidth < dtprev.download.bandwidth) {
colr = "#a00";
titl = "Downward trend";
}
} else {
if (dt.upload.bandwidth < dtprev.upload.bandwidth) {
colr = "#a00";
titl = "Downward trend";
}
}
return "<span style='color:" + colr + "' title='" + titl + "'>" + icon + "</span>";
}
function show_one_sample(dt, dtprev, idx, dest) {
dest.append($("<div class='recent_sample' id='recent_" + idx + "'></div>")
.append($("<div class='recent_spd_tbl'></div>")
.append($("<table></table>")
.append($("<tr class='recent_spd'></tr>")
.append($("<td class='recent_dn' title='Download speed in MB/s'>↓"
+ get_speed(dt.download.bandwidth) + get_up_dn_icon(dt, dtprev, 1) + "</td>"),
$("<td class='recent_up' title='Upload speed in MB/s'>↑"
+ get_speed(dt.upload.bandwidth) + get_up_dn_icon(dt, dtprev, 0) + "</td>")))))
.append($("<div class='recent_box'></div>")
.append($("<div class='recent_fld'></div>")
.append($("<div class='recent_fld_val'>" + getFmtDate(dt.timestamp) + "<a class='result_url' target='_blank' href='"
+ dt.result.url + "'> ➔</a></div>")),
$("<div class='recent_fld'></div>")
.append($("<div class='recent_fld_val'>" + dt.server.name + " ("
+ dt.server.location + ", "
+ dt.server.country + ")</div>")))));
}
function load_data() {
var data_js = (shortContent) ? "speedtest.short.js" : "speedtest.js";
var data_path = "/data/speed/" + data_js;
$.getScript(data_path, function() { show_content() });
}
function show_content() {
var snaps = 5;
isp = speeddata[speeddata.length-1].isp;
console.log(isp);
for (let itr = speeddata.length-1, idx = 1; idx <= snaps; itr --, idx ++ ) {
show_one_sample(speeddata[itr], speeddata[itr-1], idx, $('#recent'));
}
if (!shortContent) {
show_chart();
}
// On done
$("#spinner-desc").hide();
$("#spinner")
.after("<h2>Most recent samples</h2>")
.after("<h5>(ISP: " + isp + ")</h5>")
.after("<h1>Network Speed Data</h1>")
.hide();
console.timeEnd("speedtest");
}
$(document).ready(function() {
console.time("speedtest");
load_data();
});
// vim: ts=4 sts=4 sw=4 et
</script>
</body>
</html>