rotate points from csv on d3 orthogonal projection - javascript

I have been trying to project a heat map with data loaded from csv onto a orthogonal projection on D3. While rotating the earth (i.e. D3, orthogonal projection), the points/circles remain static. I have tried many combinations but failed to figure out what is missing.
Basically, i need the small circles move along the path of countries.
Here is the complete code :
<script>
var width = 600,
height = 500,
sens = 0.25,
focused;
//Setting projection
var projection = d3.geo.orthographic()
.scale(245)
.rotate([0,0])
.translate([width / 2, height / 2])
.clipAngle(90);
var path = d3.geo.path()
.projection(projection);
//SVG container
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// Define the gradient
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#FFFF00")
.attr("stop-opacity", 0);
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#FF0000")
.attr("stop-opacity", 1);
//Adding water
svg.append("path")
.datum({type: "Sphere"})
.attr("class", "water")
.attr("d", path)
var countryTooltip = d3.select("body").append("div").attr("class", "countryTooltip"),
countryList = d3.select("body").append("select").attr("name", "countries");
queue()
.defer(d3.json, "world-110m.json")
.defer(d3.tsv, "world-110m-country-names.tsv")
.await(ready);
//Main function
function ready(error, world, countryData) {
var countryById = {},
countries = topojson.feature(world, world.objects.countries).features;
//Adding countries to select
countryData.forEach(function(d) {
countryById[d.id] = d.name;
option = countryList.append("option");
option.text(d.name);
option.property("value", d.id);
});
//circles for heatmap are coming from the csv below
d3.csv("cities.csv", function(error, data) {
svg.selectAll("circle")
.data(data)
.enter()
.append("a")
.attr("xlink:href", function(d) {
return "https://www.google.com/search?q="+d.city;}
)
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5.5)
.attr('fill', 'url(#gradient)');
var world = svg.selectAll("path.circle")
.data(countries) //countries from the tsc file is used to populate the names
.enter().append("path")
.attr("class", "land")
.attr("d", path)
//.attr('fill', 'url(#gradient)')
//Drag event
.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: r[0] / sens, y: -r[1] / sens}; })
.on("drag", function() {
var rotate = projection.rotate();
projection.rotate([d3.event.x * sens, -d3.event.y * sens, rotate[2]]);
svg.selectAll("path.land").attr("d", path);
svg.selectAll(".focused").classed("focused", focused = false);
}))
//Mouse events
.on("mouseover", function(d) {
countryTooltip.text(countryById[d.id])
.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("display", "block")
.style("opacity", 1);
})
.on("mouseout", function(d) {
countryTooltip.style("opacity", 0)
.style("display", "none");
})
.on("mousemove", function(d) {
countryTooltip.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px");
});
});//closing d3.csv here
//Country focus on option select
d3.select("select").on("change", function() {
var rotate = projection.rotate(),
focusedCountry = country(countries, this),
p = d3.geo.centroid(focusedCountry);
svg.selectAll(".focused").classed("focused", focused = false);
//Globe rotating
(function transition() {
d3.transition()
.duration(2500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
svg.selectAll("path").attr("d", path)
.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
//svg.selectAll("circle").attr("d", data)
//.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
};
})
})();
});
function country(cnt, sel) {
for(var i = 0, l = cnt.length; i < l; i++) {
if(cnt[i].id == sel.value) {return cnt[i];}
}
};
};
</script>
Please help.
Thank you in advance

Here is an option, using point geometries:
.enter().append('path')
.attr('class', 'circle_el')
.attr('fill', function(d) {return d.fill; })
.datum(function(d) {
return {type: 'Point', coordinates: [d.lon, d.lat], radius: some_radius};
})
.attr('d', path);
This is cool because you will update the circles simultaneously with a path redraw. And in addition it will account for the projection as a sphere, not showing circles that should be on the non-visible side of the sphere. I got the idea from this post by Jason Davies.

Related

Return simulation and radius value for Bubble Graph after applying nest() in D3 v4

