Created
November 27, 2017 20:37
-
-
Save bumbeishvili/d8f5f1cb6d0b25eb02c7ebb7def84975 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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