summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-05-06 01:17:47 +1200
committerFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-05-13 00:40:07 +1200
commit2b3bf882d6b53f09e0dddf3f8d4f159832471fb3 (patch)
treeaeab8858a8095d8b21b7e6d5e086c542028924f7
parentf2c37f5801456d447b3c92dd90b9674a52f6e886 (diff)
downloadmanavis-2b3bf882d6b53f09e0dddf3f8d4f159832471fb3.tar.gz
manavis-2b3bf882d6b53f09e0dddf3f8d4f159832471fb3.tar.bz2
manavis-2b3bf882d6b53f09e0dddf3f8d4f159832471fb3.tar.xz
manavis-2b3bf882d6b53f09e0dddf3f8d4f159832471fb3.zip
Broadcast active filters
There is now a simple node application to enable broadcasting of active filters, using socket.io. Currently, everyone on the server will send and receive all changes. This might get chaotic depending on how crowded things become. Perhaps `channels' might help keep things scalable. Broadcasting filters for the stat trellis chart would be interesting. You can also set and change your own nickname. This closes #12.
-rw-r--r--.gitignore4
-rw-r--r--css/style.css41
-rw-r--r--index.html39
-rw-r--r--index.js99
-rw-r--r--js/mv/chart.js10
-rw-r--r--js/mv/connect.js156
-rw-r--r--js/mv/heap.js1
-rw-r--r--js/mv/main.js4
-rw-r--r--js/util/trellis-chart.js12
9 files changed, 349 insertions, 17 deletions
diff --git a/.gitignore b/.gitignore
index eb08486..0d1be3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
*~
-map-logs \ No newline at end of file
+map-logs
+node_modules
+manavis.log
diff --git a/css/style.css b/css/style.css
index 0d2ec08..2690b1f 100644
--- a/css/style.css
+++ b/css/style.css
@@ -13,6 +13,19 @@ body {
overflow: hidden;
}
+#mask {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: #333;
+ opacity: .8;
+ -moz-transition: opacity 1s linear;
+ -o-transition: opacity 1s linear;
+ -webkit-transition: opacity 1s linear;
+ transition: opacity 1s linear;
+ top: 0;
+}
+
body, #main, #status, .side {
min-height: 40px;
}
@@ -26,6 +39,8 @@ body {
font-size: smaller;
}
+/* Columns */
+
.med {
width: 400px;
}
@@ -34,16 +49,29 @@ body {
width: 250px;
}
-.side {
+#main, .side {
font-size: smaller;
}
+/* Titles */
+h3 {
+ margin: 0.3em 0.8em;
+}
+
/* Loadinfo panel */
#loadinfo {
+ position: absolute;
+ top: 0px;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
width: 647px;
+ height: 400px;
border: 1px grey solid;
margin: auto;
+ background-color: #fff;
+ padding: 20px;
}
/* Hide charts while loadinfo is shown */
@@ -83,11 +111,22 @@ body {
shape-rendering: crispEdges;
}
+/* User list */
+
+#users-status {
+ padding: 0;
+}
+
+#users-status li.user {
+ list-style: none;
+}
+
/* Utility */
.fader {
-moz-transition: opacity 1s linear;
-o-transition: opacity 1s linear;
-webkit-transition: opacity 1s linear;
+ transition: opacity 1s linear;
}
.help {
diff --git a/index.html b/index.html
index dd93596..baebbb2 100644
--- a/index.html
+++ b/index.html
@@ -7,6 +7,10 @@
<body onload="mv.init();">
<div class="side med">
<div class="vis-hide">
+ <div id="connect-status">
+ <h3>Users</h3>
+ <ul id="users-status"></ul>
+ </div>
<div id="blvl-chart">
<h3>Instance breakdown by Character Base Level <a class="reset" style="display: none;" href="javascript:mv.charts.blvl.filterAll();dc.redrawAll();">clear</a></h3>
</div>
@@ -17,13 +21,13 @@
</div>
<div class="side thin">
<div class="vis-hide">
- <div id="type-chart">
<div id="target-chart">
<h3>Instance breakdown by Target <a class="reset" style="display: none;" href="javascript:mv.charts.target.filterAll();dc.redrawAll();">clear</a></h3>
</div>
<div id="wpn-chart">
<h3>Instance breakdown by Weapon <a class="reset" style="display: none;" href="javascript:mv.charts.wpn.filterAll();dc.redrawAll();">clear</a></h3>
</div>
+ <div id="type-chart">
<h3>Instance breakdown by Type <a class="reset" style="display: none;" href="javascript:mv.charts.type.filterAll();dc.redrawAll();">clear</a></h3>
</div>
<div id="def-chart">
@@ -36,19 +40,6 @@
Manavis
TODO: Load icons et al for when new records need loading?
</div>-->
- <div id="mask"><noscript>Javascript is required for this website.</noscript>
- <div id="loadinfo" class="fader">
- <h3>Select records to load and display</h3>
- <input type="file" id="input" name="records[]" multiple />
- <output id="list"></output>
- <div id="filesbar" class="progressbar fader">
- <div class="percent">0%</div>
- </div>
- <div id="loadbar" class="progressbar fader">
- <div class="percent">0%</div>
- </div>
- </div>
- </div>
<div class="vis-hide">
<div id="map-chart">
<h3>Breakdown by Map <span class="help" title="Bubble size indicates instances of experience gain for that map. X axis position indicates the sum of level experience gain for that map. Y axis position indicates the sum of job experience gain for that map.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.charts.map.filterAll();dc.redrawAll();">clear</a></h3>
@@ -61,6 +52,23 @@
</div>
</div>
</div>
+ <div id="mask"><noscript><h1>Javascript is required for this website.</h1></noscript>
+ <div id="loadinfo" class="fader">
+ <h1>Manavis</h1>
+ <input id="connect-option" name="connect" type="checkbox" checked></input>
+ <label for="connect">Connect to server</label>
+ <h3>Select records to load and display</h3>
+ <p>You can load any number of files at once.</p>
+ <input type="file" id="input" name="records[]" multiple />
+ <output id="list"></output>
+ <div id="filesbar" class="progressbar fader">
+ <div class="percent">0%</div>
+ </div>
+ <div id="loadbar" class="progressbar fader">
+ <div class="percent">0%</div>
+ </div>
+ </div>
+ </div>
</body>
<!-- Libs -->
@@ -70,6 +78,8 @@
<script src="js/crossfilter/crossfilter.js"></script>
<script src="js/dc/dc.js"></script>
+<script src="/socket.io/socket.io.js"></script>
+
<!-- Components -->
<script src="js/comp/item.js"></script>
<script src="js/comp/map.js"></script>
@@ -78,6 +88,7 @@
<!-- Processing -->
<script src="js/mv/chart.js"></script>
+<script src="js/mv/connect.js"></script>
<script src="js/mv/heap.js"></script>
<script src="js/mv/load.js"></script>
<script src="js/mv/main.js"></script>
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..2c3e894
--- /dev/null
+++ b/index.js
@@ -0,0 +1,99 @@
+/* Deps */
+var http = require('http')
+ , path = require('path')
+ , connect = require('connect')
+ , express = require('express')
+ , app = express()
+ , logger = require('logger').createLogger('manavis.log')
+ , cookieParser = express.cookieParser('your secret sauce')
+ , sessionStore = new connect.middleware.session.MemoryStore()
+ ;
+
+app.configure(function () {
+ app.use(express.bodyParser());
+ app.use(express.methodOverride());
+ app.use(cookieParser);
+ app.use(express.session({ store: sessionStore }));
+ app.use(app.router);
+ app.use(express.static(__dirname));
+});
+
+var server = http.createServer(app)
+ , io = require('socket.io').listen(server)
+ , SessionSockets = require('session.socket.io')
+ , sessionSockets = new SessionSockets(io, sessionStore, cookieParser)
+ ;
+/* End deps */
+
+/* Only one level is logged, and numerical timestamps are easier to compare. */
+logger.format = function(level, date, message) {
+ return (+date) + ":" + message;
+}
+
+/* Number of sessions we've seen. */
+var count = 0;
+/* nid -> { nick, filters, following } */
+var users = {};
+
+sessionSockets.on('connection', function (err, socket, session) {
+ /*
+ * Don't do anything until they send a login message.
+ * Later versions might also check a protocol version here.
+ */
+ socket.on('login', function() {
+ /* Someone new connected. Restore or initialise their session data. */
+ session.nid = session.nid || (++count);
+ session.nick = session.nick || null;
+ session.save();
+ /* New user! */
+ logAction("CONNECT", socket.handshake.address.address);
+ users[session.nid] = { nick: session.nick, filters: {} };
+ /* Let them know of their data. */
+ socket.emit('selflogin', {
+ id: session.nid,
+ nick: session.nick
+ });
+ /* Let everyone else know that someone connected. */
+ socket.broadcast.emit('login', {
+ id: session.nid,
+ nick: session.nick
+ });
+ /* Send the new user the userlist. */
+ socket.emit('users', { users: users });
+ /* Set up various handlers for the new socket. */
+ socket.on('nick', function (d) {
+ /* TODO Collision checking? */
+ users[session.nid].nick = session.nick = d.nick;
+ session.save();
+ logAction("NICK", d.nick);
+ io.sockets.emit('nickset', {
+ id: session.nid,
+ nick: d.nick
+ });
+ });
+ socket.on('filter', function(d) {
+ users[session.nid].filters = d.filters;
+ logAction("FILTER", d.filters);
+ socket.broadcast.emit('filterset', {
+ id: session.nid,
+ filters: d.filters
+ });
+ });
+ socket.on('disconnect', function() {
+ logAction("DISCONNECT");
+ delete users[session.nid];
+ socket.broadcast.emit('logout', {
+ id: session.nid
+ });
+ });
+ });
+ function logAction(action, content) {
+ logger.info(session.nid
+ , action
+ , content
+ );
+ }
+});
+
+logger.info(0, "STARTUP");
+server.listen(3000);
diff --git a/js/mv/chart.js b/js/mv/chart.js
index 3042acd..d06d40e 100644
--- a/js/mv/chart.js
+++ b/js/mv/chart.js
@@ -61,6 +61,16 @@ var mv = function(mv) {
dc.renderlet(function() { mv.charts.stats(); });
dc.renderAll();
}
+ charter.filters = function() {
+ var r = {}, f;
+ for (var k in mv.charts) {
+ f = mv.charts[k].filter();
+ if (f != null) {
+ r[k] = f;
+ }
+ }
+ return r;
+ }
function defLevelVerbose(level) {
switch (level) {
case 0: return "Undefined";
diff --git a/js/mv/connect.js b/js/mv/connect.js
new file mode 100644
index 0000000..aba4c33
--- /dev/null
+++ b/js/mv/connect.js
@@ -0,0 +1,156 @@
+var mv = function(mv) {
+ mv.socket = {
+ connect: connect
+ };
+ /* If we're updating due to something we received, then don't broadcast back. */
+ var netrendering = false;
+ /* Our ID */
+ var id = 0;
+ /* List of all users */
+ var users = {};
+ /* The user status box */
+ var usersStatus = d3.select("#users-status");
+ /* io.socket's socket */
+ var socket;
+ function connect() {
+ socket = io.connect('http://localhost:3000');
+ socket.on("connect", function() { console.log("CONNECT", arguments); });
+ socket.on("disconnect", function() { console.log("DISCONNECT", arguments); });
+ socket.emit('login');
+ /*
+ * Protocol:
+ * selflogin -> id (I)
+ * login -> id (I), nick (S)
+ * nickset -> id (I), nick (S)
+ * users -> { id -> {nick (S), filters ({dim (S) -> filter (*)}) } }
+ * logout -> id (I)
+ */
+ socket.on('selflogin', function(d) {
+ /* Acknowledged that we logged in */
+ /* Take note of our ID. */
+ id = d.id;
+ });
+ socket.on('login', function(d) {
+ /* Someone else logging in */
+ users[d.id] = { nick: d.nick, filters: {} };
+ updateUsersStatus();
+ });
+ socket.on('users', function(d) {
+ /* We've got a list of all users. */
+ /* The server is always right. */
+ users = d.users;
+ updateUsersStatus();
+ });
+ socket.on('nickset', function(d) {
+ /* Someone, possibly us, changed their nick. */
+ users[d.id].nick = d.nick;
+ updateUsersStatus();
+ });
+ socket.on('filterset', function(d) {
+ /* Someone changed their filter */
+ users[d.id].filters = d.filters;
+ /* Use the variable netrendering to denote that we're rendering due to a change received from the network. */
+ netrendering = true;
+ setOwnFilters(d.filters);
+ netrendering = false;
+ });
+ socket.on('logout', function(d) {
+ /* Someone disconnected, take them off the list. */
+ delete users[d.id];
+ updateUsersStatus();
+ });
+ dc.renderlet(function() {
+ /* Hook a listener into dc's rendering routine. If it rerenders, broadcast the change. */
+ if (netrendering) {
+ /* If we rendered due a change we received, don't broadcast it again. That would be A Bad Thing. */
+ return;
+ }
+ socket.emit("filter", { filters: mv.charter.filters() });
+ });
+ }
+ function setOwnFilters(filters) {
+ /* See if there's any difference - if there isn't, don't update. */
+ var change = false;
+ var key;
+ /* Check for keys in the filters to apply which are not in our charts. */
+ for (key in filters) {
+ if (!(key in mv.charts))
+ continue;
+ var filter = mv.charts[key].filter();
+ if (typeof(filter) == "array") {
+ /* Crossfilter uses arrays to filter ranges. Exactly the first two elements are significant. */
+ if (filter[0] == filters[key][0] &&
+ filter[1] == filters[key][1]) {
+ continue;
+ }
+ } else if (filter == filters[key]) {
+ continue;
+ }
+ /* This filter differs. Apply it. */
+ change = true;
+ mv.charts[key].filter(filters[key]);
+ }
+ /* Check for keys in our charts which are not in the filters to apply. */
+ for (key in mv.charts) {
+ if (mv.charts[key].filter() != null) {
+ if (key in filters) {
+ /* This has already been handled above */
+ continue;
+ }
+ /* There is no longer a filter applying here, clear it. */
+ change = true;
+ mv.charts[key].filterAll();
+ }
+ }
+ if (change) {
+ dc.redrawAll();
+ }
+ }
+ function updateUsersStatus() {
+ /* Convert the user list to a form suitable for a d3 selection */
+ var data = [];
+ for (var uid in users) { users[uid].id = uid; data.push(users[uid]); }
+ /* Data join */
+ var userlist = usersStatus.selectAll(".user")
+ .data(data, function(d) { return d.id; });
+ /* Enter */
+ userlist
+ .enter().append("li").attr("class", "user")
+ ;
+ /* Update */
+ userlist
+ .each(function(d,i) {
+ var elm = d3.select(this);
+ console.log("Userlist para appending", d,i);
+ var name = elm.select(".name");
+ var nick = d.nick == null ? "Anonymous User " + d.id : d.nick;
+ if (d.id == id) {
+ /* This is us! We can edit our name. */
+ if (name.empty()) {
+ console.log("Found our entry. id:", id, "datum", d);
+ name = elm.append("input").attr("class", "name")
+ .attr("type", "text")
+ .attr("placeholder", "Enter name here")
+ .on("change", function () {
+ /* A d3 select must be dereferenced twice to access the DOM entity */
+ socket.emit("nick", { nick: name[0][0].value });
+ })
+ ;
+ }
+ name.attr("value", nick);
+ } else {
+ /* This is someone else. We can't edit their name. */
+ if (name.empty()) {
+ name = elm.append("p").attr("class", "name");
+ }
+ name.text(nick);
+ }
+ })
+ ;
+ /* Remove */
+ userlist
+ .exit().remove()
+ ;
+ }
+ return mv;
+}(mv || {});
diff --git a/js/mv/heap.js b/js/mv/heap.js
index a0a69be..03f3c00 100644
--- a/js/mv/heap.js
+++ b/js/mv/heap.js
@@ -1,6 +1,7 @@
var mv = function(mv) {
mv.heap = function() {
var heap = {};
+ var monoGroups = {};
var statGran = 10;
heap.init = function() {
function ea(p, d) { p.e += d.e; p.j += d.j; p.r++; return p; }
diff --git a/js/mv/main.js b/js/mv/main.js
index 4db9973..b7fcb1e 100644
--- a/js/mv/main.js
+++ b/js/mv/main.js
@@ -48,6 +48,10 @@ var mv = function(mv) {
.style("opacity", 1)
;
mv.charter.init();
+ console.log(document.getElementById("connect-option").checked);
+ if (document.getElementById("connect-option").checked) {
+ mv.socket.connect();
+ }
}, 2000);
}
};
diff --git a/js/util/trellis-chart.js b/js/util/trellis-chart.js
index 139d663..65e07a2 100644
--- a/js/util/trellis-chart.js
+++ b/js/util/trellis-chart.js
@@ -10,11 +10,11 @@ function trellisChart(anchor, monoGroups) {
var subChartLength = 57;
var subChartUnpaddedLength = 50;
var subChartPadding = 7;
+ var filler = d3.scale.log().domain([1, 2]).range([0, 255]);
var margin = {top: 10, right: 10, bottom: 20, left: 10};
var anchor = d3.select(anchor);
var g = anchor.select("g");
- var filler = d3.scale.log().domain([1, 2]).range([0, 255]);
var _chart = function() {
if (g.empty()) {
@@ -126,5 +126,15 @@ function trellisChart(anchor, monoGroups) {
;
}
+ _chart.filter = function() {
+ /*
+ * TODO:
+ * This is going to be interesting. As the chart is not charting a single
+ * monogroup, and most code is built around this assumption, this might
+ * well end up being a messy special case.
+ */
+ return null;
+ }
+
return _chart;
}