how to create a dynamic append using d3? - javascript

I'm using d3 and I'd like to append a group with basic shapes attached to it, like the following:
startEvent (a circle)
task (a recangle)
endEvent (two circles)
since I'm new to d3 I'd like to know how to append each group dynamically depending on the 'shape type' and avoid to append each shape one by one using a foreach.
this is the code:
var shapes ={
startEvent:function(id,x,y,params){
var radius = 18,
cy = Math.floor(Number(y) + radius),
cx = Math.floor(Number(x) + radius),
g = d3.select('g');
var circle = g.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radius)
.attr('id', id);
if(params.label!==undefined){
var txt = g.append('text')
.attr('y',y).text(params.label);
txt.attr('x',Number(x));
txt.attr('y',Number(y));
}
return g;
},
endEvent:function(id,x,y, params){
// something similar to startEvent, but with two circles instead of one
},
task:function(id,x,y, params){
// something similar but with a rectangle
}
};
passing the data and rendering the elements:
svg.selectAll('g')
.data(data)
.enter()
.append(function(d){
params={label: d.meta.name};
return shapes[d.type](d.id,d.x,d.y,params);
});
but I'm getting
Error: Failed to execute 'appendChild' on 'Node': The new child
element is null.
I guess that's because I'm returning the selector, any ideas?

based on this and this answers I got to the following point, it seems like you need to create an instance manually under the d3 namespace, once you got that you can use a d3 selector over it and return the node() of the element which return the actual DOM code.
this is the code:
var shapes ={
startEvent:function(id,x,y,params){
var radius = 18,
cy = Math.floor(Number(y) + radius),
cx = Math.floor(Number(x) + radius),
e = document.createElementNS(d3.ns.prefix.svg,'g'),
g = d3.select(e).attr('id', id).
attr('class','node');
var circle = g.append('circle')
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radius)
.attr('class','circle');
if(params.label!==undefined){
var txt = g.append('text')
.attr('y',y).text(params.label);
txt.attr('x',Number(x));
txt.attr('y',Number(y));
}
return g;
},
endEvent:function(id,x,y, params){
// something similar to startEvent, but with two circles instead of one
},
task:function(id,x,y, params){
// something similar but with a rectangle
}
};
and then return the node
svg.selectAll('g')
.data(data)
.enter()
.append(function(d){
params={label: d.meta.name};
var v = shapes[d.type](d.id,d.x,d.y,params);
return v.node();
});

Related

how to create three-level donut chart in d3.js

I'm using trying to create a multi-level donut chart in d3 version5
This image is drawn by d3 version3. it is working fine in version3. I decided to upgrade d3 to the latest version. now, donut chart is not drawn by d3(also no errors in the console)
D3 version 3 > version 5
Here is the sample dataset I used:
Hint: first value in the array is used storage and second is free storage
{
average: [30.012, 69.988],
minimum: [10, 90],
maximum: [40, 60]
}
Note: Above data is just a sample this is not exact data.
Here is the code I tried:
var width = 300;
var height = 300;
var radius = Math.floor((width / 6) - 2);
var classFn = function(a, b) {
return a === 0 ? classes[b] : 'default';
};
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc();
var svg = d3.select(selector).append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg = svg.append("g");
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path");
path = path.data(function(d) {
return pie(d);
});
path.enter().append("path");
path.attr("class", function(d, i, j) {
return classFn(i, j);
})
path.attr("d", function(d, i, j) {
return arc.innerRadius((j === 0 ? 0 : 2) + radius * j).outerRadius(radius * (j + 1))(d);
});
Note: This code is working fine in d3 version3.
2. Update:
I've updated the answer with a better solution. I didn't do this at first, because I didn't grasp you structure. I've updated it to being more D3 idiomatic. Plus it does away with the hack I made in my first update :)
var dataset = {
average: [0, 100],
minimum: [0, 100],
maximum: [0, 100]
}
var width = 300;
var height = 300;
var radius = Math.floor((width / 6) - 2);
var pie = d3.pie().sort(null);
var arc = d3.arc();
var svg = d3.select('body').append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg = svg.append("g");
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
gs.each(function (d, j) {
d3.select(this).selectAll('path')
.data(pie(d)).enter()
.append('path')
.attr("class", function(d, i) {
// return classFn(i);
})
.attr('d', function (d) {
return arc
.innerRadius((j === 0 ? 0 : 2) + radius * j)
.outerRadius(radius * (j + 1))(d);
})
})
The updated code uses the index (here j) that is available when appending the g elements, which corresponds to you original j index. This makes it possible to calculate the radii in the original way.
To achieve this, the arc appending code is wrapped into a .each function that iterates over the g elements, making j available to us.
The class application should work as well, but I've commented it out, as the classFn function doesn't work, since the classes variable is not present.
1. Update:
Besides the original answer, when calculating the arc radii you rely on a j value that is different from D3 v3 and v5. I summise that j is used the index of the d3.values array, so I've cooked up a way to reverse look-up that index based on the input values.
First create a map for reverse mapping data values into their corresponding index:
var dataValueJoinChar = 'ยค'
var datasetValuesToIndex = d3.values(dataset).reduce((acc, curr, i) => {
acc[`0${dataValueJoinChar}${curr[0]}`] = i
acc[`1${dataValueJoinChar}${curr[1]}`] = i
return acc
}, {})
Then change the last part of your code to:
path = path.data(function(d) {
return pie(d);
}).enter().append("path");
path.attr("class", function(d, i, j) {
return classFn(i, j);
})
path.attr("d", function(d, i, j) {
var orgIndex = datasetValuesToIndex[`${i}${dataValueJoinChar}${d.data}`]
return arc
.innerRadius((orgIndex === 0 ? 0 : 2) + radius * orgIndex)
.outerRadius(radius * (orgIndex + 1))(d);
});
It might not be too pretty, but it's a simple adaption of your code that works.
------- Original answer --------
In D3 v5 pie and arc are found at d3.pie and d3.arc respectively. Therefore, try changing:
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc();
To this instead:
var pie = d3.pie().sort(null);
var arc = d3.arc();
Pie API reference: https://github.com/d3/d3-shape/blob/v1.3.4/README.md#pie
Arc API reference: https://github.com/d3/d3-shape/blob/v1.3.4/README.md#arc
If you use a bundler to bundle sub-modules, both are part of the d3-shape module. If not they are both available in the full D3 library.
Hope this helps!

D3 map, 'd' attribute

(sorry for my english bad level)
Hi I'm using D3 for the first time with mithril js. The map is ok but I have a problem with colors of provinces and it comes from the 'd' attribute to get the id of provinces.The attribute is undefined and I don't understand what is 'd' exactly. is mithril the problem? is there an other way to get 'd' attribute?
controller.map = function(el){
var width = 1160;
var height = 960;
var scale = 10000;
var offset = [width / 2, height / 2];
var center = [0, 50.64];
var rotate = [-4.668, 0];
var parallels = [51.74, 49.34];
var projection = d3.geo.albers()
.center(center)
.rotate(rotate)
.parallels(parallels)
.scale(scale)
.translate(offset)
;
var path = d3.geo.path()
.projection(projection)
;
var svg = d3.select(el).append("svg")
.attr("width",width)
.attr("height",height)
;
d3.json("belprov.json",function(error,be){
if (error) return console.error(error);
var bounds = path.bounds(topojson.feature(be, be.objects.subunits));
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale;
offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
var centroid = d3.geo.centroid(topojson.feature(be, be.objects.subunits));
center = [0, centroid[1]];
rotate = [-centroid[0],0];
projection = d3.geo.albers()
.center(center)
.rotate(rotate)
.parallels(parallels)
.scale(scale)
.translate(offset);
svg.selectAll(".province")
.data(topojson.feature(be, be.objects.provinces).features)
.enter().append("path")
.attr("class", function(d) { return "province " + d.id })
.attr("d", path)
;
})
};
The "d" attribute in a path object defines the successive coordinates of the points through which the path has to go (it also gives indication about whether the path should use bezier curves, straight lines, etc.). See some documentation here.
Be careful: in d3, d is often used as a parameter for anonymous functions representing the data currently binded to the current element. So the two are completely different things.
Here, your line
.attr("d", path)
should probably look more like
.attr("d", function(d){return d.path})
i.e., take the field path within the data elements.
You can do something like this to color diffrent paths:
//make a color scale
var color20 = d3.scale.category20();
//your code as you doing
//on making paths do
svg.selectAll(".province")
.data(topojson.feature(be, be.objects.provinces).features)
.enter().append("path")
.attr("class", function(d) { return "province " + d.id })
.style("fill", function(d){return color(d.id);})//do this to color path based on id.
.attr("d", path)

