d3js: Unable to place horizontally bars within the axis - d3.js

I'm learning to make charts in d3 from scratch without taking someone else code and modifying it. I can successfully create a x & y axis vertical bar chart. But when it comes to transform the same chart to horizontal bar chart I end up in a mess. Here is my code so far:
var data = [{
name: "China",
value: 1330141295
}, {
name: "India",
value: 1173108018
}, {
name: "Indonesia",
value: 242968342
}, {
name: "Russia",
value: 139390205
}];
//set margins
var margin = {
top: 30,
right: 30,
bottom: 30,
left: 40
};
var width = 960 - margin.left - margin.right;
var height = 600 - margin.top - margin.bottom;
//set scales & ranges
var yScale = d3.scaleBand().range([0, height]).padding(0.1)
var xScale = d3.scaleLinear().range([0, width])
//draw the svg
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom).append("g")
.attr("transform", "translate(" + margin.left * 2 + "," + margin.top + ")")
//load the data
data.forEach(function(d) {
d.population = +d.population;
});
//set domains
xScale.domain([0, d3.max(data, function(d) {
return d.population
})])
yScale.domain(data.map(d => d.name))
//add x & y Axis
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale))
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", d => yScale(d.name))
.attr("height", d => yScale(d.name))
.attr("x", d => width - xScale(d.population))
.attr("width", yScale.bandwidth())
Thank you very much.

You need to change a lot of things in your code.
TL;DR
change value to population in the array
scales are used to convert values to proportional pixel values
height is the vertical size of an element. you should use yScale(d.name)
width is the horizontal size of an element. you should use xScale(d.population)
y is the vertical position of an element. you should use yScale.bandwidth()
x is the vertical position of an element. you should use 0
use selectAll("rect") on a new appended g or the svg element not the same g element that has the axises on it
add fill attribute so that your rects have color
You have the population field labelled value but you're calling population through out the code to use it. So replace value with population in your data objects.
Next you need to change the way you're setting up the rects. use selectAll on the svg element directly or append another g to the svg element and add the rects on that element. Right now your code attempts to add the rects to the same g element as the x axis.
Make sure you are setting the attributes of the rects correctly. Height is the size in pixels of the rect element not the position. y is the position of the rects vertically from the top downwards. this means the height attribute should use yScale(d.name) and width should use xScale(d.population) because they are the width and length of the rectangles, or rect elements. x and y are the coordinate positions of the element from the top left corner of the svg element. Use them to determine the starting position of the top left pixel of your rects. You should have y as yScale.bandwidth() and x as 0.

Related

Scaling D3 chart inside svg while keeping margins the same width?

Working on rendering charts using D3 that has to scale to different resolutions.
I would like to keep everything except the chart path itself at static size which includes all text, line thickness and margins.
I think I have figured out how to handle everything except the margins and cant find anyone who has the same issue so hopefully someone can help.
I've written a short test script that hopefully explains how I have my margins and everything else setup. The red are margins and the grey rectangle is where I draw my chart. Added a text element to show how I scale texts, line thickness etc.
What I want to achieve is that the margin have a static width as I scale the window. If I scale the svg and make it 100px wider I want the red rectangle to become 100px wider while the red margins stay the same.
I had been thinking about increasing the middle rectangle using the scale variable to have it increase "faster" but trying to use something like transform(scale on the rectangle in the resized() function produced unexpected results.
var margin = {top: 100, right: 150, bottom: 100, left: 150}
var outerWidth = 1600,
outerHeight = 900;
var width = outerWidth - margin.right - margin.left,
height = outerHeight - margin.top - margin.bottom;
svg = d3.select(".plot-div").append("svg")
.attr("class", "plot-svg")
.style("background-color", "red")
.attr("width", "100%")
.attr("viewBox", "0 0 " + outerWidth + " " + outerHeight);
g = svg.append("g")
.attr("class", "plot-space")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "grey");
text = d3.select(".plot-space").append("text")
.text("Text with the background")
.attr("y", 50)
.attr("x", 50)
.attr("font-size", 16)
.attr("font-family", "monospace")
.attr("fill", "white");
window.addEventListener("resize", resized);
function resized(){
var current_width = svg.node().getBoundingClientRect().width;
var scale = outerWidth / current_width;
text.attr("transform", "scale(" + scale + " " + scale + ")");
}
resized();
jsfiddle for you to play around with.
https://jsfiddle.net/2tuompye/4/

What Transformation values to calculate for scale and translate if you want to zoom

