summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-05-27 15:25:46 +1200
committerFreeyorp <TheFreeYorp@NOSPAM.G.m.a.i.l.replace>2013-05-27 15:25:46 +1200
commit44fac3b8c3d4ea7db60943f43db63bf8b481e713 (patch)
tree6882cf6bf21bfb63601724fec5ea43db985948c6
parent751f746d817ce8059d87afbb365d3b340cad9d7c (diff)
downloadmanavis-44fac3b8c3d4ea7db60943f43db63bf8b481e713.tar.gz
manavis-44fac3b8c3d4ea7db60943f43db63bf8b481e713.tar.bz2
manavis-44fac3b8c3d4ea7db60943f43db63bf8b481e713.tar.xz
manavis-44fac3b8c3d4ea7db60943f43db63bf8b481e713.zip
Initial brushable trellis chart
Deselecting areas, applying filters, and broadcasting filters are not implemented yet.
-rw-r--r--public/css/style.css17
-rw-r--r--public/js/util/trellis-chart.js368
2 files changed, 255 insertions, 130 deletions
diff --git a/public/css/style.css b/public/css/style.css
index 4d7299e..cb3ee03 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -104,29 +104,38 @@ h3 {
/* Stat chart */
-#stat-chart .border-line {
+/* Border lines */
+#stat-chart .x-axis-dim-label line, #stat-chart .y-axis-dim-label line {
fill: none;
- stroke: #ccc;
opacity: .5;
shape-rendering: crispEdges;
}
+#stat-chart .x-axis-dim-label line {
+ stroke: #666;
+}
+#stat-chart .y-axis-dim-label line {
+ stroke: #bbb;
+}
+
+/* Text labels */
+/* Domain labels */
#stat-chart text {
font: 10px sans-serif;
}
-
#stat-chart .x-axis-dim-label text {
text-anchor: middle;
}
#stat-chart .y-axis-dim-label text {
text-anchor: end;
}
-
+/* Dimension labels, keeping a consistent style as axis labels */
#stat-chart text.dim-label, .axis-label {
font: 12px sans-serif;
font-weight: bold;
}
+/* Axis labels */
.axis-label {
height: 0;
width: 0;
diff --git a/public/js/util/trellis-chart.js b/public/js/util/trellis-chart.js
index 31cece6..1d24248 100644
--- a/public/js/util/trellis-chart.js
+++ b/public/js/util/trellis-chart.js
@@ -19,7 +19,10 @@ function trellisChart(anchor, monoGroups) {
/* 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;
@@ -27,136 +30,174 @@ function trellisChart(anchor, monoGroups) {
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()) {
- /* Make stuff! */
- var svg = anchor.append("svg").attr("height", 400).attr("width", 400).attr("class", "mv-chart");
- /* Group of dimension labels. */
- var dimLabelsG = svg
- .append("g")
- .attr("transform", "translate(" + (margin.left + labelPadding) + "," + (margin.top) + ")")
- ;
- var dimLabels;
- dimLabels = dimLabelsG.selectAll("g.y-axis-dim-label")
- .data(attrs)
- ;
- /* 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)
- ;
- /* 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)
- ;
- })
- ;
- dimLabels = dimLabelsG.selectAll("g.x-axis-dim-label")
- .data(attrs)
- ;
- 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)
- ;
- /* 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)
- ;
- })
- ;
- /* 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. */
- g = svg
- .append("g")
- .attr("transform", "translate(" + (margin.left + labelPadding + radius) + "," + (margin.top - radius) + ")");
+ 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._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 }
- */
- 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)"; })
- ;
+ * 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")
+ .each(function(d, i, j) { console.log("Created chart body", d, i, j); })
+ ;
+ }
+
+ 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 = columns.selectAll(".colpos")
+ * 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; });
@@ -168,9 +209,9 @@ function trellisChart(anchor, monoGroups) {
;
/* 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
- */
+ * 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] }; }));
@@ -211,6 +252,81 @@ function trellisChart(anchor, monoGroups) {
;
}
+ 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);
+
+ if (brushIsEmpty(extent, d.brush)) {
+ dc.events.trigger(function () {
+ _chart.filter(null);
+ dc.redrawAll();
+ });
+ } else {
+ dc.events.trigger(function () {
+ _chart.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) {
+ if (d.filter() && d.brush().empty())
+ d.brush.extent(d.filter());
+
+ var gBrush = d3.select(this).select("g.brush");
+ gBrush.call(d.brush.x(_scale));
+ gBrush.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: