I use this example: https://codepen.io/AndrewGHC/pen/mPXjKr
I have 2 questions:
1) How can I restrict drag-and-drop? For example, I want to restrict movement to 50px around the circle. It should not move beyond 50px away from the circle's starting position.
2) When you refresh the page, you see elements in different places.
One circle may be right, and the next time the left. How can this be avoided? I think I need to set the starting positions.
// Request data, generate bg with Trianglify & set up loading function.
function slowPrint(tgt, i, msg, spd) {
if (i < msg.length) {
$(tgt).append(msg[i]);
i++;
var writeTimer = setTimeout(function() {
slowPrint(tgt, i, msg, spd)
}, spd);
}
}
function writeThis(tgt, msg, spd) {
if ($(tgt).html() === msg) {
return;
}
$(tgt).html('');
slowPrint(tgt, 0, msg, spd);
}
writeThis('#info', 'Loading . . .', 100);
var url = "https://raw.githubusercontent.com/AndrewGHC/kevin-bacon-number/master/kevinBacon.json";
d3.json(url, drawGraph);
// Credit to Trianglify # https://github.com/qrohlf/trianglify
var pattern = Trianglify({
height: $(document).height(),
width: $(document).width(),
cell_size: 40
});
document.body.style.backgroundImage = "url(" + pattern.png() + ")";
// Create the drawGraph callback
function drawGraph(err, data) {
if (err) throw err;
var width = $('#graph').width(),
height = $('#graph').height();
// Prepare the data for the force graph, beinning by creating an array of movies (strings)
var movies = [];
(function() {
data.actors.forEach(function(actor) {
actor.movies.forEach(function(movie) {
if (movies.indexOf(movie) === -1) {
movies.push(movie);
}
});
});
}())
// Create the links array for the force graph, mapping actors to movies. This will draw a line between the two.
var links = [];
(function() {
data.actors.forEach(function(actor, actorIndex) {
actor.movies.forEach(function(movie, movieIndex) {
links.push({
"source": actorIndex,
"target": data.actors.length + movies.indexOf(movie)
});
});
});
}())
// Now prepare the nodes array, concatenating data.actors and the movies array. The order here is important, and movie indices must be converted into objects.
var nodes = data.actors;
movies.forEach(function(movie) {
nodes.push({
"movie": movie
});
});
// Create the SVG canvas & force layout
var canvas = d3.select('#graph')
.append('svg')
.attr("height", height)
.attr("width", width);
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.links(links)
.linkDistance(50)
.charge(function(d) {
if (d.name === "Kevin Bacon") {
return -1000;
} else if (d.name) {
return -(d.weight) * 50;
}
return -((d.weight * 50) * 5);
})
.gravity(0.1)
.start();
// Helper function to remove whitespace, later used for assigning IDs
function rmWs(string) {
if (typeof string !== 'string') {
return false;
}
string = string.split(' ').join('');
return string;
}
// Create the links
var link = canvas.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
// Create a colour scale for movie nodes. Find the min and max no. of links for the range of the colour domain.
var arrMax = [];
links.forEach(function(link) {
arrMax.push(link.target.weight);
});
var colour = d3.scale.linear()
.domain([1, d3.max(arrMax)])
.range(["white", "black"])
.interpolate(d3.interpolateHcl);
// Set up the pop up on mouse hover
// Call circles on SVG chart, with colours along a white - black gradient generated based on the max weight & variable sizing. Then place text on these movie elements.
var circleRadius = 17;
var circles = canvas.selectAll('.movies')
.data(nodes)
.enter()
.append('circle')
.attr('r', function(d, i) {
if (d.name) {
return circleRadius;
}
return circleRadius + (d.weight * 2);
})
.attr('stroke', '#777')
.attr('stroke-width', '2px')
.attr('fill', function(d, i) {
return colour(d.weight) || 'black';
})
.call(force.drag)
var text = canvas.selectAll('.moviesText')
.data(nodes)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.text(function(d) {
return d.movie;
});
// Set up clip path for each forthcoming image node to clip rectangular images to circles. Then call images on the canvas.
var clip = canvas.selectAll('clipPath')
.data(nodes)
.enter()
.append('clipPath')
.attr('id', function(d) {
return rmWs(d.name) || rmWs(d.movie)
})
.append('circle')
.attr('r', circleRadius);
var imgWidth = 50,
imgHeight = 50;
var node = canvas.selectAll('.node')
.data(nodes)
.enter()
.append('image')
.attr('xlink:href', function(d) {
return d.thumbnail;
})
.attr("class", "image")
.attr("width", imgWidth)
.attr("height", imgHeight)
.attr("clip-path", function(d) {
return "url(#" + (rmWs(d.name) || rmWs(d.movie)) + ")"
})
.call(force.drag);
// Handle operations on each tick.
force.on("tick", function() {
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("x", function(d) {
return d.x - (imgWidth / 2);
})
.attr("y", function(d) {
return d.y - (imgHeight / 2);
});
clip.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
circles.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
text.attr('x', function(d) {
return d.x;
})
.attr('y', function(d) {
return d.y - 30;
})
});
// When all initial calculations are done, print title to replace 'Loading . . .'
force.on('end', function() {
writeThis('#info', 'D3 Force Graph - Distance from Kevin Bacon', 100);
})
}
#import url(https://fonts.googleapis.com/css?family=Questrial' rel='stylesheet' type='text/css);
#header {
text-align: center;
font-family: 'Jockey One', sans-serif;
}
#graph {
margin: 15px auto;
background: white;
height: 750px;
width: 750px;
-webkit-box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.75);
box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.75);
}
.link {
stroke: #777;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.1/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="header">
<h2 id="info"></h2>
</div>
<div id="graph"></div>
Here is how you can restrict your dragging:
First, set your drag functions:
var drag = force.drag()
.on("dragstart", dragstarted)
.on("drag", dragged);
Then, in dragstarted, get the current x and y positions:
function dragstarted(d) {
currentX = d.x;
currentY = d.y;
}
Finally, in the dragged function, set your rules:
function dragged(d) {
d.px = (d.px > currentX + 50) ? currentX + 50 : d.px;
d.py = (d.py > currentY + 50) ? currentY + 50 : d.py;
d.px = (d.px < currentX - 50) ? currentX - 50 : d.px;
d.py = (d.py < currentY - 50) ? currentY - 50 : d.py;
}
Here is the Pen: https://codepen.io/anon/pen/eBzyYd?editors=0010
PS: Here at SO is a good practice dealing with 1 problem at a time, one question per problem. So, I suggest you post your second problem as a separate question.
Related
I'm making a bubble map similar to this one: https://observablehq.com/#d3/bubble-map
Everything is working except that my smaller bubbles are not always showing on top of the larger ones. I can't see why, as I've sorted the data before drawing the circles. Can anyone see what I'm doing wrong?
Here is a plunker:
https://plnkr.co/edit/JKWeQKkhN2TQwvNZ?open=lib%2Fscript.js
Code is below. The other files are too large for stack overflow but can be accessed via the Plunker.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.states {
fill: #d3d3d3;
stroke: #ffffff;
stroke-linejoin: round;
}
div.tooltip {
position: absolute;
left: 75px;
text-align: center;
height: 12px;
padding: 8px;
font-size: 13px;
font-family: 'Proxima-Nova', sans-serif;
background: #FFFFFF;
border: 1px solid #989898;
pointer-events: none;
}
.block {
width: 18%;
height: 15px;
display: inline-block;
}
</style>
<body>
<div class="g-chart"></div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/geo-albers-usa-territories#0.1.0/dist/geo-albers-usa-territories.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script>
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var margin = { top: 10, left: 10, bottom: 10, right: 10 },
width = window.outerWidth,
width = width - margin.left - margin.right,
mapRatio = .5,
height = width * mapRatio;
const epsilon = 1e-6;
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
var map = d3.select(".g-chart").append("svg")
.style('height', height + 'px')
.style('width', width + 'px')
.call(d3.zoom().on("zoom", function () {
map.attr("transform", d3.event.transform)
d3.selectAll()
}))
.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "test.csv")
.await(ready);
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveToBack = function () {
return this.each(function () {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
function ready(error, us, data) {
if (error) throw error;
data.forEach(function (d) {
d.amount = +d.amount;
})
map.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
// sort by descending size so that the smaller circles are drawn on top - not working
map.append('g')
.attr('class', 'facility')
.selectAll("circle")
.data(data.sort((a, b) => +b.amount - +a.amount))
.enter()
.append("circle")
.attr("cx", function (d) {
return projection([d.longitude, d.latitude])[0];
})
.attr("cy", function (d) {
return projection([d.longitude, d.latitude])[1];
})
.attr('r', function (d) {
if (d.amount <= 25) { return 3 }
else if (d.amount > 25 && d.amount <= 50) { return 5 }
else if (d.amount > 50 && d.amount <= 75) { return 7 }
else { return 9 }
})
.style("fill", "#EF4136")
.style("stroke", "#BE2C2D")
.style("stroke-width", 1)
.style("opacity", 0.5)
.on("mouseover", function (d) {
var sel = d3.select(this);
sel.moveToFront();
d3.select(this).transition().duration(0)
.style("opacity", 0.8)
.style("stroke", "#FFFFFF")
div.transition().duration(0)
.style("opacity", .85)
div.text(d.amount)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 30) + "px");
})
.on("mouseout", function () {
var sel = d3.select(this);
d3.select(this)
.transition().duration(0)
.style("opacity", 0.5)
.style("stroke", "#BE2C2D")
div.transition().duration(0)
.style("opacity", 0);
});
//RESPONSIVENESS
d3.select(window).on('resize', resize);
function resize() {
var w = d3.select(".g-chart").node().clientWidth;
width = w - margin.left - margin.right;
height = width * mapRatio;
var newProjection = d3.geoAlbersUsa()
.scale(width)
.translate([width / 2, height / 2]);
path = d3.geoPath()
.projection(newProjection);
map.selectAll("circle")
.attr("cx", function (d) {
return newProjection([d.longitude, d.latitude])[0];
})
.attr("cy", function (d) {
return newProjection([d.longitude, d.latitude])[1];
})
.attr("r", 5)
map
.style('width', width + 'px')
.style('height', height + 'px');
map.selectAll("path").attr('d', path);
}
}
</script>
I would suggest you to split your data to a couple separate datasets grouped by size and create distinct group (g element) for each one. This will also fix issues with circles highlighting.
I slightly updated your plunker to make it work as described (check the lines 91-167) https://plnkr.co/edit/rayo5IZQrBqfqBWR?open=lib%2Fscript.js&preview
Also check the raise and lower methods. They might be a good replacement for your moveToFront and moveToBack methods.
https://riptutorial.com/d3-js/example/18029/svg--the-drawing-order
I am trying to get the node details (id attribute) when it is right clicked and the contextmenu function is called. I am able to get the node object using var self = d3.select(this); but I am not able to work out the
id attribute of the node (i can see it in the console log though)
I am planning to pass the id to the menu function once I'll get the node.id
JSFiddle
var circle = svg.append("g").selectAll("circle") .data(force.nodes())
.enter().append("circle").attr("r", 6) .call(force.drag)
.on('contextmenu', function(){
d3.event.preventDefault();
var self = d3.select(this);
console.log(self);
var n1=(self[0])[0];
console.log(n1);
menu(d3.mouse(svg.node())[0], d3.mouse(svg.node())[1]);
});
You can pass the datum as a parameter of the function called on the contextmenu event:
.on('contextmenu', function(d) { ... }
which allows you to get the id within the function:
console.log(d.id);
.node {
fill: #000;
}
.cursor {
fill: green;
stroke: brown;
pointer-events: none;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
#licensing {
fill: green;
}
.link.licensing {
stroke: green;
}
.link.resolved {
stroke-dasharray: 0,2 1;
}
circle {
fill: green;
stroke: red;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 500, height = 300;
var links = [{source:"simulator",target:"monitor" ,type:"resolved"} , {source:"web",target:"monitor" ,type:"resolved"} ];
var nodes = [ {"id":"monitor", "grp":"system"}, {"id":"simulator", "grp":"system"}, {id:"web", grp:"client"}];
function reset() {
}
function contextMenu() {
var height,
width,
margin = 0.1, // fraction of width
items = [],
rescale = false,
style = {
'rect': {
'mouseout': {
'fill': 'rgb(244,244,244)',
'stroke': 'white',
'stroke-width': '1px'
},
'mouseover': {
'fill': 'rgb(200,200,200)'
}
},
'text': {
'fill': 'steelblue',
'font-size': '13'
}
};
function menu(x, y) {
d3.select('.context-menu').remove();
scaleItems();
// Draw the menu
d3.select('svg')
.append('g').attr('class', 'context-menu')
.selectAll('tmp')
.data(items).enter()
.append('g').attr('class', 'menu-entry')
.style({'cursor': 'pointer'})
.on('mouseover', function(){
d3.select(this).select('rect').style(style.rect.mouseover) })
.on('mouseout', function(){
d3.select(this).select('rect').style(style.rect.mouseout) });
d3.selectAll('.menu-entry')
.append('rect')
.attr('x', x)
.attr('y', function(d, i){ return y + (i * height); })
.attr('width', width)
.attr('height', height)
.style(style.rect.mouseout);
d3.selectAll('.menu-entry')
.append('text')
.text(function(d){ return d; })
.attr('x', x)
.attr('y', function(d, i){ return y + (i * height); })
.attr('dy', height - margin / 2)
.attr('dx', margin)
.style(style.text);
// Other interactions
d3.select('body')
.on('click', function() {
d3.select('.context-menu').remove();
});
}
menu.items = function(e) {
if (!arguments.length) return items;
for (i in arguments) items.push(arguments[i]);
rescale = true;
return menu;
}
// Automatically set width, height, and margin;
function scaleItems() {
if (rescale) {
d3.select('svg').selectAll('tmp')
.data(items).enter()
.append('text')
.text(function(d){ return d; })
.style(style.text)
.attr('x', -1000)
.attr('y', -1000)
.attr('class', 'tmp');
var z = d3.selectAll('.tmp')[0]
.map(function(x){ return x.getBBox(); });
width = d3.max(z.map(function(x){ return x.width; }));
margin = margin * width;
width = width + 2 * margin;
height = d3.max(z.map(function(x){ return x.height + margin / 2; }));
// cleanup
d3.selectAll('.tmp').remove();
rescale = false;
}
}
return menu;
}
var width = 400,
height = 200,
radius = 8;
var map = {}
nodes.forEach(function(d,i){
map[d.id] = i;
})
links.forEach(function(d) {
d.source = map[d.source];
d.target = map[d.target];
})
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(50)
.charge(-200)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// Per-type markers, as they don't inherit styles.
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
var path = svg.append("g").selectAll("path")
.data(force.links())
.enter().append("path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
var menu = contextMenu().items('first item', 'second option', 'whatever, man');
var circle = svg.append("g").selectAll("circle")
.data(force.nodes())
.enter().append("circle")
.attr("r", 6)
.call(force.drag)
.on('contextmenu', function(d){
d3.event.preventDefault();
var self = d3.select(this);
var n1=(self[0])[0];
console.log(d.id);
menu(d3.mouse(svg.node())[0], d3.mouse(svg.node())[1]);
});
var text = svg.append("g").selectAll("text")
.data(force.nodes())
.enter().append("text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) { return d.id; });
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", linkArc);
circle.attr("transform", transform);
text.attr("transform", transform);
}
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
</script>
In addition: For all the ones that want to know how to be able to show node details (for example the node name) in the context menu itself - this is my solution.
The node details can be taken from the "data" element
The placeholder can be taken from the "d" element
The desired information to show in the context menu has to be written into the "text" attribute
if(d.title == 'ConfigMenuPlaceholder'){
text = 'Config: '+data.name;
}
These lines should be written at the following position:
function createNestedMenu(parent, root, depth = 0) {
var resolve = function (value) {
return utils.toFactory(value).call(root, data, index);
};
parent.selectAll('li')
.data(function (d) {
var baseData = depth === 0 ? menuItems : d.children;
return resolve(baseData);
})
.enter()
.append('li')
.each(function (d) {
var elm = this;
// get value of each data
var isDivider = !!resolve(d.divider);
var isDisabled = !!resolve(d.disabled);
var hasChildren = !!resolve(d.children);
var hasAction = !!d.action;
var text = isDivider ? '<hr>' : resolve(d.title);
if(d.title == 'ConfigMenuPlaceholder'){
text = 'Config: '+data.name;
}
var listItem = d3.select(this)
.classed('is-divider', isDivider)
.classed('is-disabled', isDisabled)
.classed('is-header', !hasChildren && !hasAction)
.classed('is-parent', hasChildren)
.html(text)
.on('click', function () {
// do nothing if disabled or no action
if (isDisabled || !hasAction) return;
d.action(elm, data, index);
//d.action.call(root, data, index);
closeMenu();
});
if (hasChildren) {
// create children(`next parent`) and call recursive
var children = listItem.append('ul').classed('is-children', true);
createNestedMenu(children, root, ++depth)
}
});
}
Consider the following:
var dataAsCsv = `date,col_1,col_2
11/1/2012,1977652,1802851
12/1/2012,1128739,948687
1/1/2013,1201944,1514667
2/1/2013,1863148,1834006
3/1/2013,1314851,1906060
4/1/2013,1283943,1978702
5/1/2013,1127964,1195606
6/1/2013,1773254,977214
7/1/2013,1929574,1127450
8/1/2013,1980411,1808161
9/1/2013,1405691,1182788
10/1/2013,1336790,937890
11/1/2013,1851053,1358400
12/1/2013,1472623,1214610
1/1/2014,1155116,1757052
2/1/2014,1571611,1935038
3/1/2014,1898348,1320348
4/1/2014,1444838,1934789
5/1/2014,1235087,950194
6/1/2014,1272040,1580656
7/1/2014,980781,1680164
8/1/2014,1391291,1115999
9/1/2014,1211125,1542148
10/1/2014,1020824,1782795
11/1/2014,1685081,926612
12/1/2014,1469254,1767071
1/1/2015,1168523,935897
2/1/2015,1602610,1450541
3/1/2015,1830278,1354876
4/1/2015,1275158,1412555
5/1/2015,1560961,1839718
6/1/2015,949948,1587130
7/1/2015,1413765,1494446
8/1/2015,1166141,1305105
9/1/2015,958975,1202219
10/1/2015,902696,1023987
11/1/2015,961441,1865628
12/1/2015,1363145,1954046
1/1/2016,1862878,1470741
2/1/2016,1723891,1042760
3/1/2016,1906747,1169012
4/1/2016,1963364,1927063
5/1/2016,1899735,1936915
6/1/2016,1300369,1430697
7/1/2016,1777108,1401210
8/1/2016,1597045,1566763
9/1/2016,1558287,1140057
10/1/2016,1965665,1953595
11/1/2016,1800438,937551
12/1/2016,1689152,1221895
1/1/2017,1607824,1963282
2/1/2017,1878431,1415658
3/1/2017,1730296,1947106
4/1/2017,1956756,1696780
5/1/2017,1746673,1662892
6/1/2017,989702,1537646
7/1/2017,1098812,1592064
8/1/2017,1861973,1892987
9/1/2017,1129596,1406514
10/1/2017,1528632,1725020
11/1/2017,925850,1795575`;
var margin = {
top: 50,
right: 20,
bottom: 50,
left: 80
},
width = 1400 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// parse the date / time
// look at the .csv in Notepad! DO NOT LOOK AT EXCEL!
var parseDate = d3.timeParse("%m/%d/%Y");
var sessions_desktop_col = "#CE1126",
sessions_mobile_col = "#00B6D0";
var x = d3.scaleTime()
.range([0, width - margin.left - margin.right]);
var y = d3.scaleLinear().range([height, 0]);
var z = d3.scaleOrdinal()
.range([sessions_desktop_col, sessions_mobile_col]); // red and blue
var xMonthAxis = d3.axisBottom(x)
.ticks(d3.timeMonth.every(1))
.tickFormat(d3.timeFormat("%b")); // label every month
var xYearAxis = d3.axisBottom(x)
.ticks(d3.timeYear.every(1))
.tickFormat(d3.timeFormat("%Y")); // label every year
var yAxis = d3.axisLeft(y).tickFormat(d3.format('.2s')).tickSize(-width + margin.left + margin.right);
var formatNum = d3.format(",");
var barPad = 8;
var data = d3.csvParse(dataAsCsv, function(d, i, columns) {
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
});
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var keys = data.columns.slice(1);
data.sort(function(a, b) {
return b.date - a.date;
});
x.domain(d3.extent(data, function(d) {
return d.date
}));
var max = x.domain()[1];
var min = x.domain()[0];
var datePlusOneMonth = d3.timeDay.offset(d3.timeMonth.offset(max, 1), -1); // last day of current month: move up one month, back one day
x.domain([min, datePlusOneMonth]);
y.domain([0, d3.max(data, function(d) {
return d.total;
})]).nice();
z.domain(keys);
// x-axis
var monthAxis = g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xMonthAxis);
var defaultSliderValue = 25;
// calculate offset date based on initial slider value
var offsetDate = defaultSliderValue ? d3.timeDay.offset(d3.timeMonth.offset((new Date(datePlusOneMonth.getFullYear(), datePlusOneMonth.getMonth() - defaultSliderValue)), 1), -1) : min
// set x domain and re-render xAxis
x.domain([offsetDate, datePlusOneMonth]);
g.select('.x.axis').call(xMonthAxis);
var filteredData = data.filter(function(d) {
return d.date >= offsetDate;
});
barWidth = (width - margin.right - margin.left) / (filteredData.length + barPad);
// the bars
g.append("g").classed('bars', true)
.selectAll("g")
.data(d3.stack().keys(keys)(filteredData))
.enter().append("g")
.attr('class', function(d) {
return d.key;
})
.attr("fill", function(d) {
return z(d.key);
})
.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.data.date);
})
.attr("y", function(d) {
return y(d[1]);
})
.attr("height", function(d) {
return y(d[0]) - y(d[1]);
})
.attr("width", barWidth)
.on("mousemove", function(d) {
//Get this bar's x/y values, then augment for the tooltip
var xPosition = parseFloat(d3.mouse(this)[0]) + 88;
var yPosition = parseFloat(d3.mouse(this)[1]) + 50;
var value = d.data[d3.select(this.parentNode).attr('class')]; // differentiating between col1 and col2 values
var totalCount = d.data["total"];
var valuePercent = value / totalCount;
var valueClass = d3.select(this.parentNode).attr('class').replace("col_1", "Column 1").replace("col_2", "Column 2");
//Update the tooltip position and value
d3.select("#tooltip")
.style("left", xPosition + "px")
.style("top", yPosition + "px")
d3.select("#tooltip")
.select("#count")
.text(formatNum(value)); // return the value
d3.select("#tooltip")
.select("#totalCount")
.text(formatNum(totalCount)); // return the value
d3.select("#tooltip")
.select("#percent")
.text((valuePercent * 100).toFixed(1) + "%"); // return the value
d3.select("#tooltip")
.select("#month")
.text(d3.timeFormat("%B %Y")(d.data.date)); // return the value
d3.select("#tooltip")
.select("#sessionLabel")
.text(valueClass); // return the class
//Show the tooltip
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function() {
//Hide the tooltip
d3.select("#tooltip").classed("hidden", true);
});
g.selectAll("bars")
.attr("transform", "translate(0,300)");
const firstDataYear = x.domain()[0];
if (firstDataYear.getMonth() == 11) { // When .getmonth() == 11, this shows two years overlapping with each other, making the label look ugly
const firstDataYearOffset = d3.timeDay.offset(firstDataYear, 1);
var tickValues = x.ticks().filter(function(d) {
return !d.getMonth()
});
if (tickValues.length && firstDataYearOffset.getFullYear() !== tickValues[1].getFullYear()) {
tickValues = [firstDataYearOffset].concat(tickValues);
} else {
tickValues = [firstDataYearOffset];
}
} else {
var tickValues = x.ticks().filter(function(d) {
return !d.getMonth()
});
if (tickValues.length && firstDataYear.getFullYear() !== tickValues[0].getFullYear()) {
tickValues = [firstDataYear].concat(tickValues);
} else {
tickValues = [firstDataYear];
}
}
xYearAxis.tickValues(tickValues);
var yearAxis = g.append("g")
.attr("class", "yearaxis axis")
.attr("transform", "translate(0," + (height + 25) + ")")
.call(xYearAxis);
var valueAxis = g.append("g")
.attr("class", "y axis grid")
.call(yAxis);
monthAxis.selectAll("g").select("text")
.attr("transform", "translate(" + barWidth / 2 + ",0)");
var options = d3.keys(data[0]).filter(function(key) {
return key !== "date";
}).reverse();
var legend = svg.selectAll(".legend")
.data(options.slice().filter(function(type) {
return type != "total"
}))
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.attr('class', function(d) {
return d;
})
.style("fill", z)
.on("mouseover", function(d) {
if (d == 'col_2') {
d3.selectAll(".col_2").style("fill", '#008394');
}
if (d == 'col_1') {
d3.selectAll(".col_1").style("fill", '#80061b');
};
})
.on("mouseout", function() {
d3.selectAll(".col_2").style("fill", col_2_col);
d3.selectAll(".col_1").style("fill", col_1_col);
});
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return capitalizeFirstLetter(d.replace("col_1", "Column 1").replace("col_2", "Column 2"));
})
.on("mouseover", function(d) {
if (d == 'col_2') {
d3.selectAll(".col_2").style("fill", '#008394');
}
if (d == 'col_1') {
d3.selectAll(".col_1").style("fill", '#80061b');
};
})
.on("mouseout", function() {
d3.selectAll(".col_2").style("fill", col_2_col);
d3.selectAll(".col_1").style("fill", col_1_col);
});;
// Initialize jQuery slider
$('div#month-slider').slider({
min: 1,
max: 25,
value: defaultSliderValue,
create: function() {
// add value to the handle on slider creation
$(this).find('.ui-slider-handle').html($(this).slider("value"));
},
slide: function(e, ui) {
// change values on slider handle and label based on slider value
$(e.target).find('.ui-slider-handle').html(ui.value);
// calculate offset date based on slider value
var offsetDate = ui.value ? d3.timeDay.offset(d3.timeMonth.offset((new Date(datePlusOneMonth.getFullYear(), datePlusOneMonth.getMonth() - ui.value)), 1), -1) : min
// set x domain and re-render xAxis
x.domain([offsetDate, datePlusOneMonth]);
g.select('.x.axis').call(xMonthAxis);
const firstDataYear = x.domain()[0];
if (firstDataYear.getMonth() == 11) { // When .getmonth() == 11, this shows two years overlapping with each other, making the label look ugly
const firstDataYearOffset = d3.timeDay.offset(firstDataYear, 1);
var tickValues = x.ticks().filter(function(d) {
return !d.getMonth()
});
if (tickValues.length == 2 && firstDataYearOffset.getFullYear() !== tickValues[1].getFullYear()) {
tickValues = [firstDataYearOffset].concat(tickValues);
} else {
tickValues = [firstDataYearOffset];
}
} else {
var tickValues = x.ticks().filter(function(d) {
return !d.getMonth()
});
if (tickValues.length && firstDataYear.getFullYear() !== tickValues[0].getFullYear()) {
tickValues = [firstDataYear].concat(tickValues);
} else {
tickValues = [firstDataYear];
}
}
xYearAxis.tickValues(tickValues);
g.select('.yearaxis.axis').call(xYearAxis);
// calculate filtered data based on new offset date, set y axis domain and re-render y axis
var filteredData = data.filter(function(d) {
return d.date >= offsetDate;
});
y.domain([0, d3.max(filteredData, function(d) {
return d.total;
})]).nice();
g.select('.y.axis').transition().duration(200).call(yAxis);
// re-render the bars based on new filtered data
// the bars
var bars = g.select("g.bars")
.selectAll("g")
.data(d3.stack().keys(keys)(filteredData));
var barRects = bars.enter().append("g").merge(bars)
.attr('class', function(d) {
return d.key;
})
.attr("fill", function(d) {
return z(d.key);
})
.selectAll("rect")
.data(function(d) {
return d;
});
barRects.exit().remove();
barRects.enter()
.append("rect");
barWidth = (width - margin.right - margin.left) / (filteredData.length + barPad);
monthAxis.selectAll("g").select("text")
.attr("transform", "translate(" + barWidth / 2 + ",0)");
g.select("g.bars").selectAll('g rect')
.attr("x", function(d) {
return x(d.data.date);
})
.attr("y", function(d) {
return y(d[1]);
})
.attr("height", function(d) {
return y(d[0]) - y(d[1]);
})
.attr("width", barWidth)
.on("mousemove", function(d) {
//Get this bar's x/y values, then augment for the tooltip
var xPosition = parseFloat(d3.mouse(this)[0]) + 88;
var yPosition = parseFloat(d3.mouse(this)[1]) + 50;
var value = d.data[d3.select(this.parentNode).attr('class')]; // differentiating between col1 and col2 values
var totalCount = d.data["total"];
var valuePercent = value / totalCount;
var valueClass = d3.select(this.parentNode).attr('class').replace("col_1", "Column 1").replace("col_2", "Column 2");
//Update the tooltip position and value
d3.select("#tooltip")
.style("left", xPosition + "px")
.style("top", yPosition + "px")
d3.select("#tooltip")
.select("#count")
.text(formatNum(value)); // return the value
d3.select("#tooltip")
.select("#totalCount")
.text(formatNum(totalCount)); // return the value
d3.select("#tooltip")
.select("#percent")
.text((valuePercent * 100).toFixed(1) + "%"); // return the value
d3.select("#tooltip")
.select("#month")
.text(d3.timeFormat("%B %Y")(d.data.date)); // return the value
d3.select("#tooltip")
.select("#sessionLabel")
.text(valueClass); // return the class
//Show the tooltip
d3.select("#tooltip").classed("hidden", false);
})
.on("mouseout", function() {
//Hide the tooltip
d3.select("#tooltip").classed("hidden", true);
});
}
});
g.append("text")
.attr("class", "title")
.attr("x", (width / 2))
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.text("Title");
#tooltip {
position: absolute;
width: 290px;
z-index: 2;
height: auto;
padding: 10px;
background-color: white;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
pointer-events: none;
}
.legend {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 60%;
}
#tooltip.hidden {
display: none;
}
#tooltip p {
margin: 0;
font-family: sans-serif;
font-size: 16px;
line-height: 20px;
}
g[class="col_1"] rect:hover {
fill: #80061b;
}
g[class="col_2"] rect:hover {
fill: #008394;
}
div.slider-container {
margin: 20px 20px 20px 80px;
}
div#month-slider {
width: 50%;
margin: 0px 0px 0px 330px;
background: #e3eff4;
}
div#month-slider .ui-slider .ui-slider-handle {
width: 25%;
}
div#month-slider .ui-slider-handle {
text-align: center;
}
.title {
font-size: 30px;
font-weight: bold;
}
.grid line {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
pointer-events: none;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js" integrity="sha256-eGE6blurk5sHj+rmkfsGYeKyZx3M4bG+ZlFyA7Kns7E=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.7.0/d3-legend.min.js"></script>
<body>
<div id="tooltip" class="hidden">
<p><strong>Month: </strong><span id="month"></span>
<p>
<p><strong><span id="sessionLabel"></span>: </strong><span id="count"></span> (<span id="percent"></span>)</p>
<p><strong>Total: </strong><span id="totalCount"></span></p>
</div>
<div class="slider-container" style='inline-block;'>
<span style='margin-left:6em;display:inline-block;position:relative;top:15px;'><strong>Number of Months Displayed:</strong></span>
<div id="month-slider" style='inline-block;'></div>
</div>
I would like the title to be above the slider in this. My understanding for why this isn't working is because the slider is in a div completely separate from the svg element. The thing that makes the most sense here, then, is to insert the div with class slider-container into the svg element. How can this be done?
Here's your options:
Use a forigenObject svg node to insert your html slider but this is poorly supported under IE.
Remove the title from your svg make it html.
Create an svg slider like here.
Is there a way to add zoomable treemap in RAW. Using the enter-update-exit pattern of d3.js does not make sense in RAW, so without this functionality, is it possible to add d3.js zoomable treemap in RAW.js
The RAW docs in adding new chart says
In this way, D3's enter-update-exit pattern does not make too much
sense within RAW's charts, since the selection is always empty when
passed to the draw function. Since RAW is meant to be a tool for the
production of non-interactive visualizations, to be elaborated using
vector-graphics tools, this should not be perceived as a limitation,
but, at the contrary, as a way to simplify charts' drawing code.
I have been able to add zoomable treemap in RAW. Following is the code.
zoomableTreemap.js
(function(){
var tree = raw.models.tree();
var chart = raw.chart()
.title('Zoomable Treemap')
.description(
"A space filling visualization of data hierarchies and proportion between elements. The different hierarchical levels create visual clusters through the subdivision into rectangles proportionally to each element's value. Treemaps are useful to represent the different proportion of nested hierarchical data structures.<br/>Based on <a href='http://bl.ocks.org/mbostock/4063582'>http://bl.ocks.org/mbostock/4063582</a>")
.thumbnail("/raw/imgs/treemap.png")
.category('Hierarchies')
.model(tree)
var rawWidth = chart.number()
.title('Width')
.defaultValue(100)
.fitToWidth(true)
var rawHeight = chart.number()
.title("Height")
.defaultValue(500)
var padding = chart.number()
.title("Padding")
.defaultValue(0)
var colors = chart.color()
.title("Color scale")
chart.draw(function (selection, root){
root.name = 'ZoomableTree';
var margin = {top: 20, right: 0, bottom: 0, left: 0},
width = +rawWidth(),
height = +rawHeight() - margin.top - margin.bottom,
formatNumber = d3.format(",d"),
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
console.log(padding());
console.log(+padding());
var treemap = d3.layout.treemap()
.padding(+padding())
.children(function(d, depth) { return depth ? null : d._children; })
.sort(function(a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
// Values are required in d3 treemap layout
// and our DB table do not have values field in it, so we are going to use 1 for all nodes.
// The value decides the size/area of rectangle in d3 treemap layout so effectively we are going to have
// even sized rectangles
.value(function(d) { return 1; })
.round(false);
var svg = selection
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges")
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top)
.style("fill", function (d) { return colors()(d.color); })
.style("stroke","#fff")
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
initialize(root);
//console.log(root);
//throw '';
accumulate(root);
layout(root);
display(root);
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
? d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0)
: d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
//console.log(d);
//throw 'stop';
treemap.nodes({_children: d._children});
d._children.forEach(function(c) {
//console.log(d);
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
//console.log(c);
layout(c);
});
}
}
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) { return d._children; })
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function(d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function(d) { return formatNumber(d.value); })
.style("fill", function (d) { return colors()(d.color); })
.style("stroke","#fff")
g.append("text")
.attr("dy", ".75em")
.text(function(d) { return d.name; })
.call(text);
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function(d) { return x(d.x) + 6; })
.attr("y", function(d) { return y(d.y) + 6; });
}
function rect(rect) {
rect.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y); })
.attr("width", function(d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function(d) { return y(d.y + d.dy) - y(d.y); })
.style("fill", function (d) { return colors()(d.color); })
.style("stroke","#fff");
}
function name(d) {
return d.parent
? name(d.parent) + "." + d.name
: d.name;
}
})
})();
chart.css
#chart {
background: #ddd;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
.grandparent rect {
fill: orange;
}
.grandparent:hover rect {
fill: #ee9700;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #bbb;
}
Index.html
Just include css and js in index page of raw.
I have a responsive force directed graph, and it's working great, except I can't get it to stay within the browser screen.
The D3 code suggested to create the bounding box is :
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)); });
However, this does NOT work, because I'm not defining a width (it's responsive) and I'm not defining r because it's a function, so the nodes are different sizes.
I tried to set:
var r= function(d) {return d.instances;};
Because that's where I'm putting the node size from, but it doesn't work... All of the force directed examples I see use the same size nodes.. I'm just not familiar enough with javascript to figure out a workaround... help?
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
*{
margin:0;
padding:0;
}
/*
svg {
display: block;
width: 100%;
margin: 0;
}
*/
.node {
stroke: #fff;
stroke-width: 1.5px;
}
#graph{
max-width:100%;
height:100vh ;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
.node-active{
stroke: #555;
stroke-width: 1.5px;
}
.node:hover{
stroke: #555;
stroke-width: 1.5px;
}
marker {
display:none;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 200%;
left: 0;
}
script {
display:none;
}
</style>
<body>
<div id="graph"></div>
<script src="d3/d3.js"></script>
<script src="d3/d3tip.js"></script>
<script type='text/javascript' src='http://code.jquery.com/jquery-1.11.0.js'></script>
<script>
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-2010)
.linkDistance(function(d) { return d.distance; })
// .size([width, height])
.gravity(0.7);
//var width = 1000,
// height = 1000;
var svg = d3.select("#graph")
.append("svg")
.attr({
"width": "100%",
"height": "100%"
})
// .attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMid meet") //.attr("pointer-events", "all")
.call(d3.behavior.zoom().on("zoom", redraw));
var vis = svg
.append('svg:g');
function redraw() {
vis.attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function (d) {
return d.name + " (" + d.instances + ")";
})
svg.call(tip);
d3.json("datawords.json", function(error, graph) {
var link = vis.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("width", function(d) { return d.totalLength; })
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = vis.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) {return d.instances;})
.style("fill", function(d) { return color(d.instances); })
.call(force.drag)
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.on('click', connectedNodes)
force
.nodes(graph.nodes)
.links(graph.links)
.start();
force.on("tick", function() {
node[0].x = svg / 2;
node[0].y = svg / 2;
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 = 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)); });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.each(collide(0.5));
});
///////////////////
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
//Reduce the op
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
toggle = 0;
};
};
var padding = 10, // separation between circles
radius=15;
function collide(alpha) {
var quadtree = d3.geom.quadtree(graph.nodes);
return function(d) {
var rb = 4*radius + padding,
nx1 = d.x - rb,
nx2 = d.x + rb,
ny1 = d.y - rb,
ny2 = d.y + rb;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y);
if (l < rb) {
l = (l - rb) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
};
window.addEventListener('resize', resize);
function resize() {
width = window.innerWidth, height = window.innerHeight;
svg.attr("width", width).attr("height", height);
force.size([width, height]).resume();
}
});
</script>
</body>
I cannot comment due to level restrictions however, I believe the issue is caused from your width/height variables. Uncomment your var width/height and change it to var width = innerWidth, height = innerHeight;. Then in your svg set the attr to .attr('width', width).attr('height', height);.
node.attr("cx", (function(w) {
return function(d) {
var r = d.instances;
return d.x = Math.max(r, Math.min(w - r, d.x));
}
})(width()))
.attr("cy", (function(h) {
return function(d) {
var r = d.instances;
return d.y = Math.max(r, Math.min(h - r, d.y));
}
})(height()));
And define width() and height() as functions returning live values.