(function() { 'use strict'; var module = angular.module('redash.visualization'); module.directive('sankeyRenderer', function() { return { restrict: 'E', link: function(scope, element) { var refreshData = function() { var queryData = scope.queryResult.getData(); if (queryData) { // do the render logic. angular.element(element[0]).empty(); createSankey(element[0], scope.visualization.options.height, queryData); } }; angular.element(window).on("resize", refreshData); scope.$watch("queryResult && queryResult.getData()", refreshData); scope.$watch('visualization.options.height', function(oldValue, newValue) { if (oldValue !== newValue) { refreshData(); } }); } } }); module.directive('sankeyEditor', function() { return { restrict: 'E', templateUrl: '/views/visualizations/sankey_editor.html' } }); module.config(['VisualizationProvider', function(VisualizationProvider) { var renderTemplate = ''; var editTemplate = ''; var defaultOptions = { height: 300 }; VisualizationProvider.registerVisualization({ type: 'SANKEY', name: 'Sankey', renderTemplate: renderTemplate, editorTemplate: editTemplate, defaultOptions: defaultOptions }); } ]); function createSankey(element, height, data) { var margin = {top: 10, right: 10, bottom: 10, left: 10}, width = $(element).parent().width() - margin.left - margin.right, height = height - margin.top - margin.bottom; data = graph(data); var formatNumber = d3.format(",.0f"); // zero decimal places var format = function(d) { return formatNumber(d); }; var color = d3.scale.category20(); // append the svg canvas to the page var svg = d3.select(element).append("svg") .attr("class", "sankey") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // Set the sankey diagram properties var sankey = d3.sankey() .nodeWidth(15) .nodePadding(10) .size([width, height]); var path = sankey.link(); sankey .nodes(data.nodes) .links(data.links) .layout(0); spreadNodes(height, data); sankey.relayout(); // add in the links var link = svg.append("g").selectAll(".link") .data(data.links) .enter().append("path") .filter(function(link) { return link.target.name != 'Exit'; }) .attr("class", "link") .attr("d", path) .style("stroke-width", function(d) { return Math.max(1, d.dy); }) .sort(function(a, b) { return b.dy - a.dy; }); // add the link titles link.append("title") .text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); }); // add in the nodes var node = svg.append("g").selectAll(".node") .data(data.nodes) .enter().append("g") .filter(function(node) { return node.name != 'Exit'; }) .attr("class", "node") .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) .on("mouseover", nodeMouseOver) .on("mouseout", nodeMouseOut); // add the rectangles for the nodes node.append("rect") .attr("height", function(d) { return d.dy; }) .attr("width", sankey.nodeWidth()) .style("fill", function(d) { return d.color = color(d.name.replace(/ .*/, "")); }) .style("stroke", function(d) { return d3.rgb(d.color).darker(2); }) .append("title").text(function(d) { return d.name + "\n" + format(d.value); }); // add in the title for the nodes node.append("text") .attr("x", -6) .attr("y", function(d) { return d.dy / 2; }) .attr("dy", ".35em") .attr("text-anchor", "end") .attr("transform", null) .text(function(d) { return d.name; }) .filter(function(d) { return d.x < width / 2; }) .attr("x", 6 + sankey.nodeWidth()) .attr("text-anchor", "start"); function nodeMouseOver(currentNode) { var nodes = getConnectedNodes(currentNode); nodes = _.pluck(nodes, 'id'); node.filter(function(d) { if (d === currentNode) { return false; } if (_.contains(nodes, d.id)) { return false; } return true; }).style('opacity', 0.2); link.filter(function(l) { return !(_.include(currentNode.sourceLinks, l) || _.include(currentNode.targetLinks, l)); }).style('opacity', 0.2); } function nodeMouseOut(currentNode) { node.style('opacity', 1); link.style('opacity', 1); } function spreadNodes(height, data) { var nodesByBreadth = d3.nest() .key(function(d) { return d.x; }) .entries(data.nodes) .map(function(d) { return d.values; }); nodesByBreadth.forEach(function(nodes) { nodes = _.filter(_.sortBy(nodes, function(node) { return -node.value; }), function(node) { return node.name !== 'Exit'; }); var sum = d3.sum(nodes, function(o) { return o.dy; }); var padding = (height - sum) / nodes.length; _.reduce(nodes, function(y0, node) { node.y = y0; return y0 + node.dy + padding; }, 0); }); } function getConnectedNodes(node) { // source link = this node is the source, I need the targets var nodes = []; _.each(node.sourceLinks, function(link) { nodes.push(link.target); }); _.each(node.targetLinks, function(link) { nodes.push(link.source); }); return nodes; } function graph(data) { var nodesDict = {}; var links = {}; var nodes = []; var keys = _.sortBy(_.without(_.keys(data[0]), 'value'), _.identity); data.forEach(function(row) { addLink(row[keys[0]], row[keys[1]], row.value, 1); addLink(row[keys[1]], row[keys[2]], row.value, 2); addLink(row[keys[2]], row[keys[3]], row.value, 3); addLink(row[keys[3]], row[keys[4]], row.value, 4); }); return {nodes: nodes, links: _.values(links)}; function normalizeName(name) { if (name) { return name; } return 'Exit'; } function getNode(name, level) { name = normalizeName(name); var key = name + ":" + String(level); var node = nodesDict[key]; if (!node) { node = {name: name}; var id = nodes.push(node) - 1; node.id = id; nodesDict[key] = node; } return node; } function getLink(source, target) { var link = links[[source, target]]; if (!link) { link = {target: target, source: source, value: 0}; links[[source, target]] = link; } return link; } function addLink(sourceName, targetName, value, depth) { if ((sourceName === '' || !sourceName) && depth > 1) { return; } var source = getNode(sourceName, depth); var target = getNode(targetName, depth+1); var link = getLink(source.id, target.id); link.value += parseInt(value); } } } })();