Why subtract margins only add them back in when setting svg width - d3.js

In the following d3 line graph the author created a margin object with the properties top, right, bottom, and left.
var margin = {top: 20, right: 20, bottom: 60, left: 80}
Then sets the width variable in the following way.
width = 700 - margin.left - margin.right // 700 - 80 - 20 = 600
// So, width variable is 600
Only to then, add the margins back in when setting the width of the svg.
attr('width', width + margin.left + margin.right) // 600 + 80 + 20 = 700
// So, width attr of the svg is 700
Why not just set width 700 without having to subtract the margins? That what the svg ends up being set to. What is the purpose of this pattern I've seen it before and trying to understand the purpose of doing it this way. Thank you.
Full code below.
var margin = {top: 20, right: 20, bottom: 60, left: 80},
width = 700 - margin.left - margin.right,
height = 700 - margin.top - margin.bottom;
var svg = d3.select("body") //create Svg element
.append("svg")
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.style("border", "solid 1px red")
.attr("transform","translate(100,0)"); // To align svg at the center in the output tab.
var data = [
{ date:"2020/01/01 00:00:00", patients: 600 },
{ date:"2020/02/01 00:00:00", patients: 500 },
{ date:"2020/03/01 00:00:00", patients: 400 },
{ date:"2020/04/01 00:00:00", patients: 500 },
{ date:"2020/05/01 00:00:00", patients: 300 },
{ date:"2020/06/01 00:00:00", patients: 100 },
{ date:"2020/07/01 00:00:00", patients: 50 },
{ date:"2020/08/01 00:00:00", patients: 500 },
{ date:"2020/09/01 00:00:00", patients: 550 },
{ date:"2020/10/01 00:00:00", patients: 550 },
];
data=data.map(d => ({
date: new Date(d.date),
patients: d.patients
}))
var xscale = d3.scaleTime()
.domain(d3.extent(data, d=>d.date))
.range([0,width]);
var yscale= d3.scaleLinear() // drawing y scale
.domain([0,600])
.range([height,0])
var chart=svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
chart.append('g')
.call(d3.axisBottom(xscale).tickFormat(d3.timeFormat("%b")))
.attr('transform', 'translate(0,' + height + ')')
chart.append('g')
.call(d3.axisLeft(yscale))
svg.append("text") // labelling x-axis
.text("Month")
.attr("transform","translate(350,680)");
svg.append("text") // labelling y-axis
.text("Number of patients")
.attr('transform', "translate(40,400) rotate(-90)");
var generator = d3.line()
.x(function (d) { return xscale(d.date); })
.y(function (d) { return yscale(d.patients); });
chart.append('path')
.datum(data)
.attr("d", generator)
.attr("fill","none")
.attr("stroke","blue");

This was standard practice with D3, since as far back as at least version 3. Mike Bostock uses this convention in his examples. I've always found it useful. You cite a fairly simple example, but as your d3 code gets more complicated you'll need to be referencing that plot width more often than you total svg width.

Related

Categorical labels in bar chart partially hidden

I have a bar chart where my x-axis tick labels are being cut off within the svg and I can't seem to find the proper action to take to reveal them. I have tried playing around with the .range() properties associated with the x variable with no success and am wondering if I should add a "transform" attribute or maybe a "margin-bottom" attribute to resolve the issue. Any guidance on what could be causing this issue and the best resolution?
<html>
<!-- Load d3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<meta charset="utf-8">
<body>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
</body>
<script>
var data = [
{ "x": "Friday", "y": 7.620000000000001 },
{ "x": "Monday", "y": 6.463333333333335 },
{ "x": "Saturday", "y": 7.236666666666666 },
{ "x": "Sunday", "y": 7.388571428571429 },
{ "x": "Thursday", "y": 8.051111111111112 },
{ "x": "Tuesday", "y": 7.717999999999999 },
{ "x": "Wednesday", "y": 7.742 }
]
var margin = { top: 10, right: 30, bottom: 30, left: 60 }
var width = 800 - margin.left - margin.right;
var height = 800 - margin.top - margin.bottom;
var svg = d3.select("#my_dataviz")
.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 + ")"); // translate(margin left, margin top)
var x = d3.scaleBand()
.domain(d3.max(data, function(d) { return d.x }))
.range([0, width])
.padding(0.2);
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");
// Append x-scale title
svg.append("text")
.attr("transform",
"translate(" + (width/2) + " ," + (height + margin.top + 20) + ")")
.style("text-anchor", "middle")
.text("Weekday");
</script>
</html>
The data of x-axis should use .map to get ...
var x = d3.scaleBand()
.domain(data.map(function(d) { return d.x; }))
.range([0, width])
.padding(0.2);

