I am trying to make a small multiples chart by following Mike Bostock's example.
This example uses enter().append("svg") to create a new SVG for each data point. In each SVG you would then create the chart.
I have data that is in a CSV file that looks like this:
count, radius
15, 5
10, 3
With this data I'd like to create two SVGs (one for each data point), with the first one containing 15 circles each with a radius of 5, and the second svg containg 10, each with a radius of 3. I have a function drawCircles that I wish to use to draw the circles based on my dataset, however I'm having trouble passing the data through to my function.
Here's my code:
d3.csv("nations.csv", function(data) {
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
drawCircles(function (d) {return +d.count;}, function (d) {return +d.radius;})
I can't seem to pass d.count and d.radius through as arguments to my drawCircles function. Can anyone please help?
Here's a very d3ish way of doing what you are after:
d3.csv("some.csv", function(d){
// coerce your data to numbers
return {
count: +d.count,
radius: +d.radius
}
},
function(data){
// create your svg for each row of data
var s = d3.selectAll("svg")
.data(data)
.enter()
.append("svg")
.attr("width", 600)
.attr("height", 100);
// use a sub-selection to create a circle for each count
s.selectAll('circle')
.data(function(d){
// the bound data will simply be an array with repeating radius
return d3.range(d.count).map(function(_){ return d.radius });
})
.enter()
.append('circle')
// radius is the same for each circle
.attr('r', function(d){
return d;
})
// space the circles so they look good;
.attr('cx', function(d,i,j){
return ((d + 2) * 2) * i + 10;
})
.attr('cy', 50)
.style('fill', 'steelblue');
});
Example here.
Related
I would like to draw few text box and chain it with arrow lines. I use below code to draw the text box few issues there:
text box is black and no text show there.
One box is missing, it should be 5 box but only 4 can be seen.
how can I add a arrow line to connect each other!
test()
function test() {
var data = ["a","b","c","d","e"]
width = 800
height = 600
margin = 10
//var svg = d3.select("svg");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom);
svg.style("border","5px solid red");
svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var group = svg.selectAll('g')
.data(data).enter()
.append('g')
.attr('transform',function(d,i) {
//console.log(i,d);
return 'translate('+(100*i)+',0)';
});
var box = group.selectAll('rect')
.data(function(d) {
return d;
});
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr('font-size',2)
.attr("x", function(d, i) {
//console.log(d);
return 60 + 2*d;
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
text box is black and no text shown.
You aren't appending any text. Text also can't be appended to a rectangle, so there is no need to apply font properties to a rectangle. Text can be appended to a g though. So we can use a parent g to hold both rectangle and text. Something like:
group.append("rect")...
group.append("text")...
The boxes are black because you haven't applied a fill. The default fill is black.
One box is missing, it should be 5 box but only 4 can be seen.
This is because when you enter the parent g elements, you select all g elements. This includes the one you've already appended (svg.append("g")). The enter selection is intended to create elements such that every item in the data array is paired with an element in the DOM. Since you already have a g in your selection, the enter selection will only create 4 new ones (representing data array items with indexes 1-4 but not 0).
Instead of selectAll("g") you could specify a class name or, in the event you simply want to enter everything and there isn't a need to ever update a selection: selectAll(null). The latter option will always return an empty selection, which will result in the enter selection containing one element per item in the data array.
Note, that the parent's datum is passed to appended children automagically, there is no need to use the .data method to pass this onward unless you are handling nested data.
Here's a snippet addressing issues in one and two:
test()
function test() {
var data = ["a","b","c","d","e"]
width = 800
height = 600
margin = 10
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.style("border","5px solid red");
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var group = svg.selectAll(null)
.data(data).enter()
.append('g')
.attr('transform',function(d,i) {
return 'translate('+(40*i)+',0)';
});
group
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("fill","yellow")
group.append("text")
.text(function(d) { return d; })
.attr("y", 20)
.attr("x", 15)
.attr("text-anchor","middle");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
I also changed svg to refer to the parent g, the one with the margin applied. Before the g with the margin remained unused, along with the margin. I also modified the spacing to keep everything in view.
how can I add a arrow line to connect each other!
This can be done in many ways and really is a separate issue from the others, so I'll only quickly demonstrate one of many options. I'll modify your data structure a bit so that each datum has positional data and then add arrows using SVG markers:
test()
function test() {
var data = [{name:"a"},{name:"b"},{name:"c"},{name:"d"},{name:"e"}]
width = 800
height = 600
margin = 10
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.style("border","5px solid red")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs")
.append("marker")
.attr("id","pointer")
.attr("markerWidth", 10)
.attr("markerHeight", 10)
.attr("orient","auto")
.attr("refY", 5)
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z")
var group = svg.selectAll(null)
.data(data).enter()
.append('g')
.attr('transform',function(d,i) {
d.x = 40*i+15, d.y=30;
return 'translate('+(40*i)+',0)';
});
group
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("fill","yellow")
group.append("text")
.text(function(d) { return d.name; })
.attr("y", 20)
.attr("x", 15)
.attr("text-anchor","middle");
links = [
{source: data[0], target: data[1]},
{source: data[0], target: data[2]}
]
svg.selectAll(null)
.data(links)
.enter()
.append("path")
.attr("d", function(d) {
var midX = (d.source.x+d.target.x)/2;
return "M"+d.source.x+" "+d.source.y+"Q"+midX+" "+200+" "+d.target.x+" "+(d.target.y+6);
})
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-width",1)
.attr("marker-end","url(#pointer)");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
I am really new to the realm of D3 and based on the book of 'Interactive Data visualization for the web', I managed to create a Bar chart which is mostly based on the code from the following link.
The problem is I don't manage to add a legend to my bar chart based on an object dynamically.
I have tried consulting youtube videos and other stackoverflow questions related to 'adding a legend to a bar-chart', however in my opinion I couldn't find the question concerning how one is able to retrieve keys from an array of objects and use the data to add as an legend to the bar-chart.
For now all my bars also have the same color, see the second code below.
See the code below for the formatting of my object which is embedded in an array.
The name 'key' and 'value' are fixed, while the amount of the objects and their corresponding name and value differ after an click event of the user ( which determines which variables will be included in the object).
The following example is able create a legend, however in this case the formatting of the object is somehow different than in my case and my current knowledge of D3 is limitd, so I have no idea in which ways I have to adapt the code.
2: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
3: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
4: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
5: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
6: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
7: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Based on the code from the book and accounting for other variables, I have currently the following code for visualizing a bar chart, in which the values (see object above) are shown in the bar charts and the color of the bar are all blueish. However there is not yet an legend included in my current code. Therefore I am wondering how one is able to dynamically create a legend based on the 'keys' ( in my case)in the object and represent the corresponding color bound to the bars. I would like to achieve the lowest image which I have drawn a sketch of.
var svg = d3.select("#barchart")
.select("svg")
.remove("svg");
//Width and height
var w = 600;
var h = 250;
var padding=20;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([w - padding,padding ])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function (d) {
return d.value;
})])
.range([padding,h - padding]);
console.log("yscale",yScale);
//Define key function, to be used when binding data
var key = function (d) {
console.log("key", d);
return d.key;
};
// d3.select("svg").remove();
//Create SVG element
var svg = d3.select("#barchart")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("svg", svg);
//Create bars
svg.selectAll("rect")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d.value);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d.value);
})
// .attr("data-legend", function (d) { return d.key })
.attr("fill", function (d) {
return "rgb(0, 0, " + (d.value * 10) + ")";
});
//Create labels
svg.selectAll("text")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("text")
.text(function (d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
If I understood correctly this is what you should need.
Plunker with working code.
First of all I would encourage to use an margin object which will allow better flexibility when dealing with charts
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
};
We want to display the data with an odinal scale from the data and example you provided.
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Taking into account that probably the first element is a sum of sorts of the dataset I think it shouldn't be included in the chart since it is an aggregation of the elements we want to display.
(In case you need to display it as an element you should be able to do it quickly after reviewing the answer)
The element structure in your dataset is the following:
{
key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie",
value: 85
}
The domain of our xScale should be all the key values in our dataset, since the key is a huge string, I created a custom property in each element called label
{
key:
"bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening",
label: "Business Services",
value: 165
}
Lets create our scale with the correct domain and range:
var xScale = d3
.scaleBand()
.domain(dataset.map(d => d.label)) // All our label properties
.rangeRound([0, w - margin.left - margin.right]) // This scale will map our values from [0, width - margin.left - margin.right]
.paddingInner(0.05);
The yScale was almost correct, we just need to change it a little to use our margin object and use the correct range
The range must start from 0, if we used padding as the starting point our values will have an offset, since our values would be mapped from [padding, h - padding]. If we wanted to display a zero the value would be mapped to the padding value, if this is way you want to show the information keep it that way. In this case we will modify the scale.
var yScale = d3
.scaleLinear()
.domain([
0,
d3.max(dataset, function(d) {
return d.value;
})
])
.range([0, h - margin.top - margin.bottom]);
Next we will create a function to get the desired value from our elements
var xKey = function(d) {
return d.label;
};
Add our svg with some visual cues to help visualizing the way the elements are layed out:
var svg = d3
.select("#barchart")
.append("svg")
.style("background", "rgb(243, 243, 243)")
.style("border", "1px dashed #b4b4b4")
.attr("width", w)
.attr("height", h);
We want to use a margin, so lets use a group tag to achieve this, we could individually set the margin in each group/element we desired but I find this way simpler and clearer
var g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
We will need the width and height of the chart with the margins taken into account, lets define them really quick:
const customWidth = w - margin.left - margin.right;
const customHeight = h - margin.top - margin.bottom;
Let us add a rect to show where will our rects will be displayed:
g.append("rect")
.attr("fill", "#e3e3e3")
.attr("width", customWidth)
.attr("height", customHeight);
Lets deal with the rect creation, in your code you had a custom fill function which modified the b value within the RGB color values. In this case since we are dealing with categorical data we will use an array of colors for the rects.
g.append("g")
.attr("class", "rect__container")
.selectAll("rect")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(xKey(d)); // use our key function
})
.attr("y", function(d) {
return customHeight - yScale(d.value); // use our custom size values
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
return yScale(d.value);
})
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use an array of colors and use the index to decide which color to use
});
We have two options to show the labels of the chart:
We can create an x-axis or the desired legends. We will do both since it won't affect the outcome of the chart and either one of them can be removed.
var margin = {
top: 20,
right: 300, // modifiy our margin to have space to display the legends
bottom: 50,
left: 20
};
var legendElement = g
.append("g")
.attr("class", "legend__container")
.attr("transform", `translate(${customWidth}, ${margin.top})`) // set our group position to the end of the chart
.selectAll("g.legend__element")
.data(xScale.domain()) // use the scale domain as data
.enter()
.append("g")
.attr("transform", function(d, i) {
return `translate(${10}, ${i * 30})`; // provide an offset for each element found in the domain
});
legendElement
.append("text")
.attr("x", 30)
.attr("font-size", "14px")
.text(d => d);
legendElement
.append("rect")
.attr("x", 0)
.attr("y", -15)
.attr("width", 20)
.attr("height", 20)
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use the same category color that we previously used in rects
});
Now lets use the axis approach:
// create axis
var x_axis = d3.axisBottom().scale(xScale);
//Append group and insert axis
g.append("g")
.attr("transform", `translate(${0}, ${customHeight})`)
.call(x_axis);
g.append("g")
.attr("transform", `translate(${customWidth / 2}, ${customHeight + 40})`)
.append("text")
.text("Activities")
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.style("text-transform", "uppercase")
.attr("text-anchor", "middle");
And finally create the labels for the value in our data:
//Create labels
g.append("g")
.attr("class", "text__container")
.selectAll("text")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("text")
.text(function(d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(xKey(d)) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
return customHeight - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
I am attempting to access the data index of a shape on mouseover so that I can control the behavior of the shape based on the index.
Lets say that this block of code lays out 5 rect in a vertical line based on some data.
var g_box = svg
.selectAll("g")
.data(controls)
.enter()
.append("g")
.attr("transform", function (d,i){
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+ 5))+")"
})
.attr("class", "controls");
g_box
.append("rect")
.attr("class", "control")
.attr("width", 15)
.attr("height", 15)
.style("stroke", "black")
.style("fill", "#b8b9bc");
When we mouseover rect 3, it transitions to double size.
g_box.selectAll("rect")
.on("mouseover", function(d){
d3.select(this)
.transition()
.attr("width", controlBoxSize*2)
.attr("height", controlBoxSize*2);
var additionalOffset = controlBoxSize*2;
g_box
.attr("transform", function (d,i){
if( i > this.index) { // want to do something like this, what to use for "this.index"?
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5)+additionalOffset)+")"
} else {
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5))+")"
}
})
})
What I want to do is move rect 4 and 5 on mouseover so they slide out of the way and do not overlap rect 3 which is expanding.
So is there a way to detect the data index "i" of "this" rect in my mouseover event so that I could implement some logic to adjust the translate() of the other rect accordingly?
You can easily get the index of any selection with the second argument of the anonymous function.
The problem here, however, is that you're trying to get the index in an anonymous function which is itself inside the event handler, and this won't work.
Thus, get the index in the event handler...
selection.on("mouseover", function(d, i) {
//index here ---------------------^
... and, inside the inner anonymous function, get the index again, using different parameter name, comparing them:
innerSelection.attr("transform", function(e, j) {
//index here, with a different name -----^
This is a simple demo (full of magic numbers), just to show you how to do it:
var svg = d3.select("svg");
var data = d3.range(5);
var groups = svg.selectAll("foo")
.data(data)
.enter()
.append("g");
var rects = groups.append("rect")
.attr("y", 10)
.attr("x", function(d) {
return 10 + d * 20
})
.attr("width", 10)
.attr("height", 100)
.attr("fill", "teal");
groups.on("mouseover", function(d, i) {
d3.select(this).select("rect").transition()
.attr("width", 50);
groups.transition()
.attr("transform", function(e, j) {
if (i < j) {
return "translate(40,0)"
}
})
}).on("mouseout", function() {
groups.transition().attr("transform", "translate(0,0)");
rects.transition().attr("width", 10);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: don't do...
g_box.selectAll("rect").on("mouseover", function(d, i){
... because you won't get the correct index that way (which explain your comment). Instead of that, attach the event to the groups, and get the rectangle inside it.
I'm pretty sure d3 passes in the index as well as the data in the event listener.
So try
.on("mouseover", function(d,i)
where i is the index.
Also you can take a look at a fiddle i made a couple months ago, which is related to what you're asking.
https://jsfiddle.net/guanzo/h1hdet8d/1/
You can find the index usign indexOf(). The second argument in the event mouseover it doesn't show the index in numbers, it shows the data info you're working, well, you can pass this info inside indexOf() to find the number of the index that you need.
.on("mouseover", (event, i) => {
let index = data.indexOf(i);
console.log(index); // will show the index number
})
I have a line chart (or, more properly a connected scatterplot) where I plot pies/donuts around the points. So, there is a data set that specifies the date and mood for plotting the points along with two other parameters, pos and neg, providing the values to go into the pie chart. The overall data that describes the points is called plotPoints.
That all works great, but what I still would like to do is to set the radius of the pie to be a function of the sum of pos + neg.
When I plot out the points, I can access all of the data with a function(d). In each pie, however, function(d) returns the data about the slices, one at a time. d contains the data for the current slice, not the overall data. For each pie, the first time arc is called it has the frequency for the first pie slice and the second time it is called it has the frequency for the second pie slice.
How do I refer to the current plotPoints properties when drawing the arc so that I can change the radius of the pie/donut to represent the sum of plotPoints[i].pos + plotPoints[i].neg?
The relevant code looks like this:
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(8);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var p = moodChart.selectAll(".pieContainer")
.data(plotPoints).enter()
.append("g")
.attr("class","pieContainer")
.attr("transform", function(d,i) {return "translate(" + (x(d.date)) + "," + (y(d.mood)) + ")"});
p.append("title")
.text(function(d) { return shortDateFormat(d.date) +", " + d.mood.toFixed(2) });
var g = p.selectAll(".arc")
.data(function (d) {return pie([d.neg, d.pos])})
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
It's tough to answer this authoritatively without a bit more code/data to look at but in this situation I usually stash the needed variables in my data-bindings so they are available later:
var g = p.selectAll(".arc")
.data(function (d) {
var total = d.neg + d.pos,
pie_data = pie([d.neg, d.pos]),
point_arc = d3.svg.arc()
.outerRadius((total * radius) - 10) //<-- set radius based on total
.innerRadius((total * radius) - 8);
pie_data.forEach(function(d1){
d1.data.arc = point_arc; //<-- stash the arc for this point in our data bindings
});
return pie_data;
});
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", function(d){
return d.data.arc
})
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
I'm trying to create something similar to this example: Wealth and Health of Nations:
My data comes from a JSON file, just like the example, but when I add the transitions, I'm getting duplicate bubbles. Instead of the bubble transitioning from point A to point B I'm getting 2 bubbles (one for point A, one for point B). Generally speaking, the transition is not able to differentiate between 2 data points for the same bubble or 2 separate bubbles.
Looking at the example, I'm missing the interpolate and bisect functions. I haven't been able to grasp how they work and what exactly i'm doing wrong. Is this what's causing the problem in my graph?
Also, can someone give me an example on how bisectors and interpolate works in d3?
Code:
g = d3.select("#animation")
.append("svg")
.attr("width", width)
.attr("height", height);
x_extent = [0, 100];
x_scale = d3.scale.linear().domain(x_extent).range([margin + 20, width - 30]);
y_extent = [0, 60];
y_scale = d3.scale.linear().domain(y_extent).range([height - margin, margin]);
r_scale = d3.scale.linear().domain([0, d3.max(jsondata, function (d) { return d.MSVMMboe; })]).range([2, 30]);
g.selectAll("circle").data(jsondata, function (d) { return d.EffectiveDate; }).enter().append("circle")
.attr("cx", function (d) { return x_scale(d.PercentageComplete * 100) })
.attr("cy", function (d) { return y_scale(d.GPoS * 100) })
.attr("r", function (d) { return r_scale(d.MSVMMboe) })
.attr("stroke", "blue")
.attr("stroke-width", 1)
.attr("opacity", 0.6)
.attr("fill", "red");
//add transition
g.selectAll("circle").data(jsondata, function (d) { return d.EffectiveDate; })
.transition()
.duration(1000);
You haven't told the transition what you want to change. You need to add some attribute changes for example. Have a look at the d3 website for examples and tutorials.