I had a D3js code which produces bar graphs and works fine with version 3.x. I wanted to upgrade the code to version 5 in the interest of being updated. Upon doing so I was able to correct a number of syntax updates such as scaleLinear, scaleBand, etc. The data is imported via tsv. The code is able to show the graph on the page with the correct x axis widths for the bars. However, the yAxis bars go out of bounds and the scale on the y-axis is very short. For example, the data shows the maximum value of the data to be 30000, but the yaxis is only from 0-90. Upon further investigation the d.RFU values from which the y data is generated seems to be not converted from string to integers. In the v3 code, I had a function at the end which converted the type of d.RFU to integer using the unary operator
d.RFU = +d.RFU
However, it seems to be not working in v5. Could this be due to the promises implementation in replacement of the asynchronous code?
Any solutions on how to fix this in version 5?
Please let me know if you need any more information and forgive me if I have missed out anything as I am new to programming and this website. Any help is appreciated.
Here is parts of the code which I have right now:
//set dimensions of SVG and margins
var margin = { top: 30, right: 100, bottom: 50, left: 100, },
width = divWidth - margin.left - margin.right,
height = 250 - margin.top - margin.bottom,
x = d3.scaleBand()
.range([0, width - 20], 0.1),
y = d3.scaleLinear()
.range([height,0]);
//setup the axis
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var svg = d3.select("#bargraphID")
.append("svg")
.attr("width", width + margin.left + margin.right - 100)
.attr("height", height + margin.top + margin.bottom - 10)
.append("g")
.attr("transform", "translate (" + margin.left + "," + margin.top + ")");
d3.tsv(filename).then(function(data) {
// get x values from the document id
x.domain(data.map(function(d) {
return d.ID;
}));
yMax = d3.max(data, function(d) {
return d.RFU;
});
// get the y values from the document RFU tab
y.domain([0, yMax]);
//create the x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate (0, " + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("dx", "0em")
.attr("dy", "-0.55em")
.attr("y", 30)
.attr("class", "x-axisticks");
//create the y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
//add the data as bars
var bar = svg.selectAll("bar")
.data(data)
.enter()
.append("rect")
.style("fill", barColor)
.attr("fill-opacity", "0.3")
.attr("x", function(d) {
return x(d.ID);
})
.attr("width", x.bandwidth())
//set initial coords for bars for animation.
.attr("y", height)
.attr("height", 0)
//animate bars to final heights
.transition()
.duration(700)
.attr("y", function(d) {
return y(d.RFU);
})
.attr("height", function(d) {
return height - y(d.RFU);
})
.attr("fill-opacity", "1.0")
.attr("class", "y-data");
});
//convert RFU to integers
function type(d) {
d.RFU = +d.RFU;
return d;
}
Just like with the old v3 and v4 versions, you have to pass the row conversion function to d3.tsv in D3 v5:
d3.tsv(filename, type)
Then, use the promise with then. Have in mind that d3.tsv always return strings (be it D3 v3, v4 or v5), because:
If a row conversion function is not specified, field values are strings.
Here is the demo with fake data:
var tsv = URL.createObjectURL(new Blob([
`name RFU
foo 12
bar 42
baz 17`
]));
d3.tsv(tsv, type).then(function(data) {
console.log(data)
})
function type(d) {
d.RFU = +d.RFU;
return d;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: Since SO snippet may have a problem loading that blob in some browsers, here is the same code in JSFiddle, check the console: https://jsfiddle.net/kv0ea0x2/
Related
Dear stack overflow community,
I've been trying to create a bar chart using d3v7 for one of my Uni projects, following the _Blocks_ example. However, I am getting an error as mentioned in this question title. Apparently, my code has a problem reading my data but I can't figure out how to fix this. It would be really helpful if you could help me here :)
Here is my code:
var margin = {top: 5, right:10, bottom:5, left:10},
width = 575 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleBand()
.range([0, width])
.padding(0.1);
var y = d3.scaleLinear()
.range([height, 0]);
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("#bar").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// get the data
d3.csv("canton_pop.csv").then(function(data) {
// format the data
data.forEach(function(d) {
d.Population = +d.Population;
});
// Scale the range of the data in the domains
x.domain(data.map(function(d) { return d.Canton; }));
y.domain([0, d3.max(data, function(d) { return d.Population; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar_chart")
.data(data)
.enter().append("rect")
.attr("class", "bar_chart")
.attr("x", function(d) { return x(d.Canton); })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.Population); })
.attr("height", function(d) { return height - y(d.Population); });
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g")
.call(d3.axisLeft(y));
});
Here is what my CSV file looks like:
Canton;Population
Zürich;1553423
Bern;1043132
Luzern;416347
Uri;36819
Schwyz;162157
Obwalden;38108
Nidwalden;43520
Glarus;40851
Zug;128794
Fribourg;325496
Solothurn;277462
Basel-Stadt;196735
Basel-Landschaft;290969
Schaffhausen;83107
Appenzell Ausserrhoden;55309
Appenzell Innerrhoden;16293
St. Gallen;514504
Graubünden;200096
Aargau;694072
Thurgau;282909
Ticino;350986
Vaud;814762
Valais;348503
Neuchâtel;175894
Genève;506343
Jura;73709
When running my code I am able to visualize the graphics axis but no bar appears and the axis doesn't have any graduation (below is an image of the result I get and of the error that appears in my console). Hopefully, someone can help me here.
Blank bar chart:
The error message that appears in my console:
Okay so I just realized the problem was coming from my CSV file, indeed I was using ";" as separation when I needed to use ","
I am building a dot plot histogram with d3.js v3 and I have pretty much finished everything up - except for whatever reason some of my data points are duplicating (certain circles repeating themselves - not all of them, just some). I tried tweaking the axis parameters, as well as the data itself [deleted rows with null values, etc]- however sadly to no avail.
Any help would be immensely appreciated.
Here's my relevant code:
<div id="dotHappy"></div>
var data = d3.csv('happy_dot_modified.csv', function(data) {
data.forEach(function(d) {
d["city"] = d["city"];
d["Happy"] = +d["Happy"];
d["thc"] = +d["thc"];
});
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
},
width = 1560 - margin.left - margin.right,
height = 1260 - margin.top - margin.bottom;
I tried this coder block but it wasn't working. (Not sure if this is even what's giving me the issue anyways - perhaps not).
// var x = d3.scale.linear()
// .range([0, width]);
So I went with this:
var x = d3.scale.ordinal()
.rangePoints([0, width])
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("#dotHappy")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var chart = svg.append("g")
.attr("id", "chart");
Also tried tweaking this, which may or may not even be part of the problem.
x.domain(data.map(d => d.Happy));
y.domain([5, 33]);
// y.domain(data.map(d => d.city));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
// .append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Happy");
svg.append("g")
.attr("class", "y axis")
// .attr("transform", "translate(0," + width + ")")
.call(yAxis)
// .append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("THC");
var groups = svg.selectAll(".groups")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + x(d.Happy) + ".0)";
});
var dots = groups.selectAll("circle")
.data(function(d) {
return d3.range(1, +d.thc + 1)
// return d3.range(d.thc)
})
.enter().append("circle")
.transition().duration(1000)
.attr("class", "dot")
.attr("r", 10)
.attr("cy", function(d) {
return y(d)
})
.style("fill", "blue")
.style("opacity", 1);
})
Here is a snapshot of my csv file:
city. |. Happy. | thc
Boston. 37. 23
NYC. 22. 30
Chicago. 88. 5
Following is a screenshot of what it currently looks like. So in this case, the tooltip displaying the text box 'The Sister' should be only for one circle (because it should only be one data point), however if you hover over the other 10 orange circles below it, it's all the same - indicating it has been repeated 11 times total:
Actually, all of the circles are repeating vertically. You may not see them all because the repeated circles are being overlapped by other colored circles as these other circles get drawn. For example, the yellow data point "The Sister" is repeating all the way down to the bottom, but the data points below the yellow ones, in blue, pink, green, blue, etc., drew themselves on top of the yellow repeats.
The culprit is this code:
.selectAll("circle")
.data(function(d) {
return d3.range(1, +d.thc + 1)
// return d3.range(d.thc)
})
.enter().append("circle")
which, if you don't want it to repeat, should have been just one line:
.append("circle")
To explain what happened, this code:
var groups = svg.selectAll(".groups")
.data(data)
.enter()
.append("g")
.attr("class", "groups") //NOTE: you should add this line since you have 'selectAll(".groups")'
.attr("transform", function(d) {
return "translate(" + x(d.Happy) + ".0)";
});
already creates a g element for every row in the csv file. And for every g, you created an array using d3.range(1, +d.thc + 1), and appended a circle for each item in that array.
As an example, let's take the row representing "The Sister" data point that has a THC of 33. For that one data point, the code creates one <g>, inside of which it binds the array [1, 2, 3, ..., 33], and therefore appends 33 circles to the <g> element, with the cy attribute between y(1) and y(33).
Now, the question that follows is that, you specified a domain with a minimum of 5 with y.domain([5, 33]). Yet the data-bounded array, generated with d3.range, always begins with 1 and increments up to the value of THC. So some of the values in the array (1,2,3, and 4) always fall outside the y-axis, but d3 was able to translate it to a proper y-position. Is that possible? By default, yes, d3.scale extrapolates when the data is outside of the domain.
By default, clamping is disabled, such that if a value outside the input domain is passed to the scale, the scale may return a value outside the output range through linear extrapolation. For example, with the default domain and range of [0,1], an input value of 2 will return an output value of 2.
I've been looking around a while for an answer to this, and I haven't been able to figure it out.
I'm ultimately creating a TopoJSON file from grid based data (GRIB files).
I can pretty easily interpolate the data down to a finer resolution grid so the plot points appear smoother when zoomed out, but when zoomed in, it's inevitable to see the blocky grid points.
I've also looked into simplification, which does help a bit but its not quite smoothing.
I'm using D3 to render the data.
Is this something that can be done on the front end or should/can it be done in the raw TopoJSON data?
I essentially don't want you to be able to tell that it's a grid, even if you zoom in 10,000%.
Here's an example of what I'm after:
Is this something that can be done on the front end or should/can it be done in the raw TopoJSON data?
This is something that should be done on the front end. If you were to smooth the data before you wrote it to the JSON file, the file would be needlessly big.
If you're using D3.js, and you're working with lines, the built-in interpolate() function is the way to go.
Here is a working example of D3's line.interpolate() using "cardinal" smoothing:
http://codepen.io/gracefulcode/pen/doPmOK
Here's the code:
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y).orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.interpolate("cardinal")
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.close);
});
// Adds the svg canvas
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.json('https://api.myjson.com/bins/175jl', function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
// Starting with a basic graph 14
x.domain(d3.extent(data, function(d) {
return d.date;
}));
y.domain([0, d3.max(data, function(d) {
return d.close;
})]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
Maybe d3 line.interpolate() is what you're looking for?
More info:
http://www.d3noob.org/2013/01/smoothing-out-lines-in-d3js.html
I have an otherwise fine working grouped bar chart script to which I'm trying to add simple reference lines. The relevant code:
//Set up margins and dimensions according to http://bl.ocks.org/3019563
var margin = {top: 20, right: 10, bottom: 20, left: 30},
width = 810 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
/* Set up the primary x scale */
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function (d) {
return options.xPrimaryScaleAccessor(d);
}));
/* Set up the secondary x scale */
var x1 = d3.scale.ordinal()
.domain(xSecondaryScaleValues)
.rangeRoundBands([0, x0.rangeBand()]);
/* Set up the y scale as a linear (continous) scale with a total range of 0 - full height and a domain of 0-100 */
var y = d3.scale.linear()
.range([height, 0])
.domain([0, 100]);
/* Set up a color space of 20 colors */
var color = d3.scale.category20();
/* Set up the x axis using the primary x scale and align it to the bottom */
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
/* Set up the y axis using the y scale and align it to the left */
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
/* Create an SVG element and append it to the body, set its dimensions, append a <g> element to
* it and apply a transform translating all coordinates according to the margins set up. */
var svg = d3.select(options.target).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Create a space for definitions
var defs = svg.append("defs");
setupDropShadowFilter(defs, 3, 3, 3); //Sets up a gaussian blur filter with id 'drop-shadow'
/* Append a <g> element to the chart and turn it into a representation of the x axis */
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
/* Append a <g> element to the chart and turn it into a representation of the y axis */
svg.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(options.yLabel);
var dataArr = y.ticks(yAxis.ticks());
/* Draw the reference lines */
svg.selectAll("line")
.data(dataArr)
.enter().append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", y)
.attr("y2", y)
.style("stroke", "#ccc");
/* Set up the bar groups */
var group = svg.selectAll(".group")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(options.xPrimaryScaleAccessor(d)) + ",0)"; });
/* Draw the bars */
group.selectAll("rect")
.data(options.valueAccessor)
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.label); })
.attr("y", function(d) { return y(d.value); })
.attr('rx', options.barCornerRadius)
.attr('ry', options.barCornerRadius)
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return getStripedPattern(defs, color(d.label)); //Sets up a pattern and returns its ID })//Todo: fill with pattern instead. see http://tributary.io/tributary/2929255
.style("filter", "url(#drop-shadow)");
/* Draw a legend */
var legend = svg.selectAll(".legend")
.data(xSecondaryScaleValues)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + (xSecondaryScaleValues.length-i-.25) * (height/xSecondaryScaleValues.length) + ")"; });
legend.append("rect")
.attr("x", width - 9)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("y", 9)
//.attr("dy", ".35em")
.attr("transform", "translate(" + (width - 6) + ",-8)rotate(-90)" )
.style("text-anchor", "start")
.text(function(d) { return d; });
EDIT: I have also tried to append rect elements instead with hardcoded coordinates and dimensions, but those also didn't make it to the DOM.
EDIT 2: More or less full code now included.
Basically, nothing happens. No lines are appended and there are no errors in the console. The dataArr is a plain array of numbers and y(number) is confirmed to return good values in the output range.
I think (and debug suggests) that the chain dies at the append() stage, possibly because .enter() return something useless.
Console log after .data():
Console log after .enter():
Console log after .append():
I've been stuck on this for a good while now, so grateful for any ideas about what may go wrong. I'm sure I'm overlooking something obvious...
The problem is that the code that generates the axes appends line elements to the SVG. As it is run before appending the reference lines, calling svg.selectAll("line").data(...) matches the existing lines with the data. There are more lines than data elements, so no new elements need to be added and the .enter() selection is empty.
There are a few ways to fix this. You could move the code that generates the reference lines further up. You add a g element that contains these lines. You could have a special class for these lines and adjust the selector accordingly. Or you could provide a custom matching function to .data().
I have a vertical bar chart that is grouped in pairs. I was trying to play around with how to flip it horizontally. In my case, the keywords would appear on the y axis, and the scale would appear on the x-axis.
I tried switching various x/y variables, but that of course just produced funky results. Which areas of my code do I need to focus on in order to switch it from vertical bars to horizontal ones?
My JSFiddle: Full Code
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w], 0.05);
// ternary operator to determine if global or local has a larger scale
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function (d) {
return (d.local > d.global) ? d.local : d.global;
})])
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(xScale)
.tickFormat(function (d) {
return dataset[d].keyword;
})
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
var commaFormat = d3.format(',');
//SVG element
var svg = d3.select("#searchVolume")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Graph Bars
var sets = svg.selectAll(".set")
.data(dataset)
.enter()
.append("g")
.attr("class", "set")
.attr("transform", function (d, i) {
return "translate(" + xScale(i) + ",0)";
});
sets.append("rect")
.attr("class", "local")
.attr("width", xScale.rangeBand() / 2)
.attr("y", function (d) {
return yScale(d.local);
})
.attr("x", xScale.rangeBand() / 2)
.attr("height", function (d) {
return h - yScale(d.local);
})
.attr("fill", colors[0][1])
;
sets.append("rect")
.attr("class", "global")
.attr("width", xScale.rangeBand() / 2)
.attr("y", function (d) {
return yScale(d.global);
})
.attr("height", function (d) {
return h - yScale(d.global);
})
.attr("fill", colors[1][1])
;
sets.append("rect")
.attr("class", "global")
.attr("width", xScale.rangeBand() / 2)
.attr("y", function (d) {
return yScale(d.global);
})
.attr("height", function (d) {
return h - yScale(d.global);
})
.attr("fill", colors[1][1])
;
I just did the same thing last night, and I basically ended up rewriting the code as it was quicker than fixing all the bugs but here's the tips I can give you.
The biggest issues with flipping the x and y axis will be with things like return h - yScale(d.global) because height is calculated from the "top" of the page not the bottom.
Another key thing to remember is that when you set .attr("x", ..) make sure you set it to 0 (plus any padding for the left side) so = .attr("x", 0)"
I used this tutorial to help me think about my own code in terms of horizontal bars instead - it really helped
http://hdnrnzk.me/2012/07/04/creating-a-bar-graph-using-d3js/
here's my own code making it horizontal if it helps:
var w = 600;
var h = 600;
var padding = 30;
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d){
return d.values[0]; })]) //note I'm using an array here to grab the value hence the [0]
.range([padding, w - (padding*2)]);
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([padding, h- padding], 0.05);
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", 0 + padding)
.attr("y", function(d, i){
return yScale(i);
})
.attr("width", function(d) {
return xScale(d.values[0]);
})
.attr("height", yScale.rangeBand())
An alternative is to rotate the chart (see this). This is a bit hacky as then you need to maintain the swapped axes in your head (the height is actually the width etc), but it is arguably simpler if you already have a working vertical chart.
An example of rotating the chart is below. You might need to rotate the text as well to make it nice.
_chart.select('g').attr("transform","rotate(90 200 200)");
Here is the procedure I use in this case:
1) Inverse all Xs and Ys
2) Remember that the 0 for y is on top, thus you will have to inverse lots of values as previous values for y will be inversed (you don't want your x axis to go from left to right) and the new y axis will be inversed too.
3) Make sure the bars display correctly
4) Adapt legends if there are problems
This question may help in the sense that it shows how to go from horizontal bar charts to vertical: d3.js histogram with positive and negative values