Why is d3.histogram merging the last two bins of this chart? - javascript

In this D3 chart I'm working on, I have a band scale containing 60 elements in a non-linear pattern (101-110; 201-210; 301-310; 401-410; 501-510 and 601-610 — the first digit represents the season and the last two digits represent the last two digits).
I'm able to render the chart perfectly fine, but the last bar gets merged into the one before it:
(Ignore the fact the x-axis labels are all messed up.)
Any idea why this is happening? Is it just because I've totally perverted the meaning of a histogram, or what?
Full example code here.

You are correct: you totally perverted the meaning of a histogram! This should be a bar chart instead.
Bar charts, by their very nature, are made of bars representing a categorical variable. It means that the bars are positioned over a label that represents a categorical variable, i.e., a qualitative variable. Both bar charts and histograms use rectangles to encode data, but in a histogram, unlike a bar chart, the label represents a quantitative variable. It's very common, if you search the web, finding "histograms" which are in fact bar charts, or "bar charts" which are in fact histograms.
Back to your question: It's possible to keep your histogram, in a hacky solution: you just have to define the domain:
If domain is specified, sets the domain accessor to the specified function or array and returns this histogram generator.
So, your histogram generator should be something like this:
const histogram = d3.histogram()
.value(d => d.death)
.domain([0, 1000])//in your case: [101 or less, more than 610]
.thresholds(x.domain());
(I'm using magic numbers here just to show the idea, change them accordingly)
And here is the updated code: https://www.webpackbin.com/bins/-KgCh8IjK6J56Dj_SA9X
Analysis
Apparently, d3.histogram is not creating the last bin when the value coincides with the threshold. According to the docs the last bin should be created, since x1 (the top value for each bin) is not inclusive.
Let's see this snippet:
var data = d3.range(10);
const histogram = d3.histogram()
.value(d => d)
.thresholds(data);
var bins = histogram(data);
console.log(bins)
<script src="https://d3js.org/d3.v4.js"></script>
You can see that 8 and 9 were placed together in the last bin.
The same thing doesn't happen with the other thresholds. First, thresholdFreedmanDiaconis:
var data = d3.range(100);
const histogram = d3.histogram()
.value(d => d)
.thresholds(d3.thresholdFreedmanDiaconis(data, d3.min(data), d3.max(data)));
var bins = histogram(data);
console.log(bins)
<script src="https://d3js.org/d3.v4.js"></script>
Then, thresholdScott:
var data = d3.range(100);
const histogram = d3.histogram()
.value(d => d)
.thresholds(d3.thresholdScott(data, d3.min(data), d3.max(data)));
var bins = histogram(data);
console.log(bins)
<script src="https://d3js.org/d3.v4.js"></script>
Finally, thresholdSturges:
var data = d3.range(100);
const histogram = d3.histogram()
.value(d => d)
.thresholds(d3.thresholdSturges(data, d3.min(data), d3.max(data)));
var bins = histogram(data);
console.log(bins)
<script src="https://d3js.org/d3.v4.js"></script>

Related

Plotting heatmap with available attributes instead of range D3 React

I'm working on a heatmap which basically plots the graph between taxIDs and KeywordNames from an external JSON. While I'm able to plot the values I see many blank spaces on the graph and clueless how I can plot them with the available data.
Here's the link to codesandbox: https://codesandbox.io/s/40mnzk9xv4
On the X-Axis I'm plotting the TaxIDs which are being calculated within the given range. I did try using the function rangeBands() but I get an error everytime.
Its the similar case with Y-Axis where I'am plotting the keywordIDs which are also being calculated within a range. I'm trying to print all the KeywordNames on Y axis and all taxIDs on the X-Axis and plot their corresponding spectracount on graph.
Please help me where have I gone wrong.
The output I'm looking for is something similar to this: https://bl.ocks.org/Bl3f/cdb5ad854b376765fa99
Thank you.
Some things to help you get you one your way:
First, your scales should use scaleBand(), not scaleLinear(), as they have discrete domains (i.e. categories of something, rather than continuous)
Second, your scale domains is taking every value of taxId and keywordName in your data as a possible value. But some values are repeated more than once. You need to be filtering them so you only have unique values. So your scale code should be:
const xValues = d3.set(data.map(d => d.taxId)).values();
const yValues = d3.set(data.map(d => d.keywordName)).values();
const xScale = d3.scaleBand()
.range([0, width])
.domain(xValues); //X-Axis
const yScale = d3.scaleBand()
.range([0, height])
.domain(yValues); //Y-Axis
Finally, your code that places the heatmap tiles needs to be calling the scale functions so it works out the position of each rect correctly:
chart.selectAll('g')
.data(data).enter().append('g')
.append('rect')
.attr('x', d => { return xScale(d.taxId) })
.attr('y', d => { return yScale(d.keywordName) })
That should get you most of the way there. You'll want to also reduce cellSize and remove the tickFormat calls on the axes as they are trying to convert your text labels to numbers.

How to change sizing of D3 axis?

Working within a React framework, I have a JSON data set of weights and I am trying to display on a simple number line using D3.js, where the left end is the min, the right end is the max, and a particular individuals weight is a tick with the color red.
I've coded up a solution that creates this, however I cannot for the life of me figure out how to make it larger. Here's the code, where this.state.range = [126.4, 212.2] and this.state.avg = 167:
var svgContainer = d3.select("body").append("svg").attr("width",500).attr("height", 200);
var axisScale = d3.scaleLinear().domain(this.state.range).range(this.state.range);
var axisBot = d3.axisBottom(axisScale);
var ticks = [this.state.range[0], this.state.range[1], this.state.avg, this.state.patient.weight];
axisBot.tickValues(ticks);
var xAxisGroup = svgContainer.append("g").call(axisBot);
d3.selectAll('g.tick').filter(function(d){ return d==186.4;} ).select('line').style('stroke','red');
This code produces this result:
Can anyone please tell me how to a) make this number line larger and b) how to center the line?
I apologize if this is a bit remedial, however it is my first time working with D3.js.
In your code, the domain and the range are the same (assuming that this.state.range is an array):
var axisScale = d3.scaleLinear()
.domain(this.state.range)
.range(this.state.range);
You have to set the range according to the positions you want in your SVG. For instance, without any padding, if your SVG has a width of 500px:
var axisScale = d3.scaleLinear()
.domain(this.state.range)
.range([0, 500]);
So, the range goes from left border (x = 0) to the right border (x = 500).
Remember: domain refers to the input values, while range refers to the output positions.

