Skip to content

Instantly share code, notes, and snippets.

@bumbeishvili
Created November 27, 2017 20:37
Show Gist options
  • Save bumbeishvili/d8f5f1cb6d0b25eb02c7ebb7def84975 to your computer and use it in GitHub Desktop.
Save bumbeishvili/d8f5f1cb6d0b25eb02c7ebb7def84975 to your computer and use it in GitHub Desktop.
function renderPathAnalysis(params) {
// exposed variables
var attrs = {
svgWidth: 400,
svgHeight: 400,
marginTop: 10,
marginBottom: 5,
marginRight: 5,
marginLeft: 5,
container: 'body',
nodePadding: 5,
data: null,
node: {
width: 180,
height: 50
},
nodeHeightInfo: [{
index: 0,
rectHeight: 30
}]
};
/*############### IF EXISTS OVERWRITE ATTRIBUTES FROM PASSED PARAM ####### */
var attrKeys = Object.keys(attrs);
attrKeys.forEach(function (key) {
if (params && params[key]) {
attrs[key] = params[key];
}
})
//innerFunctions which will update visuals
var updateData;
//main chart object
var main = function (selection) {
selection.each(function scope() {
//calculated properties
var calc = {}
calc.chartLeftMargin = attrs.marginLeft;
calc.chartTopMargin = attrs.marginTop;
calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin;
calc.chartHeight = attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin;
//drawing containers
var container = d3.select(this);
// remove earlier svg if existss...
container.selectAll('.svg-chart-container').remove();
//add svg
var svg = container.patternify({ tag: 'svg', selector: 'svg-chart-container' })
.attr('width', attrs.svgWidth)
.attr('height', attrs.svgHeight)
// .attr("viewBox", "0 0 " + attrs.svgWidth + " " + attrs.svgHeight)
// .attr("preserveAspectRatio", "xMidYMid meet")
//add container g element
var chart = svg.patternify({ tag: 'g', selector: 'chart' })
.attr('transform', 'translate(' + (calc.chartLeftMargin) + ',' + calc.chartTopMargin + ')');
//######################### LAYOUTS ###############
var layouts = {};
layouts.sankey = d3.sankeyC()
.nodeWidth(attrs.node.width + 20)
.nodePadding(attrs.nodePadding)
// Roo removeed this as it is not registered on ajax refresh
.extent([[1, 1], [calc.chartWidth - 51, calc.chartHeight - 6]])
;
//########################### DRAWINGS #################
//links wrapper
var linkWrapper = chart.append("g")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
//nodes wrapper
var nodesWrapper = chart.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", '9pt')
//mocked parameters
var width = calc.chartWidth;
var height = calc.chartHeight;
//var sankey = layouts.sankey;
d3.csv(attrs['data'], function (error, rawData) {
if (error) throw error;
// transforming .csv data into node & links dataset
var data = { nodes: null, links: [] };
//creating nodes
data.nodes = rawData.map(function (d, i) {
return {
id: ("id" + i), name: translateVisStrings(d[''])
}
})
//creating links
rawData.forEach(function (row, i) {
console.log(Object.keys(row))
var keys = Object.keys(row).filter(function (d) {
// Countries under 1000000000
return row[d] != '';
});
console.log(keys)
keys.forEach(function (key, j) {
if (!row[key] && row[key] != '') {
return;
}
var content = row[key].trim()
if (content != 'x' && content != '') {
// data.links.push({ source: i, target: j, content: content, value: 1, id: "id" + i + j })
data.links.push({ source: i, target: j, content: content, value: 1, id: "id" + i + "id" + j })
}
})
});
// Roo if you apply an if row not undefined on line 112 sankey gets snakey not a function error
// applying sankey transformation to the data
layouts.sankey(data);
// counting nodes per depth with help of sankey calculations
var depthCounts = { '0': 0 };
data.nodes.forEach(function (d) {
if (!depthCounts[d.depth]) {
depthCounts[d.depth] = 0;
}
depthCounts[d.depth]++;
})
//finding out which depth has max nodes
var maxNodesPerDepth = d3.max(Object.keys(depthCounts), function (d) { return depthCounts[d] });
//calculating maximum size svg can get based on node counts
var overallSize = maxNodesPerDepth * (attrs.node.height + attrs.nodePadding);
// making sankey accept new dimension
layouts.sankey.extent([[1, 1], [calc.chartWidth - 51, overallSize]]);
//setting new svg height
svg.attr('height', Math.max(attrs.svgHeight, overallSize));
//recalculating sankey data
layouts.sankey(data);
//manually setting node positions
Object.keys(depthCounts).forEach(function (depth) {
// get nodes with same depth
var depthNodes = data.nodes.filter(function (d) { return d.depth == depth });
depthNodes.orderBy(function (d) { return d.y0 });
var minY = depthNodes[0].y0;
var maxY = depthNodes[depthNodes.length - 1].y1;
var diff = maxY - minY;
var height = diff / depthNodes.length;
//overrider sankey calculated values
depthNodes.forEach(function (d, i) {
d.y0 = height * i + minY;
d.y1 = attrs.node.height + d.y0;
})
})
//manually overriding link positions
data.links.forEach(normalizeLinkPosition);
// link paths
var links = linkWrapper
.selectAll("path")
.data(data.links)
.enter().append("path")
.attr("d", d3.sankeyLinkHorizontal())
//.attr("stroke-width", 2)
.attr("stroke-width", function (d) {
return ((Math.abs(d.content) * 20))
})
.attr('stroke', function (d) {
return (+d.content) < 0 ? "#FB1C90" : "#1DA6AC"
})
//node groups
var nodes = nodesWrapper.selectAll("g")
.data(data.nodes)
.enter()
.append("g")
.style('cursor', 'pointer');
//node rects
nodes.append("rect")
.attr("x", function (d) { return d.x0; })
.attr("y", function (d) { return d.y0 - attrs.node.height;; })
.attr("height", function (d) { return attrs.node.height; })
.attr("width", function (d) { return attrs.node.width + 20; })
.attr("fill", function (d) { return '#e3e3e3' })
.attr("stroke", "#d3d3d3")
.attr("rx", 2);
//node texts
var texts = nodes.append("text")
.attr("class", "node-txt")
.attr("dy", "0.45em")
.attr("text-anchor", "middle")
.attr('alignment-baseline', 'middle')
.text(function (d) {
return d.name.trim();
})
//modify rect height and y cord depend on text height
nodes.selectAll("rect")
.attr("y", function (d) {
return (d.y1 + d.y0) / 2 - attrs.node.height / 2;
})
texts.attr("x", function (d) { return (d.x0 + d.x1) / 2 - 6; })
.attr("y", function (d) { return (d.y1 + d.y0) / 2 + (d.name.length > 30 ? 10 : 17) - attrs.node.height / 2 })
.attr('alignment-baseline', 'middle')
.each(function (d) {
wrap(this, d);
});
nodes.on('click', function (d) {
//reset
nodes.attr('opacity', 1).attr('filter', 'none');
links.attr('opacity', 1).attr("stroke-opacity", 0.2).attr('filter', 'none');
var linkedNodeIds = [].concat(d.targetLinks.map(function (d) { return t.source }), d.sourceLinks.map(function (s) { return s.target }), [{ id: d.id }]).map(function (r) { return r.id });
var linkIDs = [].concat(d.targetLinks, d.sourceLinks).map(function (d) { return r.id });
nodes.filter(function (v) { return !contains(linkedNodeIds, v.id) }).attr('opacity', 0.1);
links.attr("stroke-opacity", 1).filter(function (v) { return !contains(linkIDs, v.id) }).attr('opacity', 0.1);
})
})
// smoothly handle data updating
updateData = function () {
}
//######################################### UTIL FUNCS ##################################
function contains(array, element) {
return array.indexOf(element) > -1;
}
function normalizeLinkPosition(link) {
var avgSource = d3.mean([link.source.y0, link.source.y1]);
var avgTarget = d3.mean([link.target.y0, link.target.y1]);
link.y0 = avgSource;
link.y1 = avgTarget;
}
function wrap(element, data) {
var text = d3.select(element)
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("class", "text-ts-span").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > attrs.node.width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("class", "text-ts-span").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
var rectHeight = text.node().getBBox().height + 5;
attrs.nodeHeightInfo.push({
index: data.index,
rectHeight: rectHeight
})
});
}
function debug() {
if (attrs.isDebug) {
//stringify func
var stringified = scope + "";
// parse variable names
var groupVariables = stringified
//match var x-xx= {};
.match(/var\s+([\w])+\s*=\s*{\s*}/gi)
//match xxx
.map(function (d) { return d.match(/\s+\w*/gi).filter(function (s) { return s.trim() }) })
//get xxx
.map(function (v) { return v[0].trim() })
//assign local variables to the scope
groupVariables.forEach(function (v) {
main['P_' + v] = eval(v)
})
}
}
debug();
});
};
//----------- PROTOTYPE FUNCTIONS ----------------------
Array.prototype.orderBy = function (func) {
this.sort(function (a, b) {
var a = func(a);
var b = func(b);
if (typeof a === 'string' || a instanceof String) {
return a.localeCompare(b);
}
return a - b;
});
return this;
}
d3.selection.prototype.patternify = function (params) {
var container = this;
var selector = params.selector;
var elementTag = params.tag;
var data = params.data || [selector];
// pattern in action
var selection = container.selectAll('.' + selector).data(data)
selection.exit().remove();
selection = selection.enter().append(elementTag).merge(selection)
selection.attr('class', selector);
return selection;
}
//dinamic keys functions
Object.keys(attrs).forEach(function (key) {
// Attach variables to main function
return main[key] = function (_) {
var string = "attrs['" + key + "'] = _";
if (!arguments.length) { return eval("attrs[' " + { key } + "'];"); }
eval(string);
return main;
};
});
//set attrs as property
main.attrs = attrs;
//debugging visuals
main.debug = function (isDebug) {
attrs.isDebug = isDebug;
if (isDebug) {
if (!window.charts) window.charts = [];
window.charts.push(main);
}
return main;
}
//exposed update functions
main.data = function (value) {
if (!arguments.length) return attrs.data;
attrs.data = value;
if (typeof updateData === 'function') {
updateData();
}
return main;
}
// run visual
main.run = function () {
d3.selectAll(attrs.container).call(main);
return main;
}
return main;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment