Separated, Sorted d3 Circles displayed along the X axis - javascript

I'm trying to achieve the following display utilizing the d3js.org library:
I get the circle svg objects to display with varying radius based on an attribute I'm getting from the JSON, but where I'm getting stuck is grouping them together and displaying along a linear, horizontal axis.
Here is my JSON structure:
[
{
"category" : "Foo",
"radius" : "3"
},
{
"category" : "Bar",
"radius" : "2"
},
{
"category" : "Foo",
"radius" : "3"
},
{
"category" : "Bar",
"radius" : "1"
},
{
"category" : "Bar",
"radius" : "2"
},
{
"category" : "Foo",
"radius" : "1"
}
]
d3 Javascript
var height = 50,
width = 540;
var companyProfileVis = d3.select(".myDiv").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
d3.json("data/myData.json", function(data){
companyProfileVis.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", function (d) { return d.radius * 4; })
.attr("cx", function(d) { return d.radius * 20; })
.attr("cy", 20)
});
And finally my HTML
<div class="myDiv"></div>

Expanding on Pablo's answer a bit, you would also need to sort the values in the grouped elements to achieve the order you have in the picture. The code would look like this.
var nested = d3.nest()
.key(function(d) { return d.category; })
.sortValues(function(a, b) { return b.radius - a.radius; })
.entries(data);
The nested selection based on this would look as follows.
var gs = svg.selectAll("g").data(nested)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + (20 + i * 100) + ")"; });
gs.selectAll("circle").data(function(d) { return d.values; })
.enter().append("circle");
Note that you're moving the g elements according to the index, so you don't have to worry about the y coordinate of the circles later. The x coordinate is computed based on the index similar to how the y coordinate is computed for the g.
All that you then have to do is set a few more attributes and append the text elements. Complete demo here.

You can use d3.nest to group the data items by category, and then use nested selections to create both groups of circles.
// Nest the items by category
var nestedData = d3.nest(data)
.key(function(d) { return d.category; })
.map(data, d3.map)
.values();
This will give you the following array:
nestedData = [
[
{category: "Foo", radius: "3"},
{category: "Foo", radius: "3"},
{category: "Foo", radius: "1"}
],
[
{category: "Bar", radius: "2"},
{category: "Bar", radius: "1"},
{category: "Bar", radius: "2"}
]
]
Regards,

Related

d3.js: Access data nested 2 level down