Change the Y axis range based on the selected data

I have a chart done in the D3. It works but I would like to add the ability of the axis scaling.
I have several sets of data that have a completely different range.
So I would like the Y axis range to be altered according to the selected series.
This is the entire code: https://plnkr.co/edit/89ERO3GuxREvYQ3DRX5L?p=preview
For the moment the example linked has 4 series: I would like that if for example the user selects "Kiwi" and "Apple" then the Y axis has range [0-1] and not [0-15] (or something like that).
The important thing is that the range is changed according to the selected data.
An example of what I mean is http://bl.ocks.org/andrewleith/3824854.
To do so I thought to save in an array the tags that have opacity 0.5 or 1 (the visible lines) and then according to that change the domain Y.
I created the array, and then I tried to identify the tag with opacity 0.5 (for simplicity) in this way:
var actives = new Array();
var activeElem = d3.select(".line").attr("opacity", 0.5);
console.log(activeElem);
or
var actives = new Array();
var activeElem = d3.selection.style({opacity: "0.5"});
console.log(activeElem);
or
var actives = new Array();
var activeElem = d3.selectAll("path[opacity='0.5']");
console.log(activeElem);
None of these works.
I also did other tests but I don't remember exactly what, and still did not work.
I am going crazy.
What is the right way to select items with opacity 0.5 and 1 and change the Y-axis based on the selected data?
Thank you
Hi the main issue here is that you need to make another calculation of the min and max values of your data and afterwards update your yAxis with the new domain with the min and max values.
// find the new max value of the selected series
var max = d3.max(filteredData, function(d) {
return d3.max(d.values, function(e) {
return e.value;
});
});
// find the new min value of the selected series
var min = d3.min(filteredData, function(d) {
return d3.min(d.values, function(e) {
return e.value;
});
});
// set the new y domain
y.domain([min, max]);
// update our new y axis with the new domain
svg.selectAll("g .y.axis").transition()
.duration(800).call(yAxis);
Since you want to filter your data by selecting certain lines I recommend using the data function which will allow you to join the specified array of data with the current selection (more info here). You are making a huge forEach loop when you could be using the d3 utilities. Something in the lines of:
// apend a group element which will contain our lines
svg.append('g')
.attr('class', 'line-container')
.selectAll('.normal-line-paths')
.data(dataNest) // set our data
.enter() // enter the data and start appending elements
.append('path')
.call(path); // calling our path function for later use when appending lines
I modified your code to use a more document driven approach, you can click on the labels and the y axis will be scaled accordingly t the new min and max values of the filtered dataset. I left the dashed group in blank so you can try and code the remaining parts and understand the logic behind the data join.
https://plnkr.co/edit/Wso0Gu4yHkdsHhl8NY8I?p=preview
EDIT:
Here is the plnkr with the dashed lines :)
https://plnkr.co/edit/yHGGtrLXZyyq0KpiVWYf?p=preview

