wip moving historical graph to uPlot, remove flot.js dependency

This commit is contained in:
Nick Krecklow 2020-05-11 02:28:41 -05:00
parent 9987434fea
commit 19a7ce7d91
No known key found for this signature in database
GPG Key ID: 5F149FDE156FFA94
5 changed files with 145 additions and 87 deletions

@ -91,7 +91,6 @@
</footer> </footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.slim.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script>
<script src="../js/main.js" defer></script> <script src="../js/main.js" defer></script>

@ -1,38 +1,12 @@
import uPlot from '../lib/uPlot.esm'
import { RelativeScale } from './scale'
import { formatNumber, formatTimestamp, isMobileBrowser } from './util' import { formatNumber, formatTimestamp, isMobileBrowser } from './util'
import { uPlotTooltipPlugin } from './tooltip'
import { FAVORITE_SERVERS_STORAGE_KEY } from './favorites' import { FAVORITE_SERVERS_STORAGE_KEY } from './favorites'
export const HISTORY_GRAPH_OPTIONS = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: '#E3E3E3'
},
show: false
},
yaxis: {
show: true,
ticks: 20,
minTickSize: 10,
tickLength: 10,
tickFormatter: formatNumber,
font: {
color: '#E3E3E3'
},
labelWidth: -5,
min: 0
},
grid: {
hoverable: true,
color: '#696969'
},
legend: {
show: false
}
}
const HIDDEN_SERVERS_STORAGE_KEY = 'minetrack_hidden_servers' const HIDDEN_SERVERS_STORAGE_KEY = 'minetrack_hidden_servers'
const SHOW_FAVORITES_STORAGE_KEY = 'minetrack_show_favorites' const SHOW_FAVORITES_STORAGE_KEY = 'minetrack_show_favorites'
@ -43,6 +17,7 @@ export class GraphDisplayManager {
constructor (app) { constructor (app) {
this._app = app this._app = app
this._graphData = [] this._graphData = []
this._graphTimestamps = []
this._hasLoadedSettings = false this._hasLoadedSettings = false
this._initEventListenersOnce = false this._initEventListenersOnce = false
this._showOnlyFavorites = false this._showOnlyFavorites = false
@ -57,15 +32,7 @@ export class GraphDisplayManager {
return return
} }
const graphData = this._graphData[serverId] // FIXME
// Push the new data from the method call request
graphData.push([timestamp, playerCount])
// Trim any outdated entries by filtering the array into a new array
if (graphData.length > this._app.publicConfig.graphMaxLength) {
graphData.shift()
}
} }
loadLocalStorage () { loadLocalStorage () {
@ -126,20 +93,17 @@ export class GraphDisplayManager {
} }
} }
// Converts the backend data into the schema used by flot.js
getVisibleGraphData () { getVisibleGraphData () {
return Object.keys(this._graphData) return this._app.serverRegistry.getServerRegistrations()
.map(Number) .filter(serverRegistration => serverRegistration.isVisible)
.map(serverId => this._app.serverRegistry.getServerRegistration(serverId)) .map(serverRegistration => this._graphData[serverRegistration.serverId])
.filter(serverRegistration => serverRegistration !== undefined && serverRegistration.isVisible) }
.map(serverRegistration => {
return { getPlotSize () {
data: this._graphData[serverRegistration.serverId], return {
yaxis: 1, width: Math.max(window.innerWidth, 800) * 0.9,
label: serverRegistration.data.name, height: 400
color: serverRegistration.data.color }
}
})
} }
buildPlotInstance (graphData) { buildPlotInstance (graphData) {
@ -150,12 +114,107 @@ export class GraphDisplayManager {
this.loadLocalStorage() this.loadLocalStorage()
} }
this._graphData = graphData // FIXME: timestamps are not shared!
this._graphTimestamps = graphData[0].map(val => Math.floor(val[0] / 1000))
this._graphData = Object.values(graphData).map(val => {
return val.map(element => {
// Safely handle null data points, they represent gaps in the graph
return element === null ? null : element[1]
})
})
// Explicitly define a height so flot.js can rescale the Y axis const series = this._app.serverRegistry.getServerRegistrations().map(serverRegistration => {
document.getElementById('big-graph').style.height = '400px' return {
scale: 'Players',
stroke: serverRegistration.data.color,
width: 2,
value: (_, raw) => formatNumber(raw) + ' Players',
show: serverRegistration.isVisible
}
})
this._plotInstance = $.plot('#big-graph', this.getVisibleGraphData(), HISTORY_GRAPH_OPTIONS) const tickCount = 10
// eslint-disable-next-line new-cap
this._plotInstance = new uPlot({
plugins: [
uPlotTooltipPlugin((pos, id, plot) => {
if (pos) {
// FIXME
let text = '<strong>' + formatTimestamp(this._graphTimestamps[id]) + '</strong><br><br>'
for (let i = 1; i < plot.series.length; i++) {
const serverRegistration = this._app.serverRegistry.getServerRegistration(i - 1)
const serverGraphData = this._graphData[serverRegistration.serverId]
let playerCount
if (id >= serverGraphData.length) {
playerCount = '-'
} else {
playerCount = formatNumber(serverGraphData[id])
}
text += serverRegistration.data.name + ': ' + playerCount + '<br>'
}
this._app.tooltip.set(pos.left, pos.top, 10, 10, text)
} else {
this._app.tooltip.hide()
}
})
],
...this.getPlotSize(),
cursor: {
y: false
},
series: [
{
},
...series
],
axes: [
{
font: '14px "Open Sans", sans-serif',
stroke: '#FFF',
grid: {
show: false
},
space: 60
},
{
font: '14px "Open Sans", sans-serif',
stroke: '#FFF',
size: 60,
grid: {
stroke: '#333',
width: 1
},
split: () => {
const visibleGraphData = this.getVisibleGraphData()
const [, max, scale] = RelativeScale.scaleMatrix(visibleGraphData, tickCount)
const ticks = RelativeScale.generateTicks(0, max, scale)
return ticks
}
}
],
scales: {
Players: {
auto: false,
range: () => {
const visibleGraphData = this.getVisibleGraphData()
const [, scaledMax] = RelativeScale.scaleMatrix(visibleGraphData, tickCount)
return [0, scaledMax]
}
}
},
legend: {
show: false
}
}, [
this._graphTimestamps,
...this._graphData
], document.getElementById('big-graph'))
// Show the settings-toggle element // Show the settings-toggle element
document.getElementById('settings-toggle').style.display = 'inline-block' document.getElementById('settings-toggle').style.display = 'inline-block'
@ -166,11 +225,12 @@ export class GraphDisplayManager {
// This may cause unnessecary localStorage updates, but its a rare and harmless outcome // This may cause unnessecary localStorage updates, but its a rare and harmless outcome
this.updateLocalStorage() this.updateLocalStorage()
// Fire calls to the provided graph instance // Copy application state into the series data used by uPlot
// This allows flot.js to manage redrawing and creates a helper method to reduce code duplication for (const serverRegistration of this._app.serverRegistry.getServerRegistrations()) {
this._plotInstance.setData(this.getVisibleGraphData()) this._plotInstance.series[serverRegistration.serverId + 1].show = serverRegistration.isVisible
this._plotInstance.setupGrid() }
this._plotInstance.draw()
this._plotInstance.redraw()
} }
requestResize () { requestResize () {
@ -189,11 +249,7 @@ export class GraphDisplayManager {
} }
resize = () => { resize = () => {
if (this._plotInstance) { this._plotInstance.setSize(this.getPlotSize())
this._plotInstance.resize()
this._plotInstance.setupGrid()
this._plotInstance.draw()
}
// undefine value so #clearTimeout is not called // undefine value so #clearTimeout is not called
// This is safe even if #resize is manually called since it removes the pending work // This is safe even if #resize is manually called since it removes the pending work
@ -204,21 +260,6 @@ export class GraphDisplayManager {
this._resizeRequestTimeout = undefined this._resizeRequestTimeout = undefined
} }
// Called by flot.js when they hover over a data point.
handlePlotHover = (event, pos, item) => {
if (!item) {
this._app.tooltip.hide()
} else {
let text = formatNumber(item.datapoint[1]) + ' Players<br>' + formatTimestamp(item.datapoint[0])
// Prefix text with the series label when possible
if (item.series && item.series.label) {
text = '<strong>' + item.series.label + '</strong><br>' + text
}
this._app.tooltip.set(item.pageX, item.pageY, 10, 10, text)
}
}
initEventListeners () { initEventListeners () {
if (!this._initEventListenersOnce) { if (!this._initEventListenersOnce) {
this._initEventListenersOnce = true this._initEventListenersOnce = true
@ -231,8 +272,6 @@ export class GraphDisplayManager {
}) })
} }
$('#big-graph').bind('plothover', this.handlePlotHover)
// These listeners should be bound each #initEventListeners call since they are for newly created elements // These listeners should be bound each #initEventListeners call since they are for newly created elements
document.querySelectorAll('.graph-control').forEach((element) => { document.querySelectorAll('.graph-control').forEach((element) => {
element.addEventListener('click', this.handleServerButtonClick, false) element.addEventListener('click', this.handleServerButtonClick, false)
@ -317,6 +356,7 @@ export class GraphDisplayManager {
} }
reset () { reset () {
this._graphTimestamps = []
this._graphData = [] this._graphData = []
this._plotInstance = undefined this._plotInstance = undefined
this._hasLoadedSettings = false this._hasLoadedSettings = false

@ -21,6 +21,26 @@ export class RelativeScale {
} }
} }
static scaleMatrix (data, tickCount) {
let max = Number.MIN_VALUE
for (const row of data) {
let testMax = Number.MIN_VALUE
for (const point of row) {
if (point > testMax) {
testMax = point
}
}
if (testMax > max) {
max = testMax
}
}
return RelativeScale.scale([0, max], tickCount)
}
static generateTicks (min, max, step) { static generateTicks (min, max, step) {
const ticks = [] const ticks = []
for (let i = min; i <= max; i += step) { for (let i = min; i <= max; i += step) {

@ -54,7 +54,6 @@ export class SocketManager {
// Display the main page component // Display the main page component
// Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn // Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn
// Otherwise flot.js will cause visual alignment bugs
this._app.setPageReady(true) this._app.setPageReady(true)
// Allow the graphDisplayManager to control whether or not the historical graph is loaded // Allow the graphDisplayManager to control whether or not the historical graph is loaded

@ -50,7 +50,7 @@ class App {
// Send graphData in object wrapper to avoid needing to explicity filter // Send graphData in object wrapper to avoid needing to explicity filter
// any header data being appended by #MessageOf since the graph data is fed // any header data being appended by #MessageOf since the graph data is fed
// directly into the flot.js graphing system // directly into the graphing system
client.send(MessageOf('historyGraph', { client.send(MessageOf('historyGraph', {
graphData: graphData graphData: graphData
})) }))