How to fix a display issue in a multiple line graph. The svg.append("path") is not displaying anything

Im trying to create a multiline graph for my angular web app to visualise some data that will add lines according to the items in the data variable. As i'm trying to append the path line in the svg element i can't manage to display it. Right now only the two axes are displayed correctly.
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleTime().domain([new Date(2000, 0, 0), new Date(2017, 0, 0)]).range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var svg = d3.select(this.htmlElement).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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%Y")).ticks(d3.timeYear, 1));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
// define the line
var line = d3.line()
.x(function(d) {
return d.name;
})
.y(function(d) {
return d.value;
});
data.forEach(function(d) {
svg.append("path")
.attr("class", "line")
.attr('d', line(d.series))
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
});
the structure of the data is
var data = [{
"key": "Germany",
"series": [
{
"a": "Germany",
"name": "2010",
"value": 0.2
},
{
"a": "Germany",
"name": "2011",
"value": 0.5
}
]
}, {
"key": "uk",
"series": [
{
"a":"uk",
"name": "2010",
"value": 0.3
},
{
"a":"uk",
"name": "2011",
"value": 0.6
}
]
}];
i guess that the problem lies on the data.foreach loop but i can't manage to find it.
according to the comments above the working code can be seen below
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleTime().domain([new Date(2000, 0, 0), new Date(2017, 0, 0)]).range([0, width]);
var y = d3.scaleLinear().domain([0,1]).range([height, 0]);
var svg = d3.select(this.htmlElement).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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%Y")).ticks(d3.timeYear, 1));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
// define the line
var line = d3.line()
.x(function(d) {
return x(new Date(d.name, 0));
})
.y(function(d) {
return y(d.value);
});
data.forEach(function(d) {
svg.append("path")
.attr("class", "line")
.attr('d', line(d.series))
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
});

Get In between points of an Ordinal Scale Axis in d3

