I'm having some problems while updating a pack layout that is composed by a circle inside a g element, where the translation is controled by g and the radius is controled by the circle:
<g transform="translate(1,1)"><circle r="1"></circle></g>
The update was possible with the translation controlled by the circle, but I'm having the wrong x, y and r with a g as a container, as in demo.
var diameter = 300;
function translate(x, y) {
return "translate(" + x + "," + y + ")";
}
// The layout I'm using now
var pack = d3.layout.pack()
.size([diameter - 4, diameter - 4])
.value(function(d) { return 1; });
// The basic container
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(2,2)");
// My initial data source
var data = {
name: "Languages",
children: [{
name: "Functional",
children: [
{ name: "OCaml" },
{ name: "Haskell" },
{ name: "Erlang" }
]
}, {
name: "Imperative",
children: [
{ name: "BASIC" },
{ name: "Clipper" }
]
}]
};
(window.update = function() {
// Modify the current data object
data.children.push({
name: "NEW ELEMENT " + Math.floor(Math.random() * 100)
});
// Select *ALL* elements
var selection = svg.datum(data).selectAll(".node")
.data(pack.nodes);
// Select *ONLY NEW* nodes and work on them
selection
.enter()
.append("g")
.classed("node", true)
.append("circle")
.style("fill", "black");
// Recompute *ALL* elements
// Here the results aren't consistent, I always get the wrong translation
// and radius, therefore, the elements are badly positioned
selection
.transition()
.duration(500)
.attr("transform", function(d) {
return translate(d.x, d.y);
})
.selectAll("circle")
.attr("r", function(d) {
return d.r;
});
})();
// Set the height
d3.select(self.frameElement).style("height", diameter + "px");
I see I'm selecting the correct elements, but why I'm having the wrong results whenever I call update?
Well, the code is mostly correct, but selectAll("circle") will select all the circle elements and apply the radius of the inner element for all of them. We need to select just the inner circle, therefore, select("circle") solves the question (demo).
Related
I want to repeat a group of shapes specifically
text rect text circles
Where in circles is again a repeat of circle
My data is
Jsondata =[
{ "name":"A", "WidthOfRect":50, "justAnotherText":"250", "numberOfCircles" :3 },
{ "name":"B", "WidthOfRect":150, "justAnotherText":"350","numberOfCircles" :2 },
{ "name":"C", "WidthOfRect":250, "justAnotherText":"450","numberOfCircles" :1 }]
Basically Out of this data i am trying to construct a customized bar chart.
The width of the rect is based upon the data widthofrect from the json, as well as number of circles is based upon numberofcircles property.
I looked out for a number of options to repeat group of shapes but couldn't find one.
First of all, you're right in your comment: do not use loops to append elements in a D3 code. Also, your supposition about the length of the data is correct.
Back to the question:
The text and rect part is pretty basic, D3 101, so let's skip that. The circles is the interesting part here.
My proposed solution involves using d3.range to create an array whose number of elements (or length) is specified by numberOfCircles. That involves two selections.
First, we create the groups (here, scale is, obviously, a scale):
var circlesGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(20," + scale(d.name) + ")"
});
And then we create the circles. Pay attention to the d3.range:
var circles = circlesGroups.selectAll(null)
.data(function(d) {
return d3.range(d.numberOfCircles)
})
.enter()
.append("circle")
//etc...
Here is a demo, I'm changing the numberOfCircles in your data to paint more circles:
var width = 500,
height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = [{
"name": "A",
"WidthOfRect": 50,
"justAnotherText": "250",
"numberOfCircles": 13
},
{
"name": "B",
"WidthOfRect": 150,
"justAnotherText": "350",
"numberOfCircles": 22
},
{
"name": "C",
"WidthOfRect": 250,
"justAnotherText": "450",
"numberOfCircles": 17
}
];
var scale = d3.scalePoint()
.domain(data.map(function(d) {
return d.name
}))
.range([20, height - 20])
.padding(0.5);
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
var circlesGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(20," + scale(d.name) + ")"
})
.style("fill", function(d) {
return colorScale(d.name)
})
var circles = circlesGroups.selectAll(null)
.data(function(d) {
return d3.range(d.numberOfCircles)
})
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return 10 + 12 * d
});
var axis = d3.axisLeft(scale)(svg.append("g").attr("transform", "translate(20,0)"));
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: I'm using D3 v5.
Do you have any idea why the below code doesn't work? I'm trying to create 3 groups with different y coordinate for each. But as soon as I do it like that the transformation is not applied at all and all the <g>s are overlapping at 0,0.
If I change the function to explicit x,y coordinates in transformation it works correctly.
var dataset = [{
data: 100
}, {
data: 200
}, {
data: 300
}];
var groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.attr("transform", "translate(0" + function(d,i) {return i * 100} + ")");
You have to return the whole translate value in a function:
.attr("transform", function (d, i){
return "translate(0," + (i * 100) + ")";
});
I have a JavaScript code where the dataset is hard-coded in a variable, like this -
var dataset = [
{category: "Dept 1", measure: 0.30},
{category: "Dept 2", measure: 0.25},
{category: "Dept 4", measure: 0.15},
{category: "Dept 3", measure: 0.05},
{category: "Dept 5", measure: 0.18},
{category: "Dept 6", measure: 0.04},
{category: "Dept 7", measure: 0.03}
]
;
Now I want to use the json data which is getting returned from a php file (fetching through mysql query).
Whats the effective way to do this. does getJSON works well in this case?
Note- I am working on a pie chart in d3.js and this dataset requirement is for that chart.
EDIT -
This is how the code looks after suggested changes-
function dsPieChart(){
var width = 400,
height = 400,
outerRadius = Math.min(width, height) / 2,
innerRadius = outerRadius * .999,
// for animation
innerRadiusFinal = outerRadius * .5,
innerRadiusFinal3 = outerRadius* .45,
color = d3.scale.category20() //builtin range of colors
;
d3.json("data/mixchart.php", function(error, dataset) {
if (error) return console.warn(error);
else
{
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 + ": " + formatAsPercentage(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.
// source: http://bl.ocks.org/1305337#index.html
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 formatAsPercentage(d.value); })
.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("Revenue Share 2012")
.attr("class","title")
;
}
});
function mouseover() {
d3.select(this).select("path").transition()
.duration(750)
//.attr("stroke","red")
//.attr("stroke-width", 1.5)
.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));
}
}
dsPieChart();
Edit 2 -
<script type="text/javascript">
/*
################ FORMATS ##################
-------------------------------------------
*/
var formatAsPercentage = d3.format("%"),
formatAsPercentage1Dec = d3.format(".1%"),
formatAsInteger = d3.format(","),
fsec = d3.time.format("%S s"),
fmin = d3.time.format("%M m"),
fhou = d3.time.format("%H h"),
fwee = d3.time.format("%a"),
fdat = d3.time.format("%d d"),
fmon = d3.time.format("%b")
;
var width = 400,
height = 400,
outerRadius = Math.min(width, height) / 2,
innerRadius = outerRadius * .999,
// for animation
innerRadiusFinal = outerRadius * .5,
innerRadiusFinal3 = outerRadius* .45,
color = d3.scale.category20() //builtin range of colors
;
d3.json("data/mixchart.php", function(error,data) {
data.forEach(function(d) {
d.category =d.category;
d.measure = d.measure;
});
//if (err) return console.warn(err);
var vis = d3.select("#pieChart")
.append("svg:svg") //create the SVG element inside the <body>
.data(data) //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 + ": " + formatAsPercentage(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.
// source: http://bl.ocks.org/1305337#index.html
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 formatAsPercentage(d.value); })
.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("Revenue Share 2012")
.attr("class","title")
;
function mouseover() {
d3.select(this).select("path").transition()
.duration(750)
//.attr("stroke","red")
//.attr("stroke-width", 1.5)
.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));
}
</script>
var dataset = [];
$.getJSON("your_php_file", function(result){
dataset = result;
});
this will work but keep in mind that your php file is returning only json... rest you can play with the options.
There are numerous ways of fetching JSON, but as you're already working with d3, d3.json would be a good way to go.
E.g.
d3.json('your/json.json', function(error, json) {
if (error) return console.warn(error);
doSomethingWithJson(json)
});
Also see the d3 API
I am trying to make a simple rectangular structure for single-nested data in D3. I would like the result to look like the following image:
In other words, each group's items should be sized so that all groups take up the same space.
The JSFiddle that I have made does not yield the correct result:
var item_groups_enter = item_groups.enter()
.append("g")
.classed("item-group", true)
.attr("transform", function (d, i) {
return ("translate(0, " + 50 * i + ")"); // !!! THIS NEEDS TO CHANGE !!!
});
// Append a rectangle for each item
item_groups_enter.append("rect")
.attr("width", main_group_width)
.attr("height", 50) // !!! THIS NEEDS TO CHANGE !!!
.attr("fill", function (d, i) {
return colours(i)
});
// Also append a label for each item
item_groups_enter.append("text")
.text(function (d) {
return (d)
})
.attr("x", main_group_width * 0.5)
.attr("y", 25) // !!! THIS NEEDS TO CHANGE !!!
.style("text-anchor", "middle");
I realise that I would somehow need to need to pass the main-groups' data (specifically, the number of children) to the item_groups, but I am unsure how to do this. I did try setting a custom attribute childCount on the main_group, but the code became quite messy referencing parent nodes (and then grandparent nodes later on).
What would be the way to do this? I am unsure if I should even be thinking about the solution in terms of D3, or in terms of CSS?
When you use a function with selection.attr, this is set to the current DOM element. You can use it to access the parent selection and through it the underlying data :
For example,
var item_groups_enter = item_groups.enter()
.append("g")
.classed("item-group", true)
.attr("transform", function (d, i) {
// this is the g node
var parentdatum = d3.select(this.parentNode).datum();
var itemY = available_height/parentdatum.values.length * i;
return ("translate(0, " + itemY + ")");
});
var data = [{
group: "Fruits",
values: ["Apple", "Banana", "Pear", "Plum"]
}, {
group: "Cakes",
values: ["Chocolate Cake", "Red Velvet Cake", "Carrot Cake"]
}, {
group: "Dogs",
values: ["Spaniel", "Chow", "Dachshund", "Bulldog", "Beagle", "Boxer", "Pug"]
}]
// Get a handle on the svg HTML element
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
// Calculate spacing
var available_width = parseInt(svg.style("width"));
var available_height = parseInt(svg.style("height"));
var main_group_width = available_width / data.length;
// Define the colours to use
var colours = d3.scale.category10();
// Make an HTML group for each of the groups in the data
var main_groups = svg.selectAll("g")
.data(data);
// For each datum entered, append a new HTML group
main_groups.enter()
.append("g")
.classed("main-group", true)
.attr("transform", function (d, i) {
return ("translate(" + i * main_group_width + ", 0)");
})
// Append a new group, an "item group" for each of the values in each of the main groups
var item_groups = main_groups.selectAll("g")
.data(function (d) {
return (d.values)
});
var item_groups_enter = item_groups.enter()
.append("g")
.classed("item-group", true)
.attr("transform", function (d, i) {
var parentdatum = d3.select(this.parentNode).datum();
return ("translate(0, " + available_height/parentdatum.values.length * i + ")");
});
// Append a rectangle for each item
item_groups_enter.append("rect")
.attr("width", main_group_width)
.attr("height", function() {
// we want the grand parent node
var parentdatum = d3.select(this.parentNode.parentNode).datum();
return available_height/parentdatum.values.length;
})
.attr("fill", function (d, i) {
return colours(i)
});
// Also append a label for each item
item_groups_enter.append("text")
.text(function (d) {
return (d)
})
.attr("x", main_group_width * 0.5)
.attr("y", 25)
.style("text-anchor", "middle");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You could could also traverse the groups defined by the item_groups selection (one group per parent element in main_groups.selectAll("g")) and assign your properties. For example
item_groups_enter.forEach(function(g, i) {
var parentdatum = d3.select(g.parentNode).datum();
var h = available_height/parentdatum.values.length;
var selection = d3.selectAll(g);
selection.attr("transform", function (d, i) {
return ("translate(0, " + h * i + ")");
});
selection.select('rect')
.attr("height", h);
selection.select('text')
.attr("y", h/2);
});
You can use the parentNode defined on each group to determine the correct parent data.
var data = [{
group: "Fruits",
values: ["Apple", "Banana", "Pear", "Plum"]
}, {
group: "Cakes",
values: ["Chocolate Cake", "Red Velvet Cake", "Carrot Cake"]
}, {
group: "Dogs",
values: ["Spaniel", "Chow", "Dachshund", "Bulldog", "Beagle", "Boxer", "Pug"]
}]
// Get a handle on the svg HTML element
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500)
// Calculate spacing
var available_width = parseInt(svg.style("width"));
var available_height = parseInt(svg.style("height"));
var main_group_width = available_width / data.length;
// Define the colours to use
var colours = d3.scale.category10();
// Make an HTML group for each of the groups in the data
var main_groups = svg.selectAll("g")
.data(data);
// For each datum entered, append a new HTML group
main_groups.enter()
.append("g")
.classed("main-group", true)
.attr("transform", function (d, i) {
return ("translate(" + i * main_group_width + ", 0)");
})
// Append a new group, an "item group" for each of the values in each of the main groups
var item_groups = main_groups.selectAll("g")
.data(function (d) {
return (d.values)
});
var item_groups_enter = item_groups.enter()
.append("g")
.classed("item-group", true);
item_groups_enter.append("rect")
.attr("width", main_group_width)
.attr("fill", function (d, i) {
return colours(i)
});
item_groups_enter.append("text")
.text(function (d) {
return (d)
})
.attr("x", main_group_width * 0.5)
.attr("y", 25)
.style("text-anchor", "middle");
item_groups_enter.forEach(function(g, i) {
var parentdatum = d3.select(g.parentNode).datum();
var h = available_height/parentdatum.values.length;
var selection = d3.selectAll(g);
selection.attr("transform", function (d, i) {
return ("translate(0, " + h * i + ")");
});
selection.select('rect')
.attr("height", h);
selection.select('text')
.attr("y", h/2);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I'm stuck with this in the past 2 weeks. I've been browsing all StackOverflow questions about updating the pack layout and similars and I read the d3js documentation about it and the update pattern, but none of them solve this problem and I really can't append a new element to the pack layout without redrawing everything.
Demo: http://jsfiddle.net/v74qp3a7/1/
var diameter = 300;
// The layout I'm using now
var pack = d3.layout.pack()
.size([diameter - 4, diameter - 4])
.value(function(d) { return 1 });
// The basic container
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(2,2)");
// My initial data source
var data = {
name: "Languages",
children: [{
name: "Functional",
children: [
{ name: "OCaml" },
{ name: "Haskell" },
{ name: "Erlang" }
]
}, {
name: "Imperative",
children: [
{ name: "BASIC" },
{ name: "Clipper" }
]
}]
};
// The node basis
var node = svg.datum(data).selectAll(".node")
.data(pack.nodes)
.enter().append("circle")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("r", function(d) {
return d.r;
});
function addToRoot(obj) {
// Modify the current data object
data.children.push(obj);
// I try to modify by entering the node and applying a transition
svg.datum(data).selectAll(".node")
.data(pack.nodes)
.enter().append("circle")
.transition()
.duration(500)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("r", function(d) {
return d.r;
});
}
d3.select(self.frameElement).style("height", diameter + "px");
I read about enter and exit, but I didn't figure out how I can update the data object (not the data properties, that is pretty simple). I've been trying to use the three update patterns and got no results.
How can I add a simple element and update the graphical interface without redrawing everything when I call addToRoot({ name: "foo" })?
First, your selector relies on a class that you're never assigning to the circles in question. Hence, your selection will always be empty and simply add everything again. Second, you need to handle the update selection as well, not just the enter selection.
var sel = svg.datum(data).selectAll(".node")
.data(pack.nodes);
sel.enter().append("circle")
.classed("node", true);
sel
.transition()
.duration(500)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("r", function(d) {
return d.r;
});
Complete demo here.