diff --git a/app.js b/app.js
index 53f8886..c3bafe9 100644
--- a/app.js
+++ b/app.js
@@ -10,6 +10,9 @@ var config = require('./config.json');
var networkHistory = [];
var connectedClients = 0;
+var graphData = [];
+var lastGraphPush = [];
+
function pingAll() {
var servers = config.servers;
@@ -79,6 +82,27 @@ function pingAll() {
if (config.logToDatabase) {
db.log(network.ip, util.getCurrentTimeMs(), res ? res.players.online : 0);
}
+
+ // Push it to our graphs.
+ var timeMs = util.getCurrentTimeMs();
+
+ if (!lastGraphPush[network.ip] || timeMs - lastGraphPush[network.ip] >= 60 * 1000) {
+ lastGraphPush[network.ip] = timeMs;
+
+ // Don't have too much data!
+ if (graphData[network.ip].length >= 24 * 60) {
+ graphData[network.ip].shift();
+ }
+
+ graphData[network.ip].push([timeMs, res ? res.players.online : 0]);
+
+ // Send the update.
+ server.io.sockets.emit('updateHistoryGraph', {
+ ip: network.ip,
+ players: (res ? res.players.online : 0),
+ timestamp: timeMs
+ });
+ }
});
})(servers[i]);
}
@@ -98,6 +122,14 @@ function startMainLoop() {
if (config.logToDatabase) {
// Setup our database.
db.setup();
+
+ var timestamp = util.getCurrentTimeMs();
+
+ db.queryPings(24 * 60 * 60 * 1000, function(data) {
+ graphData = util.convertPingsToGraph(data);
+
+ logger.log('info', 'Queried and parsed ping history in %sms', util.getCurrentTimeMs() - timestamp);
+ });
} else {
logger.warn('Database logging is not enabled. You can enable it by setting "logToDatabase" to true in config.json. This requires sqlite3 to be installed.');
}
@@ -130,6 +162,9 @@ server.start(function() {
for (var i = 0; i < networkHistoryKeys.length; i++) {
client.emit('add', [networkHistory[networkHistoryKeys[i]]]);
}
+
+ // Send them the big 24h graph.
+ client.emit('historyGraph', graphData);
}, 1);
// Attach our listeners.
diff --git a/assets/css/main.css b/assets/css/main.css
index ea8706e..627a599 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -177,3 +177,11 @@ h3 {
height: 42px;
width: 42px;
}
+
+/* The big graph */
+#big-graph {
+ height: 500px;
+ width: 800px;
+ margin: 15px auto 0 auto;
+ padding-left: 500px;
+}
\ No newline at end of file
diff --git a/assets/html/index.html b/assets/html/index.html
index abf0fb3..6a20306 100644
--- a/assets/html/index.html
+++ b/assets/html/index.html
@@ -34,6 +34,8 @@
+
+
diff --git a/assets/js/site.js b/assets/js/site.js
index 344d918..749bf54 100644
--- a/assets/js/site.js
+++ b/assets/js/site.js
@@ -30,6 +30,36 @@ var smallChartOptions = {
]
};
+var bigChartOptions = {
+ series: {
+ shadowSize: 0
+ },
+ xaxis: {
+ font: {
+ color: "#E3E3E3"
+ },
+ show: false
+ },
+ yaxis: {
+ show: true,
+ tickLength: 10,
+ tickFormatter: function(value) {
+ return formatNumber(value);
+ },
+ font: {
+ color: "#E3E3E3"
+ },
+ labelWidth: -5
+ },
+ grid: {
+ hoverable: true,
+ color: "#696969"
+ },
+ legend: {
+ show: false
+ }
+};
+
var lastMojangServiceUpdate;
var graphs = {};
@@ -169,6 +199,38 @@ function safeName(name) {
return name.replace(/ /g, '');
}
+function handlePlotHover(event, pos, item) {
+ if (item) {
+ var text = getTimestamp(item.datapoint[0] / 1000) + '\
+
\
+ ' + formatNumber(item.datapoint[1]) + ' Players';
+
+ if (item.series && item.series.label) {
+ text = item.series.label + '
' + text;
+ }
+
+ renderTooltip(item.pageX + 5, item.pageY + 5, text);
+ } else {
+ hideTooltip();
+ }
+}
+
+function convertGraphData(rawData) {
+ var data = [];
+
+ var keys = Object.keys(rawData);
+
+ for (var i = 0; i < keys.length; i++) {
+ data.push({
+ data: rawData[keys[i]],
+ yaxis: 1,
+ label: keys[i]
+ });
+ }
+
+ return data;
+}
+
$(document).ready(function() {
var socket = io.connect({
reconnect: true,
@@ -179,6 +241,9 @@ $(document).ready(function() {
var mojangServicesUpdater;
var sortServersTask;
+ var historyPlot;
+ var historyData;
+
socket.on('connect', function() {
$('#tagline-text').text('Loading...');
});
@@ -203,6 +268,27 @@ $(document).ready(function() {
$('#quick-jump-container').html('');
});
+ socket.on('historyGraph', function(rawData) {
+ historyData = rawData;
+
+ historyPlot = $.plot('#big-graph', convertGraphData(rawData), bigChartOptions);
+
+ $('#big-graph').bind('plothover', handlePlotHover);
+ });
+
+ socket.on('updateHistoryGraph', function(rawData) {
+ if (historyData[rawData.ip].length > 24 * 60) {
+ historyData[rawData.ip].shift();
+ }
+
+ historyData[rawData.ip].push([rawData.timestamp, rawData.players]);
+
+ historyPlot.setData(convertGraphData(historyData));
+ historyPlot.setupGrid();
+
+ historyPlot.draw();
+ });
+
socket.on('add', function(servers) {
for (var i = 0; i < servers.length; i++) {
var history = servers[i];
@@ -265,15 +351,7 @@ $(document).ready(function() {
updateServerStatus(lastEntry);
- $('#chart_' + safeName(info.name)).bind('plothover', function(event, pos, item) {
- if (item) {
- renderTooltip(item.pageX + 5, item.pageY + 5, getTimestamp(item.datapoint[0] / 1000) + '\
-
\
- ' + formatNumber(item.datapoint[1]) + ' Players');
- } else {
- hideTooltip();
- }
- });
+ $('#chart_' + safeName(info.name)).bind('plothover', handlePlotHover);
}
sortServers();
@@ -323,7 +401,7 @@ $(document).ready(function() {
sortServersTask = setInterval(function() {
sortServers();
- }, 30 * 1000);
+ }, 10 * 1000);
// Our super fancy scrolly thing!
$(document).on('click', '.quick-jump-icon', function(e) {
diff --git a/config.json b/config.json
index 07b3f14..a17715b 100644
--- a/config.json
+++ b/config.json
@@ -190,8 +190,8 @@
"rates": {
"upateMojangStatus": 5000,
"mojangStatusTimeout": 3500,
- "pingAll": 3000,
- "connectTimeout": 2500
+ "pingAll": 2000,
+ "connectTimeout": 1500
},
- "logToDatabase": false
+ "logToDatabase": true
}
diff --git a/lib/database.js b/lib/database.js
index 90bc86b..08da012 100644
--- a/lib/database.js
+++ b/lib/database.js
@@ -1,3 +1,5 @@
+var util = require('./util');
+
exports.setup = function() {
var sqlite = require('sqlite3');
@@ -10,6 +12,21 @@ exports.setup = function() {
exports.log = function(ip, timestamp, playerCount) {
var insertStatement = db.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)');
- insertStatement.run(timestamp, ip, playerCount);
+ db.serialize(function() {
+ insertStatement.run(timestamp, ip, playerCount);
+ });
+
+ insertStatement.finalize();
+ };
+
+ exports.queryPings = function(duration, callback) {
+ var currentTime = util.getCurrentTimeMs();
+
+ db.all("SELECT * FROM pings WHERE timestamp >= ? AND timestamp <= ?", [
+ currentTime - duration,
+ currentTime
+ ], function(err, data) {
+ callback(data);
+ });
};
};
\ No newline at end of file
diff --git a/lib/util.js b/lib/util.js
index c8aa50a..bab3dfc 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -1,3 +1,31 @@
+// This method is a monstrosity.
+// Since we loaded ALL pings from the database, we need to filter out the pings so each entry is a minute apart.
+// This is done by iterating over the list, since the time between each ping can be completely arbitrary.
+function trimUselessPings(data) {
+ var keys = Object.keys(data);
+
+ for (var i = 0; i < keys.length; i++) {
+ var listing = data[keys[i]];
+ var lastTimestamp = 0;
+
+ var filteredListing = [];
+
+ for (var x = 0; x < listing.length; x++) {
+ var entry = listing[x];
+
+ // 0 is the index of the timestamp.
+ // See the convertPingsToGraph method.
+ if (entry[0] - lastTimestamp >= 60 * 1000) {
+ filteredListing.push(entry);
+
+ lastTimestamp = entry[0];
+ }
+ }
+
+ data[keys[i]] = filteredListing;
+ }
+}
+
exports.getCurrentTimeMs = function() {
return new Date().getTime();
};
@@ -8,4 +36,23 @@ exports.setIntervalNoDelay = function(func, delay) {
func();
return task;
+};
+
+exports.convertPingsToGraph = function(sqlData) {
+ var graphData = {};
+
+ for (var i = 0; i < sqlData.length; i++) {
+ var entry = sqlData[i];
+
+ if (!graphData[entry.ip]) {
+ graphData[entry.ip] = [];
+ }
+
+ graphData[entry.ip].push([entry.timestamp, entry.playerCount]);
+ }
+
+ // Break it into minutes.
+ trimUselessPings(graphData);
+
+ return graphData;
};
\ No newline at end of file