replace Flot.js with uPlot for server graphs

This commit is contained in:
Nick Krecklow 2020-05-10 23:39:35 -05:00
parent 414775f630
commit b96b9dacc5
No known key found for this signature in database
GPG Key ID: 5F149FDE156FFA94
6 changed files with 2769 additions and 45 deletions

@ -298,11 +298,8 @@ footer a:hover {
}
.server .column-graph {
float: right;
height: 100px;
width: 400px;
margin-right: -3px;
margin-bottom: 5px;
}
/* Highlighted values */

@ -6,6 +6,8 @@
<link rel="stylesheet" type="text/css" href="../css/main.css">
<link rel="stylesheet" type="text/css" href="../lib/uPlot.min.css">
<link rel="icon" type="image/svg+xml" href="../images/logo.svg">
<meta charset="UTF-8">

53
assets/js/scale.js Normal file

@ -0,0 +1,53 @@
class RelativeScale {
static scale (data, tickCount) {
const [min, max] = RelativeScale.calculateBounds(data)
let factor = 1
while (true) {
const scale = Math.pow(10, factor)
const scaledMin = min - (min % scale)
const scaledMax = max + (max % scale === 0 ? 0 : (scale - (max % scale)))
const ticks = (scaledMax - scaledMin) / scale
if (ticks + 1 <= tickCount) {
return [scaledMin, scaledMax, scale]
} else {
// Too many steps between min/max, increase factor and try again
factor++
}
}
}
static generateTicks (min, max, step) {
const ticks = []
for (let i = min; i <= max; i += step) {
ticks.push(i)
}
return ticks
}
static calculateBounds (data) {
if (data.length === 0) {
return [0, 0]
} else {
let min = Number.MAX_VALUE
let max = Number.MIN_VALUE
for (const point of data) {
if (point > max) {
max = point
}
if (point < min) {
min = point
}
}
return [min, max]
}
}
}
module.exports = RelativeScale

@ -1,37 +1,11 @@
import uPlot from '../lib/uPlot.esm'
import RelativeScale from './scale'
import { formatNumber, formatTimestamp, formatDate, formatMinecraftServerAddress, formatMinecraftVersions } from './util'
import MISSING_FAVICON from '../images/missing_favicon.svg'
export const SERVER_GRAPH_OPTIONS = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: '#E3E3E3'
},
show: false
},
yaxis: {
minTickSize: 100,
tickDecimals: 0,
show: true,
tickLength: 10,
tickFormatter: formatNumber,
font: {
color: '#E3E3E3'
},
labelWidth: -10
},
grid: {
hoverable: true,
color: '#696969'
},
colors: [
'#E9E581'
]
}
export class ServerRegistry {
constructor (app) {
this._app = app
@ -93,15 +67,71 @@ export class ServerRegistration {
}
addGraphPoints (points, timestampPoints) {
for (let i = 0; i < points.length; i++) {
const point = points[i]
const timestamp = timestampPoints[i]
this._graphData.push([timestamp, point])
}
this._graphData = [
timestampPoints.map(val => Math.floor(val / 1000)),
points
]
}
buildPlotInstance () {
this._plotInstance = $.plot('#chart_' + this.serverId, [this._graphData], SERVER_GRAPH_OPTIONS)
const tickCount = 5
// eslint-disable-next-line new-cap
this._plotInstance = new uPlot({
height: 100,
width: 400,
cursor: {
y: false,
drag: {
setScale: false,
x: false,
y: false
}
},
series: [
{},
{
scale: 'Players',
stroke: '#E9E581',
width: 2,
value: (_, raw) => formatNumber(raw) + ' Players'
}
],
axes: [
{
show: false
},
{
ticks: {
show: false
},
font: '14px "Open Sans", sans-serif',
stroke: '#A3A3A3',
size: 55,
grid: {
stroke: '#333',
width: 1
},
split: (self) => {
const [min, max, scale] = RelativeScale.scale(self.data[1], tickCount)
const ticks = RelativeScale.generateTicks(min, max, scale)
return ticks
}
}
],
scales: {
Players: {
auto: false,
range: (self) => {
const [scaledMin, scaledMax] = RelativeScale.scale(self.data[1], tickCount)
return [scaledMin, scaledMax]
}
}
},
legend: {
show: false
}
}, this._graphData, document.getElementById('chart_' + this.serverId))
}
handlePing (payload, timestamp) {
@ -110,11 +140,14 @@ export class ServerRegistration {
// Only update graph for successful pings
// This intentionally pauses the server graph when pings begin to fail
this._graphData.push([timestamp, this.playerCount])
this._graphData[0].push(Math.floor(timestamp / 1000))
this._graphData[1].push(this.playerCount)
// Trim graphData to within the max length by shifting out the leading elements
if (this._graphData.length > this._app.publicConfig.serverGraphMaxLength) {
this._graphData.shift()
for (const series of this._graphData) {
if (series.length > this._app.publicConfig.serverGraphMaxLength) {
series.shift()
}
}
this.redraw()
@ -133,9 +166,7 @@ export class ServerRegistration {
redraw () {
// Redraw the plot instance
this._plotInstance.setData([this._graphData])
this._plotInstance.setupGrid()
this._plotInstance.draw()
this._plotInstance.setData(this._graphData)
}
updateServerRankIndex (rankIndex) {

2640
assets/lib/uPlot.esm.js Normal file

File diff suppressed because it is too large Load Diff

1
assets/lib/uPlot.min.css vendored Normal file

@ -0,0 +1 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: max-content;}.uplot .title {text-align: center;font-size: 18px;font-weight: bold;}.uplot .wrap {position: relative;user-select: none;}.uplot .over, .uplot .under {position: absolute;overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.uplot .legend {font-size: 14px;margin: auto;text-align: center;}.uplot .legend.inline {display: block;}.uplot .legend.inline * {display: inline-block;}.uplot .legend.inline tr {margin-right: 16px;}.uplot .legend th {font-weight: 600;}.uplot .legend th > * {vertical-align: middle;display: inline-block;}.uplot .legend .ident {width: 1em;height: 1em;margin-right: 4px;border: 2px solid transparent;}.uplot .legend.inline th::after {content: ":";vertical-align: middle;}.uplot .legend .series > * {padding: 4px;}.uplot .legend .series th {cursor: pointer;}.uplot .legend .off > * {opacity: 0.3;}.uplot .select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.uplot .select.off {display: none;}.uplot .cursor-x, .uplot .cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.uplot .cursor-x {height: 100%;border-right: 1px dashed #607D8B;}.uplot .cursor-y {width: 100%;border-bottom: 1px dashed #607D8B;}.uplot .cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;filter: brightness(85%);pointer-events: none;will-change: transform;z-index: 100;}