I am using D3 v4 to build a tree.
Fiddle:
https://jsfiddle.net/a6pLqpxw/
I am now trying to add support for dynamically adding (and removing) children from a selected node.
However I cannot get the chart to redraw without having to perform a complete redraw. I have modified the code from the collapsible tree diagram code at: https://bl.ocks.org/d3noob/43a860bc0024792f8803bba8ca0d5ecd
Specifically the following block does not perform a recalculation of the layout for its children.
document.getElementById('add-child').onclick = function() {
console.log(selected);
selected.children.push({
type: 'resource-delete',
name: new Date().getTime(),
attributes: [],
children: []
});
update(selected);
};
Does anyone have any good examples of dynamically adding/removing nodes to tree's in D3.js v4?
I came up with this solution for Adding new Node dynamically to D3 Tree v4. .
D3 v4 tree requires Nodes.
Create Nodes from your tree data (json) using d3.hierarchy(..) and pushed it into it's parent.children array and update the tree.
Code Snippet
//Adding a new node (as a child) to selected Node (code snippet)
var newNode = {
type: 'node-type',
name: new Date().getTime(),
children: []
};
//Creates a Node from newNode object using d3.hierarchy(.)
var newNode = d3.hierarchy(newNode);
//later added some properties to Node like child,parent,depth
newNode.depth = selected.depth + 1;
newNode.height = selected.height - 1;
newNode.parent = selected;
newNode.id = Date.now();
//Selected is a node, to which we are adding the new node as a child
//If no child array, create an empty array
if(!selected.children){
selected.children = [];
selected.data.children = [];
}
//Push it to parent.children array
selected.children.push(newNode);
selected.data.children.push(newNode.data);
//Update tree
update(selected);
Fiddle
// ### DATA MODEL START
var data = {
type: 'action',
name: '1',
attributes: [],
children: [{
type: 'children',
name: '2',
attributes: [{
'source-type-property-value': 'streetlight'
}],
children: [{
type: 'parents',
name: '3',
attributes: [{
'source-type-property-value': 'cable'
}],
children: [{
type: 'resource-delete',
name: '4',
attributes: [],
children: []
}]
}, {
type: 'children',
name: '5',
attributes: [{
'source-type-property-value': 'lantern'
}],
children: []
}]
}]
};
// ### DATA MODEL END
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").
append("svg").
attr("width", width + margin.right + margin.left).
attr("height", height + margin.top + margin.bottom).
append("g").
attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var i = 0, duration = 750, root;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(data, function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
update(root);
var selected = null;
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d){
d.y = d.depth * 180
});
// ### LINKS
// Update the links...
var link = svg.selectAll('line.link').
data(links, function(d) {
return d.id;
});
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().
append('line').
attr("class", "link").
attr("stroke-width", 2).
attr("stroke", 'black').
attr('x1', function(d) {
return source.y0;
}).
attr('y1', function(d) {
return source.x0;
}).
attr('x2', function(d) {
return source.y0;
}).
attr('y2', function(d) {
return source.x0;
});
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition().
duration(duration).
attr('x1', function(d) {
return d.parent.y;
}).
attr('y1', function(d) {
return d.parent.x;
}).
attr('x2', function(d) {
return d.y;
}).
attr('y2', function(d) {
return d.x;
});
// Transition back to the parent element position
linkUpdate.transition().
duration(duration).
attr('x1', function(d) {
return d.parent.y;
}).
attr('y1', function(d) {
return d.parent.x;
}).
attr('x2', function(d) {
return d.y;
}).
attr('y2', function(d) {
return d.x;
});
// Remove any exiting links
var linkExit = link.exit().
transition().
duration(duration).
attr('x1', function(d) {
return source.x;
}).
attr('y1', function(d) {
return source.y;
}).
attr('x2', function(d) {
return source.x;
}).
attr('y2', function(d) {
return source.y;
}).
remove();
// ### CIRCLES
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().
append('g').
attr('class', 'node').
attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
}).
on('click', click);
// Add Circle for the nodes
nodeEnter.append('circle').
attr('class', 'node').
attr('r', 25).
style("fill", function(d) {
return "#0e4677";
});
// Update
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition().
duration(duration).
attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node').
attr('r', 25).
style("fill", function(d) {
return "#0e4677";
}).
attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().
transition().
duration(duration).
attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
}).
remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle').attr('r', 0);
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// Toggle children on click.
function click(d) {
selected = d;
document.getElementById('add-child').disabled = false;
document.getElementById('remove').disabled = false;
update(d);
}
}
document.getElementById('add-child').onclick = function() {
//creates New OBJECT
var newNodeObj = {
type: 'resource-delete',
name: new Date().getTime(),
attributes: [],
children: []
};
//Creates new Node
var newNode = d3.hierarchy(newNodeObj);
newNode.depth = selected.depth + 1;
newNode.height = selected.height - 1;
newNode.parent = selected;
newNode.id = Date.now();
if(!selected.children){
selected.children = [];
selected.data.children = [];
}
selected.children.push(newNode);
selected.data.children.push(newNode.data);
update(selected);
};
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id="add-child" disabled="disabled">Add Child</button>
The height calculation in the accepted answer fails to update the ancestors of the created node. This means, for example, that the height of the root will never increase, even as many children are added.
The following code fixes these problems:
function insert(par, data) {
let newNode = d3.hierarchy(data);
newNode.depth = par.depth + 1;
newNode.parent = par;
// Walk up the tree, updating the heights of ancestors as needed.
for(let height = 1, anc = par; anc != null; height++, anc=anc.parent) {
anc.height = Math.max(anc.height, height);
}
if (!par.data.children) {
par.children = [];
par.data.children = [];
}
par.children.push(newNode);
par.data.children.push(newNode.data);
}
It should be noted that the d3.tree layout algorithm doesn't actually use the height parameter, which is probably why it wasn't noted before.
If we take this route of "minimum code that makes the code work", we can also get rid of the par.data update and just use:
function insert(par, data) {
let newNode = d3.hierarchy(data);
newNode.depth = par.depth + 1;
newNode.parent = par;
if (!par.children)
par.children = [];
par.children.push(newNode);
}
To be functionally equivalent to the previous answer, we would write:
insert(selected, {
type: 'node-type',
name: new Date().getTime()
});
update(selected);
Related
I am playing around with D3 charts. One of the examples they provide is a chart to draw a tree structure (https://observablehq.com/#d3/tidy-tree)
I took this chart and embeded in Power BI as per example here (https://azurebi-docs.jppp.org/powerbi-visuals/d3js.html?tabs=docs%2Cdocs-open#sample), but I hit a wall with the data I currently have. The chart uses json as an input (https://raw.githubusercontent.com/d3/d3-hierarchy/v1.1.8/test/data/flare.json). The script to generate such tree structure json is as follows:
function toJSON(data) {
var flare = { name: "ROOT", children: [] },
levels = ["parentname","categoryname", "categoryname"];
// For each data row, loop through the expected levels traversing the output tree
data.forEach(function(d){
// Keep this as a reference to the current level
var depthCursor = flare.children;
// Go down one level at a time
levels.forEach(function( property, depth ){
// Look to see if a branch has already been created
var index;
depthCursor.forEach(function(child,i){
if ( d[property] == child.name ) index = i;
});
// Add a branch if it isn't there
if ( isNaN(index) ) {
depthCursor.push({ name : d[property], children : []});
index = depthCursor.length - 1;
}
// Now reference the new child array as we go deeper into the tree
depthCursor = depthCursor[index].children;
// This is a leaf, so add the last element to the specified branch
if ( depth === levels.length - 1 ) depthCursor.push({ name : d.product, size : d.revenue });
});
});
// End of conversion
return flare;
}
The problem I am facing is that my data is of a different struture...
The script assumes that levels variable consists of all levels. However my data structure is such, that every row has a name, i.e. CategoryName and an indicator to the parent, i.e. ParentName from the same table. So effectively, the records are
CategoryName | ParentName
Category 1
Category 2 | Category 1
Category 3 | Category 2
Category 4 | Category 2
How should I approch modification of provided javascript to build up a json based on the data structure I currently have?
Thanks for any kind of support or refernces I could base on.
Edit
The format of the data is:
data = [
{categoryname: 'Category 1', parentname: 'null'},
{categoryname: 'Category 2', parentname: 'Category 1'},
{categoryname: 'Category 3', parentname: 'Category 2'},
{categoryname: 'Category 4', parentname: 'Category 2'},
];
The whole PowerBI D3 script looks like this:
/*
* All D3 visuals run in a frame with the following elements/variables:
*
* SVG element:
* - <svg xmlns="http://www.w3.org/2000/svg" class="chart" id="chart" >
*
* pbi object:
* - 'dsv' : function that retrieves the data via the provided callback: pbi.dsv(callback)
e.g. pbi.dsv(function(data) { //Process data function });
* - 'height' : height of the sandbox frame
* - 'width' : width of the sandbox frame
* - 'colors' : color array with 8 colors; changable via options
*
* Code is based on: https://bl.ocks.org/mbostock/4339083
*/
// ADD: translate function for the data
function toJSON(data) {
var flare = { name: "ROOT", children: [] },
levels = ["parentname","categoryname", "categoryname"];
// For each data row, loop through the expected levels traversing the output tree
data.forEach(function(d){
// Keep this as a reference to the current level
var depthCursor = flare.children;
// Go down one level at a time
levels.forEach(function( property, depth ){
// Look to see if a branch has already been created
var index;
depthCursor.forEach(function(child,i){
if ( d[property] == child.name ) index = i;
});
// Add a branch if it isn't there
if ( isNaN(index) ) {
depthCursor.push({ name : d[property], children : []});
index = depthCursor.length - 1;
}
// Now reference the new child array as we go deeper into the tree
depthCursor = depthCursor[index].children;
// This is a leaf, so add the last element to the specified branch
if ( depth === levels.length - 1 ) depthCursor.push({ name : d.product, size : d.revenue });
});
});
// End of conversion
return flare;
}
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = pbi.width - margin.left - margin.right, // ALTER: Changed fixed width with the 'pbi.width' variable
height = pbi.height - margin.top - margin.bottom; // ALTER: Changed fixed height with the 'pbi.height' variable
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("#chart") // ALTER: Select SVG object; no need to create it
.attr("width", width + margin.left + margin.right) // ALTER: Add complete width
.attr("height", height + margin.top + margin.bottom) // ALTER: Add complete height
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// ALTER: Replaced the d3.json function with the pbi variant: pbi.dsv
pbi.dsv(function(data) {
var flare = toJSON(data); // ALTER: add extra convertion step to parent/child JSON
root = flare;
root.x0 = height / 2;
root.y0 = 0;
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
root.children.forEach(collapse);
update(root);
});
d3.select(self.frameElement).style("height", height + margin.top + margin.bottom);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? pbi.colors[0] : pbi.colors[1]; });
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { return d._children ? pbi.colors[0] : pbi.colors[1]; });
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
It seems to me that your question is another case of XY problem: instead of asking about the creation of the hierarchy itself you're asking about a function you believe is necessary for creating that hierarchy.
You don't need that function to create a hierarchical structure. Based on you data, you can just use d3.stratify:
const csv = `CategoryName,ParentName
Category 1,
Category 2,Category 1
Category 3,Category 2
Category 4,Category 2`;
const data = d3.csvParse(csv);
const stratify = d3.stratify()
.id(function(d) {
return d.CategoryName;
})
.parentId(function(d) {
return d.ParentName;
});
let root = stratify(data);
console.log(root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Since you already have the data parsed, it's even easier:
const data = [{
categoryname: 'Category 1',
parentname: ''
},
{
categoryname: 'Category 2',
parentname: 'Category 1'
},
{
categoryname: 'Category 3',
parentname: 'Category 2'
},
{
categoryname: 'Category 4',
parentname: 'Category 2'
},
];
const stratify = d3.stratify()
.id(function(d) {
return d.categoryname;
})
.parentId(function(d) {
return d.parentname;
});
let root = stratify(data);
console.log(root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
PS: use your browser's console to check the root object, not the Stack snippet one.
I am trying to replicate the visualization I see on this github profile
https://github.com/mojoaxel/d3-sunburst/tree/master/examples
I copied the following code and changed the path to the respective files.
suburst.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sequences sunburst</title>
<link rel="stylesheet" type="text/css" href="sunburst.css"/>
<link rel="stylesheet" type="text/css" href="examples.css"/>
<script src="http://d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="sunburst.js" type="text/javascript"></script>
</head>
<body>
<div id="main">
<div id="sunburst-breadcrumbs"></div>
<div id="sunburst-chart">
<div id="sunburst-description"></div>
</div>
</div>
<div id="sidebar">
<input type="checkbox" id="togglelegend"> Legend<br/>
<div id="sunburst-legend" style="visibility: hidden;"></div>
</div>
<script type="text/javascript">
(function() {
var sunburst = new Sunburst({
colors: {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}
});
sunburst.loadCsv("/Users/Documents/data/visit-sequences.csv");
d3.select("#togglelegend").on("click", function() {
var legend = d3.select('#sunburst-legend');
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
});
})();
</script>
</body>
</html>
sunburst.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['d3'], factory);
} else {
root.Sunburst = factory(root.d3);
}
}(this, function (d3) {
var defaultOptions = {
// DOM Selectors
selectors: {
breadcrumbs: '#sunburst-breadcrumbs',
chart: '#sunburst-chart',
description: '#sunburst-description',
legend: '#sunburst-legend'
},
// Dimensions of sunburst.
width: 750,
height: 600,
// Mapping of step names to colors.
colors: {},
// If a color-name is missing this color-scale is used
colorScale: d3.scale.category20(),
colorScaleLength: 20,
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
breadcrumbs: {
w: 75,
h: 30,
s: 3,
t: 10
},
// parser settings
separator: '-'
};
/**
* This hashing function returns a number between 0 and 4294967295 (inclusive) from the given string.
* #see https://github.com/darkskyapp/string-hash
* #param {String} str
*/
function hash(str) {
var hash = 5381;
var i = str.length;
while(i) {
hash = (hash * 33) ^ str.charCodeAt(--i);
}
return hash >>> 0;
}
var Sunburst = function(options, data) {
this.opt = Object.assign({}, defaultOptions, options);
// Total size of all segments; we set this later, after loading the data.
this.totalSize = 0;
if (data) {
this.setData(data);
}
}
Sunburst.prototype.getColorByName = function(name) {
return this.opt.colors[name] || this.opt.colorScale(hash(name) % this.opt.colorScaleLength);
}
Sunburst.prototype.setData = function(data) {
var json = this.buildHierarchy(data);
this.createVisualization(json);
}
Sunburst.prototype.loadCsv = function(csvFile) {
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text(csvFile, function(text) {
var array = d3.csv.parseRows(text);
var json = this.buildHierarchy(array);
this.createVisualization(json);
}.bind(this));
}
// Main function to draw and set up the visualization, once we have the data.
Sunburst.prototype.createVisualization = function(json) {
var that = this;
var radius = Math.min(this.opt.width, this.opt.height) / 2
this.vis = d3.select(this.opt.selectors.chart).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", this.opt.height)
.append("svg:g")
.attr("id", "sunburst-container")
.attr("transform", "translate(" + this.opt.width / 2 + "," + this.opt.height / 2 + ")");
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
// Basic setup of page elements.
this.initializeBreadcrumbTrail();
this.drawLegend();
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var all = this.vis.data([json])
.selectAll("path")
.data(nodes)
.enter();
all.append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return that.getColorByName(d.name); })
.style("opacity", 1)
.on("mouseover", that.mouseover.bind(this));
// some tests with text
/*
var arcText = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y * 0.4); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy * 0.4); })
var arcsText = arcs.append("svg:path")
.attr("d", arcText)
.style("fill", "none")
.attr("id", function(d, i){
return "s" + i;
});
var texts = all.append("svg:text")
.attr("dx", "0")
.attr("dy", "0")
.style("text-anchor","middle")
.append("textPath")
.attr("xlink:href", function(d, i){
return "#s" + i;
})
.attr("startOffset",function(d,i){return "25%";})
.text(function (d) {
return d.depth === 1 ? d.name : '';
});
*/
// Add the mouseleave handler to the bounding circle.
d3.select(this.opt.selectors.chart).on("mouseleave", that.mouseleave.bind(this));
// Get total size of the tree = value of root node from partition.
var node = all.node();
this.totalSize = node ? node.__data__.value : 0;
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
Sunburst.prototype.mouseover = function(d) {
var percentage = (100 * d.value / this.totalSize).toPrecision(3);
var sequenceArray = this.getAncestors(d);
this.updateDescription(sequenceArray, d.value, percentage)
this.updateBreadcrumbs(sequenceArray, d.value, percentage);
// Fade all the segments.
this.vis.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
this.vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
Sunburst.prototype.mouseleave = function(d) {
var that = this;
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
//TODO cancel this transition on mouseover
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", that.mouseover.bind(that));
});
d3.select(this.opt.selectors.description)
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
Sunburst.prototype.getAncestors = function(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
Sunburst.prototype.initializeBreadcrumbTrail = function() {
// Add the svg area.
var trail = d3.select(this.opt.selectors.breadcrumbs).append("svg:svg")
.attr("width", this.opt.width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
Sunburst.prototype.breadcrumbPoints = function(d, i) {
var points = [];
var b = this.opt.breadcrumbs;
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// format the description string in the middle of the chart
Sunburst.prototype.formatDescription = function(sequence, value, percentage) {
return percentage < 0.1 ? "< 0.1%" : percentage + '%';
}
Sunburst.prototype.updateDescription = function(sequence, value, percentage) {
d3.select(this.opt.selectors.description)
.html(this.formatDescription(sequence, value, percentage))
.style("visibility", "");
}
// format the text at the end of the breadcrumbs
Sunburst.prototype.formatBreadcrumbText = function(sequence, value, percentage) {
return value + " (" + (percentage < 0.1 ? "< 0.1%" : percentage + "%") + ")";
}
// Update the breadcrumb trail to show the current sequence and percentage.
Sunburst.prototype.updateBreadcrumbs = function(sequence, value, percentage) {
var that = this;
var b = this.opt.breadcrumbs;
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(sequence, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", this.breadcrumbPoints.bind(that))
.style("fill", function(d) { return that.getColorByName(d.name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (sequence.length + 1) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.html(this.formatBreadcrumbText(sequence, value, percentage));
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
Sunburst.prototype.drawLegend = function() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select(this.opt.selectors.legend).append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(this.opt.colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(this.opt.colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
Sunburst.prototype.buildHierarchy = function(array) {
var root = {"name": "root", "children": []};
for (var i = 0; i < array.length; i++) {
var sequence = array[i][0];
var size = +array[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split(this.opt.separator);
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"] || [];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
}
return Sunburst;
}));
All the html, js and css files are stored in same folder.
But when I run it, I get the browser window like this with no visualization but just a legend box on the right.
What am I missing? The dataset is downloaded from the same github page.
Provided the path to visit-sequences.csv is correct, its local loading will be blocked by the browser (see CORS).
You can either:
run it via a (even local) webserver
start Chrome with the --allow-file-access-from-files flag (if you're using Chrome)
The problem is here
sunburst.loadCsv("/Users/i854319/Documents/Mike_UX_web/data/visit-sequences.csv");
Your browser cannot just load a file from your PC.
I'm trying to customize a sunburst chart from different examples that I've found accross the internet. I'm working in R (and Shiny as well) and there is one package that I thought could a good working base, that is SunburstR.
A working example can be viewed here.
What I want to have :
breadcrumbs
zoomable (forward when cliking a child, and backward when clicking the center)
fading as you hover the childs it fades other elements except current curent hierarchy
the percentage in the middle of the chart (or elsewhere relevant) as well as the breadcrumbs. Could be one or the other as long as it is somewhere on the page.
emphasizing with a bigger circle all around the chart. You'd see the whole circle if hovering the center, and you'd see the current footprint of current hovered element (from startangle to endangle). This is just to help seeing how much what you're hovering is reprensenting compare to the whole thing. This circle would also be the bounding circle, eg when you are exiting it, it cancels the fading since you're not hovering the chart anymore.
I have a little bit of code of everything but struggle to put everything together. Here is where things are currently standing :
I've got the fading ok.
I've got the percentages (both center and breadcrumbs)
I've got the outerRadius defined to allow for the bounding circle around the chart, currently visible at all times. I can't find a way to make it appear when I'm hovering (only the arc of the current hovered element, whole circle when hovering the center) and put it back to #fff
when not hovering anymore this circle.
I don't have (at all) the abitlity to zoom
Current sunburst.js :
HTMLWidgets.widget({
name: 'sunburst',
type: 'output',
factory: function(el, width, height) {
var instance = {};
instance.chart = {};
var dispatch = d3.dispatch("mouseover","mouseleave","click");
d3.rebind(instance.chart, dispatch, 'on');
var draw = function(el, instance) {
// would be much nicer to implement transitions/animation
// remove previous in case of Shiny/dynamic
d3.select(el).select(".sunburst-chart svg").remove();
var x = instance.x;
var json = instance.json;
var chart = instance.chart;
// Dimensions of sunburst
var width = el.getBoundingClientRect().width - (x.options.legend.w ? x.options.legend.w : 75);
var height = el.getBoundingClientRect().height - 70;
var radius = Math.min(width, height) / 2;
var outerRadius = radius/3.5; // reserved pixels all around the vis
d3.select(el).select(".sunburst-chart").append("svg")
.style("width", width + "px") // shouldnt have to do this
.style("height", height + "px"); // shouldnt have to do this
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
// these will be the defaults
var b = {
w: 0, h: 30, s: 3, t: 10
};
// if breadcrumb is provided in the option, we will overwrite
// with what is provided
Object.keys(x.options.breadcrumb).map(function(ky){
b[ky] = x.options.breadcrumb[ky];
});
/*
// Mapping of step names to colors.
var colors = {
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
};
*/
var colors = d3.scale.category20();
if(x.options.colors !== null){
// if an array then we assume the colors
// represent an array of hexadecimal colors to be used
if(Array.isArray(x.options.colors)) {
try{
colors.range(x.options.colors)
} catch(e) {
}
}
// if an object with range then we assume
// that this is an array of colors to be used as range
if(x.options.colors.range){
try{
colors.range(x.options.colors.range)
} catch(e) {
}
}
// if an object with domain then we assume
// that this is an array of colors to be used as domain
// for more precise control of the colors assigned
if(x.options.colors.domain){
try{
colors.domain(x.options.colors.domain);
} catch(e) {
}
}
// if a function then set to the function
if(typeof(x.options.colors) === "function") {
colors = x.options.colors;
}
}
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select(el).select(".sunburst-chart").select("svg")
.append("g")
.attr("id", el.id + "-container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
//.size([2 * Math.PI, radius * radius])
.size([2 * Math.PI, (radius - outerRadius) * (radius - outerRadius)])
.value(function(d) { return d[x.options.valueField || "size"]; });
// check for sort function
if(x.options.sortFunction){
partition.sort(x.options.sortFunction);
}
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return (d.dy == d.y) ? Math.sqrt(d.dy)/3 : Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
createVisualization(json);
// set up a container for tasks to perform after completion
// one example would be add callbacks for event handling
// styling
if (!(typeof x.tasks === "undefined") ){
if ( (typeof x.tasks.length === "undefined") ||
(typeof x.tasks === "function" ) ) {
// handle a function not enclosed in array
// should be able to remove once using jsonlite
x.tasks = [x.tasks];
}
x.tasks.map(function(t){
// for each tasks call the task with el supplied as `this`
t.call({el:el,x:x,instance:instance});
});
}
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("circle")
.attr("r", radius - outerRadius)
.style("opacity", 0);
var highlight = vis.append("circle")
.attr("r", radius)
.style("fill", "#eee");
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors.call(this, d.name); })
.style("opacity", 1)
.on("mouseover", mouseover)
//.on("mouseleave", mouseleave)
.on("click", click);
// Add the mouseleave handler to the bounding circle.
d3.select(el).select("#"+ el.id + "-container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
drawLegend(nodes);
d3.select(el).select(".sunburst-togglelegend").on("click", toggleLegend);
}
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
//highlight.attr("d", arc(d));
//highlight.style("fill", "#eee");
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
var countString = [
'<span style = "font-size:.7em">',
d3.format("1.2s")(d.value) + ' of ' + d3.format("1.2s")(totalSize),
'</span>'
].join('')
var explanationString = "";
if(x.options.percent && x.options.count){
explanationString = percentageString + '<br/>' + countString;
} else if(x.options.percent){
explanationString = percentageString;
} else if(x.options.count){
explanationString = countString;
}
//if explanation defined in R then use this instead
if(x.options.explanation !== null){
explanationString = x.options.explanation.bind(totalSize)(d);
}
d3.select(el).selectAll(".sunburst-explanation")
.style("visibility", "")
.style("top",((height - 35)/2) + "px")
.style("width",width + "px")
.html(explanationString);
var sequenceArray = getAncestors(d);
chart._selection = sequenceArray.map(
function(d){return d.name}
);
dispatch.mouseover(chart._selection);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.select(el).selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
//highlight.attr("d", null);
//highlight.style("fill", "#fff");
dispatch.mouseleave(chart._selection);
chart._selection = [];
// Hide the breadcrumb trail
d3.select(el).select("#" + el.id + "-trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.select(el).selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.select(el).selectAll("path")
.transition()
.duration(250)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select(el).selectAll(".sunburst-explanation")
.style("visibility", "hidden");
}
function click(d,i) {
var sequenceArray = getAncestors(d);
dispatch.click(sequenceArray.map(
function(d){return d.name}
));
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select(el).select(".sunburst-sequence").append("svg")
.attr("width", width)
//.attr("height", 50)
.attr("id", el.id + "-trail");
// Add the label at the end, for the percentage.
trail.append("text")
.attr("id", el.id + "-endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
if (b.w <= 0) {
// calculate breadcrumb width based on string length
points.push(d.string_length + ",0");
points.push(d.string_length + b.t + "," + (b.h / 2));
points.push(d.string_length + "," + b.h);
} else {
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
}
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select(el).select("#" + el.id + "-trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("g");
if (b.w <= 0) {
// Create a node array that contains all the breadcrumb widths
// Calculate positions of breadcrumbs based on string lengths
var curr_breadcrumb_x = 0;
nodeArray[0].breadcrumb_x = 0;
nodeArray[0].breadcrumb_h = 0;
entering.append("polygon")
.style("z-index",function(d,i) { return(999-i); })
.style("fill", function(d) { return colors.call(this, d.name); });
entering.append("text")
.attr("x", b.t + 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "left")
.text(function(d) { return d.name; });
// Remove exiting nodes.
g.exit().remove();
// loop through each g element
// calculate string length
// draw the breadcrumb polygon
// and determine if breadcrumb should be wrapped to next row
g.each(function(d,k){
var crumbg = d3.select(this);
var my_string_length = crumbg.select("text").node().getBoundingClientRect().width;
nodeArray[k].string_length = my_string_length + 12;
crumbg.select("polygon").attr("points", function(d){
return breadcrumbPoints(d, k);
});
var my_g_length = crumbg.node().getBoundingClientRect().width;
curr_breadcrumb_x += k===0 ? 0 : nodeArray[k-1].string_length + b.s;
nodeArray[k].breadcrumb_h = k===0 ? 0 : nodeArray[k-1].breadcrumb_h;
if (curr_breadcrumb_x + my_g_length > width*0.99) {
nodeArray[k].breadcrumb_h += b.h; // got to next line
curr_breadcrumb_x = b.t + b.s; // restart counter
}
nodeArray[k].breadcrumb_x = curr_breadcrumb_x;
});
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + d.breadcrumb_x + ", "+d.breadcrumb_h+")";
});
// Now move and update the percentage at the end.
d3.select(el).select("#" + el.id + "-trail").select("#" + el.id + "-endlabel")
.attr("x", function(d){
var bend = d3.select(this);
var curr_breadcrumb_x = nodeArray[nodeArray.length-1].breadcrumb_x + nodeArray[nodeArray.length-1].string_length + b.t + b.s;
var my_g_length = bend.node().getBoundingClientRect().width;
var curr_breadcrumb_h = nodeArray[nodeArray.length-1].breadcrumb_h + b.h/2;
if (curr_breadcrumb_x + my_g_length > width*0.99) {
curr_breadcrumb_h += b.h + b.h/2;
curr_breadcrumb_x = b.t + b.s; // restart counter
}
bend.datum({
"breadcrumb_x": curr_breadcrumb_x,
"breadcrumb_h": curr_breadcrumb_h
});
return curr_breadcrumb_x;
})
.attr("y", function(d){return d.breadcrumb_h})
.attr("dy", "0.35em")
.attr("text-anchor", "start")
.text(percentageString);
} else {
entering.append("polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(d) { return colors.call(this, d.name); });
entering.append("text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select(el).select("#" + el.id + "-trail").select("#" + el.id + "-endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
}
// Make the breadcrumb trail visible, if it's hidden.
d3.select(el).select("#" + el.id + "-trail")
.style("visibility", "");
}
function drawLegend(nodes) {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
// if legend is provided in the option, we will overwrite
// with what is provided
Object.keys(x.options.legend).map(function(ky){
li[ky] = x.options.legend[ky];
});
// remove if already drawn
d3.select(el).select(".sunburst-legend svg").remove();
// get labels from node names
var labels = d3.nest()
.key(function(d) {return d.name})
.entries(
nodes.sort(
function(a,b) {return d3.ascending(a.depth,b.depth)}
)
)
.map(function(d) {
return d.values[0];
})
.filter(function(d) {
return d.name !== "root";
});
var legend = d3.select(el).select(".sunburst-legend").append("svg")
.attr("width", li.w)
.attr("height", labels.length * (li.h + li.s));
var g = legend.selectAll("g")
.data( function(){
if(x.options.legendOrder !== null){
return x.options.legendOrder;
} else {
// get sorted by top level
return labels;
}
})
.enter().append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors.call(this, d.name); });
g.append("text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
}
function toggleLegend() {
var legend = d3.select(el).select(".sunburst-legend")
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
};
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
return {
renderValue: function(x) {
instance.x = x;
// x.data should be a data.frame in R so an Javascript Object of Objects
// but buildHierarchy expects an Array of Arrays
// so use d3.zip and apply to do this
var json = [];
if(x.csvdata !== null){
json = buildHierarchy(
d3.zip.apply(
null,
Object.keys(x.csvdata).map(function(ky){return x.csvdata[ky]})
)
);
} else {
json = x.jsondata
}
instance.json = json;
draw(el, instance);
},
resize: function(width, height) {
draw(el, instance);
},
instance: instance
};
}
});
I have found this question where Skip Jack gives this pen, but I can't get it to work along with the rest of the sunburstR package... And still this pen would miss the bounding circle and the percentages.
I must emphasize that I'm pretty new to JS so please excuse me if this actually quite easy to solve.
Any help would be welcome, thanks ahead of time for your support !
its my json
var data =
[{
name:"Words",
children:[{
name:"Nouns",
children:[
{
name:"Table",
size:1025
},
{
name:"Box",
size:2925
},
{
name:"Bat",
size:4025
},
],
size: 2000
},
{
name:"Adverbs",
children:[
{
name:"eagerly",
size:2520
},
{
name:"badly",
size:3259
},
{
name:"easily",
size:2512
},
],
size: 425
},
{
name:"Adjectives",
children:[
{
name:"Positive",
children:[
{
name:"Amazing",
size:1250
},
{
name:"Beautiful",
size:2529
}],
size:1000
},
{
name:"Negative",
children:[
{
name:"Destructive",
size:1250
},
{
name:"Hot",
size:2529
}],
size:1000
},
],
size: 343
},
{
name:"Verbs",
children:[
{
name:"Play",
size:4310
},
{
name:"Say",
size:2943
},
{
name:"Ride",
size:4430
},
],
size: 343
},
],
size: 2000
}];
initialized with null
var g = {
data: null,
force: null
};
created graph
$(function () {
//use a global var for the data:
g.data = data;
var width = 1300,
height = 630;
//Create a sized SVG surface within viz:
var svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height);
g.link = svg.selectAll(".link"),
g.node = svg.selectAll(".node");
//Create a graph layout engine:
g.force = d3.layout.force()
.linkDistance(150)
.charge(-300)
.gravity(0.01)
.size([width, height])
//that invokes the tick method to draw the elements in their new location:
.on("tick", tick);
//Draw the graph:
//Note that this method is invoked again
//when clicking nodes:
update();
});
function update() {
//iterate through original nested data, and get one dimension array of nodes.
var nodes = flatten(g.data);
//Each node extracted above has a children attribute.
//from them, we can use a tree() layout function in order
//to build a links selection.
var links = d3.layout.tree().links(nodes);
// pass both of those sets to the graph layout engine, and restart it
Here is where error occur
g.force.nodes(nodes) //Here is where error occur
.links(links)
.start();
///////////////////////////////////////////////////////////////////
//-------------------
// create a subselection, wiring up data, using a function to define
//how it's suppossed to know what is appended/updated/exited
g.link = g.link.data(links, function (d) {return d.target.id;});
//Get rid of old links:
g.link.exit().remove();
//Build new links by adding new svg lines:
g.link
.enter()
.insert("line", ".node")
.attr("class", "link");
// create a subselection, wiring up data, using a function to define
//how it's suppossed to know what is appended/updated/exited
g.node = g.node.data(nodes, function (d) {return d.id;});
//Get rid of old nodes:
g.node.exit().remove();
//-------------------
//create new nodes by making groupd elements, that contain circls and text:
var nodeEnter = g.node.enter()
.append("g")
.attr("class", "node")
.on("click", click)
.call(g.force.drag);
//circle within the single node group:
nodeEnter.append("circle")
.attr("r", function (d) {return Math.sqrt(d.size) || 50;});
//text within the single node group:
nodeEnter.append("text")
.attr("dy", ".35em")
.text(function (d) {
return d.name;
});
//All nodes, do the following:
g.node.select("circle")
.style("fill", color); //calls delegate
//-------------------
}
// Invoked from 'update'.
// The original source data is not the usual nodes + edge list,
// but that's what's needed for the force layout engine.
// So returns a list of all nodes under the root.
function flatten(data) {
var nodes = [],
i = 0;
//count only children (not _children)
//note that it doesn't count any descendents of collapsed _children
//rather elegant?
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(data);
//Done:
return nodes;
}
update();
//Invoked from 'update'
//Return the color of the node
//based on the children value of the
//source data item: {name=..., children: {...}}
function color(d) {
return d._children ? "#3182bd" // collapsed package
:
d.children ? "#c6dbef" // expanded package
:
"#fd8d3c"; // leaf node
}
// Toggle children on click by switching around values on _children and children.
function click(d) {
if (d3.event.defaultPrevented) return; // ignore drag
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
//
update();
}
//event handler for every time the force layout engine
//says to redraw everthing:
function tick() {
width=1300;
height=630;
r=60;
g.node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(width - r, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(r, Math.min(height - r, d.y)); });
//redraw position of every link within the link set:
g.link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
//same for the nodes, using a functor:
g.node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
**Whenever i add my new json with the same format it doesnt show anything. and the error tracebacks to the code the == update(); ==
jquery version: 2.1.3
d3 version : 3.4.13
dont know if it matters or not but i am intergrating this in http://metroui.org.ua/
Metro Ui tabs
Please help!**
I'm attempting to put multiple D3 force layouts on a page at the same time. The number of force layouts is ideally variable, depending on the number of roots returned from a dynamic API. I have followed the answers on this question regarding multiple force layouts and have successfully put each layout in a separate div, in a separate svg.
However, the issue is twofold:
1) The svgs seem to be drawn at the same time, causing conflicts in the alpha cooling parameter (on "tick" of each graph). Thus, the only layout that is positioned the way it is intended is the last svg drawn on the page. The tick function contains code that shapes the force layout similar to a weeping willow tree, with the root node sitting on top and the children falling below it.
2) Setting a loop to iterate on the full results list from the API causes D3 to crash, and an error "Uncaught TypeError: Cannot read property 'textContent' of null."
I think the ideal solution would be to draw each force layout after the previous one has been successfully rendered, in a way that does not cause the alpha cooling parameters (on "tick") to conflict, or overloading the D3 library with too many instances of the force layout at once. Does someone have insight into this issue? Here is my code:
/* ... GET THE RESULTS FROM THE API ...*/
function handleRequest2(json) {
allroots = json[1]['data']['children'];
(function() {
var index = 0;
function LoopThrough() {
currentRoot = allroots[index];
if (index < allroots.length) {
/* DRAW THE GRAPH */
draw_graphs(currentRoot, index);
++index;
LoopThrough();
};
}
LoopThrough();
})();
}
//Force Layout Code
function draw_graphs(root, id) {
var root_id = "map-" + id.toString();
var force;
var vis;
var link;
var node;
var w = 980;
var h = 1000;
var k = 0;
// Create a separate div to house each SVG graph
div = document.createElement("div");
div.style.width = "980px";
div.style.height = "1000px";
div.style.cssFloat="left";
div.id = root_id;
$(div).addClass("chattermap-map");
// Append the div to the chart container
$('#chart').append(div);
force = d3.layout.force()
.size([w, h])
.charge(-250)
.gravity(0)
.on("tick", tick);
// Create the SVG and append it to the created div
vis = d3.select("#"+root_id)
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("id",root_id);
// Put the Reddit JSON in the correct format for the Force Layout
nodes = flatten(root),
links = optimize(d3.layout.tree().links(nodes));
// Calculations for the sizing of the nodes
avgNetPositive = getAvgNetPositive();
maxNetPositive = d3.max(netPositiveArray);
minNetPositive = d3.min(netPositiveArray);
// Create a logarithmic scale that sizes the nodes
radius = d3.scale.pow().exponent(.3).domain([minNetPositive,maxNetPositive]).range([5,30]);
// Fix the root node to the top of the svg
root.data.fixed = true;
root.data.x = w/2;
root.data.y = 50;
// Start the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update the links
link = vis.selectAll("line.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links.
link.enter().insert("svg:line", ".node")
.attr("class", "link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
// Exit any old links.
link.exit().remove();
// Update the nodes
node = vis.selectAll("circle.node")
.data(nodes, function(d) {return d.id; })
.style("fill", function(d) {
return '#2960b5';
});
// Enter any new nodes.
node.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) {return d.x; })
.attr("cy", function(d) {return d.y; })
.attr("r", function(d) {
//Get the net positive reaction
var netPositive = d.ups - d.downs;
var relativePositivity = netPositive/avgNetPositive;
//Scale the radii based on the logarithmic scale defined earlier
return radius(netPositive);
})
.style("fill", function(d) {
return '#2960b5';
})
// Allow dragging on click
.call(force.drag);
// Exit any old nodes.
node.exit().remove();
//This will add the name of the author to the node HTML
node.append("author").text(function(d) {return d.author});
//Add the body of the comment to the node
node.append("comment").text(function(d) {return Encoder.htmlDecode(d.body_html)});
//Add the UNIX timestamp to the node
node.append("timestamp").text(function(d) {return moment.unix(d.created_utc).fromNow();})
//On load, assign the root node to the tooltip
numberOfNodes = node[0].length;
rootNode = d3.select(node[0][parseInt(numberOfNodes) - 1]);
rootNodeComment = rootNode.select("comment").text();
rootNodeAuthor = rootNode.select("author").text();
rootNodeTimestamp = rootNode.select("timestamp").text();
// Create the tooltip div for the comments
tooltip_div = d3.select("#"+root_id).append("div")
.attr("class", "tooltip")
.style("opacity", 1);
//Add the HTML to the tooltip for the root
tooltip_div .html("<span class='commentAuthor'>" + rootNodeAuthor + "</span><span class='bulletTimeAgo'>•</span><span class='timestamp'>" + rootNodeTimestamp + "</span><br>" + rootNodeComment)
//Position the tooltip based on the position of the current node, and it's size
.style("left", (rootNode.attr("cx") - (-rootNode.attr("r")) - (-9)) + "px")
.style("top", (rootNode.attr("cy") - 15) + "px");
node.on("mouseover", function() {
currentNode = d3.select(this);
currentTitle = currentNode.select("comment").text();
currentAuthor = currentNode.select("author").text();
currentTimestamp = currentNode.select("timestamp").text();
tooltip_div.transition()
.duration(200)
.style("opacity", 1);
// Add the HTML for all other tooltips on mouseover
tooltip_div .html("<span class='commentAuthor'>" + currentAuthor + "</span><span class='bulletTimeAgo'>•</span><span class='timestamp'>" + currentTimestamp + "</span><br>" + currentTitle)
//Position the tooltip based on the position of the current node, and it's size
.style("left", (currentNode.attr("cx") - (-currentNode.attr("r")) - (-9)) + "px")
.style("top", (currentNode.attr("cy") - 15) + "px");
});
// Fade out the tooltip on mouseout
node.on("mouseout", function(d) {
tooltip_div.transition()
.duration(500)
.style("opacity", 1);
});
// Optimize the JSON output of Reddit for D3
function flatten(root) {
var nodes = [], i = 0, j = 0;
function recurse(node) {
if (node['data']['replies'] != "" && node['kind'] != "more") {
node['data']['replies']['data']['children'].forEach(recurse);
}
if (node['kind'] !="more") {
//Add an ID value to the node starting at 1
node.data.id = ++i;
node.data.name = node.data.body;
//Put the replies in the key 'children' to work with the tree layout
if (node.data.replies != "") {
node.data.children = node.data.replies.data.children;
//Remove the extra 'data' layer for each child
for (j=0; j < node.data.children.length; j++) {
node.data.children[j] = node.data.children[j].data;
}
} else {
node.data.children = "";
}
var comment = node.data;
nodes.push(comment);
}
}
recurse(root);
return nodes;
}
// Optimize the JSON for use with Links
function optimize(linkArray) {
optimizedArray = [];
for (k=0; k < linkArray.length; k++) {
if(typeof linkArray[k].target.count == 'undefined') {
optimizedArray.push(linkArray[k]);
}
}
return optimizedArray;
}
// Get the average net positive upvotes for use in sizing
function getAvgNetPositive() {
var sum = 0;
netPositiveArray = []
//Select all the nodes
var allNodes = d3.selectAll(nodes)[0];
//For each node, get the net positive votes and add it to the sum
for (i=0; i < allNodes.length; i++) {
var netPositiveEach = allNodes[i]["ups"] - allNodes[i]["downs"];
sum += netPositiveEach;
netPositiveArray.push(netPositiveEach);
}
var avgNetPositive = sum/allNodes.length;
return avgNetPositive;
}
function tick(e) {
var kx = .4 * e.alpha, ky = 1.4 * e.alpha;
links.forEach(function(d, i) {
d.target.x += (d.source.x - d.target.x) * kx;
d.target.y += (d.source.y + 80 - d.target.y) * ky;
});
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// // Remove the animation effect of the force layout
// while ((force.alpha() > 1e-2) && (k < 150)) {
// force.tick(),
// k = k + 1;
// }
}
Thanks in advance!
You should be able to make several force layouts work at the same time if you encapsulate them in their own namespace, e.g. through separate functions. You can however also do what you want by listening to the end event -- see the documentation. This way, you can "chain" the layouts, starting each one once the previous has finished.
Regarding the other error, it looks like this would be caused by incomplete/faulty data.