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.
Related
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>
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>
I have a simple (o.k. not so simple) barchart which shows the electric power consumption of one consumer (C1). I add the consumption of another consumer (C2) as line. The max consumption of C2 if higher then the max consumption of C1 so I have to rescale. I have solved this problem but not as beautiful I wanted to.
I calculate the new yMax, set the domain, rescale the axis (beautiful) remove all 'rect' and redraw (not beautiful). Is there a possibility to say: hey bars, I have a new scale, go down with a beautiful animation :)
Here the rescale method:
var rescale = function () {
//in this function the new _maxYValue is set
renderLineView();
var data = _data;
y.domain([_minYValue, _maxYValue]);
_svg.select(".y.axis")
.transition().duration(1500).ease("sin-in-out")
.call(yAxis());
_svg.selectAll("rect").remove();
var barWidth = getBarWidth(data.length);
var bars = d3.select("#layer_1").selectAll(".bar").data(data, function (d) {
return d.xValue;
});
bars.enter().append("rect")
.attr("class", "daybarincomplete")
.attr("x", function (d, i) {
return x(d.xValue) + 4;
})
.attr("width", barWidth)
.attr("y", function (d) {
return Math.min(y(0), y(d.value));
})
.attr("height", function (d) {
return Math.abs(y(d.value) - y(0));
});
}
Here is the jsfiddle: http://jsfiddle.net/axman/v4qc7/5/
thx in advance
©a-x-i
Use the .transition() call on bars, to determine the behaviour you want when the data changes (e.g. Bar heights change). You'd chain the .attr() function after it to set bar height etc.
To deal with data points that disappear between refreshes (e.g. You had 10 bars originally but now only have 9), chain the .exit().remove() functions to bars.
With both of the above, you can additionally chain something like .duration(200).ease('linear') to make it look all pretty.
pretty much what #ninjaPixel said. There's a easy to follow example here
http://examples.oreilly.com/0636920026938/chapter_09/05_transition.html
In the code below Chrome's debugger I get the following error
Error: Invalid value for <rect> attribute y="NaN"
when this is exectued
this.options.barDemo.selectAll("rect")
.data(data)
.enter()
.append("svg:rect")
.attr("x", function (d, i) {
//console.log(x(i));
return x(i);
})
.attr("y", function (d) {
console.log(height - y(d.attributes.contract));
return height - y(d.attributes.contract);
})
.attr("height", function (d) {
console.log(y(d.attributes.contract));
return y(d.attributes.contract);
})
.attr("width", barWidth)
.attr("fill", "#2d578b");
I've isolated the problem to this function for the y coordinate value
y(d.attributes.contract)
My question is this: Given a value in between 0 and 200000, why would the above statement evaluate to NaN given var y is equal to this:
var y = d3.scale.ordinal().domain([0, 200000]).rangeBands([height, 0]);
It's my understanding that the D3 .ordinal() function will return a coordinate based on where that value falls within a given range. This is my first time using D3, however, so any suggestions/hints/tips greatly appreciated.
I think an ordinal scale is the wrong choice here. An ordinal scale maps values the discrete set of values in the domain directly to values in the range, so it will only return a value if you pass it one of the values in the domain. In your case it will return the height when you pass 0, and 0 when you pass 200000. Anything else will return NaN.
Probably what you want is a linear scale. Linear scales take in a continuous range, and then return a value within the range. Given this scale:
var y = d3.scale.linear().domain([0, 200000]).range([height, 0]);
you can pass it any value between 0 and 200000 and it will return a value between height and 0.
I was getting NaN in a different scenario because I was passing undefined as my padding when configuring via .rangeBands([0,1], padding). Something to watch out for.
I'm trying to make a d3 scatterplot with two drop-down menus. The drop-down menus are used to select which datasets to plot against each other. I use two global variables to keep track of which datasets are currently used. "currentX" is the name of the first dataset (on the x-axis) and "currentY" is the name of the second dataset.
My scale functions depend on the values of "currentX" and "currentY". Here is an example of my xScale function:
var xScale = d3.scale.linear()
.domain([d3.min(dataset, function(d){return d.data[currentX]}), d3.max(dataset, function(d){return d.data[currentX]})
.range([padding, w - padding])
.nice();
My yScale function is the same, but uses currentY instead of currentX. My problem is that when I try to change views of the data, my scale doesn't update. Here is the code for changing between views of the data:
d3.selectAll('select')
.on('change', function() {
// Update currentX and currentY with the currently selected datasets
key = Object.keys(dataset[0].data)[this.selectedIndex];
if (this.getAttribute('id') == 'xSelect') {currentX = key}
if (this.getAttribute('id') == 'ySelect') {currentY = key}
// Change data used in the scatterplot
svg.selectAll('circle')
.data(dataset)
.transition()
.duration(1000)
.attr('cx', function(d) { return xScale(d.data[currentX]) })
.attr('cy', function(d) { return yScale(d.data[currentY]) })
.attr('r', 2)
};
I want the xScale and yScale functions to update, to reflect the new values of currentX and currentY. But for some reason, these functions are not updating. If anyone could help me fix this, I would really appreciate it! Thanks!
UPDATE: Just to clarify, my problem is that my xScale and yScale functions do not change, even though xCurrent and yCurrent (and their minimum and maximum values) have changed. For example, "console.log(xScale(-5))" always produces the same value. This value should change as xCurrent and yCurrent change! Thanks again.
UPDATE 2: The global variables "xCurrent" and "yCurrent" ARE being updated. Furthermore, if I define NEW xScale and yScale functions in the .on('change') function, then my scales are updated. This actually fixes my problem, but I would still like to know why I can't do it the other way. Still trying to learn D3!
You need to update the x scale domain inside your change function. Also, you can use d3.extent instead of d3.min and d3.max. For example:
.on('change', function () {
// Update currentX and currentY with the currently selected datasets
key = Object.keys(dataset[0].data)[this.selectedIndex];
if (this.getAttribute('id') == 'xSelect') {currentX = key}
if (this.getAttribute('id') == 'ySelect') {currentY = key}
xScale.domain(d3.extent(dataset, function(d){return d.data[currentX];}));
// Change data used in the scatterplot
svg.selectAll('circle')
.data(dataset)
.transition()
.duration(1000)
.attr('cx', function(d) { return xScale(d.data[currentX]) })
.attr('cy', function(d) { return yScale(d.data[currentY]) })
.attr('r', 2)
})
This is because the scale generator isn't aware of changes to your data. It's domain method isn't going to be called every time the data changes. Thus, when the data does change, you have to explicitly set the scale's domain before re-drawing any data that depends on it.