Get In between points of an Ordinal Scale Axis in d3 - d3.js

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>

Related

Why subtract margins only add them back in when setting svg width

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.

d3 labelled horizontal chart with labels and animation

I am working on a d3js horizontal chart - the designers are specific in having the labels this way.
I've built the following - but would like to model it more on older code that had animation properties.
//current chart
https://codepen.io/anon/pen/ZmJzXZ
//static vertical chart http://jsfiddle.net/pg886/201/
//animated vertical chart http://jsfiddle.net/Qh9X5/12073/
-- d3js code
var data = [{
"name": "Apples",
"value": 20,
},
{
"name": "Bananas",
"value": 12,
},
{
"name": "Grapes",
"value": 19,
},
{
"name": "Lemons",
"value": 5,
},
{
"name": "Limes",
"value": 16,
},
{
"name": "Oranges",
"value": 26,
},
{
"name": "Pears",
"value": 30,
}];
//sort bars based on value
data = data.sort(function (a, b) {
return d3.ascending(a.value, b.value);
})
//set up svg using margin conventions - we'll need plenty of room on the left for labels
var margin = {
top: 15,
right: 25,
bottom: 15,
left: 60
};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("#graphic").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 x = d3.scale.linear()
.range([0, width])
.domain([0, d3.max(data, function (d) {
return d.value;
})]);
var y = d3.scale.ordinal()
.rangeRoundBands([height, 0], .3)
.domain(data.map(function (d) {
return d.name;
}));
//make y axis to show bar names
var yAxis = d3.svg.axis()
.scale(y)
//no tick marks
.tickSize(0)
.orient("right");
var gy = svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var bars = svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
.attr("class", "bars")
//append rects
bars.append("rect")
.attr("class", "bar")
.attr("y", function (d) {
return y(d.name);
})
.attr("height", y.rangeBand())
.attr("x", 0)
.attr("width", function (d) {
return x(d.value);
});
//add a value label to the right of each bar
bars.append("text")
.attr("class", "label")
//y position of the label is halfway down the bar
.attr("y", function (d) {
return y(d.name) + y.rangeBand() / 2 + 4;
})
//x position is 3 pixels to the right of the bar
.attr("x", function (d) {
return x(d.value) + 3;
})
.text(function (d) {
return d.value;
});
var labels =
bars.append("text")
.attr("class", "labels")
.attr("y", function (d) {
return y(d.name) + y.rangeBand() / 2 - 30;
})
.attr("x", 0)
.text(function (d) {
return d.name;
});
I had a similar chart that I've made a few modifications to that might fill your requierments, so I'll be basing my answer of my own code.
I'll just go through the most relevant part of the question and you can just have a look at the code and hopefully figure out how it works yourself.
The inital animation works exactly the same way as in the third link you posted:
.transition().duration(speed)
.delay((_, i) => delay * i)
we set a delay so that each bar appear one at a time.
I've also set it up so that you can change the data using the d3js update pattern.
var bar = svg.selectAll(".bar")
.data(data, d => d.name)
bar.exit().remove();
bar.enter().insert("g", ".y-axis").append("rect")
.attr("class", "bar")
.attr("fill", "#ccc")
.attr("x", x(0))
.attr("y", d => y(d.name))
.attr("height", y.bandwidth())
.merge(bar)
.transition().duration(speed)
.delay((_, i) => delay * i)
.attr("y", d => y(d.name))
.attr("width", d => x(d.value) - x(0));
Since you didn't specify how you want to update the new data it's just a year filter for now.
Here's all the code:
var init = [
{"year": "2017", "name": "Apples", "value": 20},
{"year": "2017", "name": "Bananas","value": 12},
{"year": "2017", "name": "Grapes", "value": 19},
{"year": "2017", "name": "Lemons", "value": 5},
{"year": "2017", "name": "Limes", "value": 16},
{"year": "2017", "name": "Oranges", "value": 26},
{"year": "2017", "name": "Pears","value": 30},
{"year": "2018", "name": "Apples", "value": 10},
{"year": "2018", "name": "Bananas","value": 42},
{"year": "2018", "name": "Grapes", "value": 69},
{"year": "2018", "name": "Lemons", "value": 15},
{"year": "2018", "name": "Limes", "value": 26},
{"year": "2018", "name": "Oranges", "value": 36},
{"year": "2018", "name": "Pears","value": 20}
];
chart(init)
function chart(result) {
var format = d3.format(",.0f")
var years = [...new Set(result.map(d => d.year))]
var fruit = [...new Set(result.map(d => d.name))]
var options = d3.select("#year").selectAll("option")
.data(years)
.enter().append("option")
.text(d => d)
var svg = d3.select("#graphic"),
margin = {top: 25, bottom: 10, left: 50, right: 45},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleLinear()
.range([margin.left, width - margin.right])
var y = d3.scaleBand()
.range([margin.top, height - margin.bottom])
.padding(0.1)
.paddingOuter(0.5)
.paddingInner(0.5)
var xAxis = svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${margin.top})`)
var yAxis = svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left},0)`)
update(d3.select("#year").property("value"), 750, 250)
function update(input, speed, delay) {
var data = result.filter(f => f.year == input)
var sum = d3.sum(data, d => d.value)
x.domain([0, d3.max(data, d => d.value)]).nice()
svg.selectAll(".x-axis").transition().duration(speed)
.call(d3.axisTop(x).tickSizeOuter(0));
data.sort((a, b) => b.value - a.value)
y.domain(data.map(d => d.name))
svg.selectAll(".y-axis").transition().duration(speed)
.call(d3.axisLeft(y));
yAxis.selectAll("text").remove()
yAxis.selectAll("line").remove()
var bar = svg.selectAll(".bar")
.data(data, d => d.name)
bar.exit().remove();
bar.enter().insert("g", ".y-axis").append("rect")
.attr("class", "bar")
.attr("fill", "#ccc")
.attr("x", x(0))
.attr("y", d => y(d.name))
.attr("height", y.bandwidth())
.merge(bar)
.transition().duration(speed)
.delay((_, i) => delay * i)
.attr("y", d => y(d.name))
.attr("width", d => x(d.value) - x(0));
var value = svg.selectAll(".value")
.data(data, d => d.name)
value.exit().remove();
value.enter().append("text")
.attr("class", "value")
.attr("opacity", 0)
.attr("dy", 4)
.attr("y", d => y(d.name) + y.bandwidth() / 2)
.merge(value)
.transition().duration(speed)
.delay((_, i) => delay * i)
.attr("opacity", 1)
.attr("y", d => y(d.name) + y.bandwidth() / 2)
.attr("x", d => x(d.value) + 5)
.text(d => format((d.value / sum) * 100) + " %")
var name = svg.selectAll(".name")
.data(data, d => d.name)
name.exit().remove();
name.enter().append("text")
.attr("class", "name")
.attr("opacity", 0)
.attr("dy", -5)
.attr("y", d => y(d.name))
.merge(name)
.transition().duration(speed)
.delay((_, i) => delay * i)
.attr("opacity", 1)
.attr("y", d => y(d.name))
.attr("x", d => x(0) + 5)
.text(d => d.name)
}
var select = d3.select("#year")
.style("border-radius", "5px")
.on("change", function() {
update(this.value, 750, 0)
})
}
body {
margin: auto;
width: 650px;
font: 12px arial;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg id="graphic" width="600" height="380"></svg><br>
Choose year:
<select id="year"></select>

d3, line chart, Error: <path> attribute d: Expected number

I have a stackbiltz here - https://stackblitz.com/edit/d3-start-above-zero-gtudz8?embed=1&file=index.js&hideNavigation=1
I'm getting an error in the console.
Error: <path> attribute d: Expected number
The console log in the code show the correct data but I dont think its getting passed to the line function.
Couple obvious issues:
Your X values are strings but you are using a scaleLinear. That's for numbers; try scaleBand.
With that fixed, you aren't setting your x domain properly.
Here's those two fixes:
var data = [
{
"date": "Jan",
"value": 1507
},
{
"date": "Feb",
"value": 1600
},
{
"date": "Mar",
"value": 1281
},
{
"date": "Apr",
"value": 1898
},
{
"date": "May",
"value": 1749
},
{
"date": "June",
"value": 1270
},
{
"date": "July",
"value": 1712
},
{
"date": "Aug",
"value": 1270
},
{
"date": "Sept",
"value": 1257
},
{
"date": "Oct",
"value": 1257
},
{
"date": "Nov",
"value": 1257
},
{
"date": "Dec",
"value": 1257
}
];
///////////////////////////// Create SVG
var w = 400;
var h = 250;
var margin = {
top: 20,
bottom: 20,
left: 40,
right: 20
}
var width = w - margin.left - margin.right
var height = h - margin.top - margin.bottom
var svg = d3.select("body").append("svg")
.attr("id", "svg")
.attr("width", w)
.attr("height", h)
var chart = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
///////////////////////////// Create Scale
var x = d3.scaleBand()
.range([0, width])
var y = d3.scaleLinear()
.rangeRound([height, 0])
///////////////////////////// Create Line
var line = d3.line()
.x(function(d) {
console.log(d.date)
console.log(x.domain())
return x(d.date)
})
.y(function(d) {
console.log(d.value)
console.log(y.domain())
return y(d.value)
})
x.domain(data.map(function(d) {
return d.date
}));
y.domain([0, d3.max(data, function(d) {
return d.value
})]);
chart.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
///////////////////////////// Create Axis
var xAxis = chart.append('g')
.classed('x-axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
var yAxis = chart.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://d3js.org/d3.v4.min.js"></script>

How to add legend and make lines random color as a new line is added to D3.JS

Every time I add to the below dataset a new line is addedd to my chart. What changes do I need to make in order for this to be a different random color each time, and how/where should I add a legend for thes random lines?
Working Fiddle https://jsfiddle.net/qvrL3ey5/
var dataset = [
[{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 }, { x: 3, y: 1222 }, { x: 4, y: 1611 }],
[{ x: 0, y: 200 }, { x: 1, y: 933 }, { x: 2, y: 1412 }, { x: 3, y: 1322 }, { x: 4, y: 1711 }]]
Code:
var dataset = [
[{ x: 0, y: 100 }, { x: 1, y: 833 }, { x: 2, y: 1312 }, { x: 3, y: 1222 }, { x: 4, y: 1611 }],
[{ x: 0, y: 200 }, { x: 1, y: 933 }, { x: 2, y: 1412 }, { x: 3, y: 1322 }, { x: 4, y: 1711 }]]
var margin = { top: 20, right: 100, bottom: 30, left: 100 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(series) {
return d3.max(series, function (d) { return d.x; })
})])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(series) {
return d3.max(series, function (d) { return d.y; })
})])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(0)
.orient("bottom")
.outerTickSize(0)
.tickPadding(0);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.outerTickSize(0)
.tickPadding(10);
var line = d3.svg.line()
.x(function (d) { console.log(d.x, xScale(d.x));return xScale(d.x); })
.y(function (d) { return yScale(d.y); });
//Create SVG element
var svg = d3.select("#visualisation")
.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(xAxis)
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
svg.selectAll("path.line").data(dataset).enter()
.append("path")
.attr("class", "line")
.attr("d", line);
Edit:
Found how to add the color..
var color = d3.scale.category10();
.attr("d", line)
.style("stroke", function (d) {
return color(d);
Still researching how to add a legend.
Edit:
Unfortunately the color's are the same, unless the data-sets are different lengths.. If I have data-set A) 25 points and B) 30 points it generates two different colors, but if both A&B are 25 points then its the same color.. Any ideas?
Regarding the colours: var color = d3.scale.category10() will create a ordinal scale with empty domain and range with ten colors, but if no domain is set the domain will be automatically created according to the sequence you call color. So, we have to set a domain:
var color = d3.scale.category10();
color.domain(d3.range(0,10));
And then painting the lines randomly:
.style("stroke", function (d) {
return color(Math.floor(Math.random()*10))});
This satisfies your request for the lines:
to be a different random color each time
Check this fiddle, every time you click "run" the colors are different: https://jsfiddle.net/gerardofurtado/qvrL3ey5/1/
Regarding the legend, you'll have to give us more information.

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