I have created the d3 Multi-line chart. I managed to generate both x and y axis along with the Lines. But unfortunately i got stuck at this point where I am not able to align the Path-lines with the Y axis i.e. ScaleBand(). I have no idea how do to that and guide line for that would be great. I have attached my code with sample data in snippet. Thanks in advance.
var dataDomains = ['automated', 'manual']
var data = [
{ automated: 1000 , manual: 3000 },
{ automated: 5000 , manual: 6000 },
{ automated: 10000, manual: 9000 },
{ automated: 50000, manual: 12000 },
{ automated: 100000, manual: 15000 },
]
// 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 = 200 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleBand().range([height, 0]);
// define the 1st line
var valueline = d3.line()
.x(function (d) { return x(d.automated); })
.y(function (d) { return y('automated'); });
// define the 2nd line
var valueline2 = d3.line()
.x(function (d) { return x(d.manual); })
.y(function (d) { return y('manual'); });
// append the svg obgect 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("#cl-AVM").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 + ")");
// Scale the range of the data
x.domain([0, d3.max(data, function (d) { return Math.max(d.automated, d.manual) })])
y.domain(dataDomains);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the valueline2 path.
svg.append("path")
.data([data])
.attr("class", "line")
.style("stroke", "red")
.attr("d", valueline2);
// 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));
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="cl-AVM"></div>
You could use a scalePoint instead of a scaleBand as your line can be considered as just a point on the y axis.
Then you can add some padding so that the values for the two points are not 0 and 100 (I picked 0.6 but you can play around with it and see what best suits you).
You can see usage of scalePoint here in more details: https://observablehq.com/#d3/d3-scalepoint
Hope that helps ! :)
var dataDomains = ['automated', 'manual']
var data = [
{ automated: 1000 , manual: 3000 },
{ automated: 5000 , manual: 6000 },
{ automated: 10000, manual: 9000 },
{ automated: 50000, manual: 12000 },
{ automated: 100000, manual: 15000 },
]
// 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 = 200 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleLinear().range([0, width]);
var y = d3.scalePoint().range([height, 0]).padding(0.6); // modified here
// define the 1st line
var valueline = d3.line()
.x(function (d) { return x(d.automated); })
.y(function (d) { return y('automated'); });
// define the 2nd line
var valueline2 = d3.line()
.x(function (d) { return x(d.manual); })
.y(function (d) { return y('manual'); });
// append the svg obgect 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("#cl-AVM").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 + ")");
// Scale the range of the data
x.domain([0, d3.max(data, function (d) { return Math.max(d.automated, d.manual) })])
y.domain(dataDomains);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the valueline2 path.
svg.append("path")
.data([data])
.attr("class", "line")
.style("stroke", "red")
.attr("d", valueline2);
// 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));
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="cl-AVM"></div>
Related
Thank you for any help you can offer in advance, I am new to D3 and having a hard time following the multiline chart examples I've seen online. I have data that looks like:
country,year,average
United States,1970,51
United States,1971,50
United States,1972,54
United States,1973,56
United States,1974,53
United States,1975,57
United States,1976,60
Brazil,1970,23
Brazil,1971,25
Brazil,1972,24
Brazil,1973,21
Brazil,1974,25
Brazil,1975,26
Brazil,1976,24
for multiple countries and I would like to make a line for each of them.
var margin = {top: 10, right: 40, bottom: 150, left: 70},
width = 760 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var w = width + margin.left + margin.right;
var h = height + margin.top + margin.bottom;
var svg = d3.select("body").append("svg") // this appends a new SVG element to body
.attr("width", w) // set the width
.attr("height", h) // set the height
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x scale will handle time
var xScale = d3.scaleBand().range([0, width]).padding(0.1);
// y scale will handle energy consumption values
var yScale = d3.scaleLinear().range([height,0]);
// Define X and Y AXIS
var yAxis = d3.axisLeft(yScale).ticks(5);
var xAxis = d3.axisBottom(xScale);
function rowConverter(data) {
return {
country : data.country,
year : +data.year,
average : +data.average // the + operator parses strings into numbers
};
}
// line generator function
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return xScale(d.year); })
.y(function(d) { return yScale(d.average); })
d3.csv("EvenMore.csv", rowConverter).then(function(data) {
var countries = d3.nest()
.key(function (d) { return d.country; })
.entries(data);
console.log(countries);
yScale.domain([0,d3.max(data, function(d) {return d.average; } )]);
xScale.domain(d3.extent(data, function(d) { return d.year; } ));
// Draw xAxis
svg.append("g") // add a new svg group element
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
// Draw yAxis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
svg.selectAll("path")
.data(countries)
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
});
});
I do not know what these errors mean, Error: attribute d: Expected number, "….33333333333334LNaN,114.27777777…":
The Problem
You're not using the band scale correctly. A band scale is not a quantitative scale and therefore does not have an upper and lower bounds. Instead, every value of the domain needs to be specified:
The first element in domain will be mapped to the first band, the
second domain value to the second band, and so on. Domain values are
stored internally in a map from stringified value to index; the
resulting index is then used to determine the band (docs)
This explains your error, you've specified two values to the domain, the first year and the last year. We can see that the domain is only these two values a few ways, when looking at the scale (a band scale's axis by default includes all ticks, but even here we see the spacing is really odd if 1970 and 1976 are the start and end values):
The error message also helps in finding the error: if the first coordinate's x value was NaN the message would read "Expected Number, "MNan,1234..." when examining the path d attribute (especially without any curve applied), we can see the x value of every coordinate except the first and last are NaN.
The solution
You need to provide all values in the domain to the scale. We can get all values with:
xScale.domain(data.map(function(d) { return d.year; }))
The scale will weed out duplicates when setting the domain.
var margin = {top: 10, right: 40, bottom: 150, left: 70},
width = 760 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var w = width + margin.left + margin.right;
var h = height + margin.top + margin.bottom;
var svg = d3.select("body").append("svg") // this appends a new SVG element to body
.attr("width", w) // set the width
.attr("height", h) // set the height
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x scale will handle time
var xScale = d3.scaleBand().range([0, width]).padding(0.1);
// y scale will handle energy consumption values
var yScale = d3.scaleLinear().range([height,0]);
// Define X and Y AXIS
var yAxis = d3.axisLeft(yScale).ticks(5);
var xAxis = d3.axisBottom(xScale);
function rowConverter(data) {
return {
country : data.country,
year : +data.year,
average : +data.average // the + operator parses strings into numbers
};
}
// line generator function
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return xScale(d.year); })
.y(function(d) { return yScale(d.average); })
var data = d3.csvParse(d3.select("pre").remove().text())
data = data.map(rowConverter);
var countries = d3.nest()
.key(function (d) { return d.country; })
.entries(data);
yScale.domain([0,d3.max(data, function(d) {return d.average; } )]);
xScale.domain(countries[0].values.map(function(d) { return d.year; }));
// Draw xAxis
svg.append("g") // add a new svg group element
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
// Draw yAxis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
svg.selectAll(null)
.data(countries)
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
});
.line {
stroke-width: 2px;
fill: none;
stroke:black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<pre>country,year,average
United States,1970,51
United States,1971,50
United States,1972,54
United States,1973,56
United States,1974,53
United States,1975,57
United States,1976,60
Brazil,1970,23
Brazil,1971,25
Brazil,1972,24
Brazil,1973,21
Brazil,1974,25
Brazil,1975,26
Brazil,1976,24</pre>
`
var margin = {top: 50, right: 50, bottom: 50, left: 50}
, width = window.innerWidth - margin.left - margin.right
, height = window.innerHeight - margin.top - margin.bottom;
// n data points
var n = 7;
// X scale
var xScale = d3.scaleBand()
.domain(['A','B','C','D','F','E','Z']) // input
.range([0, width]); // output
// Y scale
var yScale = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);
var line = d3.line()
.x(function(d, i) { return xScale(i); })
.y(function(d) { return yScale(d.y); })
.curve(d3.curveMonotoneX)
var dataset = d3.range(n).map(function(d) { return {"y": d3.randomUniform(1)()} })
// SVGs
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 + ")");
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "white");
svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x axis call
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
// y axis call
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale));
svg.append("path")
.datum(dataset)
.attr("class", "line")
.attr("d", line);
// 12. Appends a circle for each datapoint
svg.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d, i) { return xScale(i) })
.attr("cy", function(d) { return yScale(d.y) })
.attr("r", 6);
svg.append("text")
.attr("class", "title")
.attr("x", width/2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.text("Testing");
/* 13. Basic Styling with CSS */
/* Style the lines by removing the fill and applying a stroke */
.line {
fill: none;
stroke: green;
stroke-width: 3;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: red;
stroke: #fff;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
</style>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>
<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
</script>
I need for each data point to correspond to an (string) x-coordinate.
I am knew to d3 and I have yet to get accustomed to formatting axis.
I would also be great if anyone can point me out to how to add a tooltip. (Just an explanation)
Thank you everyone.
Not sure why it keeps saying your: "
It looks like your post is mostly code; please add some more details."
`
The scaleOrdinal is mapped to an array of alphabets but when you are calculating the cx you are mapping to an integer i. To resolve this:
Separate the labels as as array first:
var labels = ['A','B','C','D','F','E','Z'];
Then pass the labels to the domain:
// X scale
var xScale = d3.scaleBand()
.domain(labels) // input
.range([0, width]); // output
Finally, when you call calculate the cx, you need to send a value which was used in the domain. In your case since your domain is an array of alphabets you need to reparse the i to that particular alphabet. Hence you need to return xScale(labels[i]) as below:
svg.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d, i) { return xScale(labels[i]) })
.attr("cy", function(d) { return yScale(d.y) })
.attr("r", 6);
Full working snippet below. Hope this helps.
var margin = {top: 50, right: 50, bottom: 50, left: 50}
, width = window.innerWidth - margin.left - margin.right
, height = window.innerHeight - margin.top - margin.bottom;
// n data points
var n = 7;
//labels
var labels = ['A','B','C','D','F','E','Z'];
// X scale
var xScale = d3.scaleBand()
.domain(labels) // input
.range([0, width]); // output
// Y scale
var yScale = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);
var line = d3.line()
.x(function(d, i) { return xScale(i); })
.y(function(d) { return yScale(d.y); })
.curve(d3.curveMonotoneX)
var dataset = d3.range(n).map(function(d) { return {"y": d3.randomUniform(1)()} })
// SVGs
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 + ")");
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "white");
svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x axis call
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
// y axis call
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale));
svg.append("path")
.datum(dataset)
.attr("class", "line")
.attr("d", line);
// 12. Appends a circle for each datapoint
svg.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d, i) { return xScale(labels[i]) })
.attr("cy", function(d) { return yScale(d.y) })
.attr("r", 6);
svg.append("text")
.attr("class", "title")
.attr("x", width/2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.text("Testing");
/* 13. Basic Styling with CSS */
/* Style the lines by removing the fill and applying a stroke */
.line {
fill: none;
stroke: green;
stroke-width: 3;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: red;
stroke: #fff;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
</style>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>
<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
</script>
Updated Snippet with Lines:
var margin = {top: 50, right: 50, bottom: 50, left: 50}
, width = window.innerWidth - margin.left - margin.right
, height = window.innerHeight - margin.top - margin.bottom;
// n data points
var n = 7;
//labels
var labels = ['A','B','C','D','F','E','Z'];
// X scale
var xScale = d3.scaleBand()
.domain(labels) // input
.range([0, width]); // output
// Y scale
var yScale = d3.scaleLinear()
.domain([0, 1])
.range([height, 0]);
var line = d3.line()
.x(function(d, i) { return xScale(labels[i]); })
.y(function(d) { return yScale(d.y); })
.curve(d3.curveMonotoneX)
var dataset = d3.range(n).map(function(d) { return {"y": d3.randomUniform(1)()} })
// SVGs
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 + ")");
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "white");
svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x axis call
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
// y axis call
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale));
svg.append("path")
.datum(dataset)
.attr("class", "line")
.attr("d", line);
// 12. Appends a circle for each datapoint
svg.selectAll(".dot")
.data(dataset)
.enter().append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d, i) { return xScale(labels[i]) })
.attr("cy", function(d) { return yScale(d.y) })
.attr("r", 6);
svg.append("text")
.attr("class", "title")
.attr("x", width/2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.text("Testing");
/* 13. Basic Styling with CSS */
/* Style the lines by removing the fill and applying a stroke */
.line {
fill: none;
stroke: green;
stroke-width: 3;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: red;
stroke: #fff;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
</style>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>
<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
</script>
I am creating a line chart in D3 v4.
The x-axis is showing the year with commas like 1,998 and 1,999 instead of 1998 and 1999 etc. It is addig the thousand comma which is what I am trying to remove.
I am trying to remove the commas, but I have not been able to. tickformat is not working in v4.
<!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: 50, right: 50, bottom: 100, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
.x(function(d) { return x(d.Year); })
.y(function(d) { return y(d.Amount); });
// append the svg obgect 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.csv("australia.csv", function(error, data) {
if (error) throw error;
// format the data
data.forEach(function(d) {
d.Year = d.Year;
d.Amount = +d.Amount;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.Year}));
y.domain([0, d3.max(data, function(d) { return d.Amount; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// text label for the x axis
svg.append("text")
.attr("transform",
"translate(" + (width/2) + " ," +
(height + margin.top) + ")")
.style("text-anchor", "middle")
.text("Year");
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
// text label for the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Amount");
});
</script>
</body>
And here is my csv file:
Year,Amount
1998,103323
1999,57914.9
2003,297.969
2004,921253.8
2007,169869.2
2008,44685.5
2010,86084.5
Thanks,
You should use scaleTime for x axis, not scaleLinear:
var x = d3.scaleTime().range([0, width]);
You also should process your dataset this way:
var parseTime = d3.timeParse("%Y");
data.forEach(function(d) {
d.Year = parseTime(d.Year);
d.Amount = +d.Amount;
});
Check working example in the hidden snippet below:
var dataAsCsv = `Year,Amount
1998,103323
1999,57914.9
2003,297.969
2004,921253.8
2007,169869.2
2008,44685.5
2010,86084.5`;
// set the dimensions and margins of the graph
var margin = {top: 50, right: 50, bottom: 100, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
.x(function(d) { return x(d.Year); })
.y(function(d) { return y(d.Amount); });
// append the svg obgect 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 + ")");
var data = d3.csvParse(dataAsCsv);
var parseTime = d3.timeParse("%Y");
// format the data
data.forEach(function(d) {
d.Year = parseTime(d.Year);
d.Amount = +d.Amount;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.Year}));
y.domain([0, d3.max(data, function(d) { return d.Amount; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// text label for the x axis
svg.append("text")
.attr("transform",
"translate(" + (width/2) + " ," +
(height + margin.top) + ")")
.style("text-anchor", "middle")
.text("Year");
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
// text label for the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Amount");
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
I'm getting stuck with the D3.js v4's animation of both line & area:
It's ok to do the animation separately for line & area
When 2 animations are combined, even at the same transition duration, they do not occur together.
For the reason of styling, I cannot drop the line away.
See the illustration below:
To make thing like above, I do 2 big steps:
Do animation for line via setting the properties stroke-dasharrow and stroke-dashoffset. (Inspired from Visual Cinnamon)
Do animation for area via tweaking parameters for d3.area() function (Inspired from other Stackoverlfow post)
The result is rather disappointing because line and area underneath do not appear in parallel.
My target is to mimic the Highchart library, see an example here, and its illustration below:
It seems the Highchart library uses a different animation technique, because during DOM inspection, there is no sign of any change for the DOM paths along the animation.
Appreciated if anyone could suggest me some ideas to experiment with.
My code sample is below:
let animationDuration = 5000;
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 480 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = function (datum, boolean) {
return d3.area()
.y0(height)
.y1(function (d) { return boolean ? y(d.close) : y(d.close); })
.x(function (d) { return boolean ? x(d.date) : 0; })
(datum);
}
// define the line
var valueline = d3.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return y(d.close); });
// append the svg obgect 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 + ")");
var data = d3.csvParse(d3.select("pre#data").text());
data.reverse();
// format the data
data.forEach(function (d) {
d.date = parseTime(d.date);
d.close = +d.close;
});
// scale the range of the data
x.domain(d3.extent(data, function (d) { return d.date; }));
y.domain([0, d3.max(data, function (d) { return d.close; })]);
// add the area
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", d => area(d, false))
.attr("fill", "lightsteelblue")
.transition()
.duration(animationDuration)
.attr("d", d => area(d, true));
// add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline)
.style("stroke-dasharray", d => {
let path = document.querySelector(".line");
const totalLength = path.getTotalLength();
return `${totalLength} ${totalLength}`;
})
.style("stroke-dashoffset", d => {
let path = document.querySelector(".line");
const totalLength = path.getTotalLength();
return `${totalLength}`;
})
.transition()
.duration(animationDuration)
.style("stroke-dashoffset", 0);
// 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));
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
pre#data {display:none;}
<script src="https://d3js.org/d3.v4.min.js"></script>
<pre id="data">
date,close
1-May-12,58.13
30-Apr-12,53.98
27-Apr-12,67.00
26-Apr-12,89.70
25-Apr-12,99.00
24-Apr-12,130.28
23-Apr-12,166.70
20-Apr-12,234.98
19-Apr-12,345.44
18-Apr-12,443.34
17-Apr-12,543.70
16-Apr-12,580.13
13-Apr-12,605.23
12-Apr-12,622.77
11-Apr-12,626.20
10-Apr-12,628.44
9-Apr-12,636.23
5-Apr-12,633.68
4-Apr-12,624.31
3-Apr-12,629.32
2-Apr-12,618.63
30-Mar-12,599.55
29-Mar-12,609.86
28-Mar-12,617.62
27-Mar-12,614.48
26-Mar-12,606.98
</pre>
There is a way to animate both the line and the area, using a custom interpolator.
However, since your goal is to mimic that Highcharts animation you linked, there is a way easier alternative: use a <clipPath>.
In my proposed solution we create the area and the line the regular way. However, we reference a clipping path...
.attr("clip-path", "url(#clip)");
...in both area and line. The clipping path is created with 0 width:
var clip = svg.append("clipPath")
.attr("id", "clip");
var clipRect = clip.append("rect")
.attr("width", 0)
Then, after that, it's just a matter of applying the transition to the clipping path:
clipRect.transition()
.duration(5000)
.ease(d3.easeLinear)
.attr("width", someValue)
Here is a demo:
var svg = d3.select("svg");
var data = d3.range(30).map(d => Math.random() * 150);
var clip = svg.append("clipPath")
.attr("id", "clip");
var clipRect = clip.append("rect")
.attr("width", 0)
.attr("height", 150)
var lineGenerator = d3.line()
.x((_, i) => i * 10)
.y(d => d)
.curve(d3.curveMonotoneX)
var areaGenerator = d3.area()
.x((_, i) => i * 10)
.y1(d => d)
.y0(150)
.curve(d3.curveMonotoneX)
svg.append("path")
.attr("d", areaGenerator(data))
.attr("class", "area")
.attr("clip-path", "url(#clip)");
svg.append("path")
.attr("d", lineGenerator(data))
.attr("class", "line")
.attr("clip-path", "url(#clip)");
clipRect.transition()
.duration(5000)
.ease(d3.easeLinear)
.attr("width", 300)
.line {
fill: none;
stroke: #222;
stroke-width: 2px;
}
.area {
fill: limegreen;
stroke: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
And here is your code with those changes:
let animationDuration = 5000;
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
},
width = 480 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = function(datum, boolean) {
return d3.area()
.y0(height)
.y1(function(d) {
return boolean ? y(d.close) : y(d.close);
})
.x(function(d) {
return boolean ? x(d.date) : 0;
})
(datum);
}
// define the line
var valueline = d3.line()
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.close);
});
// append the svg obgect 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 + ")");
var clip = svg.append("clipPath")
.attr("id", "clip");
var clipRect = clip.append("rect")
.attr("width", 0)
.attr("height", height);
var data = d3.csvParse(d3.select("pre#data").text());
data.reverse();
// format the data
data.forEach(function(d) {
d.date = parseTime(d.date);
d.close = +d.close;
});
// scale the range of the data
x.domain(d3.extent(data, function(d) {
return d.date;
}));
y.domain([0, d3.max(data, function(d) {
return d.close;
})]);
// add the area
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", d => area(d, true))
.attr("fill", "lightsteelblue")
.attr("clip-path", "url(#clip)");
// add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline)
.attr("clip-path", "url(#clip)");
// 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));
clipRect.transition()
.duration(5000)
.ease(d3.easeLinear)
.attr("width", width)
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
pre#data {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<pre id="data">
date,close
1-May-12,58.13
30-Apr-12,53.98
27-Apr-12,67.00
26-Apr-12,89.70
25-Apr-12,99.00
24-Apr-12,130.28
23-Apr-12,166.70
20-Apr-12,234.98
19-Apr-12,345.44
18-Apr-12,443.34
17-Apr-12,543.70
16-Apr-12,580.13
13-Apr-12,605.23
12-Apr-12,622.77
11-Apr-12,626.20
10-Apr-12,628.44
9-Apr-12,636.23
5-Apr-12,633.68
4-Apr-12,624.31
3-Apr-12,629.32
2-Apr-12,618.63
30-Mar-12,599.55
29-Mar-12,609.86
28-Mar-12,617.62
27-Mar-12,614.48
26-Mar-12,606.98
</pre>
I am new to D3 JS and looking for a customize solution which is not available out of the box in d3 JS.
Below code produced a bar chart which denotes no. of students against 3 different classes,
Question, Can I show Circle instead of bar? please suggest some code? Thanks!
//data
let data = [{ "noOfStudents": 30, "ClassName": "Class 1" }, { "noOfStudents": 42, "ClassName": "Class 2" }, { "noOfStudents": 38, "ClassName": "Class 3" }];
// set the dimensions and margins of the graph
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]);
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 + "," + margin.top + ")");
// get and format the data
data.forEach(function (d) {
d.noOfStudents = +d.noOfStudents;
});
// Scale the range of the data in the domains
x.domain(data.map(function (d) { return d.ClassName; }));
y.domain([0, d3.max(data, function (d) { return d.noOfStudents; })]);
// 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.ClassName); })
.attr("width", x.bandwidth())
.attr("y", function (d) { return y(d.noOfStudents); })
.attr("height", function (d) { return height - y(d.noOfStudents); })
.text(function (d) { return d.noOfStudents; });
// 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));
Instead of rectangles, just append circles:
svg.selectAll(".bar")
.data(data)
.enter().append("circle")
.attr("class", "bar")
.attr("cx", function (d) { return x(d.ClassName); })
.attr("cy", function (d) { return y(d.noOfStudents); })
.attr("r", 30)
.text(function (d) { return d.noOfStudents; });
And change your band scale for a point scale:
var x = d3.scalePoint()
.range([0, width])
.padding(0.4);
Here is a fiddle: https://jsfiddle.net/kks4gcL3/