From a7da1fafe2ca8c9115ce76f99a903e1b3da692a3 Mon Sep 17 00:00:00 2001 From: Freeyorp Date: Tue, 23 Apr 2013 15:26:48 +1200 Subject: 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. --- css/style.css | 9 ++++ index.html | 22 +++++--- js/dc | 2 +- js/mv/chart.js | 3 ++ js/mv/heap.js | 40 ++++++++++++--- js/mv/parse.js | 18 +++++-- js/util/trellis-chart.js | 130 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 js/util/trellis-chart.js 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 @@
-

Experience gain instances by Character Base Level

+

Instance breakdown by Character Base Level

+
+

Instance breakdown by Stat allocation

+
+
+
+
+
-

Experience gain instances by Type

+

Instance breakdown by Type

-

Experience gain instances by Target

+

Instance breakdown by Target

-

Experience gain instances by Weapon

+

Instance breakdown by Weapon

Definedness of records [?]

@@ -44,13 +51,13 @@
-

Experience gain by Map [?]

+

Breakdown by Map [?]

-

Experience gain instances by Date

+

Instance breakdown by Date

-

Experience gain instances by Character ID [?]

+

Instance breakdown by Character ID [?]

@@ -73,3 +80,4 @@ + diff --git a/js/dc b/js/dc index 9a243a5..c8eab4e 160000 --- a/js/dc +++ b/js/dc @@ -1 +1 @@ -Subproject commit 9a243a5d684aafc3239ed06dc5a9bb15ac4aa941 +Subproject commit c8eab4e79abd32c4f7b42a20b16a18ba4af2967d 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; +} -- cgit v1.2.3-60-g2f50