Related
I am new to using d3.js. I am trying to append multiple text elements in a svg group. With that, I want to be able to drag the group of multiple text.
So for an example:
export function testNode (config = {}) {
let { svg, rectX, rectY, text1, text2 } = config
var data = [{ x: rectX, y:rectY, label: text1, label2: text2, labelX: rectX + 100, labelY: rectY + 200, labelX2: rectX + 300, labelY2: rectY + 300 }]
var group = svg.append("g")
.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform",
"translate(" + 0 + "," + 0 + ")")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
group.append("text")
.data(data)
.attr("x", (d) => { return d.labelX })
.attr("y", (d) => { return d.labelY })
.attr("font-size", "1em")
.attr("color", "black")
.text((d) => { return d.label });
group.append("text")
.data(data)
.attr("x", (d) => { return d.labelX2 })
.attr("y", (d) => { return d.labelY2 })
.attr("font-size", ".75em")
.attr("color", "black")
.attr("class", "label")
.text((d) => { return d.metricValue_01 });
function dragStarted() {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("text")
.attr("x", d.labelX = d3.event.x + 10)
.attr("y", d.labelY = d3.event.y + 20);
d3.select(this).select("text")
.attr("x", d.labelX2 = d3.event.x + 10)
.attr("y", d.labelY2 = d3.event.y + 20);
function dragended() {
d3.select(this).classed("active", false);
}
If I use the selectAll method, the text gets clumped together. Therefore, I was wondering if I can drag the text group in their proper position based on the coordinate I give it (while being dragged). Anyways please let me know if I need to provide any further information. Thank you
If you want to select the second label, but don't want to use selectAll as it selects both, you can give the labels classes when appending and select those when dragging:
d3.select(this).select(".bigLabel")
.attr("x", d.labelX = d3.event.x + 10)
.attr("y", d.labelY = d3.event.y + 20);
d3.select(this).select(".smallLabel")
.attr("x", d.labelX2 = d3.event.x + 10)
.attr("y", d.labelY2 = d3.event.y + 20);
Though of course this will set the same coordinate for both labels unless you specify an offset, as below:
var data = [
{ x: 100, x2: 100, y: 100, y2: 120, label: "label1", value: "17%" },
{ x: 300, x2: 300, y: 200, y2: 220, label: "label2", value: "83%" },
{ x: 100, x2: 100, y: 200, y2: 220, label: "label3", value: "11%" },
{ x: 300, x2: 300, y: 100, y2: 120, label: "label4", value: "96%" }
];
var svg = d3.select("svg");
var labels = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.call(d3.drag()
.on("drag",drag));
labels.append("text")
.attr("font-size", "1em")
.attr("x", function(d) { return d.x;})
.attr("y", function(d) { return d.y;})
.text(function(d) { return d.label; })
.attr("class","label1");
labels.append("text")
.attr("font-size", ".75em")
.attr("x", function(d) { return d.x2;})
.attr("y", function(d) { return d.y2;})
.text(function(d) { return d.value; })
.attr("class","label2");
function drag(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this)
.select(".label1")
.attr("x", function(d) { return d.x = x; })
.attr("y", function(d) { return d.y = y; })
d3.select(this)
.select(".label2")
.attr("x", function(d) { return d.x2 = x; })
.attr("y", function(d) { return d.y2 = y + 20; })
}
text {
text-anchor:middle;
cursor:pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="400"></svg>
I won't dive into the alternative in too much depth, but it applies the drag to the g, positioning both text labels at the same time. This can handle irregular spacing between sibling labels easier than the above:
var data = [
{ x: 100, y: 100, label: "label1", value: "17%" },
{ x: 300, y: 200, label: "label2", value: "83%" },
{ x: 100, y: 200, label: "label3", value: "11%" },
{ x: 300, y: 100, label: "label4", value: "96%" }
];
var svg = d3.select("svg");
var labels = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform",function(d) {
return "translate("+[d.x,d.y]+")";
})
.call(d3.drag().on("drag", drag));
labels.append("text")
.attr("font-size", "1em")
.text(function(d) { return d.label; });
labels.append("text")
.attr("font-size", ".75em")
.text(function(d) { return d.value; })
.attr("dy", "1em")
function drag(d) {
d3.select(this)
.attr("transform","translate("+[d.x=d3.event.x,d.y=d3.event.y]+")");
}
text {
text-anchor: middle;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="400"></svg>
Also, there is no need to use append("text").data(data), this is not doing anything - your datum is already bound to the newly appended element
Lastly, you can make this work with selectAll() if you set attributes using the second parameter of any provided function when setting attributes: (d,i)=>... i is the index of the element, so if your sibling labels are regularly spaced, you can use something like:
var data = [
{ x: 100, y: 100, label: "label1", value: "17%" },
{ x: 300, y: 200, label: "label2", value: "83%" },
{ x: 100, y: 200, label: "label3", value: "11%" },
{ x: 300, y: 100, label: "label4", value: "96%" }
];
var svg = d3.select("svg");
var labels = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.call(d3.drag()
.on("drag",drag));
labels.append("text")
.attr("font-size", "1em")
.attr("x", function(d) { return d.x;})
.attr("y", function(d) { return d.y;})
.text(function(d) { return d.label; })
labels.append("text")
.attr("font-size", ".75em")
.attr("x", function(d) { return d.x;})
.attr("y", function(d) { return d.y + 20;})
.text(function(d) { return d.value; })
function drag(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this)
.selectAll("text")
.attr("x", function(d) { return d.x = x; })
.attr("y", function(d,i) { d.y = y;
return d.y + i * 20;
})
}
text {
text-anchor:middle;
cursor:pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="400"></svg>
Click anywhere in the blue to add a node. It gets added. The drag does work -- I can console.log in dragstarted, dragged and dragended and see that it's being dragged. Even the other nodes properly move out of the phantom nodes way! But visually it just sits there.
I must be missing something fundamental. What am I doing wrong?
Here's the code and a fiddle
<body>
<script src="d3.min.js"></script>
<script>
var width = 640,
height = 480;
var graphNodes = [
{ id: 0, r: 5 },
{ id: 1, r: 5 },
{ id: 2, r: 5 },
{ id: 3, r: 5 },
{ id: 4, r: 5 },
{ id: 5, r: 5 },
{ id: 6, r: 5 },
{ id: 7, r: 5 },
{ id: 8, r: 5 }
];
var graphEdges = [
{ source: 0, target: 1 },
{ source: 0, target: 2 },
{ source: 0, target: 3 },
{ source: 0, target: 4 },
{ source: 0, target: 5 },
{ source: 0, target: 6 },
{ source: 0, target: 7 },
{ source: 0, target: 8 }
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'graph')
.attr('width', width)
.attr('height', height)
.attr('fill', 'lightblue')
.attr('opacity', 0.3)
.on('click', function(){
graphNodes.push({ id: graphNodes.length, r: 15 });
update(d3.mouse(this));
});
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d, i) { return i; }))
.force("collide",d3.forceCollide( function(d){return d.r + 8 }).iterations(16) )
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0));
var linkGroup = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graphEdges)
.enter().append("line")
.attr('stroke', 'gray')
.attr("stroke-width", 2);
var nodeGroup = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graphNodes)
.enter().append("circle")
.attr("r", function(d){ return d.r; })
.attr("fill", "gray")
.call(d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended));
function dragstarted(d){
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
simulation.restart();
/*d.fx = d.x;
d.fy = d.y;*/
}
function dragged(d){
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d){
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
simulation.alphaTarget(0.1);
}
function ticked(){
linkGroup
.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; });
//nodeGroup.attr("cx", function(d){ return d.x;}).attr("cy", function(d){ return d.y;})
nodeGroup.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")"; });
}
function update(coordinates) {
// Redefine and restart simulation
simulation.nodes(graphNodes).on("tick", ticked);
simulation.force("link").links(graphEdges);
// Update links
var link = linkGroup.selectAll("line").data(graphEdges),
linkEnter = link.enter().append("line");
link = linkEnter.merge(link);
link.exit().remove();
// Update nodes
var node = nodeGroup.data(graphNodes),
nodeEnter = node.enter().append("circle")
.attr("r", function(d){ return d.r; })
.attr("fill", "gray")
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
.call(d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended));
node = nodeEnter.merge(node);
node.exit().remove();
}
simulation.nodes(graphNodes).on("tick", ticked);
simulation.force("link").links(graphEdges);
</script>
</body>
You are relying on your ticked event to move the nodes. Your ticked event though operates on the nodeGroup variable. Your click handler and update function, though create a local node variable and operate on that (ie your new entered node is never part of the nodeGroup).
Simple change in update is for this:
node = nodeEnter.merge(node);
to become this:
nodeGroup = nodeEnter.merge(node);
Once you fix that you'll also find you don't need to explicitly set the position of the entering node (ie you no longer need these lines):
.attr('cx', coordinates[0])
.attr('cy', coordinates[1])
Running code:
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var width = 640,
height = 480;
var graphNodes = [
{ id: 0, r: 5 },
{ id: 1, r: 5 },
{ id: 2, r: 5 },
{ id: 3, r: 5 },
{ id: 4, r: 5 },
{ id: 5, r: 5 },
{ id: 6, r: 5 },
{ id: 7, r: 5 },
{ id: 8, r: 5 }
];
var graphEdges = [
{ source: 0, target: 1 },
{ source: 0, target: 2 },
{ source: 0, target: 3 },
{ source: 0, target: 4 },
{ source: 0, target: 5 },
{ source: 0, target: 6 },
{ source: 0, target: 7 },
{ source: 0, target: 8 }
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
svg.append('rect')
.attr('class', 'graph')
.attr('width', width)
.attr('height', height)
.attr('fill', 'lightblue')
.attr('opacity', 0.3)
.on('click', function(){
graphNodes.push({ id: graphNodes.length, r: 15 });
update(d3.mouse(this));
});
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d, i) { return i; }))
.force("collide",d3.forceCollide( function(d){return d.r + 8 }).iterations(16) )
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0));
var linkGroup = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graphEdges)
.enter().append("line")
.attr('stroke', 'gray')
.attr("stroke-width", 2);
var nodeGroup = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graphNodes)
.enter().append("circle")
.attr("r", function(d){ return d.r; })
.attr("fill", "gray")
.call(d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended));
function dragstarted(d){
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
simulation.restart();
/*d.fx = d.x;
d.fy = d.y;*/
}
function dragged(d){
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d){
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
simulation.alphaTarget(0.1);
}
function ticked(){
linkGroup
.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; });
//nodeGroup.attr("cx", function(d){ return d.x;}).attr("cy", function(d){ return d.y;})
nodeGroup.attr("transform", function(d) {return "translate(" + d.x + "," + d.y + ")"; });
}
function update(coordinates) {
// Redefine and restart simulation
simulation.nodes(graphNodes).on("tick", ticked);
simulation.force("link").links(graphEdges);
// Update links
var link = linkGroup.selectAll("line").data(graphEdges),
linkEnter = link.enter().append("line");
link = linkEnter.merge(link);
link.exit().remove();
// Update nodes
var node = nodeGroup.data(graphNodes),
nodeEnter = node.enter().append("circle")
.attr("r", function(d){ return d.r; })
.attr("fill", "gray")
// .attr('cx', coordinates[0])
// .attr('cy', coordinates[1])
.call(d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended));
nodeGroup = nodeEnter.merge(node);
node.exit().remove();
}
simulation.nodes(graphNodes).on("tick", ticked);
simulation.force("link").links(graphEdges);
</script>
</body>
</html>
What I'm running now is showing a root node and bunch of nodes connected to it via d3 line element. I'd like to add a caption to the line, since I have it defined and can freely grab it from d.label (in the nodes section in JSON). I'm using the following code:
var width = 500,
height = 500;
var force = d3.layout.force()
.size([width, height])
.charge(-800)
.linkDistance(d => d.distance)
.on("tick", tick);
var svg = d3.select("#target").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "mainsvg");
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("test.json", function(error, graph) {
if (error) throw error;
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("a")
node.append("g")
.attr("class", "node");
node.append("circle")
.attr("class", "circle")
.attr("r", function(d) { return d.r })
.attr("fill", function(d) { return d.color })
.attr("stroke", function(d){ return d.color });
});
function tick() {
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; });
svg.selectAll(".circle").attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
svg.selectAll(".text").attr("x", function(d) { return d.x+d.xoffs; })
.attr("y", function(d) { return d.y+d.yoffs; });
}
Now I've tried using normal <text> elements appended to the edgepath in this fashion:
var edgepaths = svg.selectAll(".edgepath")
.data(graph.links)
.enter()
.append('path')
.attr({'d': function(d) {return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y},
'class':'edgepath',
'fill-opacity':0,
'stroke-opacity':0,
'fill':'blue',
'stroke':'red',
'id':function(d,i) {return 'edgepath'+i}})
.style("pointer-events", "none");
var edgelabels = svg.selectAll(".edgelabel")
.data(graph.links)
.enter()
.append('text')
.style("pointer-events", "none")
.attr({'class':'edgelabel',
'id':function(d,i){return 'edgelabel'+i},
'dx':80,
'dy':10,
'font-size':10,
'fill':'black'});
edgelabels.append('textPath')
.attr('xlink:href',function(d,i) {return '#edgepath'+i})
.style("pointer-events", "none")
.text(function(d){return d.label});
However, the results I get are weird to say at best. Some labels are too close together, some are missing, but all of them are in the wrong place. How would I move them where they should be? To keep something in mind, I'm looking for a simple way to add caption to the paths I already have drawn out, nothing else. So the method I tried may have been too much, I'm not sure.
The problem is the dx in your edgelabels, that's always the same:
'dx': 80,
Assuming that you're using the code with the link distance (as in your last question), the solution is simple:
'dx':function(d){return d.distance/2},
Check the demo:
var nodes = [{
name: "a"
}, {
name: "b"
}, {
name: "c"
}, {
name: "d"
}, {
name: "e"
}, {
name: "f"
}];
var links = [{
source: 1,
target: 0,
distance: 80,
label: "foo"
}, {
source: 2,
target: 0,
distance: 40,
label: "bar"
}, {
source: 3,
target: 0,
distance: 180,
label: "baz"
}, {
source: 4,
target: 0,
distance: 80,
label: "foobar"
}, {
source: 5,
target: 0,
distance: 90,
label: "foobaz"
}]
var width = 400
height = 300;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(d => d.distance)
.charge(-1000)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("stroke", "black")
.attr("class", "link");
var edgepaths = svg.selectAll(".edgepath")
.data(force.links())
.enter()
.append('path')
.attr({
'd': function(d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y
},
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'fill': 'blue',
'stroke': 'red',
'id': function(d, i) {
return 'edgepath' + i
}
})
.style("pointer-events", "none");
var edgelabels = svg.selectAll(".edgelabel")
.data(force.links())
.enter()
.append('text')
.style("pointer-events", "none")
.attr({
'class': 'edgelabel',
'id': function(d, i) {
return 'edgelabel' + i
},
'dx': function(d) {
return d.distance / 2
},
'dy': 10,
'font-size': 12,
'fill': 'black'
});
edgelabels.append('textPath')
.attr('xlink:href', function(d, i) {
return '#edgepath' + i
})
.style("pointer-events", "none")
.text(function(d) {
return d.label
});
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", (d, i) => i ? 10 : 16)
.style("fill", (d, i) => i ? "teal" : "brown");
function tick() {
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("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
edgepaths.attr('d', function(d) {
var path = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
return path
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
text cannot be a child of path see this question path is not a container... per SVG spec.
best practice would be to first append g elements to your SVG as containers for the bound data, then append the path and text elements to the g so that they are siblings. Then you have complete control over relative positioning, etc.
I am experimenting with D3js Hive plots for the first time and am struggling to understand how to position labels alongside each node on the axes. Please see my fiddle, here:
http://jsfiddle.net/NovasTaylor/jgkqoasm/#base
Using this code I can place the labels for each node on its proper axis. The label (name) is not near its respective node. What code do I need to position the label adjacent to its node?
nodes.append("text")
.attr("class", "text")
.text(function(d) {return d.name;})
// Following is needed to place E,F on the vertical axis toward the top
// of the graph (group=3)
.attr("y", function(d) {
if (d.group === 3) {
return -radius(d.y);
} else {
return radius(d.y);
}
})
.attr("transform", function(d) {
if (d.group !== 3) {
return "rotate(" + (degrees(angle(d.group)) - 90) + ")";
}
})
Any help would be greatly appreciated!
Cheers,
Tim
You should be applying the same transform that's on your node circle elements: a rotation of degrees(angle(d.x)) and an x position of radius(d.y). Further, I would wrap the text in a g and then position that g using those attributes then append a text and rotate it counter to the g so the text is in a proper position:
nodes.append("g")
.attr("class", "text")
.attr("transform", function(d) {
var t = "rotate(" + degrees(angle(d.x)) + ")";
t += "translate(" + radius(d.y) + ",0)";
return t;
})
.append("text")
.attr("transform", function(d) {
var t = "rotate(" + -degrees(angle(d.x)) + ")";
return t;
})
.text(function(d) {return d.name;})
Full working code:
var width = 960,
height = 500,
innerRadius = 40,
outerRadius = 240;
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
radius = d3.scale.linear().range([innerRadius, outerRadius]),
color = d3.scale.category10().domain(d3.range(20));
var nodes = [
{name:"A", group:1, x: 0, y: .1},
{name:"B", group:1, x: 0, y: .9},
{name:"C", group:2, x: 1, y: .2},
{name:"D", group:2, x: 1, y: .3},
{name:"E", group:3, x: 2, y: .1},
{name:"F", group:3, x: 2, y: .8}
];
var links = [
{source: nodes[0], target: nodes[2]},
{source: nodes[1], target: nodes[3]},
{source: nodes[2], target: nodes[4]},
{source: nodes[2], target: nodes[5]},
{source: nodes[3], target: nodes[5]},
{source: nodes[4], target: nodes[0]},
{source: nodes[5], target: nodes[1]}
];
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", d3.hive.link()
.angle(function(d) { return angle(d.x); })
.radius(function(d) { return radius(d.y); }))
.style("stroke", function(d) { return color(d.source.x); });
var nodes = svg.selectAll("g.node")
.data(nodes, function (d) {
return d.name;
});
nodes.enter()
.append("g");
nodes.append("circle")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d.x)) + ")"; })
.attr("cx", function(d) { return radius(d.y); })
.attr("r", 5)
.style("fill", function(d) { return color(d.x); });
// Append name as label to each node
nodes.append("g")
.attr("class", "text")
.attr("transform", function(d) {
var t = "rotate(" + degrees(angle(d.x)) + ")";
t += "translate(" + radius(d.y) + ",0)";
return t;
})
.append("text")
.attr("transform", function(d) {
var t = "rotate(" + -degrees(angle(d.x)) + ")";
return t;
})
.text(function(d) {return d.name;})
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
.link {
fill: none;
stroke-width: 1.5px;
}
.axis, .node {
stroke: #000;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="http://d3js.org/d3.hive.v0.min.js"></script>
I think it would be more easy to put the circles and texts in a group. So there wont need additional calculations for text positions.
var nodes = svg.selectAll("g.node")
.data(nodes, function(d) {
return d.name;
});
var group = nodes.enter()
.append("g")
.attr("transform", function(d) {
return "rotate(" + degrees(angle(d.x)) + ")";
});
group.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return radius(d.y);
})
.attr("r", 5)
.style("fill", function(d) {
return color(d.x);
});
// Append name as label to each node
group.append("text")
.attr("class", "text")
.attr("dy", "1.2em")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return radius(d.y);
});
var width = 960,
height = 500,
innerRadius = 40,
outerRadius = 240;
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
radius = d3.scale.linear().range([innerRadius, outerRadius]),
color = d3.scale.category10().domain(d3.range(20));
var nodes = [{
name: "A",
group: 1,
x: 0,
y: .1
}, {
name: "B",
group: 1,
x: 0,
y: .9
}, {
name: "C",
group: 2,
x: 1,
y: .2
}, {
name: "D",
group: 2,
x: 1,
y: .3
}, {
name: "E",
group: 3,
x: 2,
y: .1
}, {
name: "F",
group: 3,
x: 2,
y: .8
}];
var links = [{
source: nodes[0],
target: nodes[2]
}, {
source: nodes[1],
target: nodes[3]
}, {
source: nodes[2],
target: nodes[4]
}, {
source: nodes[2],
target: nodes[5]
}, {
source: nodes[3],
target: nodes[5]
}, {
source: nodes[4],
target: nodes[0]
}, {
source: nodes[5],
target: nodes[1]
}];
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) {
return "rotate(" + degrees(angle(d)) + ")";
})
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", d3.hive.link()
.angle(function(d) {
return angle(d.x);
})
.radius(function(d) {
return radius(d.y);
}))
.style("stroke", function(d) {
return color(d.source.x);
});
var nodes = svg.selectAll("g.node")
.data(nodes, function(d) {
return d.name;
});
var group = nodes.enter()
.append("g")
.attr("transform", function(d) {
return "rotate(" + degrees(angle(d.x)) + ")";
});
group.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return radius(d.y);
})
.attr("r", 5)
.style("fill", function(d) {
return color(d.x);
});
// Append name as label to each node
group.append("text")
.attr("class", "text")
.attr("dy", "1.2em")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return radius(d.y);
});
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
.link {
fill: none;
stroke-width: 1.5px;
}
.axis,
.node {
stroke: #000;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="http://d3js.org/d3.hive.v0.min.js"></script>
I've created a sample Asp.Net MVC 4 application where I've used D3.js to append an SVG element and then inside the SVG I've appended a text element (see code below). This all works fine until I try to append an img to the SVG using a local png file. The img gets appended to the DOM, but the img is not rendered on the page. Any ideas what I'm doing wrong here, and how to go about fixing it?
#{
ViewBag.Title = "Home Page";
}
<script src="~/Scripts/d3.v3.js"></script>
<script type="text/javascript">
var svg = d3.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 100)
.style("border", "1px solid black");
var text = svg.selectAll("text")
.data([0])
.enter()
.append("text")
.text("Testing")
.attr("x", "40")
.attr("y", "60");
var imgs = svg.selectAll("img").data([0]);
imgs.enter()
.append("img")
.attr("xlink:href", "#Url.Content("~/Content/images/icons/refresh.png")")
.attr("x", "60")
.attr("y", "60")
.attr("width", "20")
.attr("height", "20");
</script>
#Richard Marr - Below is an attempt to do the same thing in straight HTML, which gives me the same result. I'm not sure about my code to get the refresh.png file from the local drive this way.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v2.js"></script>
</head>
<body>
<script type="text/javascript">
var svg = d3.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 100)
.style("border", "1px solid black");
var text = svg.selectAll("text")
.data([0])
.enter()
.append("text")
.text("Testing")
.attr("x", "40")
.attr("y", "60");
var imgs = svg.selectAll("img").data([0]);
imgs.enter()
.append("svg:img")
.attr("xlink:href", "file:///D:/d3js_projects/refresh.png")
.attr("x", "60")
.attr("y", "60")
.attr("width", "20")
.attr("height", "20");
</script>
</body>
</html>
nodeEnter.append("svg:image")
.attr('x', -9)
.attr('y', -12)
.attr('width', 20)
.attr('height', 24)
.attr("xlink:href", "resources/images/check.png")
In SVG (contrasted with HTML), you will want to use <image> instead of <img> for elements.
Try changing your last block with:
var imgs = svg.selectAll("image").data([0]);
imgs.enter()
.append("svg:image")
...
My team also wanted to add images inside d3-drawn circles, and came up with the following (fiddle):
index.html:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="timeline.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
<script src="https://code.jquery.com/jquery-2.2.4.js"
integrity="sha256-iT6Q9iMJYuQiMWNd9lDyBUStIq/8PuOW33aOqmvFpqI="
crossorigin="anonymous"></script>
<script src="./timeline.js"></script>
</head>
<body>
<div class="timeline"></div>
</body>
</html>
timeline.css:
.axis path,
.axis line,
.tick line,
.line {
fill: none;
stroke: #000000;
stroke-width: 1px;
}
timeline.js:
// container target
var elem = ".timeline";
var props = {
width: 1000,
height: 600,
class: "timeline-point",
// margins
marginTop: 100,
marginRight: 40,
marginBottom: 100,
marginLeft: 60,
// data inputs
data: [
{
x: 10,
y: 20,
key: "a",
image: "https://unsplash.it/300/300",
id: "a"
},
{
x: 20,
y: 10,
key: "a",
image: "https://unsplash.it/300/300",
id: "b"
},
{
x: 60,
y: 30,
key: "a",
image: "https://unsplash.it/300/300",
id: "c"
},
{
x: 40,
y: 30,
key: "a",
image: "https://unsplash.it/300/300",
id: "d"
},
{
x: 50,
y: 70,
key: "a",
image: "https://unsplash.it/300/300",
id: "e"
},
{
x: 30,
y: 50,
key: "a",
image: "https://unsplash.it/300/300",
id: "f"
},
{
x: 50,
y: 60,
key: "a",
image: "https://unsplash.it/300/300",
id: "g"
}
],
// y label
yLabel: "Y label",
yLabelLength: 50,
// axis ticks
xTicks: 10,
yTicks: 10
}
// component start
var Timeline = {};
/***
*
* Create the svg canvas on which the chart will be rendered
*
***/
Timeline.create = function(elem, props) {
// build the chart foundation
var svg = d3.select(elem).append('svg')
.attr('width', props.width)
.attr('height', props.height);
var g = svg.append('g')
.attr('class', 'point-container')
.attr("transform",
"translate(" + props.marginLeft + "," + props.marginTop + ")");
var g = svg.append('g')
.attr('class', 'line-container')
.attr("transform",
"translate(" + props.marginLeft + "," + props.marginTop + ")");
var xAxis = g.append('g')
.attr("class", "x axis")
.attr("transform", "translate(0," + (props.height - props.marginTop - props.marginBottom) + ")");
var yAxis = g.append('g')
.attr("class", "y axis");
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 1)
.attr("x", 0 - ((props.height - props.yLabelLength)/2) )
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text(props.yLabel);
// add placeholders for the axes
this.update(elem, props);
};
/***
*
* Update the svg scales and lines given new data
*
***/
Timeline.update = function(elem, props) {
var self = this;
var domain = self.getDomain(props);
var scales = self.scales(elem, props, domain);
self.drawPoints(elem, props, scales);
};
/***
*
* Use the range of values in the x,y attributes
* of the incoming data to identify the plot domain
*
***/
Timeline.getDomain = function(props) {
var domain = {};
domain.x = props.xDomain || d3.extent(props.data, function(d) { return d.x; });
domain.y = props.yDomain || d3.extent(props.data, function(d) { return d.y; });
return domain;
};
/***
*
* Compute the chart scales
*
***/
Timeline.scales = function(elem, props, domain) {
if (!domain) {
return null;
}
var width = props.width - props.marginRight - props.marginLeft;
var height = props.height - props.marginTop - props.marginBottom;
var x = d3.scale.linear()
.range([0, width])
.domain(domain.x);
var y = d3.scale.linear()
.range([height, 0])
.domain(domain.y);
return {x: x, y: y};
};
/***
*
* Create the chart axes
*
***/
Timeline.axes = function(props, scales) {
var xAxis = d3.svg.axis()
.scale(scales.x)
.orient("bottom")
.ticks(props.xTicks)
.tickFormat(d3.format("d"));
var yAxis = d3.svg.axis()
.scale(scales.y)
.orient("left")
.ticks(props.yTicks);
return {
xAxis: xAxis,
yAxis: yAxis
}
};
/***
*
* Use the general update pattern to draw the points
*
***/
Timeline.drawPoints = function(elem, props, scales, prevScales, dispatcher) {
var g = d3.select(elem).selectAll('.point-container');
var color = d3.scale.category10();
// add images
var image = g.selectAll('.image')
.data(props.data)
image.enter()
.append("pattern")
.attr("id", function(d) {return d.id})
.attr("class", "svg-image")
.attr("x", "0")
.attr("y", "0")
.attr("height", "70px")
.attr("width", "70px")
.append("image")
.attr("x", "0")
.attr("y", "0")
.attr("height", "70px")
.attr("width", "70px")
.attr("xlink:href", function(d) {return d.image})
var point = g.selectAll('.point')
.data(props.data);
// enter
point.enter()
.append("circle")
.attr("class", "point")
.on('mouseover', function(d) {
d3.select(elem).selectAll(".point").classed("active", false);
d3.select(this).classed("active", true);
if (props.onMouseover) {
props.onMouseover(d)
};
})
.on('mouseout', function(d) {
if (props.onMouseout) {
props.onMouseout(d)
};
})
// enter and update
point.transition()
.duration(1000)
.attr("cx", function(d) {
return scales.x(d.x);
})
.attr("cy", function(d) {
return scales.y(d.y);
})
.attr("r", 30)
.style("stroke", function(d) {
if (props.pointStroke) {
return d.color = props.pointStroke;
} else {
return d.color = color(d.key);
}
})
.style("fill", function(d) {
if (d.image) {
return ("url(#" + d.id + ")");
}
if (props.pointFill) {
return d.color = props.pointFill;
} else {
return d.color = color(d.key);
}
});
// exit
point.exit()
.remove();
// update the axes
var axes = this.axes(props, scales);
d3.select(elem).selectAll('g.x.axis')
.transition()
.duration(1000)
.call(axes.xAxis);
d3.select(elem).selectAll('g.y.axis')
.transition()
.duration(1000)
.call(axes.yAxis);
};
$(document).ready(function() {
Timeline.create(elem, props);
})
I do not know why, but the image should not be duplicated, tripled, etc ... should remove the previous one and load it again but with another rotation data. This is my code:
data.csv
enter image description here
d3.csv("data/data.csv").then(function(data){
//console.log(data);
// Clean data
formattedData = data.map(function(id){
id.rot_1 = +id.rot_1;
id.trans_1 = +id.trans_1;
return id;
});
// First run of the visualization
update(formattedData[0]);})
$("#play-button")
.on("click", function(){
var button = $(this);
if (button.text() == "Play"){
button.text("Pause");
interval = setInterval(step, 1000);
}
else {
button.text("Play");
clearInterval(interval);
}})
function step(){
// At the end of our data, loop back
time = (time < 76) ? time+1 : 0
update(formattedData[time]); }
function update(data) {
// Standard transition time for the visualization
var t = d3.transition()
.duration(1000);
//console.log(d3.selectAll(data));
//console.log(data)
// original
var imgs1 = g.append("image") // en vez de g es svg
.attr("xlink:href", "img/picturetest.png");
// EXIT old elements not present in new data.
imgs1.exit()
.attr("class", "exit")
.selectAll("svg:image")
.remove();
//console.log(data)
// ENTER new elements present in new data.
imgs1.enter()
.append("svg:image") // svg:image
//.attr("xlink:href", "img/picturetest.png")
.attr("class", "enter")
.merge(imgs1)
.transition(t)
.attr("x", 0) // 150
.attr("y", 0) // 80
.attr("width", 200)
.attr("height", 200)
.attr("transform", "rotate("+data.rot_1+") translate("+data.trans_1+")" ); }`
var svg = d3.select("body")
.append("svg")
.style("width", 200)
.style("height", 100)