dc.js bar chart with ordinal x axis scale doesn't render correctly

I've recently discovered dc.js and have been trying to implement a simple bar chart using the bar chart example provided on d3.js's website: http://bl.ocks.org/mbostock/3885304.
However, as part of dc.js implementation, a crossfilter dimension and group is required.
So using a simple TSV file with "letters" and "frequencies", I've changed the code a bit to look as follows:
d3.tsv('testdata.tsv', function(error, data) {
var cf = crossfilter(data);
var dimension = cf.dimension(function(d){
return d.letter;
}
var group = dimension.group().reduce(
function(p,v){
p.frequency += v.frequency
},
function(p,v){
p.frequency -= v.frequency
},
function(){
return { frequency: 0 };
});
I'm a little confused about what I should be setting the valueAccessor to (on my bar chart), because when I set the valueAccessor to return "frequency", and I set my xAxis scale to ordinal with a domain of all "letters" in the data set, I get a rendered bar graph with almost all x-axis values ("A - Y") at the ZERO point and one x-axis value (i.e. "Z") at the end of the x-axis line.
I've tried setting the ranges, but it doesn't seem to do the trick. Any ideas on what I'm misunderstanding would be greatly appreciated!
Turns out I had to set the .xUnits property to dc.units.ordinal() in order to get the x-axis spaced out correctly!

Grouped Bar Chart with different y-axis/scales

I'm trying to create a grouped bar chart with 70 samples and 2 series. Similar to this example:
http://bl.ocks.org/882152
However one series is [0 ... 1] and the other series is [0 ... 1.000.000]. I can't recreate the example with my numbers.
I also don't really get the example. Shouldn't be the variables switched, i.e. x -> y, y0 -> x0 and y1 -> x0? Or don't they stand for the x and y axis?
Thank's!
Edit:
Here is an example that demonstrates my problem (look in the console).
http://jsfiddle.net/kQSGF/3/
The problem seems to come from the scale definition:
var x = d3.scale.linear().domain([0, 1]).range([h, 0]);
The domain is set to [0,1] but only your first data series actually falls in that range.
you could consider setting the domain to the extent of your data, and reversing the output range so that it shows the values in your data instead of the 'non' value amount as a bar:
var x = d3.scale.linear().domain(d3.extent(d3.merge(data))).range([0,h]);
Note that you will still be unlikely to see your smaller data series, as the ranges of your data are so significantly different

Categories