Horizontal bar chart index is off - javascript

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.

Related

D3: issue with legend scaling

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>

d3.js referencing a single array column

I have an array of generated values to plot a line function. However, when I call the array, the function only returns single values of the array, rather than the column, and hence draws a straight line (http://tributary.io/inlet/8822590). What is the correct syntax here? Thanks in advance.
// Create data
var v1 = 4.137,
t = 10,
x = [],
y = [];
for (i = 0.1; i < 190; i += 0.1) {
x.push(i);
y.push((Math.pow((v1 / i), 1 / t) - 1) * 100);
}
var data = [x, y];
// Scale data
var xscale = d3.scale.linear()
.domain(d3.extent(data, function (d) {
return d[0];
}))
var yscale = d3.scale.linear()
.domain(d3.extent(data, function (d) {
return d[1];
}))
var line = d3.svg.line()
.x(function (d) {
return xscale(d[0])
})
.y(function (d) {
return yscale(d[1])
});
var svg = d3.select("svg");
var path = svg.append("path")
.data([data])
.attr("d", line) //this calls the line function with this element's data
.style("fill", "none")
.style("stroke", "#000000")
.attr("transform", "translate(" + [96, 94] + ")")
D3 expects the data for a line to be an array of array where each element determines one line and each element within the inner array the point of the line. You've passed in a single array (for one line) with two elements, so you get two points.
To plot all the points, push the coordinates as separate elements:
for (i = 0.1; i < 190; i += 0.1) {
data.push([i, (Math.pow((v1/i),1/t)-1)*102]);
}
The rest of your code can remain basically unchanged. Complete example here.

d3.js moving average with previous and next data values

I'm new to D3 and trying to do a moving average of previous and next values on my data in order to smooth it out.
Currently, I have it working using the 2 previous values + the current value. It does work but 1) how would I also use the next values, and 2) what if I wanted to use the 15 previous and 15 next values? (it would be crazy to have 30 individual vars for storing all of them)
I'm used to traditional javascript but lost as to how to traverse the data this way in D3.
Hope someone can enlighten me, thanks.
See the whole code on bl.ocks.org:
http://bl.ocks.org/jcnesci/7439277
Or just the data parsing code here:
d3.json("by_date_mod.json", function(error, data) {
// Setup each row of data by formatting the Date for X, and by converting to a number for Y.
data = data.rows;
data.forEach(function(d) {
d.key = parseDate(String(d.key));
d.value = +d.value;
});
x.domain(d3.extent(data, function(d) { return d.key; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
// Setup the moving average calculation.
// Currently is a hacky way of doing it by manually storing and using the previous 3 values for averaging.
// Looking for another way to address previous values so we can make the averaging window much larger (like 15 previous values).
var prevPrevVal = 0;
var prevVal = 0;
var curVal = 0
var movingAverageLine = d3.svg.line()
.x(function(d,i) { return x(d.key); })
.y(function(d,i) {
if (i == 0) {
prevPrevVal = y(d.value);
prevVal = y(d.value);
curVal = y(d.value);
} else if (i == 1) {
prevPrevVal = prevVal;
prevVal = curVal;
curVal = (prevVal + y(d.value)) / 2.0;
} else {
prevPrevVal = prevVal;
prevVal = curVal;
curVal = (prevPrevVal + prevVal + y(d.value)) / 3.0;
}
return curVal;
})
.interpolate("basis");
// Draw the moving average version of the data, as a line.
graph1.append("path")
.attr("class", "average")
.attr("d", movingAverageLine(data));
// Draw the raw data as an area.
graph1.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
// Draw the X-axis of the graph.
graph1.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Draw the Y-axis of the graph.
graph1.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value");
});
You need a function to calculate the moving average:
var movingWindowAvg = function (arr, step) { // Window size = 2 * step + 1
return arr.map(function (_, idx) {
var wnd = arr.slice(idx - step, idx + step + 1);
var result = d3.sum(wnd) / wnd.length;
// Check for isNaN, the javascript way
result = (result == result) ? result : _;
return result;
});
};
var avgData = movingWindowAvg(avg, 7); // 15 step moving window.
Note that this functions fudges the values a bit at the borders of the original array, when a complete window cannot be extracted.
Update: If the result is NaN, convert the result to the present number in the beginning. Checking result == result is the recommended way of testing for NaNs in Javascript.
If you really don't need a variable size window, this cumulative average might be a faster option without the slicing overhead:
function cumAvg(objects, accessor) {
return objects.reduce(
function(avgs, currObj, i) {
if (i == 1) {
return [ accessor(currObj) ];
} else {
var lastAvg = avgs[i - 2]; // reduce idxs are 1-based, arrays are 0
avgs.push( lastAvg + ( (accessor(currObj) - lastAvg) / i) );
return avgs;
}
}
}

Is there a way to create scales with domains that have a sub-domain?

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 it possible to find the distance between ticks in D3.js?

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>

Categories

Resources