I am trying to plot a map of the UK and plotting a few selected points on it.
I am following the first part of this tutorial https://bost.ocks.org/mike/map/
Here's what I have done.
var svg = d3.select('body').append('svg')
.attr('height', height)
.attr('width', width)
.call(zoom);
d3.json("/data/uk.json", function(error, uk) {
if (error) return console.error(error);
var subunits = topojson.feature(uk, uk.objects.subunits);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([50, 60])
.scale(6000)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
svg.append('path')
.datum(subunits)
.attr('d', path);
svg.selectAll('.subunit')
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append('path')
.attr('d', path);
Here is the part where I try to plot the points
d3.json('/data/places.json', function (error , result) {
if(error)
console.error(error);
svg.append('path')
.data(result.features)
.style('stroke', 'green')
.attr('d' , d3.geo.path().projection(projection))
});
The above code plots only one point on the map, i.e the first one in the JSON file
You are not correctly binding your features' data when using
svg.append('path')
.data(result.features)
.style('stroke', 'green')
.attr('d' , d3.geo.path().projection(projection))
This will append one path, bind the first element of result.features to this path and set the style and attribute accordingly.
To work the way you want it, you need to make use of the data joining mechanism of D3.
svg.selectAll('path.features')
.data(result.features)
.enter().append('path')
.attr('class', 'feature')
.style('stroke', 'green')
.attr('d' , d3.geo.path().projection(projection))
This will compute a data join for the features in result.features putting the new elements in the enter selection which is accessible by calling enter() on the seletion. Using this enter selection you can now append paths for all your features.
A further side note not directly related to your issue:
Depending on the number of features you want to append to your map, the .attr("d") setter might be called quite often. You could improve performance by reusing one instance of the path generator:
var geoPath = d3.geo.path().projection(projection); // You need just one instance
svg.selectAll('path.features')
.data(result.features)
.enter().append('path')
.attr('class', 'feature')
.style('stroke', 'green')
.attr('d' , geoPath) // Re-use the path generator
This is considered best practice, which should be generally applied.
Related
I am using j3.ds to deploy some data on a pie chart. It seems to work fine and it updates correctly when I introduce new data. The thing is, I wanted to do the transition when updating smoothly, like here:
https://www.d3-graph-gallery.com/graph/pie_changeData.html
For some reason it is not working when I introduce the merge and transition, can somebody help with the task? thanks in advance
update();
function update() {
var data = d3.selectAll('.values').nodes();
var pie = d3.pie() //we create this variable, for the values to be readeable in the console
.value(function(d) {return d.innerHTML; })(data);
console.log("pie = ",pie)
var u = svg.selectAll("path")
.data(pie)
console.log("u = ",u)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function
u
.enter()
.append('path')
.merge(u)
.transition()
.duration(2000)
.attr('d', d3.arc()
.innerRadius(0)
.outerRadius(radius)
)
.attr('fill', function(d,i){ return color[i] })
.attr("stroke", "black")
.style("stroke-width", "2px")
.style("opacity", 1)
}
Merge combines one selection with another as follows:
selectionCombined = selection1.merge(selection2);
You aren't providing a second selection, so you aren't merging anything. The selection you do have you are calling .merge() on is the enter selection, returned by .enter() - unless you add new slices to the pie, this will be empty every update after the first. As you aren't merging anything with the enter selection, post merge the selection is still empty.
The enter selection is used to create elements so that for every item in the data array there is one corresponding element in the DOM - as you already have slices, only the update selection is not empty.
The update selection is that returned by .data(), it contains existing elements which correspond to items in the data array. You want to merge this selection with the one returned by .enter():
var update = svg.selectAll("path")
.data(pie)
var enter = update.enter()
.append('path')
var merged = update.merge(enter)
However, a transition needs a start value and an end value. In your case you are trasitioning the d attribute of a path. On update the start value is the path's current d and the end value is a d representing the new value for the slice. On initial entry, what is the value that the transition should start from? It may be more appropriate to only transition on update:
var arc = d3.arc().innerRadius(0).outerRadius(radius);
var update = svg.selectAll("path")
.data(pie)
var enter = update.enter()
.append('path')
// Set initial value:
.attr('d', arc)
// If these values don't change, set only on enter:
.attr('fill', function(d,i){ return color[i] })
.attr("stroke", "black")
.style("stroke-width", "2px")
update.transition()
// transition existing slices to reflect new data.
.attr('d',arc)
Note: transitioning paths can be difficult - you'll notice the deformity to the pie in the transition in your example. This is because of how the d attribute string is interpolated. If you want to preserve the radius a different approach is needed in applying the transition.
I tried to toggle the data using
d3.interval and render the data as bar graphs on my svg.
As below.
interval--
d3.interval(function(){update(data);
flag =! flag;
},1000)
rendering--
function update(input){
var value = flag ? 'revenue':'profit'
x.domain(input.map(function(d){return d.month}));
y.domain([0,d3.max(input, function(d){return d[value]})])
var xAxisCall = d3.axisBottom(x);
var yAxisCall = d3.axisLeft(y).tickFormat(function(d){return '$'+d})
xAxisGroup.call(xAxisCall.tickSizeOuter(0))
yAxisGroup.call(yAxisCall.tickSizeOuter(0))
var rects = svg.selectAll('rect').data(data)
rects.exit().remove()
rects.enter().append('rect').attr('y', (d)=>{return height-y.range([0,height])(d[value])})
.attr('x', (d)=>{ return 50+x(d.month)})
.attr('height',(d)=>{return y(d[value])})
.attr('width',x.bandwidth())
.attr('fill','grey')
}
However,
it doesn't toggle the graph depending on the data fed into the rectangles.
It only worked when I put .append() at the end of rectangle rendering like below.
rects.attr('y', (d)=>{return height-y.range([0,height])(d[value])})
.attr('x', (d)=>{ return 50+x(d.month)})
.attr('height',(d)=>{return y(d[value])})
.attr('width',x.bandwidth())
.attr('fill','grey')
.enter().append('rect')
Why is it that I need to add enter and append at the end?
The reason that I don't get this is
Even if I put the .enter().append('rect') at the beginning,
the properties of the rect can be redefined and updated.
However, the properties of the rectangles weren't updated once I put the enter() argument beforehand.
I tested out by adding 'console.log' like below.
rects.enter().append('rect').attr('fill',function(){return flag? "rgba(100,0,0,0.2)":'rgba(0,100,0,0.2)'}).attr('y', (d)=>{return height-y.range([0,height])(d[value])})
.attr('x', (d)=>{ return 50+x(d.month)})
.attr('height',(d)=>{console.log(d[value])
;return y(d[value])})
.attr('width',x.bandwidth())
It doesn't log anything, it logs only when I put enter at the end.
Why all the arguments don't run?
In your first case you have an enter selection but nothing changing your update selection. In your second case you're changing your update selection while doing nothing to your enter selection.
Write your update and enter selections clearly and separately. This normally leads to duplicated code... if you don't like those duplications, use merge():
var rects = svg.selectAll('rect').data(data)
rects.exit().remove()
rects = rects.enter()
.append('rect')
.attr('fill', 'grey')
.merge(rects)
.attr('y', (d) => {
return height - y.range([0, height])(d[value])
})
.attr('x', (d) => {
return 50 + x(d.month)
})
.attr('height', (d) => {
return y(d[value])
})
.attr('width', x.bandwidth())
D3 uses the data join, which doens't act on elements directly, but describes their transformations. When you do:
var rects = svg.selectAll('rect').data(data)
you're not selecting all the rectangles, but only those in the "update" selection. Here's a more detailled explaination by the author of D3.
Nowadays, it's recommended to use the selection.join() method which makes selection-based code clearer:
svg.selectAll('rect')
.data(data)
.join(
enter => enter.append('rect')
.attr('fill', 'grey'),
update => update
.attr('x', (d) => 50 + x(d.month))
.attr('y', (d) => height-y.range([0,height])(d[value]))
.attr('width', x.bandwidth())
.attr('height', (d) => y(d[value])),
exit => exit.remove()
)
I have this d3.js project donut chart. For some reason, I am not able to access the data with in the onmousemove. The i value become zero is all the functions I pass within that event. I want to access the data of the particular slice where the mouse has moved.
How do I resolve this? Someone pls hlp!
Here is my code so far:
piesvg.selectAll("path")
.data(pie(data))
.enter().append("g")
.attr('class', 'slice')
var slice = d3.selectAll('g.slice')
.append('path')
.each(function(d) {
d.outerRadius = outerRadius - 20;
})
.attr("d", arc)
.attr('fill', function(d, i) {
return colorspie(i)
})
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150))
.on("mousemove", function(data){
piesvg.select(".text-tooltip")
.attr("fill", function(d,i){return colorspie(i)})
.text(function(d, i){return d[i].domain + ":" + parseInt(d[i].value * 20)}); //Considers i as 0, so no matter whichever slice the mouse is on, the data of only first one is shown
});
Here is the full code:
https://jsfiddle.net/QuikProBro/xveyLfyd/1/
I dont know how to add external files in js fiddle so it doesn't work....
Here is the .tsv that is missing:
value domain
1.3038675 Cloud
2.2541437 Networking
0.15469614 Security
0.8287293 Storage
0.7292818 Analytics
0.61878455 Intelligence
1.7016574 Infra
0.4088398 Platform
Your piesvg.select is bound to be zero-indexed for i and in all probability undefined for d as it takes those values from a single tooltip element, not the slices. Hard to be 100% sure from the snippet, but I suspect you're wanting to access and use the 'data' and 'i' from the original selectAll on the slices.
.on("mousemove", function(d, i){
piesvg.select(".text-tooltip")
.attr("fill", colorspie(i))
.text(d.data.domain + ":" + parseInt(d.data.value * 20));
});
Edited as pie slices store original data in d.data property ^^^
I have a d3.js problem and have struggled with this for a while and just can not seem to solve it. I believe it is pretty easy, but am missing something very basic.
Specifically, I have the following code, which generates a line and 2 circles for the 1st entry in the JSON - I have 'hardcoded' it for the first entry.
I'd like to now add the 2nd and 3rd entries of the JSON file to the graph and have control over line and circle colors and then generalize the code.
From reading the documentation and StackOverflow, it seems like the proper approach is to use nesting, but I can't seem to make it work?
The code is on jsfiddle at the following URL and the javascript is below.
http://jsfiddle.net/GVmVk/
// INPUT
dataset2 =
[
{
movie : "test",
results :
[
{ week: "20130101", revenue: "60"},
{ week: "20130201", revenue: "80"}
]
},
{
movie : "beta",
results :
[
{ week: "20130101", revenue: "40"},
{ week: "20130201", revenue: "50"}
]
},
{
movie : "gamm",
results :
[
{ week: "20130101", revenue: "10"},
{ week: "20130201", revenue: "20"}
]
}
];
console.log("1");
var parseDate = d3.time.format("%Y%m%d").parse;
var lineFunction = d3.svg.line()
.x(function(d) { return xScale(parseDate(String(d.week))); })
.y(function(d) { return yScale(d.revenue); })
.interpolate("linear");
console.log("2");
//SVG Width and height
var w = 750;
var h = 250;
//X SCALE AND AXIS STUFF
//var xMin = 0;
//var xMax = 1000;
var xScale = d3.time.scale()
.domain([parseDate("20130101"),parseDate("20131231")])
.range([0, w]);
console.log(parseDate("20130101"));
console.log("3");
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
console.log("4S");
//Y SCALE AND AXIS STUFF
var yScale = d3.scale.linear()
.domain([0, 100])
.range([h, 0]);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("4S1");
//CREATE X-AXIS
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - 30) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + 25 + ",0)")
.call(yAxis);
svg.selectAll("circle")
.data(dataset2[0].results)
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.append("path")
.attr("d", lineFunction(dataset2[0].results))
.attr("class", "line");
The word "nesting" comes up in two contexts in d3 -- creating nested data arrays with d3.nest, and using nested data to create nested selections.
Your data is already in the correct format for a nested selection -- an array of objects, each of which has a sub-array of individual data points. So you don't need to worry about manipulating the data, you just need to go straight to joining your data to your elements in nested d3 selections:
I'm going to take you through it quickly, but the following tutorials will be good reference for the future:
Thinking with Joins
Nested Selections
How Selections Work
On to your example: you have a top-level data structure that is an array of movie objects, each of which contains a sub-array of weekly revenue values. The first thing you need to decide is what type of elements you want associated with each level of data. You're drawing a line and a set of circles for the data in the sub-array, but aren't currently adding anything for the top-level array objects (the movies). You need to add something for them in order for nested selections to work, and it needs to be something that can contain your line and circle. In SVG, that's almost always going to be a <g> (grouping) element.
To efficiently create one <g> element for every object in your data array -- and to attach the data objects to the elements for future reference -- you create an empty selection, join your data to it, then use the enter() method of the data join selection to add elements for each data object that didn't match an element. In this case, since we don't have any elements to start, all the data objects will be in the enter() selection. However, the same pattern also works when updating some of the data.
var movies = svg //start with your svg selection,
//it will become the parent to the entering <g> elements
.selectAll("g.movie") //select all <g> elements with class "movie"
//that are children of the <svg> element
//contained in the `svg` selection
//this selection will currently be empty
.data( dataset2 ); //join the selection to a data array
//each object in the array will be associated with
//an element in the selection, if those elements exist
//This data-joined selection is now saved as `movies`
movies.enter() //create a selection for the data objects that didn't match elements
.append("g") //add a new <g> element for each data object
.attr("class", "movie") //set it's class to match our selection criteria
//for each movie group, we're going to add *one* line (<path> element),
//and then a create subselection for the circles
.append("path") //add a <path> within *each* new movie <g> element
//the path will *inherit* the data from the <g> element
.attr("class", "line"); //set the class for your CSS
var lineGraph = movies.select("path.line")
//All the entered elements are now available within the movies selection
//(along with any existing elements that we were updating).
//Using select("path") selects the first (and only) path within the group
//regardless of whether we just created it or are updating it.
.attr("d", function(d){ return lineFunction(d.results); });
//the "d" attribute of a path describes its shape;
//the lineFunction creates a "d" definition based on a data array.
//If the data object attached to the path had *only* been the d.results array
//we could have just done .attr("d", lineFunction), since d3
//automatically passes the data object to any function given as the value
//of an .attr(name, value) command. Instead, we needed to create an
//anonymous function to accept the data object and extract the sub-array.
var circles = movies.selectAll("circle")
//there will be multiple circles for each movie group, so we need a
//sub-selection, created with `.selectAll`.
//again, this selection will initially be empty.
.data( function(d) {return d.results; });
//for the circles, we define the data as a function
//The function will be called once for each *movie* element,
//and passed the movie element's data object.
//The resulting array will be assigned to circles within that particular
//movie element (or to an `enter()` selection, if the circles don't exist).
circles.enter() //get the data objects that don't have matching <circle> elements
.append("circle") //create a circle for each
//the circles will be added to the appropriate "g.movie"
//because of the nested selection structure
.attr("r", 3); //the radius doesn't depend on the data,
//so you can set it here, when the circle is created,
//the same as you would set a class.
circles //for attributes that depend on the data, they are set on the entire
//selection (including updating elements), after having created the
//newly entered circles.
.attr("cx", function(d) { return xScale( parseDate(d.week) ); })
.attr("cy", function(d) { return yScale( d.revenue ); });
Live version with the rest of your code: http://jsfiddle.net/GVmVk/3/
You'll need to adjust the domain of your x-scale so that the first data points aren't cut off, and you'll need to decide how you want to use your movie title property, but that should get you going.
Yes indeed, nested selection are the way to go for the circles, although you don't need them for the paths:
svg.selectAll("g.circle")
.data(dataset2)
.enter()
.append("g")
.attr("class", "circle")
.selectAll("circle")
.data(function(d) { return d.results; })
.enter()
.append("circle")
.attr("cx", function(d) {
// console.log(d[0]);
console.log(parseDate(d.week));
return xScale(parseDate(d.week));
})
.attr("cy", function (d) {
return yScale(d.revenue);
})
.attr("r", 3);
//create line
var lineGraph = svg.selectAll("path.line")
.data(dataset2).enter().append("path")
.attr("d", function(d) { return lineFunction(d.results); })
.attr("class", "line");
Complete example here.
I have followed the instructions at: http://bost.ocks.org/mike/path/ for creating and animating single graphs with single lines.
And, figured out how to create multiple lines in a graph: Drawing Multiple Lines in D3.js
Main Issue: I am having a hard time transitioning multiple lines after I shift & push in new data into my data array.
I create the N lines with: (time: epoch time, steps forward)
var seriesData = [[{time:1335972631000, value:23}, {time:1335972631500, value:42},...],
[{time:1335972631000, value:45}, {time:1335972631500, value:13},...],
[{time:1335972631000, value:33}, {time:1335972631500, value:23},...}],
[...],[...],...
];
var seriesColors = ['red', 'green', 'blue',...];
line = d3.svg.line()
.interpolate(interpolation)
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.value); });
graphGroup.selectAll(".line")
.data(seriesData)
.enter().append("path")
.attr("class", "line")
.attr("d", line)
.style('stroke', function(d, i) { return seriesColors[i]; })
.style('stroke-width', 1)
.style('fill', 'none');
And am trying to update N lines with a Javascript setInterval(...) calling a method with:
graph.selectAll("path")
.data(seriesData)
.attr("transform", "translate(" + x(1) + ")")
.attr("d", line)
.transition()
.ease("linear")
.duration(transitionDelay)
.attr("transform", "translate(" + x(0) + ")");
It can draw the initial set perfectly, but as soon as I update the data array, the lines just disappear.
UPDATE 01
I realized that I am using epoch time values in the x (xAxis shows date:time) as my example would probably work if I used the illustrative seriesData above.
The problem was the "transform", "translate" using x(1), x(0) was returning huge numbers, way larger than my graph needed to be transitioned.
I modified the update N lines method (above) to use a manual approach:
New Issue:
Now the graph moves left correctly, but the lines/graph pops back to the right, each setInterval update executes.
It's push/shift'ing the seriesData array correctly but it doesn't keep scrolling to the left to show the new data that IS actually being drawn.
graph.selectAll("path")
.data(seriesData)
.attr("d", line)
.attr("transform", null)
.transition()
.ease("linear")
.duration(2000)
.attr("transform", "translate(-200)");
Another reference that I have used is this: http://bl.ocks.org/1148374
Any thoughts?
One thing that jumps out at me as a possibility for error is the data calls that are used, the initial is
.data(seriesData)
the update uses
.data([seriesData])
which may cause issues, but its hard to tell without seeing the rest of what is going on, can you perhaps put it on jsfiddle?