This might be about the simplest D3 force layout ever:
const svg = main.append("svg")
.attr("width",500)
.attr("height",500)
.classed("SVG_frame",true)
.append("g")
const nodes = [{id:1},{id:2}];
const simulation = d3.forceSimulation(nodes)
.force("centering",d3.forceCenter([200,200]))
.force("collision",d3.forceCollide(20))
const node = svg
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r",20)
simulation.on("tick",function() {
console.log(nodes[0].x)
node
.attr("cx",d => d.x)
.attr("cy",d => d.y)
})
And yet, I get <circle> attribute cx: Expected length, "NaN". on the first frame. (cy displaces a tiny bit on the frame the simulation gives up moving)
I know this have been asked several times, but none seem to address version 4, where force simulation has presumably changed its inner workings. In fact, the docs even state now that when position is NaN, the position is automatically arranged in a "phyllotaxis arrangement" or whatever, so maybe this isn't supposed to happen, but it does.
Anyone has any clue?
The problem here is very simple: d3.forceCenter takes two values, not an array of values:
[d3.forceCenter] Creates a new centering force with the specified x- and y- coordinates. If x and y are not specified, they default to ⟨0,0⟩.
In an API, brackets around an argument mean that the argument is optional (see here). When you see something like this in the D3 API:
d3.forceCenter([x, y])
You have to take care for not mistaking those brackets for an array. Here, [x, y] means that the values are optional, it doesn't mean that they must be in an array.
So, instead of:
.force("centering",d3.forceCenter([200,200]))
It should be:
.force("centering",d3.forceCenter(200,200))
Here is a demo:
const svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",500)
.classed("SVG_frame",true)
.append("g")
const nodes = [{id:1},{id:2}];
const simulation = d3.forceSimulation(nodes)
.force("centering",d3.forceCenter(200,200))
.force("collision",d3.forceCollide(20))
const node = svg
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r",20);
simulation.on("tick",function() {
node.attr("cx",d => d.x).attr("cy",d => d.y)
});
<script src="https://d3js.org/d3.v4.min.js"></script>
Related
I have a basic map here, with dummy data. Basically a bubble map.
The problem is I have multiple dots (ex:20) with exact same GPS coordinates.
The following image is my csv with dummy data, color blue highlight overlapping dots in this basic example. Thats because many compagny have the same city gps coordinates.
Here is a fiddle with the code I'm working on :
https://jsfiddle.net/MathiasLauber/bckg8es4/45/
Many research later, I found that d3.js add this force simulation fonction, that avoid dots from colliding.
// Avoiding bubbles overlapping
var simulationforce = d3.forceSimulation(data)
.force('x', d3.forceX().x(d => xScale(d.longitude)))
.force('y', d3.forceY().y(d => yScale(d.latitude)))
.force('collide', d3.forceCollide().radius(function(d) {
return d.radius + 10
}))
simulationforce
.nodes(cities)
.on("tick", function(d){
node
.attr("cx", function(d) { return projection.latLngToLayerPoint([d.latitude, d.longitude]).x; })
.attr("cy", function(d) {return projection.latLngToLayerPoint([d.latitude, d.longitude]).y; })
});
The problem is I can't make force layout work and my dots are still on top of each other. (lines: 188-200 in the fiddle).
If you have any tips, suggestions, or if you notice basic errors in my code, just let me know =D
Bunch of code close to what i'm trying to achieve
https://d3-graph-gallery.com/graph/circularpacking_group.html
https://jsbin.com/taqewaw/edit?html,output
There are 3 problems:
For positioning the circles near their original position, the x and y initial positions need to be specified in the data passed to simulation.nodes() call.
When doing a force simulation, you need to provide the selection to be simulated in the on tick callback (see node in the on('tick') callback function).
The simulation needs to use the previous d.x and d.y values as calculated by the simulation
Relevant code snippets below
// 1. Add x and y (cx, cy) to each row (circle) in data
const citiesWithCenter = cities.map(c => ({
...c,
x: projection.latLngToLayerPoint([c.latitude, c.longitude]).x,
y: projection.latLngToLayerPoint([c.latitude, c.longitude]).y,
}))
// citiesWithCenter will be passed to selectAll('circle').data()
// 2. node selection you forgot
const node = selection
.selectAll('circle')
.data(citiesWithcenter)
.enter()
.append('circle')
...
// let used in simulation
simulationforce.nodes(citiesWithcenter).on('tick', function (d) {
node
.attr('cx', function (d) {
// 3. use previously computed x value
// on the first tick run, the values in citiesWithCenter is used
return d.x
})
.attr('cy', function (d) {
// 3. use previously computed y value
// on the first tick run, the values in citiesWithCenter is used
return d.y
})
})
Full working demo here: https://jsfiddle.net/b2Lhfuw5/
I'm pretty new to d3js and trying to understand the difference between using data and datum to attach data to elements. I've done a fair bit of reading the material online and I think I theoretically understand what's going on but I still lack an intuitive understanding. Specifically, I have a case where I'm creating a map using topojson. I'm using d3js v7.
In the first instance, I have the following code to create the map within a div (assume height, width, projection etc. setup correctly):
var svg = d3.select("div#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath()
.projection(projection);
var mapGroup = svg.append("g");
d3.json("json/world-110m.json").then(function(world){
console.log(topojson.feature(world, world.objects.land))
mapGroup.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
The console log for the topojson feature looks like this:
And the map comes out fine (with styling specified in a css file):
But if I change datum to data, the map disappears. I'm trying to improve my understanding of how this is working and I'm struggling a little bit after having read what I can find online. Can someone explain the difference between data and datum as used in this case and why one works and the other doesn't?
Thanks for your help!
There are several differences between data() and datum(), but for the scope of your question the main difference is that data() accepts only 3 things:
An array;
A function;
Nothing (in that case, it's a getter);
As you can see, topojson.feature(world, world.objects.land) is an object. Thus, all you'd need to use data() here (again, not the idiomatic D3, I'm just addressing your specific question) is wrapping it with an array:
.data([topojson.feature(world, world.objects.land)])
Here is your code using data():
var svg = d3.select("div#map").append("svg")
.attr("width", 500)
.attr("height", 300)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath();
var mapGroup = svg.append("g");
d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json").then(function(world) {
const projection = d3.geoEqualEarth()
.fitExtent([
[0, 0],
[500, 300]
], topojson.feature(world, world.objects.land));
path.projection(projection);
mapGroup.append("path")
.data([topojson.feature(world, world.objects.land)])
.attr("class", "land")
.attr("d", path);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson#3"></script>
<div id="map"></div>
Freshboy got several questions when creating my first bar chart by D3.js:
line 30 -- console.log(yScale); The console shows "function.....", what's this? ; Last line-- Why I can get correct answers of each column whenI give
"height" the value "yScale". What's happening there?
.attr("x", function(data, i){return xScale(i)}) xScale is a variable not a function. Why can I use xScale like a function--xScale(i)?
var data = [4,8,15,16,23,42];
const svg = d3.select("svg");
const margin = {top:25, right:25, bottom:25, left:25};
const xDomain = [0,5];
const xRange = [0,200];
const yDomain = [0,42];
const yRange = [0,200];
const rectWidth = 200 / 6;
const xScale = d3.scaleLinear()
.domain(xDomain)
.range(xRange);
var yScale = d3.scaleLinear()
.domain(yDomain)
.range(yRange);
const g = svg.append("g")
.attr("transform", "translate(50,50)");
createRect = g.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(data, i){return xScale(i)})
.attr("y", function(data,i){
console.log(yScale(data));
console.log(yScale);
return 200-yScale(data)})
.attr("width", rectWidth)
.attr("height", yScale);
<!DOCTYPE html>
<html>
<script src='https://d3js.org/d3.v5.min.js'></script>
<style> rect {fill: lightblue; stroke: black; }</style>
<body>
<svg width=300 height=300>
</svg>
<script src="w8-1-4 Axes.js">
</script>
</body>
</html>
Both xScale and yScale are functions.
This is what you see when you log yScale and why you can use xScale "like a function". d3.scaleLinear() returns a function to scale values. In javascript, functions are objects, object properties can also be functions (methods in this case to set values such as domain and range).
Most simply:
d3.scaleLinear() returns a function.
With d3 scales (and most d3 functions), you create a new scale function with d3.scaleLinear() and then use the scale's methods to set values such as the scale's domain or range. The method returns the scale itself, hence why you can chain together several methods, modifying the scale function as you go.
Why don't you need to pass any parameters to yScale when using .attr("height",yScale)?
D3 uses Function.prototype.apply() to pass paramaeters to any function provided to .attr() (or many other methods in D3, such as selection.style() or selection.each()). By using .apply D3 "calls a function with a given this value, and arguments provided as an array". The this value in D3 is generally an individual element, the first parameter, canonnically d, is the datum associated with that element, the 2nd paremeter, cannonically i, is the index of that element, and the last parameter is the group of elements in the selection.
The function passed to .attr() is called for each element in the selection.
The scale function takes one parameter, a value to be scaled. As your datum is a number, we can pass the scale function directly to .attr(): the first parameter passed to it will be the datum.
Carrying forward the above and for reference,
.attr("height",xScale)
produces the same result as:
.attr("height", function(d) {
return xScale(d);
})
as d is an element's datum (an item from the data array), we can use either approach above.
Say, I am having a 2d arr = [[1,2,3],[100,200],['A','B'...'Z']]. As you can see, arr[0].length!=arr[1].length!=arr[2].length.
I want to respresent them as text-tags within an svg.
Moreover, I want to be flexible as in where each subarray starts in terms of (x,y) and how wide the spaces are between each element of a subarray (x, y).
d3.select('svg').selectAll('g').data(arr).enter().append('g').selectAll('text').data((d)=>d).enter().append('text').text(d=>d)
But this way I am losing information within each g. I tried setting .attr('x', (d,i) => (i+1)*20) before .selectAll('text'), but it only adds the 'x'-attr to g and has no effect on the text (i.e. elements of the array) displayed on the page.
The thing is that I put all them into the DOM. But then how can I adjust their .attr('x'), .attr('y') in a group (without hardcoding) like in lines, but each line can have its own spacing between elements?
You can try to use "getBBox" or getBoundingClientRect to get node width, then make a herizontal layout.
var arr = [[1,2,3],[100,200],['A','B', 'C', 'D', 'E', 'Z']]
var container = d3.select("#box")
.append("g")
.attr("transform", "translate(10, 50)");
container
.selectAll("text")
.data(arr)
.enter()
.append("text")
.text(d => d);
/*
Using "getBBox" or "getBoundingClientRect" method to get each text node's width,
then move them one another.
You must make sure the svg element and its parent elements are visible,
which means "display: none" is should not applied on them.
*/
var xInit = 0, padding = 15;
container
.selectAll("text")
.each(function (d, i) {
d3.select(this).attr("x", xInit);
xInit += this.getBBox().width + padding;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg id="box" width="500" height="300"></svg>
I think I have got it. Not sure if that's a clear-cut solution but it works.
First, adding ids after creating gs. Then writing functions for x and y coordinates based on the ids (i.e. rows).
function x(d,i,j){
let node_id = j[0].parentNode.id; // allows to dynamically access the ids of every text element
//calculate x_value here
return x_value;
}
And that function can be passed into the original chain.
d3.select('svg').selectAll('g').data(arr).enter().append('g')
.attr('id', (d,i)=>i) // adding ids to every g based on the array index
.selectAll('text').data((d)=>d).enter().append('text').text(d=>d)
.attr('x', x) // here
If anyone knows a leaner solution, it would be much appreciated as I am new to Javascript and d3.
I have a map of the USA that I'm trying to display lat/lon points over. I've mashed together a few examples to get this far, but I've hit a wall. My points are in a csv file, which I'm not sure how to upload here, but it's just 65,000 rows of number pairs. For instance 31.4671154,-84.9486771.
I'm mostly following the example from Scott Murray's book here.
I'm using the Albers USA projection.
var projection = d3.geo.albersUsa()
.scale(1200)
.translate([w / 2, h / 2]);
And setting up the landmarks as an svg group appended to the map container.
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.on("click", stopped, true);
svg.append("rect")
.attr("class", "background")
.attr("width", w)
.attr("height", h)
.on("click", reset);
var g = svg.append("g");
var landmarks = svg.append("g")
I read the data and try to set circles at each lat/lon point.
d3.csv("./public/assets/data/landmark_latlon_edited.csv", function(error, latlon){
console.log(latlon);
landmarks.selectAll("circle")
.data(latlon)
.enter()
.append("circle")
.attr({
'fill': '#F00',
'r': 3
})
.attr('cx', function(d){
return projection([d.lon, d.lat][0]);
})
.attr('cy', function(d){
return projection([d.lon, d.lat])[1];
})
.style({
'opacity': .75
});
});
Now, the problem is that the cx property is not receiving a value. When viewed in the inspector the circles don't show a cx, and, indeed, appear in the svg at the appropriate y values, but in a stacked column at x=0.
<circle fill="#F00" r="3" cy="520.8602676002965" style="opacity: 0.75;"></circle>
I found an old issue I thought might be related here which states that the projection method will return null if you try to feed it values outside of its normal bounding box. I opened the csv in Tableau and saw a couple values that were in Canada or some U.S. territory in the middle of the Pacific (not Hawaii), and I removed those, but that didn't solve the problem.
I'm decidedly novice here, and I'm sure I'm missing something obvious, but if anyone can help me figure out where to look I would greatly appreciate it. Lots of positive vibes for you. If I can add anything to clarify the problem please let me know.
Thanks,
Brian
I had the same problem when I updated to d3 v3.5.6. Here is what I did to check for null values, so that you don't try to access the [0] position of null:
.attr("cx", function(d) {
var coords = projection([d.lon, d.lat]);
if (coords) {
return coords[0];
}
})
I'm sure there is a cleaner way to do this, but it worked for me.
You have a little error in your function generating cx values which messes it all up. It's just one parenthesis in the wrong place:
.attr('cx', function(d){
return projection([d.lon, d.lat][0]);
})
By coding [d.lon, d.lat][0] you are just passing the first value of the array, which is d.lon, to the projection and are returning the result of projection() which is an array. Instead, you have to place the [0] outside the call of projection() because you want to access the value it returned. Check your function for cy where you got things right. Adjusting it as follows should yield the correct values for cx:
.attr('cx', function(d){
return projection([d.lon, d.lat])[0];
})