"use strict"; function trellisChart(anchor, monoGroups) { /* * FIXME: There is a lot of hardcoding going on in here. */ /* attr -> {dim, group} key -> str amount, value -> { str, agi, vit, dex, int, luk } */ /* Array of all attribute names */ var attrs = monoGroups.map(function(d) { return d.name; }); var attrsIdByName = {}; monoGroups.forEach(function(d, i) { attrsIdByName[d.name] = i; }); /* Parameters. FIXME: Tightly coupled. */ var cellWidth = 5; var radius = cellWidth / 2; var subChartLength = 57; var subChartUnpaddedLength = 50; var subChartPadding = 7; var chartLen = subChartLength * attrs.length - subChartPadding; /* Padding in pixels to pad the left side of the chart with to make room for the labels. */ var labelPadding = 25; var filler = d3.scale.log().domain([1, 2]).range([0, 255]); /* FIXME: Screwy domain due to attr rounding */ var domainMin = 0, domainMax = 100; /* By definition of a trellis chart, the same scale must be used for all subcharts. */ var _scale = d3.scale.linear().domain([0, 10]).range([0, subChartUnpaddedLength]); var attrGroups = 10; var axisLabelHeight = 9; var margin = {top: 10, right: 10, bottom: 20, left: 10}; var anchor = d3.select(anchor); var g = anchor.select("g"); /* Main columns, one for each attr */ var columns; var colBodies; var _chart = function() { if (g.empty()) { renderBase(); } colBodies.each(redrawCells); } function renderBase() { /* Make stuff! */ anchor.attr("class", "dc-chart"); var svg = anchor.append("svg").attr("height", 400).attr("width", 400).attr("class", "mv-chart"); /* Group of dimension labels. */ /* Group of subcharts. */ /* Adjust the translation by the radius of each circle - the origin of each circle is the middle. */ /* This makes their starting position consistent with the rest of the graph. */ /* Create this container first, so that the labels go over the top. */ g = svg .append("g") .attr("transform", "translate(" + (margin.left + labelPadding + radius) + "," + (margin.top - radius) + ")") ; /* Label container. */ var dimLabelsG = svg .append("g") .attr("transform", "translate(" + (margin.left + labelPadding) + "," + (margin.top) + ")") ; var dimLabels; /* Y axis labels and horizontal lines */ dimLabels = dimLabelsG.selectAll("g.y-axis-dim-label") .data(monoGroups) ; /* Each label is shifted downwards by its distance from the origin. */ /* Since the origin (0,0) in svg differs from the origin (0,0) in our charts, we must go backwards down the y axis here. */ /* One instance of padding must be removed from this offset to get the correct distance, since this is working from the back. */ dimLabels.enter().append("g").attr("class", "y-axis-dim-label") .attr("transform", function(d, i) { return "translate(0," + ((attrs.length - i) * subChartLength - subChartPadding) + ")"; }) .each(function(d, i) { var t = d3.select(this); /* The -8 here is an insignificant offset to distinguish the dimension label from the domain labels. */ t .append("text") .attr("transform", "translate(-8," + (-subChartUnpaddedLength / 2 + axisLabelHeight / 2) + ")") .attr("class", "dim-label") .text(d.name) ; /* Top subchart separators */ t .append("line") .attr("x1", 0) .attr("x2", chartLen) .attr("y1", 0) .attr("y2", 0) .attr("class", "border-line") ; /* Bottom subchart separators */ t .append("line") .attr("x1", 0) .attr("x2", chartLen) .attr("y1", -subChartUnpaddedLength) .attr("y2", -subChartUnpaddedLength) .attr("class", "border-line") ; /* Y Domain markers */ t .append("text") .attr("transform", "translate(0," + (-subChartUnpaddedLength + axisLabelHeight) + ")") .text(domainMax) ; t .append("text") .attr("transform", "translate(0,0)") .text(domainMin) ; }) ; /* X axis labels and vertical lines */ dimLabels = dimLabelsG.selectAll("g.x-axis-dim-label") .data(monoGroups) ; dimLabels.enter().append("g").attr("class", "x-axis-dim-label") .attr("transform", function(d, i) { return "translate(" + (i * subChartLength) + "," + chartLen + ")"; }) .each(function(d, i) { var t = d3.select(this); /* The dimension label is placed at a y level directly after the domain labels. */ t .append("text") .attr("transform", "translate("+ (subChartUnpaddedLength / 2) + "," + (axisLabelHeight * 2) + ")") .attr("class", "dim-label") .text(d.name) ; /* Left subchart separators */ t .append("line") .attr("x1", 0) .attr("x2", 0) .attr("y1", 0) .attr("y2", -chartLen) .attr("class", "border-line") ; /* Right subchart separators */ t .append("line") .attr("x1", subChartUnpaddedLength) .attr("x2", subChartUnpaddedLength) .attr("y1", 0) .attr("y2", -chartLen) .attr("class", "border-line") ; /* X Domain markers */ t .append("text") .attr("transform", "translate(" + subChartUnpaddedLength + "," + axisLabelHeight + ")") .style("text-anchor", "end") .text(domainMax) ; t .append("text") .attr("transform", "translate(0," + axisLabelHeight + ")") .style("text-anchor", "start") .text(domainMin) ; }) ; dimLabelsG.selectAll("g.brush-container") .data(monoGroups) .enter().append("g").attr("class", "brush-container") .attr("transform", function(d, i) { return "translate(" + (i * subChartLength) + ",0)"; }) .each(function (d, i) { d.id = i; d.brush = d3.svg.brush(); d.filter = function(_) { if (!arguments.length) return d._filter; d.dim.filter(_); d._filter = _; return d; }; }) .each(renderBrush) ; /* Group first into columns for each stat. We have one column for each of the stat monoGroups. */ /* Columns only need to be created once; one for each monoGroup. The top level monoGroups shouldn't change after rendering, anyway. */ /* * 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 } */ columns = g.selectAll(".column") .data(monoGroups); var colE = columns.enter().append("g").attr("class", "column") .attr("transform", function(d) { return "translate(" + (d.id * subChartLength) + ",0)"; }) ; colBodies = columns.selectAll(".chartBody") .data(function(d) { return [d]; }).enter().append("g") .attr("class", "chartBody") ; } function redrawCells() { /* 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 = d3.select(this).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," + ((attrGroups-i) * cellWidth) + ")"; }) ; cells .exit() .remove() ; } function renderBrush(d, i) { var columnG = d3.select(this); var brushG = columnG.select(".brush"); d.brush .on("brushstart", function() {}) .on("brush", function () { var extent = extendBrush(d, brushG); redrawBrush(d, i, brushG); if (brushIsEmpty(extent, d.brush)) { dc.events.trigger(function () { d.filter(null); dc.redrawAll(); }); } else { dc.events.trigger(function () { d.filter([extent[0], extent[1]]); dc.redrawAll(); }, dc.constants.EVENT_DELAY); } }) .on("brushend", function() {}) ; if (brushG.empty()) { brushG = columnG.append("g").attr("class", "brush") .call(d.brush.x(_scale)) ; brushG.selectAll("rect").attr("height", chartLen); brushG.selectAll(".resize").append("path").attr("d", resizeHandlePath); } } function redrawBrush(d, i, brushG) { if (d.filter() && d.brush.empty()) d.brush.extent(d.filter()); brushG.call(d.brush.x(_scale)); brushG.selectAll("rect").attr("height", chartLen); // TODO: fade the deselected area } // FIXME: Consistent interface _chart.round = Math.round; function extendBrush(d, brushG) { var extent = d.brush.extent(); if (_chart.round) { extent = extent.map(_chart.round); brushG.call(d.brush.extent(extent)); } return extent; }; function brushIsEmpty(extent, brush) { return brush.empty() || !extent || extent[1] <= extent[0]; }; function resizeHandlePath(d) { var e = +(d == "e"), x = e ? 1 : -1, y = chartLen / 3; return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) + "M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8); } _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. */ if (!arguments.length) { return null; } return _chart; } return _chart; }