summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-04-13 18:10:51 +1200
committerFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-04-13 18:20:57 +1200
commit8f34d7e6ce1142af2171d9ec31e20e9738c8223f (patch)
tree3469c004607cac366bda99d40365dbb8295b8029
parent35209fb4ddf117fa0cbde1eef4cff1fb45dfc24f (diff)
downloadmanavis-8f34d7e6ce1142af2171d9ec31e20e9738c8223f.tar.gz
manavis-8f34d7e6ce1142af2171d9ec31e20e9738c8223f.tar.bz2
manavis-8f34d7e6ce1142af2171d9ec31e20e9738c8223f.tar.xz
manavis-8f34d7e6ce1142af2171d9ec31e20e9738c8223f.zip
Refactor mv.js into distinct modules
There are five modules, as follows: load.js handles initialisation and management of files parse.js handles initialisation and management of records heap.js handles initialisation and management of dimensions chart.js handles initialisation and management of charts main.js manages the other modules and status Status and file loading have been decoupled; file loading no longer directly updates the status of the progress bars. This makes the limitations of the current status system more apparent, and should make the system also easier to maintain, as progress bars are now updated at more logical times. The parser remains mostly unchanged. It will need to be altered into a full stateful parser, but this can happen later. Dimension management is now simplified, due to the addition of monoGroup. Most dimension/groups consisted of a single dimension and a group reduced by count. This convenience function combines these and unifies their access, beyond mere naming convention. Charting management is also greatly simplified, adding in chain helpers to categorise types of charts, by aspects such as being wide, being thin, being short, as well as helpers to establish common properties for bar and pie charts. There is now also a helper to take advantage of the unified monoGroup accessors.
-rw-r--r--css/style.css4
-rw-r--r--index.html26
-rw-r--r--js/mv.js332
-rw-r--r--js/mv/chart.js107
-rw-r--r--js/mv/heap.js34
-rw-r--r--js/mv/load.js60
-rw-r--r--js/mv/main.js57
-rw-r--r--js/mv/parse.js86
8 files changed, 361 insertions, 345 deletions
diff --git a/css/style.css b/css/style.css
index 486ef83..0efc85b 100644
--- a/css/style.css
+++ b/css/style.css
@@ -13,11 +13,11 @@ body {
overflow: hidden;
}
-#body, #main, #status, .side {
+body, #main, #status, .side {
min-height: 40px;
}
-#body {
+body {
min-width: 1350px;
}
diff --git a/index.html b/index.html
index ea347f2..9a45e27 100644
--- a/index.html
+++ b/index.html
@@ -4,23 +4,23 @@
<link rel="stylesheet" type="text/css" href="css/style.css" />
<link rel="stylesheet" type="text/css" href="js/dc/test/dc.css" />
-<div id="body">
+<body onload="mv.init();">
<div class="side">
<div class="vis-hide">
<div id="blvl-chart">
- <h3>Experience gain instances by Character Base Level <a class="reset" style="display: none;" href="javascript:mv.blvlChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain instances by Character Base Level <a class="reset" style="display: none;" href="javascript:mv.charts.blvl.filterAll();dc.redrawAll();">clear</a></h3>
</div>
<div id="type-chart">
- <h3>Experience gain instances by Type <a class="reset" style="display: none;" href="javascript:mv.typeChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain instances by Type <a class="reset" style="display: none;" href="javascript:mv.charts.type.filterAll();dc.redrawAll();">clear</a></h3>
</div>
<div id="target-chart">
- <h3>Experience gain instances by Target <a class="reset" style="display: none;" href="javascript:mv.targetChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain instances 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>Experience gain instances by Weapon <a class="reset" style="display: none;" href="javascript:mv.wpnChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain instances by Weapon <a class="reset" style="display: none;" href="javascript:mv.charts.wpn.filterAll();dc.redrawAll();">clear</a></h3>
</div>
<div id="def-chart">
- <h3>Definedness of records <span class="help" title="If logs are missing between server boot and the logs provided, not all information will be available for all records. Definedness of records falls into three categories. Records with undefined data; records with well defined data, but mixed in with records having undefined data, limiting validity of inferences; and records after any uncertain times, which are fully well defined. It is highly recommended that you filter results to only include records which are in fully well defined times; however, this may not always be possible.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.defChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Definedness of records <span class="help" title="If logs are missing between server boot and the logs provided, not all information will be available for all records. Definedness of records falls into three categories. Records with undefined data; records with well defined data, but mixed in with records having undefined data, limiting validity of inferences; and records after any uncertain times, which are fully well defined. It is highly recommended that you filter results to only include records which are in fully well defined times; however, this may not always be possible.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.charts.def.filterAll();dc.redrawAll();">clear</a></h3>
</div>
</div>
</div>
@@ -44,17 +44,17 @@
</div>
<div class="vis-hide">
<div id="map-chart">
- <h3>Experience gain 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.mapChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain 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>
</div>
<div id="date-chart">
- <h3>Experience gain instances by Date <a class="reset" style="display: none;" href="javascript:mv.dateChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain instances by Date <a class="reset" style="display: none;" href="javascript:mv.charts.date.filterAll();dc.redrawAll();">clear</a></h3>
</div>
<div id="player-chart">
- <h3>Experience gain instances by Character ID <span class="help" title="Older to newer characters appear left to right, respectively.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.pcChart.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Experience gain instances by Character ID <span class="help" title="Older to newer characters appear left to right, respectively.">[?]</span> <a class="reset" style="display: none;" href="javascript:mv.charts.pc.filterAll();dc.redrawAll();">clear</a></h3>
</div>
</div>
</div>
-</div>
+</body>
<!-- Libs -->
<script src="js/util/memoize.js"></script>
@@ -68,4 +68,8 @@
<script src="js/comp/stat.js"></script><!-- Depends on crossfilter for crossfilter.bisect -->
<!-- Processing -->
-<script src="js/mv.js"></script>
+<script src="js/mv/chart.js"></script>
+<script src="js/mv/heap.js"></script>
+<script src="js/mv/load.js"></script>
+<script src="js/mv/main.js"></script>
+<script src="js/mv/parse.js"></script>
diff --git a/js/mv.js b/js/mv.js
deleted file mode 100644
index b4910ed..0000000
--- a/js/mv.js
+++ /dev/null
@@ -1,332 +0,0 @@
-"use strict";
-/*
- * Globals accessible via the agent console for debugging purposes
- */
-var mv = {};
-/*
- * Processing
- */
-(function() {
- function softAssert(expr, msg) {
- if (!expr) {
- console.log("SOFTASSERT FAILURE: " + msg);
- }
- }
- /* Set up handlers for file selector */
- document.getElementById('input').addEventListener('change', function(fevt) {
- var reader = new FileReader();
- var loadbar = progress('loadbar');
- var filesbar = progress('filesbar');
-
- reader.onerror = function(evt) {
- switch(evt.target.error.code) {
- case evt.target.error.NOT_FOUND_ERR:
- alert('File Not Found!');
- break;
- case evt.target.error.NOT_READABLE_ERR:
- alert('File is not readable');
- break;
- case evt.target.error.ABORT_ERR:
- break; // noop
- default:
- alert('An error occurred reading this file.');
- };
- }
- reader.onprogress = function(evt) {
- if (evt.lengthComputable) {
- loadbar.update(evt.loaded, evt.total);
- }
- }
- reader.onabort = function(evt) {
- alert('File load aborted!');
- }
- reader.onloadstart = function(evt) {
- loadbar.reset();
- }
- reader.onload = function(evt) {
- loadbar.complete();
- ++cur;
- parseRecords(reader.result);
- if (cur == fevt.target.files.length) {
- filesbar.complete();
- /* TODO: Make this fade out nicely? */
- setTimeout(function() {
- loadbar.hide();
- }, 2000);
- makeHeap();
- setTimeout(function() {
- filesbar.hide();
- }, 2000);
- makeCharts();
- } else {
- filesbar.update(cur, fevt.target.files.length);
- nextFile();
- }
- }
- var cur = 0;
- var lbase = loadbar.label;
- loadbar.label = function() {
- return lbase() == '100%' ? "Loaded '" + fevt.target.files[cur].name + "' - Done!" : "Loading '" + fevt.target.files[cur].name + "' - " + lbase();
- };
- var fbase = filesbar.label;
- filesbar.label = function () {
- return fbase() == '100%' ? "Loaded " + fevt.target.files.length + " file(s) - Done!" : "Loading file " + (cur + 1) + " of " + fevt.target.files.length + " - " + fbase();
- }
- loadbar.show();
- filesbar.show();
- function nextFile() {
- reader.readAsBinaryString(fevt.target.files[cur]);
- }
- nextFile();
- }, false);
- var records = [];
- var pcstat = {};
- var fullyDefinedCutoff = false;
- function defLevelVerbose(level) {
- switch (level) {
- case 0: return "Undefined";
- case 1: return "Mixed";
- case 2: return "Defined";
- default: console.log(d, d.data); throw "Unknown definedness case (" + d.data.key + "); this shouldn't happen";
- }
- }
- function parseRecords(data) {
- var spl = data.split(/\r?\n/);
- spl.forEach(function(e, i) {
- var d;
- d = e.match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/);
- if (d) {
- var mapSID = parseInt(d[3]);
- var ts = new Date(0);
- ts.setUTCSeconds(d[1]);
- var rec = {
- date: ts,
- pc: parseInt(d[2]),
- map: map.nameByServerID(parseInt(d[3]), ts),
- x: parseInt(d[4]),
- y: parseInt(d[5]),
- e: parseInt(d[6]),
- j: parseInt(d[7]),
- type: d[8],
- pcstat: pcstat[d[2]],
- target: 0,
- dmg: 0,
- wpn: 0
- };
- if (pcstat[d[2]] == undefined && (!fullyDefinedCutoff || ts > fullyDefinedCutoff)) {
- fullyDefinedCutoff = ts;
- }
- /* XXX: Fragile horrible and unstructured, this whole thing needs a rewrite really */
- if (i >= 2 && rec.type == "KILLXP") {
- d = spl[i - 1].match(/^(\d+\.\d+) MOB(\d+) DEAD/);
- if (d) {
- var mID = parseInt(d[2]);
- /* There's a massive wealth of data that can be collected from this. Number of assailants, weapons used, the relationships with the assailants... this can't be done with a simple lookbehind. For now, just extract what mob it was, and what the killing weapon used was. */
- d = spl[i - 2].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) WPNDMG MOB(\d+) (\d+) FOR (\d+) WPN (\d+)/);
- if (d) {
- softAssert(mID == parseInt(d[6]), "Integrity error: MOB ID mismatch!");
-// softAssert(rec.pc == parseInt(d[2]), "Integrity error: PC ID mismatch!");
- rec.target = parseInt(d[7]);
- rec.dmg = parseInt(d[8]);
- rec.wpn = parseInt(d[9]);
- }
- } else {
- d = spl[i - 1].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/);
- if (d) {
- var clone = records[records.length - 1];
- softAssert(rec.map == clone.map, "Integrity error: MAP ID mismatch!");
- rec.target = clone.target;
- rec.dmg = clone.dmg; /* FIXME: Take into account actual assist damage */
- rec.wpn = clone.wpn;
- }
- }
- }
- records.push(rec);
- return;
- }
- d = e.match(/^(?:\d+\.\d+) PC(\d+) (?:\d+):(?:\d+),(?:\d+) STAT (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) /);
- if (d) {
- var s = {
- str: parseInt(d[2]),
- agi: parseInt(d[3]),
- vit: parseInt(d[4]),
- int: parseInt(d[5]),
- dex: parseInt(d[6]),
- luk: parseInt(d[7])
- };
- s.blvl = stat.minLevelForStats(s.str, s.agi, s.vit, s.int, s.dex, s.luk);
- pcstat[d[1]] = s;
- return;
- }
- });
- }
- var cfdata, all,
- dateDim, dateGroup,
- pcDim, pcGroup,
- mapDim, mapGroup,
- blvlDim, blvlGroup,
- typeDim, typeGroup,
- targetDim, targetGroup,
- dmgDim, dmgGroup,
- wpnDim, wpnGroup,
- /*
- * How well defined a record is.
- * 0 -> Record contains undefined data
- * 1 -> Record is defined, but undefined records follow and may impede validity of findings
- * 2 -> Record and all succeeding records are well defined
- */
- defDim, defGroup;
- /* The record files are set, do everything */
- function makeHeap() {
- function a(p, d) { return { e: p.e + d.e, j: p.j + d.j, r: p.r + 1 }; }
- function s(p, d) { return { e: p.e - d.e, j: p.j - d.j, r: p.r - 1 }; }
- function z(p, d) { return { e: 0, j: 0, r: 0 }; }
- cfdata = crossfilter(records);
- all = cfdata.groupAll().reduce(a, s, z);
- dateDim = cfdata.dimension(function(d) { return d3.time.hour.round(d.date); });
- dateGroup = dateDim.group().reduceCount();
- pcDim = cfdata.dimension(function(d) { return d.pc; });
- pcGroup = pcDim.group().reduceCount();
- mapDim = cfdata.dimension(function(d) { return d.map; });
- mapGroup = mapDim.group().reduce(a, s, z);
- blvlDim = cfdata.dimension(function(d) { return d.pcstat ? d.pcstat.blvl : 0; });
- blvlGroup = blvlDim.group().reduceCount();
- typeDim = cfdata.dimension(function(d) { return d.type; });
- typeGroup = typeDim.group().reduceCount();
- targetDim = cfdata.dimension(function(d) { return d.target; });
- targetGroup = targetDim.group().reduceCount();
- wpnDim = cfdata.dimension(function(d) { return d.wpn; });
- wpnGroup = wpnDim.group().reduceCount();
- /* Add new dimensions above here */
- defDim = cfdata.dimension(function(d) { if (d.pcstat == undefined) { return 0; } if (d.date <= fullyDefinedCutoff) { return 1; } return 2; });
- defGroup = defDim.group().reduceCount();
- defDim.filterExact(2);
- /*
- * The viewport is the bubble frame.
- * - K: Map
- * - X: Exp
- * - Y: JExp
- * - r: Record count
- */
- }
- function makeCharts() {
- d3.select("#mask")
- .transition()
- .style("opacity", 0)
- .remove();
- d3.selectAll(".vis-hide")
- .style("display", "inline")
- .transition()
- .style("opacity", 1)
- ;
- mv.dateChart = dc.barChart("#date-chart")
- .width(700)
- .height(130)
- .margins({left: 60, right: 18, top: 5, bottom: 30})
- .dimension(dateDim)
- .group(dateGroup)
- .centerBar(true)
- .gap(1)
- .elasticY(true)
- .elasticX(true)
- .x(d3.time.scale().domain([dateDim.bottom(1)[0].date, dateDim.top(1)[0].date]).nice(d3.time.hour))
- .xUnits(d3.time.hours)
- .xAxisPadding(2)
- // .renderVerticalGridLines(true)
- .renderHorizontalGridLines(true)
- .title(function(d) { return d.key + ": " + d.value; })
- .brushOn(true)
- ;
- mv.pcChart = dc.barChart("#player-chart")
- .width(700)
- .height(130)
- .margins({left: 60, right: 18, top: 5, bottom: 30})
- .dimension(pcDim)
- .group(pcGroup)
- .gap(1)
-// .elasticX(true)
- .elasticY(true)
- .x(d3.scale.linear().domain([pcDim.bottom(1)[0].pc, pcDim.top(1)[0].pc]).nice())
- .renderHorizontalGridLines(true)
- .title(function(d) { return d.key + ": " + d.value; })
- .brushOn(true)
- ;
- mv.blvlChart = dc.barChart("#blvl-chart")
- .width(380)
- .height(130)
- .margins({left: 60, right: 18, top: 5, bottom: 30})
- .dimension(blvlDim)
- .group(blvlGroup)
- .gap(1)
- .elasticY(true)
- .x(d3.scale.linear().domain([0, blvlDim.top(1)[0].pcstat.blvl]))
- .renderHorizontalGridLines(true)
- .title(function(d) { return d.key + ": " + d.value; })
- .brushOn(true)
- ;
- mv.typeChart = dc.pieChart("#type-chart")
- .width(380)
- .height(130)
- .radius(60)
- .dimension(typeDim)
- .group(typeGroup)
- .colorCalculator(d3.scale.category20c())
- ;
- mv.targetChart = dc.pieChart("#target-chart")
- .width(380)
- .height(130)
- .radius(60)
- .dimension(targetDim)
- .group(targetGroup)
- .colorCalculator(d3.scale.category20c())
- ;
- mv.targetChart = dc.pieChart("#wpn-chart")
- .width(380)
- .height(130)
- .radius(60)
- .dimension(wpnDim)
- .group(wpnGroup)
- .colorCalculator(d3.scale.category20c())
- ;
- mv.defChart = dc.pieChart("#def-chart")
- .width(380)
- .height(130)
- .radius(60)
- .dimension(defDim)
- .group(defGroup)
- .label(function(d) { return defLevelVerbose(d.data.key); })
- .title(function(d) { return defLevelVerbose(d.data.key) + ": " + d.value; })
- .colorAccessor(function(d) { return d.data.key; })
- .colorCalculator(function(k) { switch(k) {
- case 0: return "#fd350d";
- case 1: return "#fdae6b";
- case 2: return "#6baed6";
- default: throw "Definition chart: Color access key out of range!";
- }})
- .filter(2)
- ;
- mv.mapChart = dc.bubbleChart("#map-chart")
- .width(700)
- .height(500)
- .margins({left: 60, right: 18, top: 5, bottom: 30})
- .dimension(mapDim)
- .group(mapGroup)
- .colorCalculator(d3.scale.category20c())
- /* X */
- .keyAccessor(function(d) { return d.value.e + 1; })
- /* Y */
- .valueAccessor(function(d) { return d.value.j + 1; })
- /* R */
- .radiusValueAccessor(function(d) { return Math.log(d.value.r + 1); })
- .x(d3.scale.log().domain([1, 100000]))
- .y(d3.scale.log().domain([1, 300000]))
- .elasticX(true)
- .elasticY(true)
- .renderHorizontalGridLines(true)
- .renderVerticalGridLines(true)
- .title(function(d) { return "Map " + d.key; })
- .renderTitle(true)
- ;
- dc.renderAll();
- }
-})();
diff --git a/js/mv/chart.js b/js/mv/chart.js
new file mode 100644
index 0000000..9e07e2a
--- /dev/null
+++ b/js/mv/chart.js
@@ -0,0 +1,107 @@
+var mv = function(mv) {
+ mv.charts = {};
+ mv.charter = function() {
+ var charter = {};
+ charter.init = function() {
+ mv.charts.date = bar(monoGroup(wide(dc.barChart("#date-chart")), "date"))
+ .centerBar(true)
+ .elasticX(true)
+ .x(d3.time.scale().domain([mv.heap.date.dim.bottom(1)[0].date, mv.heap.date.dim.top(1)[0].date]).nice(d3.time.hour))
+ .xUnits(d3.time.hours)
+ .xAxisPadding(2)
+ ;
+ mv.charts.pc = bar(monoGroup(wide(dc.barChart("#player-chart")), "pc"))
+ .x(d3.scale.linear().domain([mv.heap.pc.dim.bottom(1)[0].pc, mv.heap.pc.dim.top(1)[0].pc]).nice())
+ ;
+ mv.charts.blvl = bar(monoGroup(thin(dc.barChart("#blvl-chart")), "blvl"))
+ .x(d3.scale.linear().domain([0, mv.heap.blvl.dim.top(1)[0].pcstat.blvl]))
+ ;
+ mv.charts.type = pie(monoGroup(dc.pieChart("#type-chart"), "type"))
+ ;
+ mv.charts.target = pie(monoGroup(dc.pieChart("#target-chart"), "target"))
+ ;
+ mv.charts.target = pie(monoGroup(dc.pieChart("#wpn-chart"), "wpn"))
+ ;
+ mv.charts.def = pie(monoGroup(dc.pieChart("#def-chart"), "def"))
+ .label(function(d) { return defLevelVerbose(d.data.key); })
+ .title(function(d) { return defLevelVerbose(d.data.key) + ": " + d.value; })
+ .colorAccessor(function(d) { return d.data.key; })
+ .colorCalculator(function(k) { switch(k) {
+ case 0: return "#fd350d";
+ case 1: return "#fdae6b";
+ case 2: return "#6baed6";
+ default: throw "Definition chart: Color access key out of range!";
+ }})
+ ;
+ mv.charts.map = monoGroup(margined(wide(dc.bubbleChart("#map-chart"))), "map")
+ .height(500)
+ .colorCalculator(d3.scale.category20c())
+ /* X */
+ .keyAccessor(function(d) { return d.value.e + 1; })
+ /* Y */
+ .valueAccessor(function(d) { return d.value.j + 1; })
+ /* R */
+ .radiusValueAccessor(function(d) { return Math.log(d.value.r + 1); })
+ .x(d3.scale.log().domain([1, 100000]))
+ .y(d3.scale.log().domain([1, 300000]))
+ .elasticX(true)
+ .elasticY(true)
+ .renderHorizontalGridLines(true)
+ .renderVerticalGridLines(true)
+ .title(function(d) { return "Map " + d.key; })
+ .renderTitle(true)
+ ;
+ dc.renderAll();
+ }
+ function defLevelVerbose(level) {
+ switch (level) {
+ case 0: return "Undefined";
+ case 1: return "Mixed";
+ case 2: return "Defined";
+ default: console.log(d, d.data); throw "Unknown definedness case (" + d.data.key + "); this shouldn't happen";
+ }
+ }
+ return charter;
+ }();
+ function wide(chart) {
+ return chart
+ .width(700)
+ ;
+ }
+ function thin(chart) {
+ return chart
+ .width(380)
+ ;
+ }
+ function short(chart) {
+ return chart
+ .height(130)
+ ;
+ }
+ function margined(chart) {
+ return chart
+ .margins({left: 60, right: 18, top: 5, bottom: 30})
+ }
+ function monoGroup(chart, name) {
+ return chart
+ .dimension(mv.heap[name].dim)
+ .group(mv.heap[name].group)
+ ;
+ }
+ function bar(chart) {
+ return margined(short(chart))
+ .elasticY(true)
+ .gap(1)
+ .renderHorizontalGridLines(true)
+ .title(function(d) { return d.key + ": " + d.value; })
+ .brushOn(true)
+ ;
+ }
+ function pie(chart) {
+ return thin(short(chart))
+ .radius(60)
+ .colorCalculator(d3.scale.category20c())
+ ;
+ }
+ return mv;
+}(mv || {});
diff --git a/js/mv/heap.js b/js/mv/heap.js
new file mode 100644
index 0000000..bd1d583
--- /dev/null
+++ b/js/mv/heap.js
@@ -0,0 +1,34 @@
+var mv = function(mv) {
+ mv.heap = function() {
+ var heap = {};
+ heap.init = function() {
+ function a(p, d) { return { e: p.e + d.e, j: p.j + d.j, r: p.r + 1 }; }
+ function s(p, d) { return { e: p.e - d.e, j: p.j - d.j, r: p.r - 1 }; }
+ function z(p, d) { return { e: 0, j: 0, r: 0 }; }
+ heap.cfdata = crossfilter(mv.parser.records);
+ heap.all = heap.cfdata.groupAll().reduce(a, s, z);
+ monoGroup("date", function(d) { return d3.time.hour.round(d.date); });
+ monoGroup("pc", function(d) { return d.pc; });
+ monoGroup("map", function(d) { return d.map; }).reduce(a, s, z);
+ monoGroup("blvl", function(d) { return d.pcstat ? d.pcstat.blvl : 0; });
+ monoGroup("type", function(d) { return d.type; });
+ monoGroup("target", function(d) { return d.target; });
+ monoGroup("wpn", function(d) { return d.wpn; })
+ /* Debugging group */
+ /*
+ * How well defined a record is.
+ * 0 -> Record contains undefined data
+ * 1 -> Record is defined, but undefined records follow and may impede validity of findings
+ * 2 -> Record and all succeeding records are well defined
+ */
+ monoGroup("def", function(d) { if (d.pcstat == undefined) { return 0; } if (d.date <= mv.parser.fullyDefinedCutoff()) { return 1; } return 2; });
+ heap.def.dim.filterExact(2);
+ }
+ function monoGroup(name, mapping) {
+ heap[name] = {};
+ return heap[name].group = (heap[name].dim = heap.cfdata.dimension(mapping)).group();
+ }
+ return heap;
+ }();
+ return mv;
+}(mv || {});
diff --git a/js/mv/load.js b/js/mv/load.js
new file mode 100644
index 0000000..2eca272
--- /dev/null
+++ b/js/mv/load.js
@@ -0,0 +1,60 @@
+var mv = function(mv) {
+ mv.loader = function() {
+ /* Set up handlers for file selector */
+ var numfiles = 0;
+ var filenames = [];
+ var curfile = 0;
+ var loader = {};
+ loader.onbulkstart = function(fevt) {};
+ loader.onloadstart = function(evt) {};
+ loader.onprogress = function(evt) {};
+ loader.onabort = function(evt) {
+ alert('File load aborted!');
+ };
+ loader.onerror = function(evt) {
+ switch(evt.target.error.code) {
+ case evt.target.error.NOT_FOUND_ERR:
+ alert('File Not Found!');
+ break;
+ case evt.target.error.NOT_READABLE_ERR:
+ alert('File is not readable');
+ break;
+ case evt.target.error.ABORT_ERR:
+ break; // noop
+ default:
+ alert('An error occurred reading this file.');
+ };
+ };
+ loader.numfiles = function() { return numfiles; };
+ loader.filenames = function() { return filenames; };
+ loader.curfile = function() { return curfile; };
+ loader.init = function(each, after) {
+ document.getElementById('input').addEventListener('change', function(fevt) {
+ numfiles = fevt.target.files.length;
+ filenames = Array.prototype.map.call(fevt.target.files, function(d) { return d.name; });
+ curfile = 0;
+ var reader = new FileReader();
+ loader.onbulkstart(fevt);
+ reader.onerror = function() { loader.onerror.apply(null, arguments) };
+ reader.onprogress = function() { loader.onprogress.apply(null, arguments) };
+ reader.onabort = function() { loader.onabort.apply(null, arguments) };
+ reader.onloadstart = function() { loader.onloadstart.apply(null, arguments) };
+ reader.onload = function(evt) {
+ each(reader.result);
+ ++curfile;
+ if (curfile == numfiles) {
+ after();
+ } else {
+ nextFile();
+ }
+ }
+ function nextFile() {
+ setTimeout(function(){ console.log("Loading file ", curfile); reader.readAsBinaryString(fevt.target.files[curfile]);}, 0);
+ }
+ nextFile();
+ }, false);
+ };
+ return loader;
+ }();
+ return mv;
+}(mv || {});
diff --git a/js/mv/main.js b/js/mv/main.js
new file mode 100644
index 0000000..51d1dae
--- /dev/null
+++ b/js/mv/main.js
@@ -0,0 +1,57 @@
+var mv = function(mv) {
+ mv.init = function() {
+ console.log("Initialising");
+ var loadbar = progress('loadbar');
+ var filesbar = progress('filesbar');
+ var lbase = loadbar.label;
+ loadbar.label = function() {
+ return lbase() == '100%' ? "Loaded '" + mv.loader.filenames()[mv.loader.curfile()]+ "' - Done!" : "Loading '" + mv.loader.filenames()[mv.loader.curfile()] + "' - " + lbase();
+ };
+ var fbase = filesbar.label;
+ filesbar.label = function () {
+ return fbase() == '100%' ? "Loaded " + mv.loader.numfiles() + " file(s) - Done!" : "Loading file " + mv.loader.curfile() + " of " + mv.loader.numfiles() + " - " + fbase();
+ }
+ loadbar.show();
+ filesbar.show();
+ mv.loader.onbulkstart = function(fevt) {
+ loadbar.show();
+ filesbar.show();
+ };
+ mv.loader.onloadstart = function(evt) {
+ filesbar.update(mv.loader.curfile(), mv.loader.numfiles());
+ loadbar.reset();
+ };
+ mv.loader.onprogress = function(evt) {
+ if (evt.lengthComputable) {
+ loadbar.update(evt.loaded, evt.total);
+ }
+ };
+ mv.loader.init(handleFile, postLoading);
+ function handleFile(data, curFileNum, numFiles) {
+ loadbar.complete();
+ mv.parser.parseRecords(data);
+ }
+ function postLoading() {
+ filesbar.complete();
+ /* TODO: This is still a total mess that doesn't really transition properly */
+ setTimeout(function() {
+ loadbar.hide();
+ }, 2000);
+ mv.heap.init();
+ setTimeout(function() {
+ filesbar.hide();
+ d3.select("#mask")
+ .transition()
+ .style("opacity", 0)
+ .remove();
+ d3.selectAll(".vis-hide")
+ .style("display", "inline")
+ .transition()
+ .style("opacity", 1)
+ ;
+ mv.charter.init();
+ }, 2000);
+ }
+ };
+ return mv;
+}(mv || {});
diff --git a/js/mv/parse.js b/js/mv/parse.js
new file mode 100644
index 0000000..23a0f79
--- /dev/null
+++ b/js/mv/parse.js
@@ -0,0 +1,86 @@
+var mv = function(mv) {
+ mv.parser = function() {
+ var parser = {};
+ var pcstat = {};
+ var fullyDefinedCutoff = 0;
+ parser.records = [];
+ parser.fullyDefinedCutoff = function() { return fullyDefinedCutoff; };
+ parser.parseRecords = function(data) {
+ var spl = data.split(/\r?\n/);
+ spl.forEach(function(e, i) {
+ var d;
+ d = e.match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/);
+ if (d) {
+ var mapSID = parseInt(d[3]);
+ var ts = new Date(0);
+ ts.setUTCSeconds(d[1]);
+ var rec = {
+ date: ts,
+ pc: parseInt(d[2]),
+ map: map.nameByServerID(parseInt(d[3]), ts),
+ x: parseInt(d[4]),
+ y: parseInt(d[5]),
+ e: parseInt(d[6]),
+ j: parseInt(d[7]),
+ type: d[8],
+ pcstat: pcstat[d[2]],
+ target: 0,
+ dmg: 0,
+ wpn: 0
+ };
+ if (pcstat[d[2]] == undefined && (!fullyDefinedCutoff || ts > fullyDefinedCutoff)) {
+ fullyDefinedCutoff = ts;
+ }
+ /* XXX: Fragile horrible and unstructured, this whole thing needs a rewrite really */
+ if (i >= 2 && rec.type == "KILLXP") {
+ d = spl[i - 1].match(/^(\d+\.\d+) MOB(\d+) DEAD/);
+ if (d) {
+ var mID = parseInt(d[2]);
+ /* There's a massive wealth of data that can be collected from this. Number of assailants, weapons used, the relationships with the assailants... this can't be done with a simple lookbehind. For now, just extract what mob it was, and what the killing weapon used was. */
+ d = spl[i - 2].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) WPNDMG MOB(\d+) (\d+) FOR (\d+) WPN (\d+)/);
+ if (d) {
+ softAssert(mID == parseInt(d[6]), "Integrity error: MOB ID mismatch!");
+ // softAssert(rec.pc == parseInt(d[2]), "Integrity error: PC ID mismatch!");
+ rec.target = parseInt(d[7]);
+ rec.dmg = parseInt(d[8]);
+ rec.wpn = parseInt(d[9]);
+ }
+ } else {
+ d = spl[i - 1].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/);
+ if (d) {
+ var clone = parser.records[parser.records.length - 1];
+ softAssert(rec.map == clone.map, "Integrity error: MAP ID mismatch!");
+ rec.target = clone.target;
+ rec.dmg = clone.dmg; /* FIXME: Take into account actual assist damage */
+ rec.wpn = clone.wpn;
+ }
+ }
+ }
+ parser.records.push(rec);
+ return;
+ }
+ d = e.match(/^(?:\d+\.\d+) PC(\d+) (?:\d+):(?:\d+),(?:\d+) STAT (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) /);
+ if (d) {
+ var s = {
+ str: parseInt(d[2]),
+ agi: parseInt(d[3]),
+ vit: parseInt(d[4]),
+ int: parseInt(d[5]),
+ dex: parseInt(d[6]),
+ luk: parseInt(d[7])
+ };
+ s.blvl = stat.minLevelForStats(s.str, s.agi, s.vit, s.int, s.dex, s.luk);
+ pcstat[d[1]] = s;
+ return;
+ }
+ });
+ };
+ function softAssert(expr, msg) {
+ if (!expr) {
+ console.error("SOFTASSERT FAILURE: " + msg);
+ }
+ }
+ return parser;
+ }();
+ return mv;
+}(mv || {});