I refered to this video in YouTube to make a bubble graph. However, the author didn't use a nest function to group his data. After I pre-processed my data using nest() function, I don't know how to pass the value to a function called radiusScale() in my code. I was thinking maybe I should pass the value of
d3.entries(groupByAgeAndtime)[i]["value"]
to radiusScale().
Here is my code snippet for my problem.
var radiusScale = d3.scaleSqrt()
.domain([d3.min(Object.values(groupByAgeAndtime), function(d){
return d.mean_time_in_hospital;
}),d3.max(Object.values(groupByAgeAndtime), function(d){
return d.mean_time_in_hospital;
})])
.range([50,150]);
for (i = 0; i < 10; i++)
{
console.log(d3.entries(groupByAgeAndtime)[i]["value"]);
}
var simulation = d3.forceSimulation()
.force("x",d3.forceX(width/2).strength(0.05))
.force("y",d3.forceY(height/2).strength(0.05))
.force("collide", d3.forceCollide(function(d){
return radiusScale(d.mean_time_in_hospital) + 2;
}))
var circles = svg.selectAll(".artist")
.data(groupByAgeAndtime)
.enter()
.append("circle")
.attr("class","artist")// the "artist" will transform into class name in HTML
.attr("r", function(d){
return radiusScale(Object.values(groupByAgeAndtime))
})
.attr("fill","lightblue")
.on("click",function(d){
console.log(d)
})
This is the screenshot: for the thing I want to pass to the function radiusScale. I think after passing the correct value, the circle will appear immediately. If not, can anyone tell me what is the value I should pass to get a circle?
Here is my JSFiddle for my js, html and .csv file. I would really appreciate anyone who can tell me what value should I pass to the function.
The grouped data groupByAgeAndtime using d3.nest() has to be used on your simulation and circle drawing.
Note that your radiusScale now gets the correct value to be mapped to chosen range range([50, 150]);
var simulation = d3.forceSimulation()
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d.mean_time_in_hospital);
}))
simulation.nodes(Object.values(groupByAgeAndtime))
.on('tick', ticked)
The same for the circles, and the circles radius now matches the simulation radius
var circles = svg.selectAll(".artist")
.data(Object.values(groupByAgeAndtime))
.enter()
.append("circle")
.attr("class", "artist")
.attr("r", function(d) {
return radiusScale(d.mean_time_in_hospital)
})
.attr("fill", "lightblue")
.on("click", function(d) {
console.log(d)
})
Here is the functional example, your text still needs to be implemented.
I've pasted your csv data here https://hastebin.com/raw/pasacimala
(function() {
var width = 800,
height = 350;
var svg = d3.select("#chart")
.append("svg")
.attr("height", height)
.attr("width", width)
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio","xMidYMid meet")
.append("g")
.attr("transform", "translate(0,0)");
// import csv file
d3.csv("https://cors-anywhere.herokuapp.com/https://hastebin.com/raw/pasacimala")
.then(function(d) {
//data preprocessing
d.forEach(e => {
e.age = e.age.replace("[", "").replace(")", "");
e.time_in_hospital = + e.time_in_hospital;
});
return d; //must return something
})
.then((data, err) => ready(err, data))
function ready(error, datapoints) {
var groupByAgeAndtime = d3.nest()
.key(function(d) {
return d.age;
})
//.key(function(d) { return d.time_in_hospital; })
.rollup(function(v) {
return {
mean_time_in_hospital: d3.mean(v, function(d) {
return d.time_in_hospital;
})
}
})
.object(datapoints); //specify the dataset used
/**************************************** SCALING PART **************************************************/
var radiusScale = d3.scaleSqrt()
.domain([d3.min(Object.values(groupByAgeAndtime), function(d) {
return d.mean_time_in_hospital;
}), d3.max(Object.values(groupByAgeAndtime), function(d) {
return d.mean_time_in_hospital;
})])
.range([50, 150]);
/* for (i = 0; i < 10; i++) {
//console.log(d3.entries(groupByAgeAndtime)[i]["key"]);
console.log(d3.entries(groupByAgeAndtime)[i]["value"]);
} */
console.log(Object.values(groupByAgeAndtime))
// STUCK HERE
var simulation = d3.forceSimulation()
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d.mean_time_in_hospital);
}))
// END OF STUCK HERE
var circles = svg.selectAll(".artist")
.data(Object.values(groupByAgeAndtime))
.enter()
.append("circle")
.attr("class", "artist")
.attr("r", function(d) {
return radiusScale(d.mean_time_in_hospital)
})
.attr("fill", "lightblue")
.on("click", function(d) {
console.log(d)
})
// append = add something
// text
var texts = svg.selectAll('.text')
.data(Object.keys(groupByAgeAndtime))
.enter()
.append('text')
.text(e => e)
.attr("text-anchor", "middle")
.attr('color', 'black')
.attr('font-size', '13')
simulation.nodes(Object.values(groupByAgeAndtime))
.on('tick', ticked)
function ticked() {
texts
.attr("x", function(d) {
return d.x
})
.attr("y", function(d) {
return d.y
})
circles
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
}
}
})();
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="chart"></div>