Im looking at this example which shows how one can use the zoom functionality to zoom in a specified domain range
https://bl.ocks.org/mbostock/431a331294d2b5ddd33f947cf4c81319
Im confused about this part:
var d0 = new Date(2003, 0, 1),
d1 = new Date(2004, 0, 1);
// Gratuitous intro zoom!
svg.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.scale(width / (x(d1) - x(d0))) // I think this is to caulcuate k which is the zoom factor
.translate(-x(d0), 0)); // but what is this?
I'm having trouble understanding the calculations that are done. Correct me if my assumptions are wrong
d3.zoomIdentity This is a transformation that does nothing when applied.
.scale(width / (x(d1) - x(d0))) This is to calculate how much scale to apply by calculating the ratio between the width and the pixel difference between the two data points d0 and d1
.translate(-x(d0), 0)) I don't understand this part. Why is x(d0) negated and how does the x coordinate of d(0) relate to how much translation need to be applied?
The translate value is aligning the graph so that x(d0) is the leftmost x value visible in the plot area. This ensures the visible portion of the plot area extends from d0 through d1 (the visible subdomain). If our full domain for the x scale has a minimum of 0, then x(0) will be shifted left (negative shift) x(d0) pixels.
I'll use a snippet to demonstrate:
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 70, left: 200},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
// Scale for Data:
var x = d3.scaleLinear()
.range([0, width])
.domain([0,20]);
// Scale for Zoom:
var xZoom = d3.scaleLinear()
.range([0,width])
.domain([0,width]);
var xAxis = d3.axisBottom(x).ticks(5);
var xZoomAxis = d3.axisBottom(xZoom);
var zoom = d3.zoom()
.scaleExtent([1, 32])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// plot area
g.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","url(#stripes)");
g.append("text")
.attr("x",width/2)
.attr("y",height/2)
.style("text-anchor","middle")
.text("plot area");
g.append("line")
.attr("y1",0)
.attr("y2",height)
.attr("stroke-width",1)
.attr("stroke","black");
// zoomed plot area:
var rect = g.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","lightgrey")
.attr("opacity",0.4);
// Axis for plot:
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Axis for zoom:
g.append("g")
.attr("class", "axis axis-zoom-x")
.attr("transform", "translate(0,"+(height+30)+")")
.call(xZoomAxis);
var text = g.append("text")
.attr("y", height+60)
.attr("text-anchor","middle")
.text("zoom units")
.attr("x",width/2);
// Gratuitous intro zoom:
var d1 = 18;
var d0 = 8;
svg.call(zoom).transition()
.duration(2000)
.call(zoom.transform, d3.zoomIdentity
.scale(width / (x(d1) - x(d0)))
.translate(-x(d0), 0));
function zoomed() {
var t = d3.event.transform, xt = t.rescaleX(x);
xZoom.range([xt(0),xt(20)]);
g.select(".axis--x").call(xAxis.scale(xt));
g.select(".axis-zoom-x").call(xZoomAxis.scale(xZoom));
rect.attr("x", xt(0));
rect.attr("width", xt(20) - xt(0));
text.attr("x", xt(10));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180">
<defs>
<pattern id="stripes" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45 0 0)">
<rect width="3" height="8" fill="orange"></rect>
</pattern>
</defs>
</svg>
Snippet Explanation:
Plot area: orange stripes
Full scaled extent of data: grey box.
Left hand side of plot area is x=0 (pixels) for the parent g that holds everything.
As we zoom in the bounds of our data exceeds the plot area. We want to show a specific subdomain of our data. We achieve part of that with the scale (as you correctly deduce) but the other portion is with the translate: we push values less than the lowest value of our x subdomain to the left. By pushing the entire graph left by an amount equal to x(d0), x(d0) appears as the leftmost coordinate of the plot area.

D3 v4 - make a horizontal bar chart with fixed width

I have made a horizontal bar chart using d3 v4, which works fine except for one thing. I am not able to make the bar height fixed. I am using bandwidth() currently and if i replace it with a fixed value say (15) the problem is that it does not align with the y axis label/tick http://jsbin.com/gigesi/3/edit?html,css,js,output
var w = 200;
var h = 400;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("transform", "translate(80,30)");
var data = [
{Item: "Item 1", count: "11"},
{Item: "Item 2", count: "14"},
{Item: "Item 3", count: "10"},
{Item: "Item 4", count: "14"}
];
var xScale = d3.scaleLinear()
.rangeRound([0,w])
.domain([0, d3.max(data, function(d) {
return d.count;
})]);
var yScale = d3.scaleBand()
.rangeRound([h,0]).padding(0.2)
.domain(data.map(function(d) {
return d.Item;
}));
var yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('class','axis')
.call(yAxis);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', function(d,i) {
return xScale(d.count);
})
.attr('height', yScale.bandwidth())
.attr('y', function(d, i) {
return yScale(d.Item);
}).attr("fill","#000");
The y axis seemed to be off SVG in the link you provided. (Maybe you have overflow: visible; for the SVG.
Anyway, I've added a few margins to the chart so that the whole chart is visible. Here it is (ignore the link description):
DEMO: H BAR CHART WITH HEIGHT POSITIONING TO THE TICKS
Relevant code changes:
As you are using a scale band, the height is computed within. You just need to use .bandWidth().
.attr('height', yScale.bandwidth())
Added a margin and transformed the axis and the bars to make the whole chart visible :
: I'm assigning margins so that the y-axis is within the viewport of the SVG which makes it easier to adjust the left margin based on the tick value as well. And I think this should be a standard practice.
Also, if you notice, the rects i.e. bars are now a part of <g class="bars"></g>. Inspect the DOM if you'd like. This would be useful for complex charts with a LOT of elements but it's always a good practice.
var margin = {top: 10, left: 40, right: 30, bottom: 10};
var xScale = d3.scaleLinear()
.rangeRound([0,w-margin.left-margin.right])
var yScale = d3.scaleBand()
.rangeRound([h-margin.top-margin.bottom,0]).padding(0.2)
svg.append('g')
.attr('class','axis')
.attr('transform', 'translate(' + margin.left+', '+margin.top+')')
Try changing the data and the bar height will adjust and align according to the ticks. Hope this helps. Let me know if you have any questions.
EDIT:
Initially, I thought you were facing a problem placing the bars at the center of the y tick but as you said you needed fixed height bars, here's a quick addition to the above code that lets you do that. I'll add another approach using the padding (inner and outer) sometime soon.
Updated JS BIN
To position the bar exactly at the position of the axis tick, I'm moving the bar from top to the scale's bandwidth that is calculated by .bandWidth() which will the position it starting right from the tick and now subtracting half of the desired height half from it so that the center of the bar matches the tick y. Hope this explains.
.attr('height', 15)
.attr('transform', 'translate(0, '+(yScale.bandwidth()/2-7.5)+')')

How to produce axes that do not intersect at (0, 0)?

I would like to generate axes that do not intersect at (0, 0) (and also do not necessarily coincide with the edges of a plot), as shown in the example below.
How can I do this with d3?
You will first need to figure out where you want to display the axis. If they are fixed to canvas, take ratios of width and height.
Here's an example that I made:
http://vida.io/documents/zB4P4fjHz79um3qzX
x-axis is at to 2/3 of height:
.attr("transform", "translate(0," + height * 2 / 3 + ")")
And y-axis is at 1/3 of width:
.attr("transform", "translate(" + width / 3 + ", 0)")
If you need the axis relative to range of values, calculate them based on range. For example:
var domain = d3.extent(data, function(d) { return d.y_axis; })
var y_axis_pos = width * (y_axis_value - domain[1]) / (domain[0] - domain[1]);
// svg code...
.attr("transform", "translate(" + y_axis_pos + ", 0)")
From the D3.js API documentation:
to change the position of the axis with respect to the plot, specify a transform attribute on the containing g element.

multiple lines d3

I'm trying to do a simple plot with d3. It works fine when I plot only one line at time but I would like to make my code more generic.
My data are in this format (note that the 2 set of data may not contain the same amount of point and the time is not perfectly synchronize between my 2 sets of measure):
var data = [{key: "kmm03", value:[{"time":1364108443000,"mesure":"1.6299999952316284"},{"time":1364108503000,"mesure":"1.100000023841858"},{"time":1364108563000,"mesure":"1.159999966621399"}},
{key: "kmm04", value:[{"time":1364108416000,"mesure":"2.690000057220459"},{"time":1364108476000,"mesure":"3.319999933242798"},{"time":1364108536000,"mesure":"3.140000104904175"},{"time":1364108596000,"mesure":"2.9800000190734863"}}]
Now I try to plot it like this but I cannot get the svg lines to dispay:
var margin = {top: 20, right: 40, bottom: 20, left: 40};
var width = 780 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var time_scale = d3.time.scale()
//.domain(time_extent)
.domain([1364108443000, 1364112559000])
.range([0, width]);
var mesure_scale = d3.scale.linear()
.domain([0,10])
.range([height, 0]);
var vis = d3.select("#box1")
.append("svg:svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var time_axis = d3.svg.axis()
.scale(time_scale)
.orient("bottom");
var mesure_axis = d3.svg.axis()
.scale(mesure_scale)
.orient("left");
var set_axis_t = vis.append("svg:g")
.attr("transform", "translate(0, " + height +")")
.call(time_axis);
var set_axis_m = vis.append("svg:g")
.attr("transform", "translate(0, 0)")
.call(mesure_axis);
var line = d3.svg.line()
.x(function(d){return time_scale(d.time);})
.y(function(d){return mesure_scale(d.mesure);});
var group = vis.selectAll('path')
.data(data)
.enter().append('path')
.attr("d", line);
d3 selection.data() takes an array of elements. In your code the data var is an array of two objects. When you call line on d, d3 looks for time and measure properties in each element. These don't exist so no paths are appended.
The amount and time values you want to render are nested one layer down in the value property of each object.
To draw these, change the .attr('d', line) to .attr("d", function(d) {return line(d.value);});
Here is a working version. To make it work I made a few other changes:
1- closing brackets of the value arrays were missing
2- .selectAll('path') was not working for the data because it was conflicting with the path elements in the axes. To address this, I assigned the data paths the class visline and use .selectAll('.visline') to access them

Resources