I'm using d3.js
Hi, I'm having an issue finding how to append two elements (path and image) to the same g (inside my svg) from the same data. I know how to do this, but the tricky thing is I need to get the BBox values of the "path" elements in order to place the "image" elements in the middle... My goal is actually to place little clouds in the center of cities on a map like this : this is the map I am trying to reproduce
On the map it's not centered but I have to do so. So this is my current code:
// Draw the map
svg.append("g")
.selectAll("path")
.data(mapEPCI.features)
.enter()
.append("path")
.attr("fill", d => d.properties.color)
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")
.append("image")
.attr("xlink:href", function(d) {
if (d.properties.plan_air == 1)
return ("data/page8_territoires/cloud.png")
else if (d.properties.plan_air == 2)
return ("data/page8_territoires/cloudgray.png")
})
.attr("width", "20")
.attr("height", "15")
.attr("x", function (d) {
let bbox = d3.select(this.parentNode).node().getBBox();
return bbox.x + 30})
.attr("y", function (d) {
return d3.select(this.parentNode).node().getBBox().y + 30})
This gets the right coordinates for my images but it's because the parent node is actually the path... If I append the image to the g element, is there a way to get the "BrotherNode", or maybe the last child of the "g" element ? I don't know if I'm clear enough but I hope you get my point.
I'm kinda new to js so maybe I'm missing something simple I just don't know yet
Thanks for your help
I would handle your data at the g level and create a group for every map feature (country) which contains the path and a sibling image:
<!doctype html>
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<svg width="600" height="600"></svg>
<script>
let svg = d3.select('svg'),
mapEPCI = {
features: [100, 200, 300, 400]
};
let g = svg.selectAll('g')
.data(mapEPCI.features)
// enter selection is collection of g
let ge = g.enter().append("g");
// append a path to each g according to data
ge.append('path')
.attr("d", (d) => "M" + d + ",10L" + d + ",100")
.style("stroke", "black");
// append a sibling image
ge.append("image")
.attr("xlink:href", "https://placeimg.com/20/15/animals")
.attr("width", "20")
.attr("height", "15")
.attr("transform", function(d) {
// find my sibling path to get bbox
let sibling = this.parentNode.firstChild;
let bbox = sibling.getBBox();
return "translate(" + (bbox.x - 20 / 2) + "," + (bbox.y + bbox.height / 2 - 15 / 2) + ")"
});
</script>
</body>
</html>
Related
I've got a page where I build a bunch of pie charts. On each one, I want to add an href to another location on the page.
Currently my code works, but it only applies the href to the individual pieces of the pie chart, as well as the text. So for example, if you click on a ring of the pie chart, it will work like it should, but if you click on the space between the rings, it will not.
The SVG itself is much larger and easier to click, but even though I append the anchor tag to the svg, it only applies to the elements within the SVG. How do I correct this behavior?
function pieChartBuilder(teamName, values) {
var dataset = values;
var trimTitle = teamName.replace(' ', '');
trimTitle = trimTitle.toLowerCase();
var width = 175,
height = 175,
cwidth = 8;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("#pieChart").append("svg")
.attr("width", width)
.attr("height", height)
.append("a") // here is where I append the anchor tag to the SVG, but it only applies to the individual elements within.
.attr("href", ("#" + trimTitle))
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path")
.data(function (d) { return pie(d); })
.enter().append("path")
.attr("fill", function (d, i) { return color(i); })
.attr("d", function (d, i, j) { return arc.innerRadius(5 + cwidth * j).outerRadius(3 + cwidth * (j + 1))(d); });
svg.append("text")
.attr("class", "title")
.attr("x", 0)
.attr("y", (0 - (height / 2.5)))
.attr("text-anchor", "middle")
.style("fill", "#808080")
.text(teamName);
}
I think you just have the selectors the wrong way round. You want to have the svg inside an a tag right?:
d3.select("#pieChart")
.append("a")
.append("svg");
You (1) select the pieChart, then (2) append an a tag to it, then you (3) append the svg to that.
This is the code I use for calling same js function in two blocks (in two div tag). The answer for the second tag (div id="frame4") also prints inside the first one (div id="frame3"). I want to print them separately. How can I do that?
<div id="frame3">
<! ----pieChart ----- !>
<h5><i>Code Coverage</i></h5>
<div id="pieChart"></div>
<script type="text/javascript">
dsPieChart(<%=coverage %>);
</script>
</div>
<!test_density !>
<div id="frame3">
<div id="pieChart"></div>
<script type="text/javascript">
dsPieChart(<%=density %>);
</script>
</div>
code for the function
function dsPieChart(x){
var formatAsPercentage = d3.format("%") ;
var dataset = [
{category: "", measure:x },
{category: "", measure:(100-x)},
]
;
var width = 100,
height = 100,
outerRadius = Math.min(width, height) / 2,
innerRadius = outerRadius * .9,
// for animation
innerRadiusFinal = outerRadius * .8,
innerRadiusFinal3 = outerRadius* .7,
color = d3.scale.category20b() //builtin range of colors
;
var vis = d3.select("#pieChart")
.append("svg:svg") //create the SVG element inside the <body>
.data([dataset]) //associate our data with the document
.attr("width", width) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", height)
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")") //move the center of the pie chart from 0, 0 to radius, radius
;
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.outerRadius(outerRadius).innerRadius(innerRadius);
// for animation
var arcFinal = d3.svg.arc().innerRadius(innerRadiusFinal).outerRadius(outerRadius);
var arcFinal3 = d3.svg.arc().innerRadius(innerRadiusFinal3).outerRadius(outerRadius);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) { return d.measure; }); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice") //allow us to style things in the slices (like text)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", up)
;
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } ) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc) //this creates the actual SVG path using the associated data (pie) with the arc drawing function
.append("svg:title") //mouseover title showing the figures
// .text(function(d) { return d.data.category + ": " + d.data.measure ; });
.text(function(d) { return d.data.measure ; });
d3.selectAll("g.slice").selectAll("path").transition()
.duration(750)
.delay(10)
.attr("d", arcFinal )
;
// Add a label to the larger arcs, translated to the arc centroid and rotated.
arcs.filter(function(d) { return d.endAngle - d.startAngle > .2; })
.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "translate(" + arcFinal.centroid(d) + ")rotate(" + angle(d) + ")"; })
.text(function(d) { return d.data.category; })
;
// Computes the label angle of an arc, converting from radians to degrees.
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
// Pie chart title
vis.append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(x +"%")
.attr("class","title")
;
function mouseover() {
d3.select(this).select("path").transition()
.duration(750)
.attr("d", arcFinal3)
;
}
function mouseout() {
d3.select(this).select("path").transition()
.duration(750)
//.attr("stroke","blue")
//.attr("stroke-width", 1.5)
.attr("d", arcFinal)
;
}
function up(d, i) {
/* update bar chart when user selects piece of the pie chart */
//updateBarChart(dataset[i].category);
updateBarChart(d.data.category, color(i));
updateLineChart(d.data.category, color(i));
}
}
Change function to pass second parameter for element ID.
function dsPieChart(x, selectorId){
Change the hard code selector:
var vis = d3.select("#pieChart");
To
var vis = d3.select("#" + selectorId);
Then when you call the function also identify the id selector in second paramater. Note that element ID's must be unique in a page by definition:
<div id="pieChart-1"></div>
<script type="text/javascript">
dsPieChart(<%=coverage %>, 'pieChart-1');
</script>
</div>
<div id="pieChart-2"></div>
<script type="text/javascript">
dsPieChart(<%=density %>, 'pieChart-2');
</script>
</div>
I am trying to use the bubble chart example as a template to build a visualisation. I have my JSON as a flat-hierarchy, such that there is one element called children and that holds an array of objects that I want to visualise.
The JSON looks like this:
{
"children":[
{
"acc":"Q15019",
"uid":"SEPT2_HUMAN",
"sym":"SEPT2",
"name":"Septin-2",
"alt_ids":"",
"ratio":0.5494271087884398,
"pval":0.990804718
},
...,
{
"acc":"Q16181",
"uid":"SEPT7_HUMAN",
"sym":"SEPT7",
"name":"Septin-7",
"alt_ids":"",
"ratio":1.1949912048567823,
"pval":0.511011887
}
]
}
I have modified the example code as follows:
var diameter = 960,
format = d3.format(",d"),
color = d3.scale.quantile().range(colorbrewer.RdBu[9]);
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.json("datagraph.json", function(datagraph) {
var node = svg.selectAll(".node")
.data(bubble.nodes(datagraph))
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.acc; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
//node.append("title").text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.attr("r", function(d) { return 30; })
.style("fill", function(d) {
if(d.ratio == null)
return "#ffffff";
else
return color(d.ratio);
});
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.acc; });
});
The resultant HTML has a ton of <g> tags responding to each element except they are never translated to the right position, but instead sort of sit on top of each other on the top left corner. By investigating in Firebug, I figured this happens presumably because the pack() algorithm does not get the objects one at a time, but the whole array as a single element, thus individual elements don't get .x and .y values.
If I change the .nodes() argument to datagraph.children I get the elements one at a time in nodes() iteration, but oddly enough I get a single <g> object. Since I don't need to flatten a hierarchy I skipped the classes(root) function, in the example. What I am wondering is whether or not the packageName attribute plays any role in the nodes()?
How can I resolve this issue?
You haven't specified a value accessor:
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5)
.value(function(d) { return d.pval; }) //<- must return a number
Example here.
While trying to understand d3 I saw the line .text(String);. I could not understand what String is suppose to be. I thought maybe its an empty string (nope), a method (i didnt see that in the api reference) and pondered what else it could be.
I commented it out below and got expected results. What I don't understand is what is String and why does it work. With this line my 3 squared boxes has text (its a internal value of the data it will represent later) while commented out it does not.
Demo
Html
<div class='chart' id='chart-10'/>
<script src="http://d3js.org/d3.v3.min.js"></script>
JS:
var w = 360;
var h = 180;
var svg = d3.select("#chart-10").append("svg")
.attr("width", w)
.attr("height", h);
var g = svg.selectAll(".data")
.data([50,150,250])
.enter().append("g")
.attr("class", "data")
.attr("transform", function(d, i) { return "translate(" + 20 * (i + 1) + ",20)"; });
g.append("circle")
.attr("class", "little")
.attr("r", 1e-6);
g.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 20)
.attr("height", 20)
.style("fill", "lightgreen")
.style("stroke", "green");
g.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
;// .text(String);
g.attr("transform", function(d, i) { return "translate(" + 20 * (i + 1) + ",20)"; });
g.select("rect").style("opacity", 1);
g.select("circle").attr("r", 1e-6);
var t = g.transition().duration(750);
t.attr("transform", function(d, i) { return "translate(" + d + ",90)"; });
t.select("circle").attr("r", Math.sqrt);
t.select("rect").style("opacity", 1e-6);
It looks like the String constructor. According to d3 documentation, as pointed out by Matt:
if value is a function, then the function is evaluated for each selected element (in order), being passed the current datum d and the current index i, with the this context as the current DOM element. The function's return value is then used to set each element's text content.
So, you set g.data to [50,150,250] a few lines before. Each number is converted to a String object by the String constructor, returned and used as the text values of your DOM nodes.
I am currently trying to place a svg:image in the centre of my arc:
var arcs = svg.selectAll("path");
arcs.append("svg:image")
.attr("xlink:href", "http://www.e-pint.com/epint.jpg ")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("width", "150px")
.attr("height", "200px");
I would appreciate it if someone could give me any advice on why it isn't appearing
thanks : http://jsfiddle.net/xwZjN/17/
Looking at the jsfiddle, you are creating the path elements after you try to append the svg:image elements to the them. It should be the other way around. You should first create the arcs and then append the images.
Second, as far as I know, the svg:path element should not contain any svg:image tags. It doesn't seem to display them if you place some inside. Instead what you should do is create svg:g tags with class arc and then use those to place the svg:images
Slightly modifying your jsfiddle could look something like this:
var colours = ['#909090','#A8A8A8','#B8B8B8','#D0D0D0','#E8E8E8'];
var arcs = svg.selectAll("path");
for (var z=0; z<30; z++){
arcs.data(donut(data1))
.enter()
//append the groups
.append("svg:g")
.attr("class", "arc")
.append("svg:path")
.attr("fill", function(d, i) { return colours[(Math.floor(z/6))]; })
.attr("d", arc[z])
.attr("stroke","black")
}
//here we append images into arc groups
var pics = svg.selectAll(".arc").append("svg:image")
.attr("xlink:href", "http://www.e-pint.com/epint.jpg ")
.attr("transform", function(d,i) {
//since you have an array of arc generators I used i to find the arc
return "translate(" + arc[i].centroid(d) + ")"; })
.attr("x",-5)
.attr("y",-10)
.attr("width", "10px")
.attr("height", "20px");
Where I also decreased the size of the images and offset them so that they fit into the arc.