how to change stack order of text label in JavaScript?

I am trying to plot a network graph using networkD3 in R. I wanted to make some changes to the display so that the text labels (which appears when mouseover) can be easily read.
Please refer to the link here for an example. Note: Jump to the d3ForceNetwork plot.
As seen in the example, the labels are hard to read due to its colour and it often gets obstructed by the surrounding nodes. I have been messing around with the JS file and managed to change the text label color to black. However, having no knowledge of JS or CSS (I can't even tell the difference between the 2 actually), I have no idea how I can change the stack order such that the text labels will always be displayed above any other objects.
Can anyone advise me on how I can achieve the desired outcome?
Below is the full JS file:
HTMLWidgets.widget({
name: "forceNetwork",
type: "output",
initialize: function(el, width, height) {
d3.select(el).append("svg")
.attr("width", width)
.attr("height", height);
return d3.layout.force();
},
resize: function(el, width, height, force) {
d3.select(el).select("svg")
.attr("width", width)
.attr("height", height);
force.size([width, height]).resume();
},
renderValue: function(el, x, force) {
// Compute the node radius using the javascript math expression specified
function nodeSize(d) {
if(options.nodesize){
return eval(options.radiusCalculation);
}else{
return 6}
}
// alias options
var options = x.options;
// convert links and nodes data frames to d3 friendly format
var links = HTMLWidgets.dataframeToD3(x.links);
var nodes = HTMLWidgets.dataframeToD3(x.nodes);
// get the width and height
var width = el.offsetWidth;
var height = el.offsetHeight;
var color = eval(options.colourScale);
// set this up even if zoom = F
var zoom = d3.behavior.zoom();
// create d3 force layout
force
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(options.linkDistance)
.charge(options.charge)
.on("tick", tick)
.start();
// thanks http://plnkr.co/edit/cxLlvIlmo1Y6vJyPs6N9?p=preview
// http://stackoverflow.com/questions/22924253/adding-pan-zoom-to-d3js-force-directed
var drag = force.drag()
.on("dragstart", dragstart)
// allow force drag to work with pan/zoom drag
function dragstart(d) {
d3.event.sourceEvent.preventDefault();
d3.event.sourceEvent.stopPropagation();
}
// select the svg element and remove existing children
var svg = d3.select(el).select("svg");
svg.selectAll("*").remove();
// add two g layers; the first will be zoom target if zoom = T
// fine to have two g layers even if zoom = F
svg = svg
.append("g").attr("class","zoom-layer")
.append("g")
// add zooming if requested
if (options.zoom) {
function redraw() {
d3.select(el).select(".zoom-layer").attr("transform",
"translate(" + d3.event.translate + ")"+
" scale(" + d3.event.scale + ")");
}
zoom.on("zoom", redraw)
d3.select(el).select("svg")
.attr("pointer-events", "all")
.call(zoom);
} else {
zoom.on("zoom", null);
}
// draw links
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.style("stroke", function(d) { return d.colour ; })
//.style("stroke", options.linkColour)
.style("opacity", options.opacity)
.style("stroke-width", eval("(" + options.linkWidth + ")"))
.on("mouseover", function(d) {
d3.select(this)
.style("opacity", 1);
})
.on("mouseout", function(d) {
d3.select(this)
.style("opacity", options.opacity);
});
// draw nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.style("fill", function(d) { return color(d.group); })
.style("opacity", options.opacity)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.call(force.drag);
node.append("circle")
.attr("r", function(d){return nodeSize(d);})
.style("stroke", "#fff")
.style("opacity", options.opacity)
.style("stroke-width", "1.5px");
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name })
.style("font", options.fontSize + "px " + options.fontFamily)
.style("opacity", options.opacityNoHover)
.style("pointer-events", "none");
function tick() {
node.attr("transform", function(d) {
if(options.bounded){ // adds bounding box
d.x = Math.max(nodeSize(d), Math.min(width - nodeSize(d), d.x));
d.y = Math.max(nodeSize(d), Math.min(height - nodeSize(d), d.y));
}
return "translate(" + d.x + "," + d.y + ")"});
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; });
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style('position','relative')
.style("opacity", 1);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d);});
d3.select(this).select("text").transition()
.duration(1250)
.attr("x", 0)
.style("font", options.fontSize + "px ")
.style("opacity", options.opacityNoHover);
}
function click(d) {
return eval(options.clickAction)
}
// add legend option
if(options.legend){
var legendRectSize = 18;
var legendSpacing = 4;
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = legendRectSize;
var vert = i * height+4;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.style('fill', 'darkOrange')
.text(function(d) { return d; });
}
// make font-family consistent across all elements
d3.select(el).selectAll('text').style('font-family', options.fontFamily);
},
});
I suspect I need to make some changes to the code over here:
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style("opacity", 1);
}
You need to resort the node groups holding the circles and text so the currently mouseover'ed one is the last in that group, and thus the last one drawn so it appears on top of the others. See the first answer here -->
Updating SVG Element Z-Index With D3
In your case, if your data doesn't have an id field you may have to use 'name' instead as below (adapted to use the mouseover function you've got):
function mouseover(d) {
d3.selectAll("g.node").sort(function (a, b) {
if (a.name != d.name) return -1; // a is not the hovered element, send "a" to the back
else return 1; // a is the hovered element, bring "a" to the front (by making it last)
});
// your code continues
The pain might be that you have to do this edit for every d3 graph generated by this R script, unless you can edit the R code/package itself. (or you could suggest it to the package author as an enhancement.)

Change single chord color in chord diagram using D3.js

D3 and Javascript newbie here. I'd like to change the color of a single chord in a chord diagram that is rendered with D3. Ideally, this color can be arbitrary, with no relationship to the source/destination entities of the chord.
How do I identify a single chord so I can later access it to fill it?
Here's an image (poorly edited with an image editor) with the desired effect (green chord).
var matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
var chord = d3.layout.chord()
.padding(.05)
.sortSubgroups(d3.descending)
.matrix(matrix);
var width = 960,
height = 500,
innerRadius = Math.min(width, height) * .41,
outerRadius = innerRadius * 1.1;
var fill = d3.scale.ordinal()
.domain(d3.range(4))
.range(["#000000", "#FFDD89", "#957244", "#F26223"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("g").selectAll("path")
.data(chord.groups)
.enter().append("path")
.style("fill", function(d) { return fill(d.index); })
.style("stroke", function(d) { return fill(d.index); })
.attr("d", d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius))
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
var ticks = svg.append("g").selectAll("g")
.data(chord.groups)
.enter().append("g").selectAll("g")
.data(groupTicks)
.enter().append("g")
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + outerRadius + ",0)";
});
ticks.append("line")
.attr("x1", 1)
.attr("y1", 0)
.attr("x2", 5)
.attr("y2", 0)
.style("stroke", "#000");
ticks.append("text")
.attr("x", 8)
.attr("dy", ".35em")
.attr("transform", function(d) { return d.angle > Math.PI ? "rotate(180)translate(-16)" : null; })
.style("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.text(function(d) { return d.label; });
svg.append("g")
.attr("class", "chord")
.selectAll("path")
.data(chord.chords)
.enter().append("path")
.attr("d", d3.svg.chord().radius(innerRadius))
.style("fill", function(d) { return fill(d.target.index); })
.style("opacity", 1)
.on("mouseover", function(){
d3.select(this).style("opacity", 0.3);
})
.on("mouseout", function(){
d3.select(this).style("opacity", 1);
});
// Returns an array of tick angles and labels, given a group.
function groupTicks(d) {
var k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, 1000).map(function(v, i) {
return {
angle: v * k + d.startAngle,
label: i % 5 ? null : v / 1000 + "k"
};
});
}
// Returns an event handler for fading a given chord group.
function fade(opacity) {
return function(g, i) {
svg.selectAll(".chord path")
.filter(function(d) { return d.source.index != i && d.target.index != i; })
.transition()
.style("opacity", opacity);
};
}
body {
font: 10px sans-serif;
}
.chord path {
fill-opacity: .67;
stroke: #000;
stroke-width: .5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I've just added functionality for mouse over and mouse out,
svg.append("g")
.attr("class", "chord")
.selectAll("path")
.data(chord.chords)
.enter().append("path")
.attr("d", d3.svg.chord().radius(innerRadius))
.style("fill", function(d) { return fill(d.target.index); })
.style("opacity", 1)
.on("mouseover", function(){
d3.select(this).style("opacity", 0.3);
})
.on("mouseout", function(){
d3.select(this).style("opacity", 1);
});
In the above code see the mouseover and mouseout callbacks,
Here I'm just changing the opacity, if you want to change the color, use fill attribute to fill the color.
Hope you are looking for this.
If not ask me for more.
:D

D3 Pie Chart: Arctween in mouseOver doesn't work

Problem: The Arctween function will not work on the .on("mouseOver").
Intention: When hovering the arcs in the Pie Chart a highlight needs to start (opacity etc.) and information needs (infoHover) to show, next to the Arctween that I also want to activate.
I am aware that the code is not perfect at all, I'm just experimenting with d3.js.
Thanks in advance!
Javascript:
d3.json("dataExample.json", function (data) {
var width = 260,
height = 260;
var outerRadius = height / 2 - 20,
innerRadius = outerRadius / 3,
cornerRadius = 10;
colors = d3.scale.category20c();
var tempColor;
var pie = d3.layout.pie()
.padAngle(.02)
.value(function(d) {
return d.value;
})
var arc = d3.svg.arc()
.padRadius(outerRadius)
.innerRadius(innerRadius);
var infoHover = d3.select('#chart').append('div')
.style('position', 'absolute')
.style('padding', '0 30px')
.style('opacity', 0)
function arcTween(outerRadius, delay) {
return function() {
d3.select(this).transition().delay(delay).attrTween("d", function(d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function(t) { d.outerRadius = i(t); return arc(d); };
});
};
}
var svg = d3.select("#chart").append("svg")
.data(data)
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll('path').data(pie(data))
.enter().append('path')
.attr('fill', function(d, i) {
return colors(i);
})
.each(function(d) { d.outerRadius = outerRadius - 20; })
.attr('d', arc)
.on("mouseover", function(d) {
infoHover.transition()
.style('opacity', .9)
.style('left', '85px')
.style('top', '120px')
infoHover.html(d.value + '%')
d3.selectAll("path")
.transition()
.duration(500)
.style("opacity", .18)
d3.select(this)
.transition()
.duration(500)
.style('opacity', 1)
.style('cursor', 'pointer')
arcTween(outerRadius, 0);
})
.on("mouseout", function(d) {
d3.selectAll("path")
.transition()
.duration(500)
.style("opacity", 1)
d3.select(this)
.style('opacity', 1)
arcTween(outerRadius - 20, 150);
});
});
Your arcTween returns a function your need to call:
arcTween(outerRadius, 0).call(this);

Dragging and panning in d3 force layout

I'm working on a force layout graph that displays relationships of writers. Since there are so many, I tried to implement zooming and dragging. Zooming works fine (with one exception), but when I drag a node it also drags the background. I tried following Mike Bostock's directions here and the StackOverflow question paired with it, but it still won't work. I based most of the code for the graph on this, which works beautifully, but since he used an older version of d3, his dragging breaks in the new version. (I can't just use the older version of d3 because I have some other parts of the graph not shown here that work only with the newer version.)
I think the problem has something to do with my grouping of SVG objects, but I also can't figure out what I'm doing wrong there. This also brings in the one zooming problem; when I zoom in or pan around, the legend also moves and zooms in. If there's an easy fix to make it stay still and sort of "hover" above the graph, that would be great.
I'm very new to coding, so I'm probably making really stupid mistakes, but any help would be appreciated.
Fiddle.
var graphData = {
nodes: [
{
id:0,
name:"Plotinus"
},
{
id:1,
name:"Iamblichus"
},
{
id:2,
name:"Porphyry"
}
],
links: [
{
relationship:"Teacher/student",
source:0,
target:1
},
{
relationship:"Enemies",
source:0,
target:2
},
{
relationship:"Family",
source:1,
target:2
}
]
};
var linkColor = d3.scale.category10(); //Sets the color for links
var drag = d3.behavior.drag()
.on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
.on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
});
var w = 300,
h = 300;
var vis = d3.select(".graph")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("pointer-events", "all")
.append('svg:g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('svg:g');
vis.append('svg:rect')
.attr('width', w)
.attr('height', h)
.attr('fill', 'rgba(1,1,1,0)');
function redraw() {
vis.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); }
var force = d3.layout.force()
.gravity(.6)
.charge(-600)
.linkDistance( 60 )
.size([w, h]);
var svg = d3.select(".text").append("svg")
.attr("width", w)
.attr("height", h);
var link = vis.selectAll("line")
.data(graphData.links)
.enter().append("line")
.style("stroke", function(d) { return linkColor(d.relationship); })
.style("stroke-width", 1)
.attr("class", "connector");
var node = vis.selectAll("g.node")
.data(graphData.nodes)
.enter().append("svg:g")
.attr("class","node")
.call(force.drag);
node.append("svg:circle")
.attr("r", 10) //Adjusts size of nodes' radius
.style("fill", "#ccc");
node.append("svg:text")
.attr("text-anchor", "middle")
.attr("fill","black")
.style("pointer-events", "none")
.attr("font-size", "9px")
.attr("font-weight", "100")
.attr("font-family", "sans-serif")
.text( function(d) { return d.name;} );
// Adds the legend.
var legend = vis.selectAll(".legend")
.data(linkColor.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(-10," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", linkColor);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.attr("class", "legendText")
.style("text-anchor", "end")
.text(function(d) { return d; });
force
.nodes(graphData.nodes)
.links(graphData.links)
.on("tick", tick)
.start();
function tick() {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")";});
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; });
}
I think I figured it out.
I had to combine the instructions from here and here, which was sort of already answered in the answer I linked.
My old way, grabbed from the first example, looked like this:
var drag = d3.behavior.drag()
.on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); })
.on("drag", function(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
});
The problem was that I was focusing on d3.behavior.drag() instead of force.drag, which I think Stephen Thomas was trying to tell me. It should look like this:
//code code code//
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
//code code code//
var drag = force.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
You can use the drag() method of the force object instead of creating a separate drag behavior. Something like:
node.call(force.drag);
or, equivalently,
force.drag(node);
A complete example is available at http://bl.ocks.org/sathomas/a7b0062211af69981ff3
Here is what is working for me:
const zoom = d3.behavior.zoom()
.scaleExtent([.1, 10])
.on('zoom', zoomed);
const force = d3.layout.force()
.(...more stuff...);
const svg = d3.select('.some-parent-div')
.append('svg')
.attr('class', 'graph-container')
.call(zoom);
const mainGroup = svg.append('g');
var node = mainGroup.selectAll('.node');
node.enter()
.insert('g')
.attr('class', 'node')
.call(force.drag)
.on('mousedown', function(){
// line below is the key to make it work
d3.event.stopPropagation();
})
.(...more stuff...);
function zoomed(){
force.stop();
const canvasTranslate = zoom.translate();
mainGroup.attr('transform', 'translate('+canvasTranslate[0]+','+canvasTranslate[1]+')scale(' + zoom.scale() + ')');
force.resume();
}
With your code, the node can be dragged but when you drag a node other nodes will move too. I come up this to stop rest of nodes and just let you finished dragging then re-generated the whole graph
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("fixed", d.fixed = true);
}
function dragged(d) {
force.stop();
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
tick();
}
function dragended(d) {
force.resume();
}

Categories