I'm trying to append circles that have a color background with an image attached.
To achieve that am using <defs>, <rect> <clipPath> and <use>. I believe that my SVG hierarchy is valid, however even though all elements have a unique ID all circles are got stuck in the same point. All <a> elements itself that contain defs in it are having different x and y, but rects inside it are having same x and y.
How is it possible that all rects having a unique ID having same x's and y's.
Codepen
DOM screenshot:
let personCircles = svg.selectAll("a")
.data(data)
.enter()
.append("a")
.attr("id", function(d) {
console.log(d["Person Name"]);
if (d && d.length !== 0) {
return d["Person Name"].replace(/ |,|\./g, '_');
}
})
.attr('x', function(d) {
return markerCirclesScale(name)
})
.attr('y', function(d) {
return fullSVGHeight / 2 + 8;
})
.style("opacity", 1)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
//Define defs
let defs = personCircles.append("defs");
defs.append('rect')
.attr('id', function(d) {
return "rect-" + d["Person Name"].replace(/ |,|\./g, '_');
})
.attr('x', function(d) {
return markerCirclesScale(name)
})
.attr('y', function(d) {
return fullSVGHeight / 2;
})
.attr('width', 60)
.attr('height', 60)
.attr('rx', 40)
.style('fill', 'red')
defs.append("clipPath")
.attr('id', function(d) {
return "clip-" + d["Person Name"].replace(/ |,|\./g, '_');
})
.append("use")
.attr('href', function(d) {
return "#rect-" + d["Person Name"].replace(/ |,|\./g, '_');
})
personCircles
.append("use")
.attr('href', function(d) {
return "#rect-" + d["Person Name"].replace(/ |,|\./g, '_');
})
personCircles.append('image')
.attr('href', function(d) {
return 'http://pngimg.com/uploads/donald_trump/donald_trump_PNG72.png'
})
.attr("clip-path", function(d) {
return "url(#clip-" + d["Person Name"].replace(/ |,|\./g, '_');+")"
})
.attr('x', function(d) {
return markerCirclesScale(name)
})
.attr('y', function(d) {
return fullSVGHeight / 2 + 8;
})
.attr("width", 60)
.attr("height", 60)
personCircles refers to the <a> (anchor) elements which wouldn't move an inch if you set x and y co-ordinates within a SVG. The elements you're trying to position are the rects and the corresponding images and so changing the ticked function to the following i.e. positioning the rects, clipPath rects and the image:
function ticked() {
personCircles.selectAll('rect, image')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
the result would be as seen in the following fork of your codepen:
https://codepen.io/anon/pen/aPOdON?editors=1010
Hope this clears up. Btw I like the sample image you're using in your testing :P
Related
I am working on an d3.js tree to display XML Schema elements in the tree based on their element type.
I have code like the following:
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', clickXSD)
.on('dblclick', dblclickXSD);
nodeEnter.append('rect')
.filter(function(d) { return d.data.elementType == "xs:element" })
.attr('class', 'node')
.attr('y', -16)
.attr('rx', 5)
.attr('ry', 5)
.attr('width', function(d) { return d.data.y_size + 50; })
.attr('height', function(d) { return 32; })
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "lemonchiffon";
});
I would like to be able to make the code a little cleaner by implementing something like:
nodeEnter.xs-element()
.filter(function(d) { return d.data.elementType == "xs:element" })
or something similar and then have a function to draw the xs:element and then one to draw the xs:attribute, etc.
I found my answer here: https://www.atlassian.com/blog/archives/extending-d3-js.
There are two possible ways. One is to make a prototype and the other is to use the call function. I am using the second way.
nodeEnter.filter(function(d) { return d.data.elementType == "xs:element" }).call(xsElement);
function xsElement(selection) {
selection.append('rect')
.attr('class', 'node')
.attr('y', -16)
.attr('rx', 5)
.attr('ry', 5)
.attr('width', function(d) {
return d.data.y_size + 50;
})
.attr('height', function(d) {
return 32;
})
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "lemonchiffon";
})
.filter(function(d) { return d.data.documentation != null })
.append("title").text(function(d) { return d.data.documentation; });
// Add labels for the nodes
selection.append('text')
.attr("dy", ".35em")
.attr("y", -6)
.attr("x", 6)
.attr("text-anchor", "start")
.text(function(d) { return d.data.name; });
.
.
.
}
Where selection is the value of the filtered nodeEnter.
I've created a chart and it works fine, but I can't find out how to add numbers to columns. Numbers appear only if I hover the columns.
Tried different variants:
svg.selectAll("text").
data(data).
enter().
append("svg:text").
attr("x", function(datum, index) { return x(index) + barWidth; }).
attr("y", function(datum) { return height - y(datum.days); }).
attr("dx", -barWidth/2).
attr("dy", "1.2em").
attr("text-anchor", "middle").
text(function(datum) { return datum.days;}).
attr("fill", "white");
A link to my example: https://jsfiddle.net/rinatoptimus/db98bzyk/5/
Alternate idea to #gerardofurtado is to instead of a rect append a g, then group the text and rect together. This prevents the need for double-data binding.
var bars = svg.selectAll(".bar")
.data(newData)
.enter().append("g")
.attr("class", "bar")
// this might be affected:
.attr("transform", function(d, i) {
return "translate(" + i * barWidth + ",0)";
});
bars.append("rect")
.attr("y", function(d) {
return y(d.days);
})
.attr("height", function(d) {
return height - y(d.days) + 1;
})
.style({
fill: randomColor
}) // color bars
.attr("width", barWidth - 1)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
bars.append("text")
.text(function(d) {
return d.days;
})
.attr("y", function(d) {
return y(d.days);
})
.attr("x", barWidth / 2)
.style("text-anchor", "middle");
Updated fiddle.
When you do this:
svg.selectAll("text")
you are selecting text elements that already exist in your SVG. In an enter selection, it's important selecting something that doesn't exist:
svg.selectAll(".text")
.data(newData)
.enter()
.append("svg:text")
.attr("x", function(datum) {
return x(datum.name) + x.rangeBand()/2
})
.attr("y", function(datum) {
return y(datum.days) - 10;
})
.attr("text-anchor", "middle")
.text(function(datum) {
return datum.days;
})
.attr("fill", "white");
Here is your fiddle: https://jsfiddle.net/c210osht/
You could try using the d3fc bar series component, which allows you to add data-labels via the decorate pattern.
Here's what the code would look like:
var svgBar = fc.seriesSvgBar()
.xScale(xScale)
.yScale(yScale)
.crossValue(function(_, i) { return i; })
.mainValue(function(d) { return d; })
.decorate(function(selection) {
selection.enter()
.append("text")
.style("text-anchor", "middle")
.attr("transform", "translate(0, -10)")
.text(function(d) { return d3.format(".2f")(d); })
.attr("fill", "black");
});
Here's a codepen with the complete example.
Disclosure: I am a core contributor to the d3fc project!
I am creating a sankey diagram using D3. I am trying to redraw the diagram with additional node and link and using transition to animate the previous diagram to the new diagram. I was able to add in new node and link but the old nodes and links did not change position. Since the new node and link could be added at any place within the diagram, I do not want to clear and redraw the entire svg, but use transition to get from the old diagram to the new one. The code to draw the sankey diagram is this:
function draw(data){
// Set the sankey diagram properties
var sankey = d3sankey()
.nodeWidth(17)
.nodePadding(27)
.size([width, height]);
var path = sankey.link();
var graph = data;
sankey.nodes(graph.nodes)
.links(graph.links)
.layout(32);
sankey.relayout();
// add in the links
link.selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("fill", "none")
.style("stroke", function(d){
return "grey";
})
.style("stroke-opacity", "0.4")
.on("mouseover", function() { d3.select(this).style("stroke-opacity", "0.7") } )
.on("mouseout", function() { d3.select(this).style("stroke-opacity", "0.4") } )
.style("stroke-width", function (d) {
return Math.max(1, d.dy);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
link.transition().duration(750);
//link.exit();
// add in the nodes
var node = nodes.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// add the rectangles for the nodes
node.append("rect")
.attr("height", function (d) {
return d.dy;
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) {
return d.dy / 2;
})
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("text-shadow", "0 1px 0 #fff")
.attr("transform", null)
.text(function (d) {
return d.name;
})
.filter(function (d) {
return d.x < width / 2;
})
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
node.transition().duration(750);
}
The JSFiddle
Is it possible to use transition to add in new node and link and reposition
old nodes and links?
Thanks!
I was able to do this by using moving the nodes and links to new position. The code for that is:
var nodes = d3.selectAll(".node")
.transition().duration(750)
.attr('opacity', 1.0)
.attr("transform", function (d) {
if(d.node == 3){
console.log(d.x, d.y);
}
return "translate(" + d.x + "," + d.y + ")";
});
var nodeRects = d3.selectAll(".node rect")
.attr("height", function (d) {
if(d.node == 3){
console.log(d.dy);
}
return d.dy;
})
var links = d3.selectAll(".link")
.transition().duration(750)
.attr('d', path)
.attr('opacity', 1.0)
Updated JSFiddle
I am having trouble adding a text to the links that connect the nodes in the following D3JS connected node graph: http://jsfiddle.net/rqa0nvv2/1/
Could anyone please explain to me what is the process to add them?
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", function(d) { if(d.value == "visible") {return "link";} else return "" })
.style("stroke-width", function(d) { return Math.sqrt(d.stroke); });
link.append("svg:title").text(function(d) { return "Unit: " + ", Formula: ";});
Thanks!
Just a couple changes are needed to get the text attached to the edges.
First, the problem with your current method is that you are shoving the <text> inside the <line>. This just can't be done in SVG, so you need to create a <g> (group) to contain both the line and the text:
var link = svg.selectAll(".link")
.data(links)
.enter()
.append("g")
.attr("class", "link-group")
.append("line")
.attr("class", function(d) { return d.value == "visible" ? "link" : ""; })
.style("stroke-width", function(d) { return Math.sqrt(d.stroke); });
Ok, just adding this won't change anything visually, but it does create the groups that we need. So we need to add the text with something like:
var linkText = svg.selectAll(".link-group")
.append("text")
.data(force.links())
.text(function(d) { return d.value == "visible" ? "edge" : ""; })
.attr("x", function(d) { return (d.source.x + (d.target.x - d.source.x) * 0.5); })
.attr("y", function(d) { return (d.source.y + (d.target.y - d.source.y) * 0.5); })
.attr("dy", ".25em")
.attr("text-anchor", "middle");
This adds the text and initially positions it. Feel free (please!) style the text and change the content according to how you want it to look. Finally, we need to make it move with the force-directed graph:
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
linkText
.attr("x", function(d) { return (d.source.x + (d.target.x - d.source.x) * 0.5); })
.attr("y", function(d) { return (d.source.y + (d.target.y - d.source.y) * 0.5); });
});
Which is simple of course since it's the same thing that we already did!
I am having some issues with d3js and I can't figure out what is going on. The idea is to draw initial graph from some endpoint data (first img), that's fine works well. Each node is clickable, on click ajax call is made for that node and data is returned, based on some criteria at that point nodes.push(xx), links.push(xx) happens to add new nodes and restart() is called to draw new nodes and links. The issue is that the main graph is doing the correct thing (Not showed on screenshots as I had to put fake data on the first graph i.e. calling an endpoint /record/id/first doesn't return a data) but there are bunch of random nodes showing up in the right bottom corner.
You can also see on the example below, even if the data doesn't change after clicking on first/second/third something wrong goes with node.enter() after restart() with the same data passed in...
JS FIDDLE: http://jsfiddle.net/5754j86e/
var w = 1200,
h = 1200;
var nodes = [];
var links = [];
var node;
var link;
var texts;
var ids = [];
var circleWidth = 10;
var initialIdentifier = "marcin";
nodes = initialBuildNodes(initialIdentifier, sparql);
links = initialBuildLinks(sparql);
//Add SVG
var svg = d3.select('#chart').append('svg')
.attr('width', w)
.attr('height', h);
var linkGroup = svg.append("svg:g").attr("id", "link-group");
var nodeGroup = svg.append("svg:g").attr("id", "node-group");
var textGroup = svg.append("svg:g").attr("id", "text-group");
//Add Force Layout
var force = d3.layout.force()
.size([w, h])
.gravity(.05)
.charge(-1040);
force.linkDistance(120);
restart();
function restart() {
force.links(links)
console.log("LINKS ARE: ", links)
link = linkGroup.selectAll(".link").data (links);
link.enter().append('line')
.attr("class", "link");
link.exit().remove();
force.nodes(nodes)
console.log("NODES ARE: ", nodes)
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
})
.on("click", function(d) {
nodeClicked (d);
})
.on('mouseenter', function(d){
nodeMouseEnter(d)
})
.on('mouseout', function(d){
nodeMouseOut(d)
});
node.exit().remove();
var annotation = textGroup.selectAll(".annotation").data (nodes);
annotation.enter().append("svg:g")
.attr("class", "annotation")
.append("text")
.attr("x", function(d) { return d.radius + 4 })
.attr("y", ".31em")
.attr("class", "label")
.text(function(d) { return d.name; });
annotation.exit().remove();
force.start();
}
function nodeClicked (d) {
// AJAX CALL happens here and bunch of nodes.push({name: "new name"}) happen
}
force.on('tick', function(e) {
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, i) {
return 'translate('+ d.x +', '+ d.y +')';
})
svg.selectAll(".annotation").attr("transform", function(d) {
var labelx = d.x + 13;
return "translate(" + labelx + "," + d.y + ")";
})
});
Okay I got it, based on the docs (https://github.com/mbostock/d3/wiki/Selections#enter):
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
From my code you can see I do enter() and then once again I add circle on node in a separate statement.
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Adding circle should be within the scope of enter() otherwise it happens to all nodes not only the new nodes therefore it should be :
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag)
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});