Data structure:
var data = [
{name: "male",
values: [
{ count: 12345,
date: Date 2015-xxx,
name: "male" },
{...}
]
},
{name: "female",
values: [
{ count: 6789,
date: Date 2015-xxx,
name: "female" },
{...}
]
}
]
The values that I want to access are data[a].values[b].count
The values are used to draw circles for my plot
code for circle plot:
focus.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", function(d,i) { return x(d.values[i].date); })
.attr("cy", function(d,i) { return y(d.values[i].count); })
.attr("r", 4)
.style("fill", function(d,i) { return color(d.values[i].name); })
The problem is that i = 1 because of its position in the object.
What I want to do is to loop through all the objects under values. How can I do that?
Edit: I wish to learn how to do it without altering the data, to improve on my skills.
Thanks.
There are several ways to do what you want using D3 only, without any other library and without altering the data. One of them is using groups to deal with "higher" levels of data (regarding the nested data). Let's see it in this code:
First, I mocked up a dataset just like yours:
var data = [
{name: "male",
values: [{ x: 123,y: 234},
{ x: 432,y: 221},
{ x: 199,y: 56}]
},
{name: "female",
values: [{ x: 223,y: 111},
{ x: 67,y: 288},
{ x: 19, y: 387}]
}
];
This is the data we're gonna use. I'm gonna make a scatter plot here (just as an example), so, let's set the domains for the scales accessing the second level of data (x and y inside values):
var xScale = d3.scaleLinear().range([20, 380])
.domain([0, d3.max(data, function(d){
return d3.max(d.values, function(d){
return d.x;
})
})]);
var yScale = d3.scaleLinear().range([20, 380])
.domain([0, d3.max(data, function(d){
return d3.max(d.values, function(d){
return d.y;
})
})]);
Now comes the most important part: we're gonna bind the data to "groups", not to the circle elements:
var circlesGroups = svg.selectAll(".circlesGroups")
.data(data)
.enter()
.append("g")
.attr("fill", function(d){ return (d.name == "male") ? "blue" : "red"});
Once in the first level of data we have 2 objects, D3 will create 2 groups for us.
I also used the groups to set the colours of the circles: if name is "male", the circle is blue, otherwise it's red:
.attr("fill", function(d){ return (d.name == "male") ? "blue" : "red"});
Now, with the groups created, we create circles according to the values in the data of each group, binding the data as follows:
var circles = circlesGroups.selectAll(".circles")
.data(function(d){ return d.values})
.enter()
.append("circle");
Here, function(d){ return d.values} will bind the data to the circles according to the objects inside values arrays.
And then you position your circles. This is the whole code, click "run code snippet" to see it:
var data = [
{name: "male",
values: [{ x: 123,y: 234},
{ x: 432,y: 221},
{ x: 199,y: 56}]
},
{name: "female",
values: [{ x: 223,y: 111},
{ x: 67,y: 288},
{ x: 19, y: 387}]
}
];
var xScale = d3.scaleLinear().range([20, 380])
.domain([0, d3.max(data, function(d){
return d3.max(d.values, function(d){
return d.x;
})
})]);
var yScale = d3.scaleLinear().range([20, 380])
.domain([0, d3.max(data, function(d){
return d3.max(d.values, function(d){
return d.y;
})
})]);
var xAxis = d3.axisBottom(xScale).tickSizeInner(-360);
var yAxis = d3.axisLeft(yScale).tickSizeInner(-360);
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0,380)")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(20,0)")
.call(yAxis);
var circlesGroups = svg.selectAll(".circlesGroups")
.data(data)
.enter()
.append("g")
.attr("fill", function(d){ return (d.name == "male") ? "blue" : "red"});
var circles = circlesGroups.selectAll(".circles")
.data(function(d){ return d.values})
.enter()
.append("circle");
circles.attr("r", 10)
.attr("cx", function(d){ return xScale(d.x)})
.attr("cy", function(d){ return yScale(d.y)});
.axis path, line{
stroke: gainsboro;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
The easiest way is to use a lib like underscore.js to edit your data array.
From underscore docs:
flatten _.flatten(array, [shallow])
Flattens a nested array (the nesting can be to any depth). If you pass shallow, >the array will only be flattened a single level.
_.flatten([1, [2], [3, [[4]]]]);
-> [1, 2, 3, 4];
_.flatten([1, [2], [3, [[4]]]], true);
-> [1, 2, 3, [[4]]];
map _.map(list, iteratee, [context]) Alias: collect
Produces a new array of values by mapping each value in list through a >transformation function (iteratee). The iteratee is passed three arguments: the >value, then the index (or key) of the iteration, and finally a reference to the >entire list.
_.map([1, 2, 3], function(num){ return num * 3; });
=> [3, 6, 9]
_.map({one: 1, two: 2, three: 3}, function(num, key){ return num * 3; });
=> [3, 6, 9]
_.map([[1, 2], [3, 4]], _.first);
=> [1, 3]
Underscore documentation
In your code you can do something like that:
var flatData = _.flatten(_.map(data, (d)=>d.values));
focus.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", function(d,i) { return x(d.date); })
.attr("cy", function(d,i) { return y(d.count); })
.attr("r", 4)
.style("fill", function(d,i) { return color(d.name); })

d3.js - Text position based on text width

I'm working with d3.js to generate a visualization that represents different hypotheses. Since the hypotheses are made of different parts , each word / part gets its own text element.
I want to base the x-position of each text element on the text width of the previous word including an offset. Having a hypothesis "IF x THEN y" i would need 4 text elements with "IF" having x=0, and since "IF" has a width of 10 and i use an offset of 5 "x" will get x=15 and so on.
I'm using json data that could look like this:
{[
{"id" : "id0",
"elements" : [
{
"text" : "IF",
"type" : "conditional"
},
{
"text" : "X",
"type" : "variable"
},
{
"text" : "THEN",
"type" : "conditional"},
{
"text" : "Y",
"type" : "variable"
}
]},
{"id" : "id1",
"elements" : [
{
"text" : "IF",
"type" : "conditional"
},
{
"text" : "abc",
"type" : "variable"
},
{
"text" : "THEN",
"type" : "conditional"},
{
"text" : "xyz",
"type" : "variable"
}
]}
]}
The code i am using to generate the text elements so far (each hypothesis is in a g-element is
var svg = d3.select("#viewport")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var content = svg.append("g").attr("id", "drawing");
var groups = content.selectAll().data(arr)
.enter()
.append("g")
.attr("class", function (d) {
return "hypothesis " + d["id"];
})
.each(function (d, i) {
d3.select(this).selectAll("text")
.data(d["elements"])
.enter()
.append("text")
.attr("class", function (d) {
return d.type;
})
.text(function (d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("x", function (d, j) {
return j++ * 100;
})
.attr("y", 50 * (i + 1));
});
When setting the x position i want to get the width of the current text element and push it onto a variable so i can get the next new x-coordinate instead of just using a currently random offset of 100 px per word.
So the question is how can i get the calculated text width (have seen things on getBBox or similar, but it didn't work for me since i don't know where to use them) and how to apply it to the text elements. Or if there is a better way to create the elements, maybe not in a single run.
The different elements need to be styled in different colors and have to react so mouse-over later, that's why they have to be single text elements.
Thanks in advance.
I always use getComputedTextLength for these sorts of things, although getBBox would also work:
.each(function(d, i) {
var runningWidth = 0; //<-- keep a running total
...
.attr("x", function(d, j) {
var w = this.getComputedTextLength(), //<-- length of this node
x = runningWidth; //<-- previous length to return
runningWidth += w; //<-- total
return x;
})
...
Full code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<div id="viewport"></div>
<script>
var arr =
[{
"id": "id0",
"elements": [{
"text": "IF",
"type": "conditional"
}, {
"text": "X",
"type": "variable"
}, {
"text": "THEN",
"type": "conditional"
}, {
"text": "Y",
"type": "variable"
}]
}, {
"id": "id1",
"elements": [{
"text": "IF",
"type": "conditional"
}, {
"text": "abc",
"type": "variable"
}, {
"text": "THEN",
"type": "conditional"
}, {
"text": "xyz",
"type": "variable"
}]
}];
var svg = d3.select("#viewport")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var content = svg.append("g").attr("id", "drawing");
var groups = content.selectAll().data(arr)
.enter()
.append("g")
.attr("class", function(d) {
return "hypothesis " + d["id"];
})
.each(function(d, i) {
var runningWidth = 0;
d3.select(this).selectAll("text")
.data(d["elements"])
.enter()
.append("text")
.attr("class", function(d) {
return d.type;
})
.text(function(d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("x", function(d, j) {
var w = this.getComputedTextLength(),
x = runningWidth;
runningWidth += w;
return x;
})
.attr("y", 50 * (i + 1));
});
</script>
</body>
</html>

Cannot make a pack layout in d3? selectAll not working?

I am relatively new to coding and very new to d3. I am currently trying to use d3 with json to make a pack layout representing current presidential candidates and how many times they have talked about a certain issue during the feedback.
I wanted to start small so I made some dummy data in a .json file, it is below:
{
"name": "John Doe",
"party": "democratic",
"issues": [
{ "issue":"issue1", "value": 25 },
{ "issue":"issue2", "value": 10 },
{ "issue":"issue3", "value": 50 },
{ "issue":"issue4", "value": 40 },
{ "issue":"issue5", "value": 5 }
]
}
I want to display bubbles with "issue" as the label and "value" as the circle radius, ending up with five different sized circles on my canvas. Below is my index.html file:
var width = 800, height = 600;
var canvas = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(50, 50)");
var pack = d3.layout.pack()
.size([width, height - 50])
.padding(10);
d3.json("fakedata.json", function (data) {
var nodes = pack.nodes(data);
var node = canvas.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.append("circle")
.attr("r", function (d) { return d.r; })
.attr("fill", "steelblue")
.attr("opacity", 0.25)
.attr("stroke", "#ADADAD")
.attr("stroke-width", "2");
node.append("text")
.text(function (d) {
return d.children ? "" : d.issue;
});
});
I keep getting the error below and I think it is because node is not being set correctly.
Error: Invalid value for <g> attribute transform="translate(NaN,NaN)"
Error: Invalid value for <circle> attribute r="NaN"
Any help would be much appreciated! Thank you!
The JSON you are passing is
{
"name": "John Doe",
"party": "democratic",
"issues": [
{ "issue":"issue1", "value": 25 },
{ "issue":"issue2", "value": 10 },
{ "issue":"issue3", "value": 50 },
{ "issue":"issue4", "value": 40 },
{ "issue":"issue5", "value": 5 }
]
}
It should have been like below note the key name children instead of issues
{
"name": "John Doe",
"party": "democratic",
"children": [
{ "issue":"issue1", "value": 25 },
{ "issue":"issue2", "value": 10 },
{ "issue":"issue3", "value": 50 },
{ "issue":"issue4", "value": 40 },
{ "issue":"issue5", "value": 5 }
]
}
Working code here.
The answer is a bit late, but in case you don't want to change your data structure and you want to keep the issues property, you can explicitly tell D3 to use the issues property for children data using the children() call:
var pack = d3.layout.pack()
.size([width, height - 50])
.children(function (d) { return d.issues; })
.padding(10);

d3 + create pie chart with different formats

This jsfiddle works on this format of data to create a piechart:
var data = [ { label: 'mylabel1', value: '1342' },
{ label: 'mylabel2', value: '1505' } ]
How do i get it to run on this format of data in this fiddle?
data=[{ country: 'Australia',
lat: '-25.274398',
lng: '133.775136',
values:
[ { label: 'ham', value: '1342' },
{ label: 'kpr', value: '1505' } ]
}]
This is the line I think I have to change but I just don't quite have it:
var pie = d3.layout.pie().value(function(d){return d.value;});
I have to get it to work on the values array in the new data object.
All my code from the 2nd jsfiddle:
var w = 400;
var h = 400;
var r = h/2;
var color = d3.scale.category20c();
var data = [{"label":"Category A", "value":20},
{"label":"Category B", "value":50},
{"label":"Category C", "value":30}];
var data = [ { label: 'mylabel1', value: '1342' },
{ label: 'mylabel2', value: '1505' } ]
data=[{ country: 'Australia',
lat: '-25.274398',
lng: '133.775136',
values:
[ { label: 'ham', value: '1342' },
{ label: 'kpr', value: '1505' } ]
}]
var vis = d3.select('#chart').append("svg:svg").data([data]).attr("width", w).attr("height", h).append("svg:g").attr("transform", "translate(" + r + "," + r + ")");
var pie = d3.layout.pie().value(function(d){return d.value;});
// declare an arc generator function
var arc = d3.svg.arc().outerRadius(r);
// select paths, use arc generator to draw
var arcs = vis.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class", "slice");
arcs.append("svg:path")
.attr("fill", function(d, i){
return color(i);
})
.attr("d", function (d) {
// log the result of the arc generator to show how cool it is :)
console.log(arc(d));
return arc(d);
});
// add the text
arcs.append("svg:text").attr("transform", function(d){
d.innerRadius = 0;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")";}).attr("text-anchor", "middle").text( function(d, i) {
return data[i].label;}
);
you have to change the data that you pass for data join. in this case it's the values array of your data:
var vis = d3.select('#chart').append("svg:svg").data([data[0].values])
also, to adjust the label drawing to this new data format, you need change return data[i].label; toreturn d.data.label;
updated jsFiddle

Show values on top of bars in a barChart

I have a bar chart with ordinal scale for the x-axis. I want to display the y-values on the top of each bar or in the bottom of each bar. It would be also acceptable to display the y-values when one hovers over the bar. Is there a function or a way in a dc.js to do that? Here is the jsfiddle and my code is below the pic>
Edit: Here is my code:
HTML
<body>
<div id='Chart'>
</div>
</body>
JS
var data = [{
Category: "A",
ID: "1"
}, {
Category: "A",
ID: "1"
}, {
Category: "A",
ID: "1"
}, {
Category: "A",
ID: "2"
}, {
Category: "A",
ID: "2"
}, {
Category: "B",
ID: "1"
}, {
Category: "B",
ID: "1"
}, {
Category: "B",
ID: "1"
}, {
Category: "B",
ID: "2"
}, {
Category: "B",
ID: "3"
}, {
Category: "B",
ID: "3"
}, {
Category: "B",
ID: "3"
}, {
Category: "B",
ID: "4"
}, {
Category: "C",
ID: "1"
}, {
Category: "C",
ID: "2"
}, {
Category: "C",
ID: "3"
}, {
Category: "C",
ID: "4"
}, {
Category: "C",
ID: "4"
},{
Category: "C",
ID: "5"
}];
var ndx = crossfilter(data);
var XDimension = ndx.dimension(function (d) {
return d.Category;
});
var YDimension = XDimension.group().reduceCount(function (d) {
return d.value;
});
dc.barChart("#Chart")
.width(480).height(300)
.dimension(XDimension)
.group(YDimension)
.transitionDuration(500)
.xUnits(dc.units.ordinal)
.label(function(d) {return d.value})
.x(d3.scale.ordinal().domain(XDimension))
dc.renderAll();
Check updated jsfiddle
.renderlet(function(chart){
var barsData = [];
var bars = chart.selectAll('.bar').each(function(d) { barsData.push(d); });
//Remove old values (if found)
d3.select(bars[0][0].parentNode).select('#inline-labels').remove();
//Create group for labels
var gLabels = d3.select(bars[0][0].parentNode).append('g').attr('id','inline-labels');
for (var i = bars[0].length - 1; i >= 0; i--) {
var b = bars[0][i];
//Only create label if bar height is tall enough
if (+b.getAttribute('height') < 18) continue;
gLabels
.append("text")
.text(barsData[i].data.value)
.attr('x', +b.getAttribute('x') + (b.getAttribute('width')/2) )
.attr('y', +b.getAttribute('y') + 15)
.attr('text-anchor', 'middle')
.attr('fill', 'white');
}
})
If you don't want the labels visible when the bars redraw (for example when bars change after user filters/clicks other chart) you can move the check of old values from de renderlet to the to a preRedraw
listener.
.on("preRedraw", function(chart){
//Remove old values (if found)
chart.select('#inline-labels').remove();
})
Alternative
D3-ish way
Demo jsfiddle
.renderlet(function (chart) {
//Check if labels exist
var gLabels = chart.select(".labels");
if (gLabels.empty()){
gLabels = chart.select(".chart-body").append('g').classed('labels', true);
}
var gLabelsData = gLabels.selectAll("text").data(chart.selectAll(".bar")[0]);
gLabelsData.exit().remove(); //Remove unused elements
gLabelsData.enter().append("text") //Add new elements
gLabelsData
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(function(d){
return d3.select(d).data()[0].data.value
})
.attr('x', function(d){
return +d.getAttribute('x') + (d.getAttribute('width')/2);
})
.attr('y', function(d){ return +d.getAttribute('y') + 15; })
.attr('style', function(d){
if (+d.getAttribute('height') < 18) return "display:none";
});
})
In dc.js version 3.* . You can just use .renderLabel(true). It will print the value at top
Here's a bit of a hacky solution using the renderlet method. jsFiddle: http://jsfiddle.net/tpsc5f9f/4/
JS
var barChart = dc.barChart("#Chart")
.width(480).height(300)
.dimension(XDimension)
.group(YDimension)
.transitionDuration(500)
.xUnits(dc.units.ordinal)
.x(d3.scale.ordinal().domain(XDimension))
dc.renderAll();
barChart.renderlet(function(chart){
moveGroupNames();
});
function moveGroupNames() {
var $chart = $('#Chart'),
bar = $chart.find('.bar');
bar.each(function (i, item) {
var bar_top = this.height.baseVal.value;
var bar_left = this.width.baseVal.value;
var bar_offset_x = 30;
var bar_offset_y = 33;
var bar_val = $(this).find('title').html().split(':')[1];
$chart.append('<div class="val" style="bottom:'+(bar_top+bar_offset_y)+'px;left:'+((bar_left*i)+(bar_offset_x))+'px;width:'+bar_left+'px">'+bar_val+'</div>');
});
}
Added CSS
body {
margin-top:20px;
}
#Chart {
position:relative;
}
#Chart .val {
position:absolute;
left:0;
bottom:0;
text-align: center;
}
If you use a specialized valueAccessor with a chart, you can make the following substitution in dimirc's "D3-ish way" solution.
Change
.text(function(d){
return d3.select(d).data()[0].data.value
})
To
.text(function(d){
return chart.valueAccessor()(d3.select(d).data()[0].data)
});
This function will reposition a row chart's labels to the end of each row. A similar technique could be used for a bar chart using the "y" attribute.
Labels animate along with the row rect, using chart's transitionDuration
Uses chart's valueAccessor function
Uses chart's xAxis scale to calculate label position
rowChart.on('pretransition', function(chart) {
var padding = 2;
chart.selectAll('g.row > text') //find all row labels
.attr('x', padding) //move labels to starting position
.transition() //start transition
.duration(chart.transitionDuration()) //use chart's transitionDuration so labels animate with rows
.attr('x', function(d){ //set label final position
var dataValue = chart.valueAccessor()(d); //use chart's value accessor as basis for row label position
var scaledValue = chart.xAxis().scale()(dataValue); //convert numeric value to row width
return scaledValue+padding; //return scaled value to set label position
});
});

Categories