diff --git a/assets/html/index.html b/assets/html/index.html index 08bfa58..5e9162c 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -91,7 +91,6 @@ - diff --git a/assets/js/graph.js b/assets/js/graph.js index 9c88a11..f64fefa 100644 --- a/assets/js/graph.js +++ b/assets/js/graph.js @@ -1,38 +1,12 @@ +import uPlot from '../lib/uPlot.esm' + +import { RelativeScale } from './scale' + import { formatNumber, formatTimestamp, isMobileBrowser } from './util' +import { uPlotTooltipPlugin } from './tooltip' 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 SHOW_FAVORITES_STORAGE_KEY = 'minetrack_show_favorites' @@ -43,6 +17,7 @@ export class GraphDisplayManager { constructor (app) { this._app = app this._graphData = [] + this._graphTimestamps = [] this._hasLoadedSettings = false this._initEventListenersOnce = false this._showOnlyFavorites = false @@ -57,15 +32,7 @@ export class GraphDisplayManager { return } - const graphData = this._graphData[serverId] - - // 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() - } + // FIXME } loadLocalStorage () { @@ -126,20 +93,17 @@ export class GraphDisplayManager { } } - // Converts the backend data into the schema used by flot.js getVisibleGraphData () { - return Object.keys(this._graphData) - .map(Number) - .map(serverId => this._app.serverRegistry.getServerRegistration(serverId)) - .filter(serverRegistration => serverRegistration !== undefined && serverRegistration.isVisible) - .map(serverRegistration => { - return { - data: this._graphData[serverRegistration.serverId], - yaxis: 1, - label: serverRegistration.data.name, - color: serverRegistration.data.color - } - }) + return this._app.serverRegistry.getServerRegistrations() + .filter(serverRegistration => serverRegistration.isVisible) + .map(serverRegistration => this._graphData[serverRegistration.serverId]) + } + + getPlotSize () { + return { + width: Math.max(window.innerWidth, 800) * 0.9, + height: 400 + } } buildPlotInstance (graphData) { @@ -150,12 +114,107 @@ export class GraphDisplayManager { 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 - document.getElementById('big-graph').style.height = '400px' + const series = this._app.serverRegistry.getServerRegistrations().map(serverRegistration => { + 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 = '' + formatTimestamp(this._graphTimestamps[id]) + '

' + + 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 + '
' + } + + 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 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.updateLocalStorage() - // Fire calls to the provided graph instance - // This allows flot.js to manage redrawing and creates a helper method to reduce code duplication - this._plotInstance.setData(this.getVisibleGraphData()) - this._plotInstance.setupGrid() - this._plotInstance.draw() + // Copy application state into the series data used by uPlot + for (const serverRegistration of this._app.serverRegistry.getServerRegistrations()) { + this._plotInstance.series[serverRegistration.serverId + 1].show = serverRegistration.isVisible + } + + this._plotInstance.redraw() } requestResize () { @@ -189,11 +249,7 @@ export class GraphDisplayManager { } resize = () => { - if (this._plotInstance) { - this._plotInstance.resize() - this._plotInstance.setupGrid() - this._plotInstance.draw() - } + this._plotInstance.setSize(this.getPlotSize()) // undefine value so #clearTimeout is not called // 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 } - // 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
' + formatTimestamp(item.datapoint[0]) - // Prefix text with the series label when possible - if (item.series && item.series.label) { - text = '' + item.series.label + '
' + text - } - - this._app.tooltip.set(item.pageX, item.pageY, 10, 10, text) - } - } - initEventListeners () { if (!this._initEventListenersOnce) { 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 document.querySelectorAll('.graph-control').forEach((element) => { element.addEventListener('click', this.handleServerButtonClick, false) @@ -317,6 +356,7 @@ export class GraphDisplayManager { } reset () { + this._graphTimestamps = [] this._graphData = [] this._plotInstance = undefined this._hasLoadedSettings = false diff --git a/assets/js/scale.js b/assets/js/scale.js index 47ee868..4a727d6 100644 --- a/assets/js/scale.js +++ b/assets/js/scale.js @@ -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) { const ticks = [] for (let i = min; i <= max; i += step) { diff --git a/assets/js/socket.js b/assets/js/socket.js index 4fb764a..b60645d 100644 --- a/assets/js/socket.js +++ b/assets/js/socket.js @@ -54,7 +54,6 @@ export class SocketManager { // Display the main page component // 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) // Allow the graphDisplayManager to control whether or not the historical graph is loaded diff --git a/lib/app.js b/lib/app.js index 82c2b0a..36fdc90 100644 --- a/lib/app.js +++ b/lib/app.js @@ -50,7 +50,7 @@ class App { // Send graphData in object wrapper to avoid needing to explicity filter // 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', { graphData: graphData }))