summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-04-23 15:26:48 +1200
committerFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-04-23 15:26:48 +1200
commita7da1fafe2ca8c9115ce76f99a903e1b3da692a3 (patch)
treeee4465cbebc54cd937f69ede32b2b99513dd76a0
parentf12d953786784ca8e8b8e9db146ce494b30f6647 (diff)
downloadmanavis-a7da1fafe2ca8c9115ce76f99a903e1b3da692a3.tar.gz
manavis-a7da1fafe2ca8c9115ce76f99a903e1b3da692a3.tar.bz2
manavis-a7da1fafe2ca8c9115ce76f99a903e1b3da692a3.tar.xz
manavis-a7da1fafe2ca8c9115ce76f99a903e1b3da692a3.zip
Add initial stat trellis chart
This should now be implemented efficiently enough for everything else to still work. This currently does not allow filtering, but the dimensions are prepared in a manner that makes this a simple addition.
-rw-r--r--css/style.css9
-rw-r--r--index.html22
m---------js/dc0
-rw-r--r--js/mv/chart.js3
-rw-r--r--js/mv/heap.js40
-rw-r--r--js/mv/parse.js18
-rw-r--r--js/util/trellis-chart.js130
7 files changed, 206 insertions, 16 deletions
diff --git a/css/style.css b/css/style.css
index 0efc85b..8571fa9 100644
--- a/css/style.css
+++ b/css/style.css
@@ -68,6 +68,15 @@ body {
white-space: nowrap;
}
+/* Stat chart */
+
+#stat-chart svg g g.column .border-line {
+ fill: none;
+ stroke: #ccc;
+ opacity: .5;
+ shape-rendering: crispEdges;
+}
+
/* Utility */
.fader {
-moz-transition: opacity 1s linear;
diff --git a/index.html b/index.html
index 9a45e27..c621789 100644
--- a/index.html
+++ b/index.html
@@ -8,16 +8,23 @@
<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.charts.blvl.filterAll();dc.redrawAll();">clear</a></h3>
+ <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>
+ <div id="stat-chart">
+ <h3>Instance breakdown by Stat allocation</h3>
+ </div>
+ </div>
+ </div>
+ <div class="side">
+ <div class="vis-hide">
<div id="type-chart">
- <h3>Experience gain instances by Type <a class="reset" style="display: none;" href="javascript:mv.charts.type.filterAll();dc.redrawAll();">clear</a></h3>
+ <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="target-chart">
- <h3>Experience gain instances by Target <a class="reset" style="display: none;" href="javascript:mv.charts.target.filterAll();dc.redrawAll();">clear</a></h3>
+ <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>Experience gain instances by Weapon <a class="reset" style="display: none;" href="javascript:mv.charts.wpn.filterAll();dc.redrawAll();">clear</a></h3>
+ <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="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.charts.def.filterAll();dc.redrawAll();">clear</a></h3>
@@ -44,13 +51,13 @@
</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.charts.map.filterAll();dc.redrawAll();">clear</a></h3>
+ <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>
</div>
<div id="date-chart">
- <h3>Experience gain instances by Date <a class="reset" style="display: none;" href="javascript:mv.charts.date.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Instance breakdown 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.charts.pc.filterAll();dc.redrawAll();">clear</a></h3>
+ <h3>Instance breakdown 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>
@@ -73,3 +80,4 @@
<script src="js/mv/load.js"></script>
<script src="js/mv/main.js"></script>
<script src="js/mv/parse.js"></script>
+<script src="js/util/trellis-chart.js"></script>
diff --git a/js/dc b/js/dc
-Subproject 9a243a5d684aafc3239ed06dc5a9bb15ac4aa94
+Subproject c8eab4e79abd32c4f7b42a20b16a18ba4af2967
diff --git a/js/mv/chart.js b/js/mv/chart.js
index 882caee..74d7b85 100644
--- a/js/mv/chart.js
+++ b/js/mv/chart.js
@@ -32,6 +32,7 @@ var mv = function(mv) {
case 2: return "#6baed6";
default: throw "Definition chart: Color access key out of range!";
}})
+ .filter(2)
;
mv.charts.map = monoGroup(margined(wide(dc.bubbleChart("#map-chart"))), "map")
.height(500)
@@ -51,6 +52,8 @@ var mv = function(mv) {
.title(function(d) { return "Map " + d.key; })
.renderTitle(true)
;
+ mv.charts.stats = trellisChart("#stat-chart", ["str", "agi", "vit", "dex", "int", "luk"].map(function(d) { mv.heap[d].name = d; return mv.heap[d]; }));
+ dc.renderlet(function() { mv.charts.stats(); });
dc.renderAll();
}
function defLevelVerbose(level) {
diff --git a/js/mv/heap.js b/js/mv/heap.js
index bd1d583..a0a69be 100644
--- a/js/mv/heap.js
+++ b/js/mv/heap.js
@@ -1,19 +1,47 @@
var mv = function(mv) {
mv.heap = function() {
var heap = {};
+ var statGran = 10;
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 }; }
+ function ea(p, d) { p.e += d.e; p.j += d.j; p.r++; return p; }
+ function es(p, d) { p.e -= d.e; p.j -= d.j; p.r--; return p; }
+ function ez(p, d) { return { e: 0, j: 0, r: 0 }; }
heap.cfdata = crossfilter(mv.parser.records);
- heap.all = heap.cfdata.groupAll().reduce(a, s, z);
+ heap.all = heap.cfdata.groupAll().reduce(ea, es, ez);
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("map", function(d) { return d.map; }).reduce(ea, es, ez);
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; })
+ monoGroup("wpn", function(d) { return d.wpn; });
+ function sa(p, d) {
+ if (!d.pcstat) return p;
+ p.str[d.pcstat.str]++ || (p.str[d.pcstat.str] = 1);
+ p.agi[d.pcstat.agi]++ || (p.agi[d.pcstat.agi] = 1);
+ p.vit[d.pcstat.vit]++ || (p.vit[d.pcstat.vit] = 1);
+ p.dex[d.pcstat.dex]++ || (p.dex[d.pcstat.dex] = 1);
+ p.int[d.pcstat.int]++ || (p.int[d.pcstat.int] = 1);
+ p.luk[d.pcstat.luk]++ || (p.luk[d.pcstat.luk] = 1);
+ return p;
+ }
+ function ss(p, d) {
+ if (!d.pcstat) return p;
+ --p.str[d.pcstat.str] || (p.str[d.pcstat.str] = undefined);
+ --p.agi[d.pcstat.agi] || (p.agi[d.pcstat.agi] = undefined);
+ --p.vit[d.pcstat.vit] || (p.vit[d.pcstat.vit] = undefined);
+ --p.dex[d.pcstat.dex] || (p.dex[d.pcstat.dex] = undefined);
+ --p.int[d.pcstat.int] || (p.int[d.pcstat.int] = undefined);
+ --p.luk[d.pcstat.luk] || (p.luk[d.pcstat.luk] = undefined);
+ return p;
+ }
+ function sz(p, d) { return { str: [], agi: [], vit: [], dex: [], int: [], luk: [] }; }
+ monoGroup("str", function(d) { return d.pcstat ? d.pcstat.str : 0; }).reduce(sa, ss, sz);
+ monoGroup("agi", function(d) { return d.pcstat ? d.pcstat.agi : 0; }).reduce(sa, ss, sz);
+ monoGroup("vit", function(d) { return d.pcstat ? d.pcstat.vit : 0; }).reduce(sa, ss, sz);
+ monoGroup("dex", function(d) { return d.pcstat ? d.pcstat.dex : 0; }).reduce(sa, ss, sz);
+ monoGroup("int", function(d) { return d.pcstat ? d.pcstat.int : 0; }).reduce(sa, ss, sz);
+ monoGroup("luk", function(d) { return d.pcstat ? d.pcstat.luk : 0; }).reduce(sa, ss, sz);
/* Debugging group */
/*
* How well defined a record is.
diff --git a/js/mv/parse.js b/js/mv/parse.js
index 23a0f79..c30a874 100644
--- a/js/mv/parse.js
+++ b/js/mv/parse.js
@@ -24,9 +24,9 @@ var mv = function(mv) {
j: parseInt(d[7]),
type: d[8],
pcstat: pcstat[d[2]],
- target: 0,
- dmg: 0,
- wpn: 0
+ target: -1010,
+ dmg: -1010,
+ wpn: -1010
};
if (pcstat[d[2]] == undefined && (!fullyDefinedCutoff || ts > fullyDefinedCutoff)) {
fullyDefinedCutoff = ts;
@@ -42,8 +42,11 @@ var mv = function(mv) {
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]);
+ softAssert(rec.target, "Unknown target!")
rec.dmg = parseInt(d[8]);
rec.wpn = parseInt(d[9]);
+ } else {
+// console.error("No match (deathblow):", spl[i - 2]);
}
} else {
d = spl[i - 1].match(/^(\d+\.\d+) PC(\d+) (\d+):(\d+),(\d+) GAINXP (\d+) (\d+) (\w+)/);
@@ -51,8 +54,11 @@ var mv = function(mv) {
var clone = parser.records[parser.records.length - 1];
softAssert(rec.map == clone.map, "Integrity error: MAP ID mismatch!");
rec.target = clone.target;
+ softAssert(rec.target, "Unknown (cloned) target!");
rec.dmg = clone.dmg; /* FIXME: Take into account actual assist damage */
rec.wpn = clone.wpn;
+ } else {
+// console.error("No match (clone):", spl[i - 1]);
}
}
}
@@ -70,6 +76,12 @@ var mv = function(mv) {
luk: parseInt(d[7])
};
s.blvl = stat.minLevelForStats(s.str, s.agi, s.vit, s.int, s.dex, s.luk);
+ s.str = Math.floor(s.str / 10);
+ s.agi = Math.floor(s.agi / 10);
+ s.vit = Math.floor(s.vit / 10);
+ s.int = Math.floor(s.int / 10);
+ s.dex = Math.floor(s.dex / 10);
+ s.luk = Math.floor(s.luk / 10);
pcstat[d[1]] = s;
return;
}
diff --git a/js/util/trellis-chart.js b/js/util/trellis-chart.js
new file mode 100644
index 0000000..139d663
--- /dev/null
+++ b/js/util/trellis-chart.js
@@ -0,0 +1,130 @@
+function trellisChart(anchor, monoGroups) {
+ /* attr -> {dim, group} key -> str amount, value -> { str, agi, vit, dex, int, luk } */
+
+ var attrs = monoGroups.map(function(d) { return d.name; });
+ var attrsIdByName = {};
+ monoGroups.forEach(function(d, i) { attrsIdByName[d.name] = i; });
+
+ var cellWidth = 5;
+ var radius = cellWidth / 2;
+ var subChartLength = 57;
+ var subChartUnpaddedLength = 50;
+ var subChartPadding = 7;
+
+ 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()) {
+ /* Make stuff! */
+ var svg = anchor.append("svg");
+ g = svg
+ .append("g");
+ attrs.forEach(function(d, i) {
+ g
+ .append("text")
+ .attr("transform", function(d) { return "translate(0," + ((attrs.length - i) * subChartLength + 10 - subChartLength / 2) + ")"; })
+ .text(d)
+ ;
+ g
+ .append("text")
+ .attr("transform", function(d) { return "translate(" + (i * subChartLength + 25 + 22) + "," + (attrs.length * subChartLength + 18) + ")"; })
+ .text(d)
+ ;
+ })
+ g = svg
+ .append("g")
+ .attr("transform", "translate(" + (margin.left + 25) + "," + (margin.top) + ")");
+ }
+ /* Group first into columns for each stat. We have one column for each of the stat monoGroups. */
+ /*
+ * monoGroups is an array of each stat dimension. We can consider each column to have data in the following format:
+ * { group: function, dim: function, name: stat }
+ */
+ var columns = g.selectAll(".column")
+ .data(monoGroups);
+ var colE = columns
+ .enter().append("g")
+ .attr("class", "column")
+ .attr("transform", function(d) { return "translate(" + (attrsIdByName[d.name] * subChartLength) + ",0)"; })
+ ;
+ colE
+ .append("line")
+ .attr("x1", -cellWidth)
+ .attr("x2", -cellWidth)
+ .attr("y1", -cellWidth)
+ .attr("y2", subChartLength * attrs.length - subChartPadding)
+ .attr("class", "border-line")
+ ;
+ colE
+ .append("line")
+ .attr("x1", subChartUnpaddedLength)
+ .attr("x2", subChartUnpaddedLength)
+ .attr("y1", -cellWidth)
+ .attr("y2", subChartLength * attrs.length - subChartPadding)
+ .attr("class", "border-line")
+ ;
+ /* Each stat has an array for its value. Group these to find the x position. */
+ /*
+ * The function transforms the data to take the grouping. We can consider each x position grouping to have data in the following format:
+ * { key: position, value: [{[stat] -> [y pos] -> count}] }
+ */
+ var colposg = columns.selectAll(".colpos")
+ .data(function(d, i) {
+// console.log("Incoming colposg format:", d, i, "Transformed to:", d.group.all().map(function(d2) { d2.name = d.name; return d2; }));
+ return d.group.all().map(function(d2) { d2.name = d.name; return d2; });
+ }, function(d) { return d.key; });
+ colposg
+ .enter().append("g")
+ .attr("class", "colpos")
+ .attr("transform", function(d) { return "translate(" + (d.key * cellWidth) + ",0)"; })
+ ;
+ /* Next, split up each x position grouping into its y stat grouping. */
+ /*
+ * We can consider each y stat grouping to have data in the following format:
+ * v[y pos] -> count; v.name -> name
+ */
+ var rows = colposg.selectAll(".row")
+ .data(function(d, i) {
+// console.log("Incoming row format:", d, i, "Transformed to:", attrs.map(function(d2) { return { name: d2, data: d.value[d2] }; }));
+ return attrs.map(function(d2) { return { name: d2, data: d.value[d2] }; });
+ });
+ rows
+ .enter().append("g")
+ .attr("class", "row")
+ .attr("transform", function(d) { return "translate(0," + ((attrs.length - attrsIdByName[d.name] - 1) * subChartLength) + ")"; })
+ ;
+ /* Finally, split up each y stat grouping into x. */
+ var vmax = 0;
+ var cells = rows.selectAll(".cell")
+ .data(function(d, i) {
+// console.log("Incoming cells format:", d, i, "Transformed to:", d.data);
+ return d.data;
+ });
+ cells
+ .enter().append("circle")
+ .attr("class", "cell")
+ .attr("r", radius)
+ ;
+ cells
+ .each(function(d) {
+ if (d > vmax) vmax = d;
+ })
+ ;
+ filler.domain([1, vmax + 1]);
+ cells
+ .attr("fill", function(d) {
+ return d ? d3.rgb(255 - filler(d + 1), 255 - filler(d + 1) / 2, 255 - filler(d + 1) / 3) : "white";
+ })
+ .attr("transform", function(d, i) { return "translate(0," + ((10-i) * cellWidth) + ")"; })
+ ;
+ cells
+ .exit()
+ .remove()
+ ;
+ }
+
+ return _chart;
+}