d3 steady horizontal transition along an SVG path

I'm using a d3 attrTween to translate a circle over a path smoothly, similar to this example and as shown in the picture below:
The circle's transition is defined here:
function transition() {
circle.transition()
.duration(2051)
.ease("linear")
.attrTween("transform", translateAlong(path.node()))
}
And the attribute tween is shown here:
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
This works well thanks to the SVG method getPointAtLength, which allows us to retrieve coordinates at different lengths of the path. However, I need a different kind of behavior and I've been unable to come up with a solution so far.
I need the circle to animate along the path, but at a steady horizontal speed. Meaning that the circle ought to take as much time to navigate this slice:
As it does with this slice:
Because both slices encompass the same width. On a low level, what I need is to be able to translate any X coordinate with its corresponding Y coordinate along the path. I've looked at all the SVG path methods and I haven't found anything particularly useful here. I'm hoping there's some way in D3 to feed an X coordinate to a d3 line and retrieve its corresponding Y coordinate.
Here's a JSFiddle working as described above. I'd really appreciate any help I can get on this. Thanks!
I ended up creating a lookup array for all my points along the line using getPointAtLength:
var lookup = [];
var granularity = 1000;
var l = path.node().getTotalLength();
for(var i = 1; i <= granularity; i++) {
var p = path.node().getPointAtLength(l * (i/granularity))
lookup.push({
x: p.x,
y: p.y
})
}
Once I had all those points in my lookup table, I used a bisector in my translate tween:
var xBisect = d3.bisector(function(d) { return d.x; }).left;
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var index = xBisect(lookup, l * t);
var p = lookup[index];
return "translate(" + p.x + "," + p.y + ")";
};
};
}
And it works as expected! Yahoo!
Fiddle

d3.js / svg - how to dynamically append text to my arcs

I am trying to complete the last bit of a d3 project which dynamically creates these blue arcs, over which I need to place arc text, as shown in this image:
The image above is something I've done by placing the arc text statically, through trial and error, but I want to place it dynamically, based on the blue arcs which sit beneath the text. This is the code that dynamically creates the arcs:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { console.log(d.__data__.key); return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group");
arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5);
The {exp:} content is preparsed data I'm pulling from my content management system in expression engine if it looks confusing.
So, I have my arcs. Now you'll notice in the groupData code block I have a console.log statement, that will give me the names I want to appear in the arc text:
console.log(d.__data__.key);
Now, the code I was using to place the arc text statically was this:
var arcData = [
{aS: 0, aE: 45,rI:radius - chartConfig.linePadding + chartConfig.arcPadding,rO:radius - chartConfig.linePadding + chartConfig.textPadding-chartConfig.arcPadding}
];
var arcJobsData = d3.svg.arc().innerRadius(arcData[0].rI).outerRadius(arcData[0].rO).startAngle(degToRad(1)).endAngle(degToRad(15));
var g = d3.select(".chart").append("svg:g").attr("class","arcs");
var arcJobs = d3.select(".arcs").append("svg:path").attr("d",arcJobsData).attr("id","arcJobs").attr("class","arc");
g.append("svg:text").attr("x",3).attr("dy",15).append("svg:textPath").attr("xlink:href","#arcJobs").text("JOBS").attr("class","arcText"); //x shifts x pixels from the starting point of the arc. dy shifts the text y units from the top of the arc
And in this above code, the only thing left that I should need to do is dynamically assign an ID to the arcs, and then reference that ID in the xlink:href attribute, as well as replace the text("JOBS") with text that pulls from d.data__key. Given the code above which dynamically creates the arcs, and given that I know how to dynamically create and retrieve the text I want to place in the arcs using d.__data.key, I should be able to finish this thing off, but I can't figure out how write code in d3 that will take the data and place it in the arcs. Can anybody help with this?
You should give this blog post on nested selections a read; I believe it'll explain what you're trying to do.
Here's the gist. When you add data to your selection, assign the selection to a variable:
var g = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { /* stuff */ }));
That way, you can perform subselections on it, which will receive a single element of the data bound to your g selection. You can use this to add your arcs and text:
g.enter().append('group') // Question: Are you sure you mean 'group' here?
.attr('class', 'group')
g.selectAll('g.arc')
.data(function(d, i) { return d; })
enter().append('path')
// Setup the path here
g.selectAll('text')
.data(function(d, i) { return d; })
.enter().append('text')
.attr('text', function(d) { return d.__data__.key })
The functions that are being used to do data binding in the nested selections (i.e., the g.selectAll()s) are being passed a single element of the data attached to g as d, and i is its index.
Figured this out. Changed the structure of things a bit so it made a little more sense, but essentially what I did is this:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group"); //MH - why do we need this group - these elements are empty. Shouldn't this just be an array? Find out how to delete the svg elements without getting rid of the data, which is needed below.
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) { return (findStartAngle(d.__data__.children)-2) * pi / 180;})
.endAngle(function(d) { console.log(d.__data__.key); return (findEndAngle(d.__data__.children)+2) * pi / 180});
var arc_and_text = arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:g")
.attr("class","arc_and_text");
var arc_path = arc_and_text.append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) { return "arc" + i; })
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5); //MH: (d.__data__.key) gives names of groupings
var arc_text = arc_and_text.append("text")
.attr("class","arc_text")
.attr("x", 3)
.attr("dy", 15);
arc_text.append("textPath")
.attr("xlink:href", function(d, i) { return "#arc" + i; })
.attr("class","arc_text_path")
.style("fill","#ffffff")
.text(function(d, i) { return d.__data__.key; });
D3 still mystifies me a bit, and I'm sure this code could be much improved, but it works.

