diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | css/style.css | 41 | ||||
-rw-r--r-- | index.html | 39 | ||||
-rw-r--r-- | index.js | 99 | ||||
-rw-r--r-- | js/mv/chart.js | 10 | ||||
-rw-r--r-- | js/mv/connect.js | 156 | ||||
-rw-r--r-- | js/mv/heap.js | 1 | ||||
-rw-r--r-- | js/mv/main.js | 4 | ||||
-rw-r--r-- | js/util/trellis-chart.js | 12 |
9 files changed, 349 insertions, 17 deletions
@@ -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 { @@ -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; } |