I am trying to amend M Bostock's US unemployment choropleth map that applies D3 scale chromatic.
I am now able to amend the bucket-sizes as I please to plot my data, however when I do so the legend seems to grow in size exponentially and I am unable to make it fit to a desired width and scale the intervals appropriately.
Please see the attached jsfiddle where I demonstrate the issue encountered. I would like to amend the legend in two ways:
space between ticks is fixed, e.g. 50px
space between ticks is a function of scale, but legend still fits within desired width (e.g. 500px)
My problem is that I do not seem to be able to amend parameters in the following line of code (I am hoping this is not a default in the scale chromatic script..)
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(function(x, i) { return i ? x : x + "%"; })
.tickValues(color.domain()))
.select(".domain")
.remove();
There are only two problems in your code, both easy to fix.
The first problem is the domain here:
var myDomain = [1, 5, 8, 9, 12, 18, 20, 25]
var x = d3.scaleLinear()
.domain(myDomain)
.rangeRound([600, 860]);
As you can see, you're passing an array with several values. However, for a linear scale with just two values in the range, you have to pass an array with just two values.
Therefore, it should be:
var myDomain = [1, 5, 8, 9, 12, 18, 20, 25]
var x = d3.scaleLinear()
.domain(d3.extent(myDomain))//just two values here
.rangeRound([600, 860]);
The second problem is here:
if (d[1] == null) d[1] = x.domain()[1];
//this is the second element -------^
Since myDomain is an array with several values, you're passing the second value here. But you don't want the second value, you want the last value.
Therefore, it should be:
if (d[1] == null) d[1] = x.domain()[x.domain().length - 1];
//now this is the last element --------------^
Here is the code with those changes (I removed the map, we don't need it for this answer, and also moved the legend to the left, so it better fits S.O. snippet):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var myDomain = [1, 5, 8, 9, 12, 18, 20, 25]
var x = d3.scaleLinear()
.domain(d3.extent(myDomain))
.rangeRound([200, 460]);
var color = d3.scaleThreshold()
.domain(myDomain)
.range(d3.schemeBlues[9]);
var g = svg.append("g")
.attr("class", "key");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[x.domain().length - 1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
g.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Unemployment rate");
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(function(x, i) { return i ? x : x + "%"; })
.tickValues(color.domain()))
.select(".domain")
.remove();
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<svg width="600" height="200"></svg>
Related
I am creating a horizontal bar chart that shows data from a CSV file dynamically. I am getting an extra space where another value would be, but without any data. I only want the top 10 values(students) plus keys(counties) and it looks like it adds one.
As you can see it adds a space and a small rect at the top. I just want to remove both.
// Parse Data
d3.csv("FA18_geographic.csv", function(data) {
data.sort(function(a,b) {
return d3.descending(+a.students, +b.students);
});
// find max county
var max = d3.max(data, function(d) { return +d.students;} );
var map = d3.map(data, function(d){return d.students; });
pmax = map.get(max);
// Add X axis
var x = d3.scaleLinear()
.domain([0, +pmax.students+500])
.range([ 0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end");
// Y axis
var count = 0
var y = d3.scaleBand()
.range([ 0, height ])
.domain(
data.map(function(d) { //only show top 10 counties
count = count+1
if (count<=10){
return d.county;
}})
)
.padding(.3);
svg.append("g")
.call(d3.axisLeft(y));
//Bars //Something must be wrong in here???
svg.selectAll("rect.bar")
.data(data)
.enter()
.append("rect")
.attr("x", x(0) )
.attr("y", function(d) { return y(d.county); })
.attr("width", function(d) { return x(d.students); })
.attr("height", y.bandwidth() )
.attr("fill", "#79200D");
});
</script>
Your problem lies here:
.domain(data.map(function(d) { //only show top 10 counties
count = count + 1
if (count <= 10) {
return d.county;
}
}))
The issue here has nothing to do with D3, that's a JavaScript issue: you cannot skip interactions using Array.prototype.map.
Let's show this with the basic demo below, in which we'll try to return the item only if count is less than 5:
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let count = -1;
const domain = data.map(function(d) {
count += 1;
if (count < 5) {
return d
};
});
console.log(domain)
As you see, it will return several undefined. You only see one empty tick because D3 band scale treats all those values as a single undefined (domain values in band scales are unique).
There are several solutions here. You can, for instance, use Array.prototype.reduce:
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let count = -1;
const domain = data.reduce(function(acc, curr) {
count += 1;
if (count < 5) {
acc.push(curr)
};
return acc;
}, []);
console.log(domain)
Alternatively, you can use a filter after your map, or even a forEach to populate the array.
Finally, some tips:
You don't need a count, since Array.prototype.map has an index (the second argument), just like Array.prototype.reduce (the third argument).
You don't need to do count = count + 1, just do count += 1;
You are skipping the first element, because a JavaScript array is zero-based. So, start count as -1 instead of 0.
I am entering d3.js at ground level. There's a steep learning curve ahead of me, I know, but I didn't quite expect to get stuck on the second simple tutorial I tried. Now's the change to make the maximal marginal contribution to a fellow programmer's understanding!
I'm trying to get a simple svg bar chart to work using my data, which is virtually identical to the sample.
Bostock's data
name value
Locke 4
Reyes 8
Ford 15
Jarrah 16
Shephard 23
Kwon 42
My data
year value
year2013 2476
year2014 7215
year2015 23633
year2016thru229 21752
Note that our second columns have the same name. Both contain numbers. I gave my dataset the same name he did. Thus I should be able to run his code (below) without changing a thing, but it returns this:
Unexpected value NaN parsing width attribute.
...uments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function
d3.min.js (line 1, col 2575)
Unexpected value NaN parsing x attribute.
...uments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function
d3.min.js (line 1, col 2575)
His code as well as the tutorial are here.
You're not feeding D3 your data in the correct format.
https://jsfiddle.net/guanzo/kdcLj5jj/1/
var data = [
{year:'year2013',value:2476},
{year:'year2014',value:7215},
{year:'year2015',value:23633},
{year:'year2016thru229',value:21752}
]
var width = 420,
barHeight = 20;
var x = d3.scale.linear()
.range([0, width]);
var chart = d3.select(".chart")
.attr("width", width);
x.domain([0, d3.max(data, function(d) { return d.value; })]);
chart.attr("height", barHeight * data.length);
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect")
.attr("width", function(d) { return x(d.value); })
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d.value) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.value; });
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
I'm trying to create a Manhattan plot, similar to the one seen below. What I have, so far, is followed.
This is what I'm trying to create:
This is what I've coded up so far (lol):
There are two factors that determine the x position of the dots. (1)The chromosome index which ranges from [1, 22], and (2)the position coordinate which ranges from [1, 10,000,000]. The greater the chromosome index, the greater the position coordinate.
The question is: how do I set the domain for the position coordinates so that the dots stay within the chromosome index boundary like seen in the first picture.
Here is some relevant code:
var x0 = d3.scale.linear().range([0, width]),
x1 = d3.scale.linear().range([0, width]),
y = d3.scale.linear().range([height, 0]),
xAxis = d3.svg.axis().scale(x0).orient("bottom"),
yAxis = d3.svg.axis().scale(y).orient("left").tickPadding(6),
color = d3.scale.category20c();
var positions = [],
pvalues = [];
this.data.forEach(function (d) {
d.values.forEach(function (v) {
positions.push(v.position);
pvalues.push(v.pvalue);
});
});
var xMax = d3.max(positions),
ymax = d3.max(pvalues);
x0.domain([1, 22]);
x1.domain([0, xMax]);
y.domain([0, ymax]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 8)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("p-value");
var chromosome = svg.selectAll(".chr")
.data(this.data)
.enter().append("g")
.attr("class", "chr")
.attr("x", function (d) {
return x0(d.chr);
})
.attr("width", width / 22)
.style("fill", function (d) {
return color(d.chr);
});
chromosome.selectAll("circle")
.data(function (d) {
return d.values;
})
.enter().append("circle")
.attr("r", 3)
.attr("cx", function (d) {
return x1(d.position);
})
.attr("cy", function (d) {
return y(d.pvalue);
});
Update: So I switched to using d3.scale.ordinal() for both x scales by following the grouped barchart example. I think I got closer but, now, how do I prevent the dots from being drawn in the same x position?
Data format:
this.data:
[
{
chr: 1,
values: [
{
snp: rs182799,
position: 1478180,
pvalue: 3.26E-05
},
{
snp: rs182349,
position: 1598180,
pvalue: 3.26E-05
},
...
]
},
chr: 2,
values: [
{
snp: rs199799,
position: 2678180,
pvalue: 3.26E-05
},
{
snp: rs182349,
position: 2998180,
pvalue: 3.26E-05
},
...
]
},
...
]
Moving to ordinal scale was the right thing to do for the primary domain [1, 22].
For the subdomain of [1, 1e7] for each primary abscissa, the way to do it would be to generate one scale for each abscissa. A few questions about the range should be answered first, though:
Are the positions uniformly distributed?
In the primary chart, it looks as if the range of the subdomain is shrinking as the primary abscissa is increasing (e.g., the width of area for x = 22 is much smaller than for x = 2). Do position for each chromosome index have the same range [1, 1e7]?
Assuming that the position is distributed uniformly in [1, 1e7] and the maximum value of position is 1e7 for all chromosome indexes (which does not seem to be the case), the way to do it would be to nudge the x axis value of each circle within the subdomain:
chromosome.selectAll("circle")
// ...
.attr("cx", function (d, i) {
/* Assuming that data provided is in the "correct" order
* and `x1` is the ordinal scale.
*/
return x1(i) - x1.rangeBand() / 2 + d.position / 1e7;
})
// ...
Here the scale of the subdomain is an implicit one, not a d3.scale.
xMax is 22, so you won't get any dots on a line other than the members of the enumeration from 1 to 22. The objects in this.data have a maximum value.position of 22 which means you're using the chromosome value to determine position. In reality you want to use a very fine-grained position value (from 1 to 10000000) to determine the actual x position of the dot, but only display the numbers 1 to 22 on the graph front-end. As far as I can tell this is the issue.
It would help if we could see what this.data refers to.
Is there a way to find out the distance between the tick marks on the x axis? I'm using the ordinal scale with rangeRoundBands with tells me it doesn't have a tick function.
var x= d3.scale.ordinal().rangePoints([_margin.left, cWidth]);
x.domain(['Dec','Jan']);
var testTicks = x.ticks(2);
It generates the axis fine (can't post an image) but I can't figure out how to get the distance
(edit: added x.domain)
var data = [45, 31, 23], // whatever your data is
graphHeight = 400,
// however many ticks you want to set
numberTicksY = 4,
// set y scale
// (hardcoded domain in this example to min and max of data vals > you should use d3.max real life)
y = d3.scale.linear().range(graphHeight, 0]).domain(23, 45),
yAxis = d3.svg.axis().scale(y).orient("left").ticks(numberTicksY),
// eg returns -> [20, 30, 40, 50]
tickArr = y.ticks(numberTicksY),
// use last 2 ticks (cld have used first 2 if wanted) with y scale fn to determine positions
tickDistance = y(tickArr[tickArr.length - 1]) - y(tickArr[tickArr.length - 2]);
(2019) We can expand techjacker solution to cover non-linear scales, instead of calculate only the distance between two ticks, you can have an array with all distances between ticks.
// ticksDistance is constant for a specific x_scale
const getTicksDistance = (scale) => {
const ticks = scale.ticks();
const spaces = []
for(let i=0; i < ticks.length - 1; i++){
spaces.push(scale(ticks[i+1]) - scale(ticks[i]))
}
return spaces;
};
//you have to recalculate when x_scale or ticks change
const ticksSpacingPow = getTicksDistance(x_scale_pow);
ticksSpacingPow is array with all distances
The example below, draws an ellipse on half of the distance between the ticks.
// normal
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0,20)")
.call(x_axis_pow)
.selectAll('.tick')
.append('ellipse')
.attr('fill', 'red')
.attr('rx', '2px')
.attr('ry', '10px')
.attr('cx', (d,i)=> ticksSpacingPow[i]/2)
ps. Using the latest D3v5
const x_scale_pow = d3.scalePow().exponent(2)
.domain([0,20000])
.range([0, 960]);
const x_axis_pow = d3.axisBottom()
.scale(x_scale_pow)
.ticks(10)
// ticksDistance is constant for a specific x_scale
const getTicksDistance = (scale) => {
const ticks = scale.ticks();
const spaces = []
for(let i=0; i < ticks.length - 1; i++){
spaces.push(scale(ticks[i+1]) - scale(ticks[i]))
}
return spaces;
};
//you have to recalculate when x_scale or ticks change
const ticksSpacingPow = getTicksDistance(x_scale_pow);
const svg = d3.select("body").append("svg")
.attr("width", "500px")
.attr("height","350px")
.style("width", "100%")
.style("height", "auto");
// normal
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0,20)")
.call(x_axis_pow)
.selectAll('.tick')
.append('ellipse')
.attr('fill', 'red')
.attr('rx', '2px')
.attr('ry', '10px')
.attr('cx', (d,i)=> ticksSpacingPow[i]/2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I am pretty new to d3. For the moment I am able to draw circles based on an array of data - wow - I know :-) But now I would like to just draw two circles at one time while I animate the whole array. Let's say I have 1000 elements in my array and I want to draw 1 and 2 at the same time, then draw 2 and 3, 3 and 4 and so on. This should get a very pretty animation :-) I have played with functions i index and with exit().remove() but this does not work.
This is what I have:
var w = 500;
var h = 300;
var padding = 20;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
[600, 150]
];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create scale functions
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
//Create circles
svg.selectAll("circle")
.data(dataset.slice(0,2))
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r",10);
for (var i=0; i<4;i++) {
svg.selectAll("circle").data(dataset.slice(i,i+2)).transition().duration(2000).delay(2000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 10);
//svg.selectAll("circle").data(dataset.slice(i,i+1)).exit().remove();
console.log(dataset.slice(i,i+2));
}
But I will get only one single animation instead of 4 .. hmm .. what is going wrong?
The delay function accepts callbacks, so there is no need to wrap your selection in a for loop.
.delay( function(d, i) { (2000*i); } )
Looking at the code, you've got a fixed duration (2s) and a fixed delay (2s). The FOR loop will run instantly, thus queueing all four animations up at once, and thus they are probably all playing at the same time - but (probably) only the last will be visible because you've rebound the data.
try something like:
svg.selectAll("circle")
.delay( (2000*i) )
.data(dataset.slice(i,i+2))
.transition()
.duration(2000)
.attr("cx", function(d) {return xScale(d[0]);})
.attr("cy", function(d) {return yScale(d[1]);})
.attr("r", 10);)
Multiplying the delay by the animation counter should offset each animation, and by putting the delay first, it should the data gets rebound just before starting the animation (thereby stopping the final animation step from rebinding it's data before the first animation has run)