Combining Parent and Nested Data with d3.js

I have a data structure like this (assume that the data structure is non-negotiable):
data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]};
Using the d3.js javascript library, I'd like to draw four rectangles, one for each color in both colors arrays. Information from each entry in the segments array is used to draw the rectangles corresponding to each color in its color array. E.g., The red and green rectangles will have a width and height of 10. The resulting html should look like this:
<div id="container">
<svg width="200" height="200">
<g>
<rect x="20" y="20" width="10" height="10" fill="#ff0000"></rect>
<rect x="30" y="30" width="10" height="10" fill="#00ff00"></rect>
</g>
<g>
<rect x="40" y="40" width="20" height="20" fill="#0000ff"></rect>
<rect x="60" y="60" width="20" height="20" fill="#000000"></rect>
</g>
</svg>
</div>
I've come up with some code that accomplishes this, but I found the part about using data from two different levels of nesting in data to be confusing, and I feel that there might be a more idiomatic way to accomplish the same with d3.js. Here's the code (full example at http://jsbin.com/welcome/39650/edit):
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
// add the top-level svg element and size it
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter()
.append('g');
// Add a rectangle for each color
nested
.selectAll('rect')
.data(function(d) {
// **** ATTENTION ****
// Is there a more idiomatic, d3-ish way to approach this?
var expanded = [];
for(var i = 0; i < d.colors.length; i++) {
expanded.push({
color : d.colors[i],
x : d.x
size : d.size });
}
return expanded;
})
.enter()
.append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
Is there a better and/or more idiomatic way to access data from two different levels of nesting in a data structure using d3.js?
Edit
Here's the solution I came up with, thanks to meetamit's answer for the closure idea, and using more idiomatic d3.js indentation thanks to nautat's answer:
$(function() {
var
vis = null,
width = 200,
height = 200,
data = {
segments : [
{x : 20, y : 0, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, y : 0, size : 20, colors : ['#0000ff','#000000']}
]
};
// set the color
function f(d,i) {return d;}
// set the position
function pos(segment) {
return function(d,i) {
return segment.x + (i * segment.size);
};
}
// set the size
function size(segment) {
return function() {
return segment.size;
};
}
// add the top-level svg element and size it
vis = d3.select('#container').append('svg')
.attr('width',width)
.attr('height',height);
// add the nested svg elements
var nested = vis
.selectAll('g')
.data(data.segments)
.enter().append('g');
// Add a rectangle for each color. Size of rectangles is determined
// by the "parent" data object.
nested
.each(function(segment, i) {
var
ps = pos(segment),
sz = size(segment);
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors)
.enter().append('rect')
.attr('x', ps)
.attr('y',ps)
.attr('width', sz)
.attr('height',sz)
.attr('fill', f);
});
});
Here's the full working example: http://jsbin.com/welcome/42885/edit
You can use closures
var nested = vis
.selectAll('g')
.data(data.segments);
nested.enter()
.append('g')
.each(function(segment, i) {
var colors = d3.select(this)
.selectAll('rect')
.data(segment.colors);
colors.enter()
.append('rect')
.attr('x', function(color, j) { return pos(segment, j); })
// OR: .attr('x', function(color, j) { return segment.x + (j * segment.size); })
.attr('width', function(color, j) { return size(segment); })
.attr('fill', String);
});
You could do something like the following to restructure your data:
newdata = data.segments.map(function(s) {
return s.colors.map(function(d) {
var o = this; // clone 'this' in some manner, for example:
o = ["x", "size"].reduce(function(obj, k) { return(obj[k] = o[k], obj); }, {});
return (o.color = d, o);
}, s);
});
This will transform your input data into:
// newdata:
[
[
{"size":10,"x":20,"color":"#ff0000"},
{"size":10,"x":20,"color":"#00ff00"}],
[
{"size":20,"x":40,"color":"#0000ff"},
{"size":20,"x":40,"color":"#000000"}
]
]
which then can be used in the standard nested data selection pattern:
var nested = vis.selectAll('g')
.data(newdata)
.enter().append('g');
nested.selectAll('rect')
.data(function(d) { return d; })
.enter().append('rect')
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f);
BTW, if you'd like to be more d3-idiomatic, I would change the indentation style a bit for the chained methods. Mike proposed to use half indentation every time the selection changes. This helps to make it very clear what selection you are working on. For example in the last code; the variable nested refers to the enter() selection. See the 'selections' chapter in: http://bost.ocks.org/mike/d3/workshop/
I would try to flatten the colors before you actually start creating the elements. If changes to the data occur I would then update this flattened data structure and redraw. The flattened data needs to be stored somewhere to make real d3 transitions possible.
Here is a longer example that worked for me. Yon can see it in action here.
Here is the code:
var data = {
segments : [
{x : 20, size : 10, colors : ['#ff0000','#00ff00']},
{x : 40, size : 20, colors : ['#0000ff','#000000']}
]
};
function pos(d,i) { return d.x + (i * d.size); } // rect position
function size(d,i) { return d.size; } // rect size
function f(d,i) { return d.color; } // rect color
function flatten(data) {
// converts the .colors to a ._colors list
data.segments.forEach( function(s,i) {
var list = s._colors = s._colors || [];
s.colors.forEach( function(c,j) {
var obj = list[j] = list[j] || {}
obj.color = c
obj.x = s.x
obj.size = s.size
});
});
}
function changeRect(chain) {
return chain
.transition()
.attr('x',pos)
.attr('y',pos)
.attr('width',size)
.attr('height',size)
.attr('fill',f)
.style('fill-opacity', 0.5)
}
vis = d3
.select('#container')
.append('svg')
.attr('width',200)
.attr('height',200);
// add the top-level svg element and size it
function update(){
flatten(data);
// add the nested svg elements
var all = vis.selectAll('g')
.data(data.segments)
all.enter().append('g');
all.exit().remove();
// Add a rectangle for each color
var rect = all.selectAll('rect')
.data(function (d) { return d._colors; }, function(d){return d.color;})
changeRect( rect.enter().append('rect') )
changeRect( rect )
rect.exit().remove()
}
function changeLater(time) {
setTimeout(function(){
var ds = data.segments
ds[0].x = 10 + Math.random() * 100;
ds[0].size = 10 + Math.random() * 100;
ds[1].x = 10 + Math.random() * 100;
ds[1].size = 10 + Math.random() * 100;
if(time == 500) ds[0].colors.push("orange")
if(time == 1000) ds[1].colors.push("purple")
if(time == 1500) ds[1].colors.push("yellow")
update()
}, time)
}
update()
changeLater(500)
changeLater(1000)
changeLater(1500)
Important here is the flatten function which does the data conversion and stores/reuses the result as _colors property in the parent data element. Another important line is;
.data(function (d) { return d._colors; }, function(d){return d.color;})
which specifies where to get the data (first parameter) AND what the unique id for each data element is (second parameter). This helps identifying existing colors for transitions, etc.

Categories

Resources