diff --git a/lib/cie.js b/lib/cie.js new file mode 100644 index 0000000..45f0132 --- /dev/null +++ b/lib/cie.js @@ -0,0 +1,155 @@ +(function(d3) { + var cie = d3.cie = {}; + + cie.lab = function(l, a, b) { + return arguments.length === 1 + ? (l instanceof Lab ? lab(l.l, l.a, l.b) + : (l instanceof Lch ? lch_lab(l.l, l.c, l.h) + : rgb_lab((l = d3.rgb(l)).r, l.g, l.b))) + : lab(+l, +a, +b); + }; + + cie.lch = function(l, c, h) { + return arguments.length === 1 + ? (l instanceof Lch ? lch(l.l, l.c, l.h) + : (l instanceof Lab ? lab_lch(l.l, l.a, l.b) + : lab_lch((l = rgb_lab((l = d3.rgb(l)).r, l.g, l.b)).l, l.a, l.b))) + : lch(+l, +c, +h); + }; + + cie.interpolateLab = function(a, b) { + a = cie.lab(a); + b = cie.lab(b); + var al = a.l, + aa = a.a, + ab = a.b, + bl = b.l - al, + ba = b.a - aa, + bb = b.b - ab; + return function(t) { + return lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + ""; + }; + }; + + cie.interpolateLch = function(a, b) { + a = cie.lch(a); + b = cie.lch(b); + var al = a.l, + ac = a.c, + ah = a.h, + bl = b.l - al, + bc = b.c - ac, + bh = b.h - ah; + if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; // shortest path + return function(t) { + return lch_lab(al + bl * t, ac + bc * t, ah + bh * t) + ""; + }; + }; + + function lab(l, a, b) { + return new Lab(l, a, b); + } + + function Lab(l, a, b) { + this.l = l; + this.a = a; + this.b = b; + } + + Lab.prototype.brighter = function(k) { + return lab(Math.min(100, this.l + K * (arguments.length ? k : 1)), this.a, this.b); + }; + + Lab.prototype.darker = function(k) { + return lab(Math.max(0, this.l - K * (arguments.length ? k : 1)), this.a, this.b); + }; + + Lab.prototype.rgb = function() { + return lab_rgb(this.l, this.a, this.b); + }; + + Lab.prototype.toString = function() { + return this.rgb() + ""; + }; + + function lch(l, c, h) { + return new Lch(l, c, h); + } + + function Lch(l, c, h) { + this.l = l; + this.c = c; + this.h = h; + } + + Lch.prototype.brighter = function(k) { + return lch(Math.min(100, this.l + K * (arguments.length ? k : 1)), this.c, this.h); + }; + + Lch.prototype.darker = function(k) { + return lch(Math.max(0, this.l - K * (arguments.length ? k : 1)), this.c, this.h); + }; + + Lch.prototype.rgb = function() { + return lch_lab(this.l, this.c, this.h).rgb(); + }; + + Lch.prototype.toString = function() { + return this.rgb() + ""; + }; + + // Corresponds roughly to RGB brighter/darker + var K = 18; + + // D65 standard referent + var X = 0.950470, Y = 1, Z = 1.088830; + + function lab_rgb(l, a, b) { + var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200; + x = lab_xyz(x) * X; + y = lab_xyz(y) * Y; + z = lab_xyz(z) * Z; + return d3.rgb( + xyz_rgb( 3.2404542 * x - 1.5371385 * y - 0.4985314 * z), + xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z), + xyz_rgb( 0.0556434 * x - 0.2040259 * y + 1.0572252 * z) + ); + } + + function rgb_lab(r, g, b) { + r = rgb_xyz(r); + g = rgb_xyz(g); + b = rgb_xyz(b); + var x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / X), + y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / Y), + z = xyz_lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / Z); + return lab(116 * y - 16, 500 * (x - y), 200 * (y - z)); + } + + function lab_lch(l, a, b) { + var c = Math.sqrt(a * a + b * b), + h = Math.atan2(b, a) / Math.PI * 180; + return lch(l, c, h); + } + + function lch_lab(l, c, h) { + h = h * Math.PI / 180; + return lab(l, Math.cos(h) * c, Math.sin(h) * c); + } + + function lab_xyz(x) { + return x > 0.206893034 ? x * x * x : (x - 4 / 29) / 7.787037; + } + + function xyz_lab(x) { + return x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29; + } + + function xyz_rgb(r) { + return Math.round(255 * (r <= 0.00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - 0.055)); + } + + function rgb_xyz(r) { + return (r /= 255) <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4); + } +})(d3); diff --git a/lib/hive.js b/lib/hive.js new file mode 100644 index 0000000..06e53ae --- /dev/null +++ b/lib/hive.js @@ -0,0 +1,80 @@ +d3.hive = {}; + +d3.hive.link = function() { + var source = function(d) { return d.source; }, + target = function(d) { return d.target; }, + angle = function(d) { return d.angle; }, + startRadius = function(d) { return d.radius; }, + endRadius = startRadius, + arcOffset = -Math.PI / 2; + + function link(d, i) { + var s = node(source, this, d, i), + t = node(target, this, d, i), + x; + if (t.a < s.a) x = t, t = s, s = x; + if (t.a - s.a > Math.PI) s.a += 2 * Math.PI; + var a1 = s.a + (t.a - s.a) / 3, + a2 = t.a - (t.a - s.a) / 3; + return s.r0 - s.r1 || t.r0 - t.r1 + ? "M" + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0 + + "L" + Math.cos(s.a) * s.r1 + "," + Math.sin(s.a) * s.r1 + + "C" + Math.cos(a1) * s.r1 + "," + Math.sin(a1) * s.r1 + + " " + Math.cos(a2) * t.r1 + "," + Math.sin(a2) * t.r1 + + " " + Math.cos(t.a) * t.r1 + "," + Math.sin(t.a) * t.r1 + + "L" + Math.cos(t.a) * t.r0 + "," + Math.sin(t.a) * t.r0 + + "C" + Math.cos(a2) * t.r0 + "," + Math.sin(a2) * t.r0 + + " " + Math.cos(a1) * s.r0 + "," + Math.sin(a1) * s.r0 + + " " + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0 + : "M" + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0 + + "C" + Math.cos(a1) * s.r1 + "," + Math.sin(a1) * s.r1 + + " " + Math.cos(a2) * t.r1 + "," + Math.sin(a2) * t.r1 + + " " + Math.cos(t.a) * t.r1 + "," + Math.sin(t.a) * t.r1; + } + + function node(method, thiz, d, i) { + var node = method.call(thiz, d, i), + a = +(typeof angle === "function" ? angle.call(thiz, node, i) : angle) + arcOffset, + r0 = +(typeof startRadius === "function" ? startRadius.call(thiz, node, i) : startRadius), + r1 = (startRadius === endRadius ? r0 : +(typeof endRadius === "function" ? endRadius.call(thiz, node, i) : endRadius)); + return {r0: r0, r1: r1, a: a}; + } + + link.source = function(_) { + if (!arguments.length) return source; + source = _; + return link; + }; + + link.target = function(_) { + if (!arguments.length) return target; + target = _; + return link; + }; + + link.angle = function(_) { + if (!arguments.length) return angle; + angle = _; + return link; + }; + + link.radius = function(_) { + if (!arguments.length) return startRadius; + startRadius = endRadius = _; + return link; + }; + + link.startRadius = function(_) { + if (!arguments.length) return startRadius; + startRadius = _; + return link; + }; + + link.endRadius = function(_) { + if (!arguments.length) return endRadius; + endRadius = _; + return link; + }; + + return link; +}; diff --git a/lib/horizon.js b/lib/horizon.js new file mode 100644 index 0000000..d84c656 --- /dev/null +++ b/lib/horizon.js @@ -0,0 +1,192 @@ +(function() { + d3.horizon = function() { + var bands = 1, // between 1 and 5, typically + mode = "offset", // or mirror + interpolate = "linear", // or basis, monotone, step-before, etc. + x = d3_horizonX, + y = d3_horizonY, + w = 960, + h = 40, + duration = 0; + + var color = d3.scale.linear() + .domain([-1, 0, 1]) + .range(["#d62728", "#fff", "#1f77b4"]); + + // For each small multiple… + function horizon(g) { + g.each(function(d, i) { + var g = d3.select(this), + n = 2 * bands + 1, + xMin = Infinity, + xMax = -Infinity, + yMax = -Infinity, + x0, // old x-scale + y0, // old y-scale + id; // unique id for paths + + // Compute x- and y-values along with extents. + var data = d.map(function(d, i) { + var xv = x.call(this, d, i), + yv = y.call(this, d, i); + if (xv < xMin) xMin = xv; + if (xv > xMax) xMax = xv; + if (-yv > yMax) yMax = -yv; + if (yv > yMax) yMax = yv; + return [xv, yv]; + }); + + // Compute the new x- and y-scales, and transform. + var x1 = d3.scale.linear().domain([xMin, xMax]).range([0, w]), + y1 = d3.scale.linear().domain([0, yMax]).range([0, h * bands]), + t1 = d3_horizonTransform(bands, h, mode); + + // Retrieve the old scales, if this is an update. + if (this.__chart__) { + x0 = this.__chart__.x; + y0 = this.__chart__.y; + t0 = this.__chart__.t; + id = this.__chart__.id; + } else { + x0 = x1.copy(); + y0 = y1.copy(); + t0 = t1; + id = ++d3_horizonId; + } + + // We'll use a defs to store the area path and the clip path. + var defs = g.selectAll("defs") + .data([null]); + + // The clip path is a simple rect. + defs.enter().append("defs").append("clipPath") + .attr("id", "d3_horizon_clip" + id) + .append("rect") + .attr("width", w) + .attr("height", h); + + defs.select("rect").transition() + .duration(duration) + .attr("width", w) + .attr("height", h); + + // We'll use a container to clip all horizon layers at once. + g.selectAll("g") + .data([null]) + .enter().append("g") + .attr("clip-path", "url(#d3_horizon_clip" + id + ")"); + + // Instantiate each copy of the path with different transforms. + var path = g.select("g").selectAll("path") + .data(d3.range(-1, -bands - 1, -1).concat(d3.range(1, bands + 1)), Number); + + var d0 = d3_horizonArea + .interpolate(interpolate) + .x(function(d) { return x0(d[0]); }) + .y0(h * bands) + .y1(function(d) { return h * bands - y0(d[1]); }) + (data); + + var d1 = d3_horizonArea + .x(function(d) { return x1(d[0]); }) + .y1(function(d) { return h * bands - y1(d[1]); }) + (data); + + path.enter().append("path") + .style("fill", color) + .attr("transform", t0) + .attr("d", d0); + + path.transition() + .duration(duration) + .style("fill", color) + .attr("transform", t1) + .attr("d", d1); + + path.exit().transition() + .duration(duration) + .attr("transform", t1) + .attr("d", d1) + .remove(); + + // Stash the new scales. + this.__chart__ = {x: x1, y: y1, t: t1, id: id}; + }); + d3.timer.flush(); + } + + horizon.duration = function(x) { + if (!arguments.length) return duration; + duration = +x; + return horizon; + }; + + horizon.bands = function(x) { + if (!arguments.length) return bands; + bands = +x; + color.domain([-bands, 0, bands]); + return horizon; + }; + + horizon.mode = function(x) { + if (!arguments.length) return mode; + mode = x + ""; + return horizon; + }; + + horizon.colors = function(x) { + if (!arguments.length) return color.range(); + color.range(x); + return horizon; + }; + + horizon.interpolate = function(x) { + if (!arguments.length) return interpolate; + interpolate = x + ""; + return horizon; + }; + + horizon.x = function(z) { + if (!arguments.length) return x; + x = z; + return horizon; + }; + + horizon.y = function(z) { + if (!arguments.length) return y; + y = z; + return horizon; + }; + + horizon.width = function(x) { + if (!arguments.length) return w; + w = +x; + return horizon; + }; + + horizon.height = function(x) { + if (!arguments.length) return h; + h = +x; + return horizon; + }; + + return horizon; + }; + + var d3_horizonArea = d3.svg.area(), + d3_horizonId = 0; + + function d3_horizonX(d) { + return d[0]; + } + + function d3_horizonY(d) { + return d[1]; + } + + function d3_horizonTransform(bands, h, mode) { + return mode == "offset" + ? function(d) { return "translate(0," + (d + (d < 0) - bands) * h + ")"; } + : function(d) { return (d < 0 ? "scale(1,-1)" : "") + "translate(0," + (d - bands) * h + ")"; }; + } +})(); diff --git a/lib/sankey.js b/lib/sankey.js new file mode 100644 index 0000000..c3bc59f --- /dev/null +++ b/lib/sankey.js @@ -0,0 +1,292 @@ +d3.sankey = function() { + var sankey = {}, + nodeWidth = 24, + nodePadding = 8, + size = [1, 1], + nodes = [], + links = []; + + sankey.nodeWidth = function(_) { + if (!arguments.length) return nodeWidth; + nodeWidth = +_; + return sankey; + }; + + sankey.nodePadding = function(_) { + if (!arguments.length) return nodePadding; + nodePadding = +_; + return sankey; + }; + + sankey.nodes = function(_) { + if (!arguments.length) return nodes; + nodes = _; + return sankey; + }; + + sankey.links = function(_) { + if (!arguments.length) return links; + links = _; + return sankey; + }; + + sankey.size = function(_) { + if (!arguments.length) return size; + size = _; + return sankey; + }; + + sankey.layout = function(iterations) { + computeNodeLinks(); + computeNodeValues(); + computeNodeBreadths(); + computeNodeDepths(iterations); + computeLinkDepths(); + return sankey; + }; + + sankey.relayout = function() { + computeLinkDepths(); + return sankey; + }; + + sankey.link = function() { + var curvature = .5; + + function link(d) { + var x0 = d.source.x + d.source.dx, + x1 = d.target.x, + xi = d3.interpolateNumber(x0, x1), + x2 = xi(curvature), + x3 = xi(1 - curvature), + y0 = d.source.y + d.sy + d.dy / 2, + y1 = d.target.y + d.ty + d.dy / 2; + return "M" + x0 + "," + y0 + + "C" + x2 + "," + y0 + + " " + x3 + "," + y1 + + " " + x1 + "," + y1; + } + + link.curvature = function(_) { + if (!arguments.length) return curvature; + curvature = +_; + return link; + }; + + return link; + }; + + // Populate the sourceLinks and targetLinks for each node. + // Also, if the source and target are not objects, assume they are indices. + function computeNodeLinks() { + nodes.forEach(function(node) { + node.sourceLinks = []; + node.targetLinks = []; + }); + links.forEach(function(link) { + var source = link.source, + target = link.target; + if (typeof source === "number") source = link.source = nodes[link.source]; + if (typeof target === "number") target = link.target = nodes[link.target]; + source.sourceLinks.push(link); + target.targetLinks.push(link); + }); + } + + // Compute the value (size) of each node by summing the associated links. + function computeNodeValues() { + nodes.forEach(function(node) { + node.value = Math.max( + d3.sum(node.sourceLinks, value), + d3.sum(node.targetLinks, value) + ); + }); + } + + // Iteratively assign the breadth (x-position) for each node. + // Nodes are assigned the maximum breadth of incoming neighbors plus one; + // nodes with no incoming links are assigned breadth zero, while + // nodes with no outgoing links are assigned the maximum breadth. + function computeNodeBreadths() { + var remainingNodes = nodes, + nextNodes, + x = 0; + + while (remainingNodes.length) { + nextNodes = []; + remainingNodes.forEach(function(node) { + node.x = x; + node.dx = nodeWidth; + node.sourceLinks.forEach(function(link) { + nextNodes.push(link.target); + }); + }); + remainingNodes = nextNodes; + ++x; + } + + // + moveSinksRight(x); + scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); + } + + function moveSourcesRight() { + nodes.forEach(function(node) { + if (!node.targetLinks.length) { + node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1; + } + }); + } + + function moveSinksRight(x) { + nodes.forEach(function(node) { + if (!node.sourceLinks.length) { + node.x = x - 1; + } + }); + } + + function scaleNodeBreadths(kx) { + nodes.forEach(function(node) { + node.x *= kx; + }); + } + + function computeNodeDepths(iterations) { + var nodesByBreadth = d3.nest() + .key(function(d) { return d.x; }) + .sortKeys(d3.ascending) + .entries(nodes) + .map(function(d) { return d.values; }); + + // + initializeNodeDepth(); + resolveCollisions(); + for (var alpha = 1; iterations > 0; --iterations) { + relaxRightToLeft(alpha *= .99); + resolveCollisions(); + relaxLeftToRight(alpha); + resolveCollisions(); + } + + function initializeNodeDepth() { + var ky = d3.min(nodesByBreadth, function(nodes) { + return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); + }); + + nodesByBreadth.forEach(function(nodes) { + nodes.forEach(function(node, i) { + node.y = i; + node.dy = node.value * ky; + }); + }); + + links.forEach(function(link) { + link.dy = link.value * ky; + }); + } + + function relaxLeftToRight(alpha) { + nodesByBreadth.forEach(function(nodes, breadth) { + nodes.forEach(function(node) { + if (node.targetLinks.length) { + var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value); + node.y += (y - center(node)) * alpha; + } + }); + }); + + function weightedSource(link) { + return center(link.source) * link.value; + } + } + + function relaxRightToLeft(alpha) { + nodesByBreadth.slice().reverse().forEach(function(nodes) { + nodes.forEach(function(node) { + if (node.sourceLinks.length) { + var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value); + node.y += (y - center(node)) * alpha; + } + }); + }); + + function weightedTarget(link) { + return center(link.target) * link.value; + } + } + + function resolveCollisions() { + nodesByBreadth.forEach(function(nodes) { + var node, + dy, + y0 = 0, + n = nodes.length, + i; + + // Push any overlapping nodes down. + nodes.sort(ascendingDepth); + for (i = 0; i < n; ++i) { + node = nodes[i]; + dy = y0 - node.y; + if (dy > 0) node.y += dy; + y0 = node.y + node.dy + nodePadding; + } + + // If the bottommost node goes outside the bounds, push it back up. + dy = y0 - nodePadding - size[1]; + if (dy > 0) { + y0 = node.y -= dy; + + // Push any overlapping nodes back up. + for (i = n - 2; i >= 0; --i) { + node = nodes[i]; + dy = node.y + node.dy + nodePadding - y0; + if (dy > 0) node.y -= dy; + y0 = node.y; + } + } + }); + } + + function ascendingDepth(a, b) { + return a.y - b.y; + } + } + + function computeLinkDepths() { + nodes.forEach(function(node) { + node.sourceLinks.sort(ascendingTargetDepth); + node.targetLinks.sort(ascendingSourceDepth); + }); + nodes.forEach(function(node) { + var sy = 0, ty = 0; + node.sourceLinks.forEach(function(link) { + link.sy = sy; + sy += link.dy; + }); + node.targetLinks.forEach(function(link) { + link.ty = ty; + ty += link.dy; + }); + }); + + function ascendingSourceDepth(a, b) { + return a.source.y - b.source.y; + } + + function ascendingTargetDepth(a, b) { + return a.target.y - b.target.y; + } + } + + function center(node) { + return node.y + node.dy / 2; + } + + function value(link) { + return link.value; + } + + return sankey; +};