If I have nested array like this
var ar = [[[1,0],[2,0],[3,0]], [[1,0],[2,0],[3,0]]]
I want to create two svg elements, this is easy
var svg = d3.select('div.main`)
.selectAll('svg')
.data(ar)
.enter()
.append('svg')
And now I want to bind subarrays to svg selection, something like this
var g = svg.selectAll('g')
.data(function(d,i) {return d[i];})
.enter()
.append('g')
after that the data attached to g should be
[[1,0],[2,0],[3,0]]
I know what this line is not correct .data(function(d,i) {return d[i];}) Just do not know how to explain it different way.
If I understand the question correctly,
Your are right, the issues arise from the identified line. You don't need to return d[i] as the data for the new selection, d represents each individual datum associated with each svg, d[i] represents only a one part of each datum.
If you want each datum, in its entirety, just append a g as normal:
var g = svg.append("g");
Try console.log on g.data() and you will see that your data is there still as you want, it is bound to each g.
You can then use each of these datums, bound to each g and carried over from each svg, as data to create new features. Passing the datum looks like: .data(function(d) { return d; }). The snippet below should help put it all together:
var data = [[[10,10],[30,30],[50,50]], [[10,20],[80,30],[50,60]] ];
var svg = d3.select('body')
.selectAll('svg')
.data(data)
.enter()
.append('svg')
.attr("height",100)
.attr("width",200);
var g = svg.append("g");
console.log("Data Bound To First G in First SVG:")
console.log(g.data()[0]);
console.log("Data Bound To Second G in Second SVG:")
console.log(g.data()[1]);
// Data is now available to make features:
g.selectAll("circle")
.data(function(d) { return d; })
.enter()
.append("circle")
.attr("r",10)
.attr("cx",function(d) { return d[0] })
.attr("cy",function(d) { return d[1] });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
Related
I do have a d3.scaleTime() as x-axis and JSON data with dates, which should be represented as an rect on this x-axis, in order to mark the date as "reserved".
The simple way is
this.svg.selectAll('rect').data(dates).enter().each(function(d) {
d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick)
.attr('x', x(d.date));
}
But I would like to group rects, if they have consecutive dates, in order to be able to move the group around on the x-axis.
What I did / tried is in the enter / each function to check, if there is already a group with a date "next" to the current date.
If not, create a new group and append the current date-rect.
If there is a group, get the group and append the date-rect to the existing group.
But now, the problem is, that the rects don't keep their data and / or I can't add the rect to the group properly.
Some (pseudo)code:
var rects = this.svg.selectAll('rect') .data(dates, function(dataElement) {
return dataElement.id;
});
// add new date rects
rects.enter().each(function(dataElement) {
var isNewDateGroup = true / false; // detect if date is "next" to another one
if (isNewDateGroup)
{
var group = svg.append("g");
var dateRect = d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick);
group.append(function() {
// works correctly, appends the rect and sets the data
return dateRect.select('rect').node();
});
// translate group to desired x(d.date)
}
else
{
var group = getGroupForDate();
var dateRect = d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick)
.attr('x', offsetToLastRectInGroup);
group.append(function() {
// does not work correctly, does not append the rect and does not set the data
return dateRect.select('rect').node();
});
}
});
Basically, the enter function does not work, if I append more then one SVG element, since the data gets confused and the appending as well.
Any hints, how I can group my rects?
The answer on this question helped me to solve my problem:
Since .select() propagate the data to the selected elements, I just have to get rid of the selection:
var dateRect = d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick)
.attr('x', offsetToLastRectInGroup);
group.append(function() {
return dateRect.node();
});
I want to draw a pie chart for every point on the map instead of a circle.
The map and the points are displaying well but the pie chart is not showing over the map points. There is no error also. I can see the added pie chart code inside map also.
Below is the code snippet .
var w = 600;
var h = 600;
var bounds = [[78,30], [87, 8]]; // rough extents of India
var proj = d3.geo.mercator()
.scale(800)
.translate([w/2,h/2])
.rotate([(bounds[0][0] + bounds[1][0]) / -2,
(bounds[0][1] + bounds[1][1]) / -2]); // rotate the project to bring India into view.
var path = d3.geo.path().projection(proj);
var map = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var india = map.append("svg:g")
.attr("id", "india");
var gDataPoints = map.append("g"); // appended second
d3.json("data/states.json", function(json) {
india.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("data/water.csv", function(csv) {
console.log(JSON.stringify(csv))
gDataPoints.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("id", function (d,i) {
return "chart"+i;
})
.attr("cx", function (d) {
return proj([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return proj([d.lon, d.lat])[1];
})
.attr("r", function (d) {
return 3;
})
.each(function (d,i) {
barchart("chart"+i);
})
.style("fill", "red")
//.style("opacity", 1);
});
function barchart(id){
var data=[15,30,35,20];
var radius=30;
var color=d3.scale.category10()
var svg1=d3.select("#"+id)
.append("svg").attr('width',100).attr('height',100);
var group=svg1.append('g').attr("transform","translate(" + radius + "," + radius + ")");
var arc=d3.svg.arc()
.innerRadius('0')
.outerRadius(radius);
var pie=d3.layout.pie()
.value(function(d){
return d;
});
var arcs=group.selectAll(".arc")
.data(pie(data))
.enter()
.append('g')
.attr('class','arc')
arcs.append('path')
.attr('d',arc)
.attr("fill",function(d,i){
return color(d.data);
//return colors[i]
});
}
water.csv:
lon,lat,quality,complaints
80.06,20.07,4,17
72.822,18.968,2,62
77.216,28.613,5,49
92.79,87.208,4,3
87.208,21.813,1,12
77.589,12.987,2,54
16.320,75.724,4,7
In testing your code I was unable to see the pie charts rendering, at all. But, I believe I still have a solution for you.
You do not need a separate pie chart function to call on each point. I'm sure that there are a diversity of opinions on this, but d3 questions on Stack Overflow often invoke extra functions that lengthen code while under-utilizing d3's strengths and built in functionality.
Why do I feel this way in this case? It is hard to preserve the link between data bound to svg objects and your pie chart function, which is why you have to pass the id of the point to your function. This will be compounded if you want to have pie chart data in your csv itself.
With d3's databinding and selections, you can do everything you need with much simpler code. It took me some time to get the hang of how to do this, but it does make life easier once you get the hang of it.
Note: I apologize, I ported the code you've posted to d3v4, but I've included a link to the d3v3 code below, as well as d3v4, though in the snippets the only apparent change may be from color(i) to color[i]
In this case, rather than calling a function to append pie charts to each circle element with selection.each(), we can append a g element instead and then append elements directly to each g with selections.
Also, to make life easier, if we initially append each g element with a transform, we can use relative measurements to place items in each g, rather than finding out the absolute svg coordinates we would need otherwise.
d3.csv("water.csv", function(error, water) {
// Append one g element for each row in the csv and bind data to it:
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("id", function (d,i) { return "chart"+i; })
.append("g").attr("class","pies");
// Add a circle to it if needed
points.append("circle")
.attr("r", 3)
.style("fill", "red");
// Select each g element we created, and fill it with pie chart:
var pies = points.selectAll(".pies")
.data(pie([0,15,30,35,20]))
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
Now, what if we wanted to show data from the csv for each pie chart, and perhaps add a label. This is now done quite easily. In the csv, if there was a column labelled data, with values separated by a dash, and a column named label, we could easily adjust our code to show this new data:
d3.csv("water.csv", function(error, water) {
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("class","pies")
points.append("text")
.attr("y", -radius - 5)
.text(function(d) { return d.label })
.style('text-anchor','middle');
var pies = points.selectAll(".pies")
.data(function(d) { return pie(d.data.split(['-'])); })
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
The data we want to display is already bound to the initial g that we created for each row in the csv. Now all we have to do is append the elements we want to display and choose what properties of the bound data we want to show.
The result in this case looks like:
I've posted examples in v3 and v4 to show a potential implementation that follows the above approach for the pie charts:
With one static data array for all pie charts as in the example: v4 and v3
And by pulling data from the csv to display: v4 and v3
I have created a d3 donut chart. Here is my code:
var width = 480;
var height = 480;
var radius = Math.min(width, height) / 2;
var doughnutWidth = 30;
var color = d3.scale.category10();
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 70);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d[1]; });
var dataset = settings.dataset;
console.log(dataset);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data[0]);
})
I have a simple form on my web page which displays a dropdown menu with several options. Every time a user changes a value on the form a new dataset is sent to my script (settings.dataset) and the donut is redrawn on the page.
Problem is, some of the values from the previous dataset remain in the DOM. In the console output below, you can see that the second dataset only has two elements. The third one is from the previous dataset. This is messing up the chart, as it is displaying a value that shouldn't be there.
My question: how do I clear the old values? I've read up on .exit() and .remove(), but I can't get my head around these methods.
Create one function that (re)draws the pie when it's created and when it's updated.
New data should be added to pie using enter() and old data should be removed using exit().remove()
It is as simple as this:
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTween);
path.exit().remove()
Full working code -> JSFIDDLE
There are two steps to implement the 'redraw' effect you want:
First, I suppose you want the svg canvas to be drawn only once when the page is loaded for the first time, and then update the chart in the svg instead of remove and redraw the svg:
var svg = d3.select("body")
.selectAll("svg")
.data([settings.dataset]);
// put data in an array with only one element
// this ensures there is only one consistent svg on the page when data is updated(when the script gets executed)
svg.enter().append("svg")
Second, understanding enter(), exit(), there are many great tutorials about this. In your case, I would suggest to draw the donut something like this:
var path = svg.selectAll(".donut")
.data(settings.data)
// bind data to elements, now you have elements belong to one of those
// three 'states', enter, exit, or update
// for `enter` selection, append elements
path.enter().append("path").attr("d", arc).attr("fill", "teal")
// for `update` selection, which means they are already binded with data
path.transition().attrTween("d", someFunction) // apply transition here if you want some animation for data change
// for `exit` selection, they shouldn't be on the svg canvas since there is no corresponding data, you can then remove them
path.exit().remove()
//remove and create svg
d3.select("svg").remove();
var svg = d3.select("body").append("svg").attr("width","960").attr("height", "600"),
inner = svg.append("g");
I am trying, usin D3js data bindins, to append rectangles as children of a transformed svg element.
My rectangles actually get appended but they are appended to the html element (I mean the root element of the html document) and not as children of the g element I thought would be their parent node.
How can I them appended as children nodes of the g element?
//var svgTranslate = (sgv:g with some transform applied)
var myclassSelection = svgTranslate.select('rect.myclass');
var myclassBinding = myclassSelection.data(data);
var myclassBindingEnter = myclassBinding.enter();
myclassBindingEnter
.append('rect')
.attr('class','myclass')
.attr('x', function(d) { return d[0]; })
.attr('y', 0)
.attr('width', function(d) { return d[1]; })
.attr('height', 25)
;
As replied by Lars Kotthoff in the comments, I had to ue .selectAll() instead of .select()
I'm attempting to make a Paired Bar Graph between glob and local within my JS Object/Array. I've made bar graphs in D3 previously, but haven't used objects. I'm finding it difficult to access the correct data.
Eventually, the keyword data will be used in the axis. And the cpc will be used as a tooltip.
Here's the code that I have so far: (or see my JSFiddle)
var w = 600;
var h = 400;
var colors = ["#377EB8", "#4DAF4A"];
var dataset = {"keyword": ["payday loans", "title loans", "personal loans"],
"glob": ["1500000", "165000", "550000"],
"local": ["673000", "165000", "301000"],
"cpc": ["14.11", "12.53", "6.14"]
};
var series = 2; // Global & Local
var x0Scale = d3.scale.ordinal()
.domain(d3.range(dataset.glob.length))
.rangeRoundBands([0, w], 0.05);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {return d.glob;})])
.range([0, h]);
var glob = function(d) {
return d.glob;
};
//SVG element
var svg = d3.select("#searchVolume")
.append("svg")
.attr("width", w)
.attr("height", h);
// Graph Bars
svg.selectAll("rect")
.data(dataset, glob) //access the series here?
.enter()
.append("rect")
.attr("x", function(d, i){
return x0Scale(i);
})
.attr("y", function(d) {
return h - yScale(d.glob);
})
.attr("width", x0Scale.rangeBand())
.attr("height", function(d) {
return yScale(d.glob); // ***************
})
.attr("fill", colors[1]);
Currently, the chart doesn't get populated. I assume I am not accessing values correctly. I'm simply trying to get data from glob to make sure I'm accessing things correctly - and then from there I was going to populate both series, etc. Is my issue not accessing key/values correctly?
Take a look at this: http://jsfiddle.net/juY5E/2/
I was able to get three bars by changing .data(dataset, glob) to .data(dataset.glob) and then changing d.glob to +d for the 'y' attr, the 'height' attr and in yScale.domain
to be able to switch between glob and local, you may want to restructure the data.