I am having a hard time in finding the points in between the given X scale domain.
For eg. I have a bar chart having its X axis values as
[Bob,Robin,Anne,Mark,Joe,Eve,Karen,Kirsty,Chris,Lisa,Tom,Stacy,Charles,Mary]
The above values are used to plot the x-axis.I have used scaleBand method since the values are discrete.
var x = d3.scaleBand()
.range([0, width])
.padding(0.1);
x.domain(data.map(function(d) { return d.salesperson; }));
My Aim is to find points in between x scale from the given points,i.e
Is there any way that we can locate a point between Bob,Robinor Robin,Anne, So that it would be possible for me to plot a point provided a Y axis value is given.
I have added a plunker which illustrates bar chart having the mentioned x-axis.I need to plot a point on the chart having X value point between Bob,Robin
and Y value as 30(any value on Y axis)
Provided that Robin is the discrete value just after Bob, you can use a combination of bandwidth() and step():
x("Bob") + x.bandwidth() + (x.step()-x.bandwidth()) / 2
Ugly, but it works. Talking about ugliness, if you want an uglier math, you can also use a combination of step() and paddingInner():
x("Bob") + x.step() * (1 - x.paddingInner() / 2))
Here is a demo, I put the point at y(10) so you can better see that it will be right between the "Bob" and "Robin" bars:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - 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("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 + ")");
var data = [
{
"salesperson": "Bob",
"sales": 33
},
{
"salesperson": "Robin",
"sales": 12
},
{
"salesperson": "Anne",
"sales": 41
},
{
"salesperson": "Mark",
"sales": 16
},
{
"salesperson": "Joe",
"sales": 59
},
{
"salesperson": "Eve",
"sales": 38
},
{
"salesperson": "Karen",
"sales": 21
},
{
"salesperson": "Kirsty",
"sales": 25
},
{
"salesperson": "Chris",
"sales": 30
},
{
"salesperson": "Lisa",
"sales": 47
},
{
"salesperson": "Tom",
"sales": 5
},
{
"salesperson": "Stacy",
"sales": 20
},
{
"salesperson": "Charles",
"sales": 13
},
{
"salesperson": "Mary",
"sales": 29
}
];
// format the data
data.forEach(function(d) {
d.sales = +d.sales;
});
// Scale the range of the data in the domains
x.domain(data.map(function(d) { return d.salesperson; }));
y.domain([0, d3.max(data, function(d) { return d.sales; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.salesperson); })
.attr("width", x.bandwidth())
.attr("y", function(d) { return y(d.sales); })
.attr("height", function(d) { return height - y(d.sales); });
// 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));
var pointBetween = svg.append("circle")
.attr("r", 2)
.attr("cy", y(10))
.attr("cx", x("Bob") + x.bandwidth() + (x.step()-x.bandwidth())/2)
.bar { fill: steelblue; }
<script src="//d3js.org/d3.v4.min.js"></script>

Format and parse time D3V4

Trying to progress again with d3v4.
Based on my research, I think the solution to my problem comes from the answer I got to a previous problem: "...the reason that this then doesn't work is that the nest.key has to be a string, so it's parsing your time back to a string which then won't work with your x scale." Despite of the clue, I'm unable to make this work. Result:
Error: attribute d: Expected number, "MNaN,262.5LNaN,37…".
My objective is to draw a line that count the number of events, grouped by date. I'm able to achieve that result when I parse the creationDate. Nonetheless, I want the results to be counted by day and not by hours. To do so, I format the date using var formatTime = d3.timeFormat("%Y-%m-%d"); but then I get the error.
I've seen many answers to this problem based on D3v3 and I tried to adapt it... :(
<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parse the date / time
var formatTime = d3.timeFormat("%Y-%m-%d");
var parseTime = d3.timeParse("%Y-%m-%dT%H:%M:%S.%L");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the 3rd line
var valueline3 = d3.line()
.x(function(d) { return x(d.key); })
.y(function(d) { return y(d.length); });
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
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("data5.json", function(error, data) {
if (error) throw error;
var groupByDate = d3.nest(data)
.key(function(d) { return formatTime(parseTime(d.creationDate)); })
.sortKeys(d3.ascending)
.rollup(function(v) {
return {
count: v.length,
};
})
.entries(data);
console.log(groupByDate);
// format the data
groupByDate.forEach(function(d) {
d.creationDate = d.key;
d.length = +d.value.count;
d.value.count = +d.length;
});
// Scale the range of the data
x.domain(d3.extent(groupByDate, function(d) { return d.key; }));
y.domain([0, d3.max(groupByDate, function(d) { return Math.max(d.length); })]);
// Add the valueline3 path.
svg.append("path")
.datum(groupByDate)
.attr("class", "line")
.style("stroke", "green")
.attr("d", valueline3);
// 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));
});
</script>
</body>
The JSON data I have in the same directory:
[
{
"id": 115,
"creationDate": "2017-12-08T16:31:09.924"
},
{
"id": 119,
"creationDate": "2017-12-08T16:31:09.924"
},
{
"id": 120,
"creationDate": "2017-12-08T16:31:09.924"
},
{
"id": 134,
"creationDate": "2017-12-08T16:31:09.924"
},
{
"id": 135,
"creationDate": "2017-12-08T16:31:09.924"
},
{
"id": 139,
"creationDate": "2018-02-05T17:17:04.473"
},
{
"id": 139,
"creationDate": "2018-02-05T17:17:04.473"
},
{
"id": 140,
"creationDate": "2018-02-04T17:17:08.707"
}
]

How to Align D3.js X-axis Dates with Supplied Data

I'm creating a time-based line chart and everything looks fine but I'm having difficulty with the x-axis tickmarks. As you can see http://jsfiddle.net/shubo/Yvupw/ the dates start at 2013-01-30 and ends at 2013-04-17 but the chart tick mark starts at 2013-2-3 and ends at 2013-4-14. What do I need to do so the first tickmark would show 2013-01-03 and the last one would show 2013-4-17?
var json = {
"data": [
{
"date": "2013-04-17",
"metric": 437792798
},
{
"date": "2013-04-10",
"metric": 437792998
},
{
"date": "2013-04-03",
"metric": 434633203
},
{
"date": "2013-03-27",
"metric": 431786310
},
{
"date": "2013-03-20",
"metric": 429614980
},
{
"date": "2013-03-13",
"metric": 427709519
},
{
"date": "2013-03-06",
"metric": 425894908
},
{
"date": "2013-02-27",
"metric": 423657524
},
{
"date": "2013-02-20",
"metric": 420392146
},
{
"date": "2013-02-13",
"metric": 417215035
},
{
"date": "2013-02-06",
"metric": 412433066
},
{
"date": "2013-01-30",
"metric": 408952856
}
]
};
var margin = {top: 20, right: 50, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 110 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y-%m-%d").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.metric); });
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 + ")");
var data = json.data;
data.forEach(function(d) {
d.date = parseDate(d.date);
});
data.sort(function(a, b) {
return a.date - b.date;
});
x.domain([data[0].date, data[data.length - 1].date]);
y.domain(d3.extent(data, function(d) { return d.metric; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
var format = d3.format(',');
Try using axis.tickValues([values]) to control exactly which tick marks appear or use one of the one of the other tick settings.

Resources