Burnup charts in d3.js
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
4
down vote
favorite
The task is creating a burn-up chart from Tickets on Trello
We make use of the Trello-API in C#, to get the tickets (Done/Created/etc...) however that is not what I want reviewed, since this has been done mostly before I joined this project. However I am a bit worried about my JavaScript for the d3.js code I created.
Code
<!DOCTYPE html>
<meta charset="utf-8">
<title>d3 burn chart</title>
<div id="charts"></div>
<style>
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const burnuplist = [
Date: "/Date(1476079569717)/", Total:1, Burn:0 ,
Date: "/Date(1476684369717)/", Total:23, Burn:2 ,
Date: "/Date(1477289169717)/", Total:32, Burn:17 ,
Date: "/Date(1477897569717)/", Total:57, Burn:40 ,
Date: "/Date(1478502369717)/", Total:74, Burn:56
]
var formatTime = d3.timeFormat("%d-%m-%Y");
var drawLine = function(xvalues, yvalues, g, xscale, yscale, options)
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// line parts
const lineparts = yvalues
.map((y, i, ar) => (
i : i,
x1: xvalues[i - 1],
x2: xvalues[i],
y1 : ar[i - 1],
y2 : y
))
.filter(d => d.i > 0);
// line
g.append("g")
.attr("id", options.id)
.selectAll("line")
.data(lineparts)
.enter()
.append("line")
.attr("x1", d => xscale(d.x1))
.attr("x2", d => xscale(d.x2))
.attr("y1", d => yscale(d.y1))
.attr("y2", d => yscale(d.y2))
.attr("stroke", options.color);
if (options.circles)
g.append("g")
.selectAll("circle")
.data(lineparts)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", d => xscale(d.x2))
.attr("cy", d => yscale(d.y2))
.attr("fill", options.color)
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
var burnchart = function(xlabels, burnlist, totallist)
const svg = d3.select('svg'),
margin = top: 20, right: 40, bottom: 75, left: 50,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
// x-axis
const x = d3.scalePoint()
.domain(xlabels)
.range([0, width]);
g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
// y-axis
const y = d3.scaleLinear()
.domain([0, 1.05 * Math.max(d3.max(burnlist), d3.max(totallist))])
.range([height, 0])
.nice();
g.append("g")
.call(d3.axisLeft(y));
// total
drawLine(xlabels, totallist, g, x, y,
color: "#000000",
circles: true,
id: "totallist"
);
// burn
drawLine(xlabels, burnlist, g, x, y,
color: "#3c763d",
circles: true,
id: "burnlist"
);
d3.select('#charts')
.selectAll('svg')
.data([burnuplist])
.enter()
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc')
.call(burnchart(
burnuplist.map(d => formatTime(new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10)))),
burnuplist.map(d => d.Burn),
burnuplist.map(d => d.Total)
)
);
</script>
Result
Questions
- Am I using d3.js correctly?
- I somehow needed to make an array over the Json data
[burnuplist]
- Maybe out of scope, but is there a better way to change how I handle the Date's?
- Any general tips on JavaScript styling?
I don't write much JS, as you can see ;) Any review is welcome.
javascript d3.js
add a comment |Â
up vote
4
down vote
favorite
The task is creating a burn-up chart from Tickets on Trello
We make use of the Trello-API in C#, to get the tickets (Done/Created/etc...) however that is not what I want reviewed, since this has been done mostly before I joined this project. However I am a bit worried about my JavaScript for the d3.js code I created.
Code
<!DOCTYPE html>
<meta charset="utf-8">
<title>d3 burn chart</title>
<div id="charts"></div>
<style>
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const burnuplist = [
Date: "/Date(1476079569717)/", Total:1, Burn:0 ,
Date: "/Date(1476684369717)/", Total:23, Burn:2 ,
Date: "/Date(1477289169717)/", Total:32, Burn:17 ,
Date: "/Date(1477897569717)/", Total:57, Burn:40 ,
Date: "/Date(1478502369717)/", Total:74, Burn:56
]
var formatTime = d3.timeFormat("%d-%m-%Y");
var drawLine = function(xvalues, yvalues, g, xscale, yscale, options)
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// line parts
const lineparts = yvalues
.map((y, i, ar) => (
i : i,
x1: xvalues[i - 1],
x2: xvalues[i],
y1 : ar[i - 1],
y2 : y
))
.filter(d => d.i > 0);
// line
g.append("g")
.attr("id", options.id)
.selectAll("line")
.data(lineparts)
.enter()
.append("line")
.attr("x1", d => xscale(d.x1))
.attr("x2", d => xscale(d.x2))
.attr("y1", d => yscale(d.y1))
.attr("y2", d => yscale(d.y2))
.attr("stroke", options.color);
if (options.circles)
g.append("g")
.selectAll("circle")
.data(lineparts)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", d => xscale(d.x2))
.attr("cy", d => yscale(d.y2))
.attr("fill", options.color)
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
var burnchart = function(xlabels, burnlist, totallist)
const svg = d3.select('svg'),
margin = top: 20, right: 40, bottom: 75, left: 50,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
// x-axis
const x = d3.scalePoint()
.domain(xlabels)
.range([0, width]);
g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
// y-axis
const y = d3.scaleLinear()
.domain([0, 1.05 * Math.max(d3.max(burnlist), d3.max(totallist))])
.range([height, 0])
.nice();
g.append("g")
.call(d3.axisLeft(y));
// total
drawLine(xlabels, totallist, g, x, y,
color: "#000000",
circles: true,
id: "totallist"
);
// burn
drawLine(xlabels, burnlist, g, x, y,
color: "#3c763d",
circles: true,
id: "burnlist"
);
d3.select('#charts')
.selectAll('svg')
.data([burnuplist])
.enter()
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc')
.call(burnchart(
burnuplist.map(d => formatTime(new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10)))),
burnuplist.map(d => d.Burn),
burnuplist.map(d => d.Total)
)
);
</script>
Result
Questions
- Am I using d3.js correctly?
- I somehow needed to make an array over the Json data
[burnuplist]
- Maybe out of scope, but is there a better way to change how I handle the Date's?
- Any general tips on JavaScript styling?
I don't write much JS, as you can see ;) Any review is welcome.
javascript d3.js
add a comment |Â
up vote
4
down vote
favorite
up vote
4
down vote
favorite
The task is creating a burn-up chart from Tickets on Trello
We make use of the Trello-API in C#, to get the tickets (Done/Created/etc...) however that is not what I want reviewed, since this has been done mostly before I joined this project. However I am a bit worried about my JavaScript for the d3.js code I created.
Code
<!DOCTYPE html>
<meta charset="utf-8">
<title>d3 burn chart</title>
<div id="charts"></div>
<style>
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const burnuplist = [
Date: "/Date(1476079569717)/", Total:1, Burn:0 ,
Date: "/Date(1476684369717)/", Total:23, Burn:2 ,
Date: "/Date(1477289169717)/", Total:32, Burn:17 ,
Date: "/Date(1477897569717)/", Total:57, Burn:40 ,
Date: "/Date(1478502369717)/", Total:74, Burn:56
]
var formatTime = d3.timeFormat("%d-%m-%Y");
var drawLine = function(xvalues, yvalues, g, xscale, yscale, options)
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// line parts
const lineparts = yvalues
.map((y, i, ar) => (
i : i,
x1: xvalues[i - 1],
x2: xvalues[i],
y1 : ar[i - 1],
y2 : y
))
.filter(d => d.i > 0);
// line
g.append("g")
.attr("id", options.id)
.selectAll("line")
.data(lineparts)
.enter()
.append("line")
.attr("x1", d => xscale(d.x1))
.attr("x2", d => xscale(d.x2))
.attr("y1", d => yscale(d.y1))
.attr("y2", d => yscale(d.y2))
.attr("stroke", options.color);
if (options.circles)
g.append("g")
.selectAll("circle")
.data(lineparts)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", d => xscale(d.x2))
.attr("cy", d => yscale(d.y2))
.attr("fill", options.color)
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
var burnchart = function(xlabels, burnlist, totallist)
const svg = d3.select('svg'),
margin = top: 20, right: 40, bottom: 75, left: 50,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
// x-axis
const x = d3.scalePoint()
.domain(xlabels)
.range([0, width]);
g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
// y-axis
const y = d3.scaleLinear()
.domain([0, 1.05 * Math.max(d3.max(burnlist), d3.max(totallist))])
.range([height, 0])
.nice();
g.append("g")
.call(d3.axisLeft(y));
// total
drawLine(xlabels, totallist, g, x, y,
color: "#000000",
circles: true,
id: "totallist"
);
// burn
drawLine(xlabels, burnlist, g, x, y,
color: "#3c763d",
circles: true,
id: "burnlist"
);
d3.select('#charts')
.selectAll('svg')
.data([burnuplist])
.enter()
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc')
.call(burnchart(
burnuplist.map(d => formatTime(new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10)))),
burnuplist.map(d => d.Burn),
burnuplist.map(d => d.Total)
)
);
</script>
Result
Questions
- Am I using d3.js correctly?
- I somehow needed to make an array over the Json data
[burnuplist]
- Maybe out of scope, but is there a better way to change how I handle the Date's?
- Any general tips on JavaScript styling?
I don't write much JS, as you can see ;) Any review is welcome.
javascript d3.js
The task is creating a burn-up chart from Tickets on Trello
We make use of the Trello-API in C#, to get the tickets (Done/Created/etc...) however that is not what I want reviewed, since this has been done mostly before I joined this project. However I am a bit worried about my JavaScript for the d3.js code I created.
Code
<!DOCTYPE html>
<meta charset="utf-8">
<title>d3 burn chart</title>
<div id="charts"></div>
<style>
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const burnuplist = [
Date: "/Date(1476079569717)/", Total:1, Burn:0 ,
Date: "/Date(1476684369717)/", Total:23, Burn:2 ,
Date: "/Date(1477289169717)/", Total:32, Burn:17 ,
Date: "/Date(1477897569717)/", Total:57, Burn:40 ,
Date: "/Date(1478502369717)/", Total:74, Burn:56
]
var formatTime = d3.timeFormat("%d-%m-%Y");
var drawLine = function(xvalues, yvalues, g, xscale, yscale, options)
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// line parts
const lineparts = yvalues
.map((y, i, ar) => (
i : i,
x1: xvalues[i - 1],
x2: xvalues[i],
y1 : ar[i - 1],
y2 : y
))
.filter(d => d.i > 0);
// line
g.append("g")
.attr("id", options.id)
.selectAll("line")
.data(lineparts)
.enter()
.append("line")
.attr("x1", d => xscale(d.x1))
.attr("x2", d => xscale(d.x2))
.attr("y1", d => yscale(d.y1))
.attr("y2", d => yscale(d.y2))
.attr("stroke", options.color);
if (options.circles)
g.append("g")
.selectAll("circle")
.data(lineparts)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", d => xscale(d.x2))
.attr("cy", d => yscale(d.y2))
.attr("fill", options.color)
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
var burnchart = function(xlabels, burnlist, totallist)
const svg = d3.select('svg'),
margin = top: 20, right: 40, bottom: 75, left: 50,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
// x-axis
const x = d3.scalePoint()
.domain(xlabels)
.range([0, width]);
g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
// y-axis
const y = d3.scaleLinear()
.domain([0, 1.05 * Math.max(d3.max(burnlist), d3.max(totallist))])
.range([height, 0])
.nice();
g.append("g")
.call(d3.axisLeft(y));
// total
drawLine(xlabels, totallist, g, x, y,
color: "#000000",
circles: true,
id: "totallist"
);
// burn
drawLine(xlabels, burnlist, g, x, y,
color: "#3c763d",
circles: true,
id: "burnlist"
);
d3.select('#charts')
.selectAll('svg')
.data([burnuplist])
.enter()
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc')
.call(burnchart(
burnuplist.map(d => formatTime(new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10)))),
burnuplist.map(d => d.Burn),
burnuplist.map(d => d.Total)
)
);
</script>
Result
Questions
- Am I using d3.js correctly?
- I somehow needed to make an array over the Json data
[burnuplist]
- Maybe out of scope, but is there a better way to change how I handle the Date's?
- Any general tips on JavaScript styling?
I don't write much JS, as you can see ;) Any review is welcome.
javascript d3.js
edited Jan 26 at 0:36
Sam Onela
5,88461545
5,88461545
asked Jan 25 at 14:04
Ludisposed
5,77621758
5,77621758
add a comment |Â
add a comment |Â
2 Answers
2
active
oldest
votes
up vote
3
down vote
accepted
First of all, your code is working, which is a good thing. However... for any seasoned D3 developer your code is somewhat weird: it's not idiomatic and it has some strange patterns (which will make your life harder in the future).
Here I'll deal with your 4 bullet-point questions together, since they are deeply interconnected.
So, let's first see the major problems:
- You're binding the data to the SVG in an enter selection, but you never use it. So, just append a single SVG, without binding any data to it.
- Drop the
burnChart
and thedrawLine
functions. If you want to create a single drawing function, which is a good approach (regarding DRY), pass just the changing data to it, avoiding repetitions (the scales are always the same, for instance). But in a chart like this one such pattern is unnecessary (have a look at theeach()
in my last snippet). - You're binding a single array of numbers both to the lines and to the circles. Instead of that, just filter the
burnuplist
array accordingly (or create two separate data arrays, as I'm doing here), and bind the whole array of objects to the circles and the path (which brings us to the next point). That way, each element has a datum with all properties in the object, that we can access easily. More importantly, that way you don't rely on the index of each element in the array as you're doing right now and which is an inadequate approach. - Don't append a bunch of
<line>
elements to create a line chart. D3 has line generators that you can easily use to create a<path>
element: the whole line will be a single SVG path. This is way more convenient, and on top of that you can use curves to smooth the path. - You have time in the x axis. However, you're converting the date objects to strings and using a point scale with those strings as the domain, which is probably not the best practice. Treat time as time, using a time scale, unless you really want to treat each individual moment as a categorical variable (for instance, when the time span doesn't matter and you want the space between the ticks to be the same, regardless the actual time).
Some minor problems:
- Name all your selections. It's easier if you want to use them in the future.
- Give meaningful name to your variables, like
xScale
, not justx
. - You're mixing
const
andvar
, which may seem strange fome some people, as well as arrow functions with regular functions. - Use lowercase for the properties:
date
instead ofDate
,total
instead ofTotal
etc...
All that being said, this is my version of your code. It's a major refactor (actually, I wrote it from scratch).
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
The totalLine
, burnLine
, totalCircles
and burnCircles
may seem a lot of repetition, but this is idiomatic D3. If it bothers you, just use an each()
for DRY, like this:
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
For instance, every now and then I see people using loops (for
,forEach
,while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.
â Gerardo Furtado
Jan 26 at 13:36
add a comment |Â
up vote
2
down vote
Since Gerardo has already given a great review and focused on d3 code, I would like to focus on the Javascript code.
Abstracting opacity setting code - D.R.Y.
Per the D.R.Y. principle, the code to set the opacity on the tooltip could be abstracted to a function which accepts the duration and opacity. Building on Gerardo's advice, the tooltip element could also have a more appropriate name, like tooltipContainer
(unless you're into that whole brevity thing... then maybe just tooltip
, though that could be confusing with the CSS class).
var tooltipContainer = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Gerardo pointed out the mix of const
and var
usage. If you wanted to keep the strict usage of const
then that container could be declared using const
.
Then after that, a function can be defined that sets the opacity
var transitionTooltipToOpacity = function(duration, opacity)
tooltipContainer.transition()
.duration(duration)
.style("opacity", opacity);
;
Then the mouseover and mouseout event handlers can utilize that function.
The following lines:
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
can be simplified to the following:
.on("mouseover", function (d)
transitionTooltipToOpacity(200, .9);
tooltipContainer.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", transitionTooltipToOpacity.bind(null, 500, 0));
Notice that the mouseout handler has a partial function, which is created using Function.bind(). That way, the function will be called with the duration and opacity values pre-set (and the other arguments that are passed to the event handlers like the current datum d
, the index i
, etc. are also passed to that function in this case).
CSS border
Bearing in mind that you mostly wanted feedback on the Javascript, I noticed that the stylesheet for the tooltip contains: border: 0px
. I don't believe that is necessary, unless there is some other style declaration that would need to be overridden. If it is required, then one can omit the units (i.e. px
) or use none
. While it is over 9 years old (and about ems), this SO question seems relevant.
1
Good advice regarding the names. I myself use pretty long names, likevar horizontalAxisTickValuesFiltered
and so on.... sometimes too long!
â Gerardo Furtado
Jan 26 at 9:21
add a comment |Â
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
3
down vote
accepted
First of all, your code is working, which is a good thing. However... for any seasoned D3 developer your code is somewhat weird: it's not idiomatic and it has some strange patterns (which will make your life harder in the future).
Here I'll deal with your 4 bullet-point questions together, since they are deeply interconnected.
So, let's first see the major problems:
- You're binding the data to the SVG in an enter selection, but you never use it. So, just append a single SVG, without binding any data to it.
- Drop the
burnChart
and thedrawLine
functions. If you want to create a single drawing function, which is a good approach (regarding DRY), pass just the changing data to it, avoiding repetitions (the scales are always the same, for instance). But in a chart like this one such pattern is unnecessary (have a look at theeach()
in my last snippet). - You're binding a single array of numbers both to the lines and to the circles. Instead of that, just filter the
burnuplist
array accordingly (or create two separate data arrays, as I'm doing here), and bind the whole array of objects to the circles and the path (which brings us to the next point). That way, each element has a datum with all properties in the object, that we can access easily. More importantly, that way you don't rely on the index of each element in the array as you're doing right now and which is an inadequate approach. - Don't append a bunch of
<line>
elements to create a line chart. D3 has line generators that you can easily use to create a<path>
element: the whole line will be a single SVG path. This is way more convenient, and on top of that you can use curves to smooth the path. - You have time in the x axis. However, you're converting the date objects to strings and using a point scale with those strings as the domain, which is probably not the best practice. Treat time as time, using a time scale, unless you really want to treat each individual moment as a categorical variable (for instance, when the time span doesn't matter and you want the space between the ticks to be the same, regardless the actual time).
Some minor problems:
- Name all your selections. It's easier if you want to use them in the future.
- Give meaningful name to your variables, like
xScale
, not justx
. - You're mixing
const
andvar
, which may seem strange fome some people, as well as arrow functions with regular functions. - Use lowercase for the properties:
date
instead ofDate
,total
instead ofTotal
etc...
All that being said, this is my version of your code. It's a major refactor (actually, I wrote it from scratch).
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
The totalLine
, burnLine
, totalCircles
and burnCircles
may seem a lot of repetition, but this is idiomatic D3. If it bothers you, just use an each()
for DRY, like this:
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
For instance, every now and then I see people using loops (for
,forEach
,while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.
â Gerardo Furtado
Jan 26 at 13:36
add a comment |Â
up vote
3
down vote
accepted
First of all, your code is working, which is a good thing. However... for any seasoned D3 developer your code is somewhat weird: it's not idiomatic and it has some strange patterns (which will make your life harder in the future).
Here I'll deal with your 4 bullet-point questions together, since they are deeply interconnected.
So, let's first see the major problems:
- You're binding the data to the SVG in an enter selection, but you never use it. So, just append a single SVG, without binding any data to it.
- Drop the
burnChart
and thedrawLine
functions. If you want to create a single drawing function, which is a good approach (regarding DRY), pass just the changing data to it, avoiding repetitions (the scales are always the same, for instance). But in a chart like this one such pattern is unnecessary (have a look at theeach()
in my last snippet). - You're binding a single array of numbers both to the lines and to the circles. Instead of that, just filter the
burnuplist
array accordingly (or create two separate data arrays, as I'm doing here), and bind the whole array of objects to the circles and the path (which brings us to the next point). That way, each element has a datum with all properties in the object, that we can access easily. More importantly, that way you don't rely on the index of each element in the array as you're doing right now and which is an inadequate approach. - Don't append a bunch of
<line>
elements to create a line chart. D3 has line generators that you can easily use to create a<path>
element: the whole line will be a single SVG path. This is way more convenient, and on top of that you can use curves to smooth the path. - You have time in the x axis. However, you're converting the date objects to strings and using a point scale with those strings as the domain, which is probably not the best practice. Treat time as time, using a time scale, unless you really want to treat each individual moment as a categorical variable (for instance, when the time span doesn't matter and you want the space between the ticks to be the same, regardless the actual time).
Some minor problems:
- Name all your selections. It's easier if you want to use them in the future.
- Give meaningful name to your variables, like
xScale
, not justx
. - You're mixing
const
andvar
, which may seem strange fome some people, as well as arrow functions with regular functions. - Use lowercase for the properties:
date
instead ofDate
,total
instead ofTotal
etc...
All that being said, this is my version of your code. It's a major refactor (actually, I wrote it from scratch).
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
The totalLine
, burnLine
, totalCircles
and burnCircles
may seem a lot of repetition, but this is idiomatic D3. If it bothers you, just use an each()
for DRY, like this:
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
For instance, every now and then I see people using loops (for
,forEach
,while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.
â Gerardo Furtado
Jan 26 at 13:36
add a comment |Â
up vote
3
down vote
accepted
up vote
3
down vote
accepted
First of all, your code is working, which is a good thing. However... for any seasoned D3 developer your code is somewhat weird: it's not idiomatic and it has some strange patterns (which will make your life harder in the future).
Here I'll deal with your 4 bullet-point questions together, since they are deeply interconnected.
So, let's first see the major problems:
- You're binding the data to the SVG in an enter selection, but you never use it. So, just append a single SVG, without binding any data to it.
- Drop the
burnChart
and thedrawLine
functions. If you want to create a single drawing function, which is a good approach (regarding DRY), pass just the changing data to it, avoiding repetitions (the scales are always the same, for instance). But in a chart like this one such pattern is unnecessary (have a look at theeach()
in my last snippet). - You're binding a single array of numbers both to the lines and to the circles. Instead of that, just filter the
burnuplist
array accordingly (or create two separate data arrays, as I'm doing here), and bind the whole array of objects to the circles and the path (which brings us to the next point). That way, each element has a datum with all properties in the object, that we can access easily. More importantly, that way you don't rely on the index of each element in the array as you're doing right now and which is an inadequate approach. - Don't append a bunch of
<line>
elements to create a line chart. D3 has line generators that you can easily use to create a<path>
element: the whole line will be a single SVG path. This is way more convenient, and on top of that you can use curves to smooth the path. - You have time in the x axis. However, you're converting the date objects to strings and using a point scale with those strings as the domain, which is probably not the best practice. Treat time as time, using a time scale, unless you really want to treat each individual moment as a categorical variable (for instance, when the time span doesn't matter and you want the space between the ticks to be the same, regardless the actual time).
Some minor problems:
- Name all your selections. It's easier if you want to use them in the future.
- Give meaningful name to your variables, like
xScale
, not justx
. - You're mixing
const
andvar
, which may seem strange fome some people, as well as arrow functions with regular functions. - Use lowercase for the properties:
date
instead ofDate
,total
instead ofTotal
etc...
All that being said, this is my version of your code. It's a major refactor (actually, I wrote it from scratch).
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
The totalLine
, burnLine
, totalCircles
and burnCircles
may seem a lot of repetition, but this is idiomatic D3. If it bothers you, just use an each()
for DRY, like this:
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
First of all, your code is working, which is a good thing. However... for any seasoned D3 developer your code is somewhat weird: it's not idiomatic and it has some strange patterns (which will make your life harder in the future).
Here I'll deal with your 4 bullet-point questions together, since they are deeply interconnected.
So, let's first see the major problems:
- You're binding the data to the SVG in an enter selection, but you never use it. So, just append a single SVG, without binding any data to it.
- Drop the
burnChart
and thedrawLine
functions. If you want to create a single drawing function, which is a good approach (regarding DRY), pass just the changing data to it, avoiding repetitions (the scales are always the same, for instance). But in a chart like this one such pattern is unnecessary (have a look at theeach()
in my last snippet). - You're binding a single array of numbers both to the lines and to the circles. Instead of that, just filter the
burnuplist
array accordingly (or create two separate data arrays, as I'm doing here), and bind the whole array of objects to the circles and the path (which brings us to the next point). That way, each element has a datum with all properties in the object, that we can access easily. More importantly, that way you don't rely on the index of each element in the array as you're doing right now and which is an inadequate approach. - Don't append a bunch of
<line>
elements to create a line chart. D3 has line generators that you can easily use to create a<path>
element: the whole line will be a single SVG path. This is way more convenient, and on top of that you can use curves to smooth the path. - You have time in the x axis. However, you're converting the date objects to strings and using a point scale with those strings as the domain, which is probably not the best practice. Treat time as time, using a time scale, unless you really want to treat each individual moment as a categorical variable (for instance, when the time span doesn't matter and you want the space between the ticks to be the same, regardless the actual time).
Some minor problems:
- Name all your selections. It's easier if you want to use them in the future.
- Give meaningful name to your variables, like
xScale
, not justx
. - You're mixing
const
andvar
, which may seem strange fome some people, as well as arrow functions with regular functions. - Use lowercase for the properties:
date
instead ofDate
,total
instead ofTotal
etc...
All that being said, this is my version of your code. It's a major refactor (actually, I wrote it from scratch).
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
The totalLine
, burnLine
, totalCircles
and burnCircles
may seem a lot of repetition, but this is idiomatic D3. If it bothers you, just use an each()
for DRY, like this:
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");
var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");
var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#000");
var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", "#3c763d");
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');
var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var burnuplist = [
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
,
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
,
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
,
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
,
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
];
var margin =
top: 20,
right: 40,
bottom: 75,
left: 50
,
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate($margin.left,$margin.top)`);
var formatTime = d3.timeFormat("%d-%m-%Y");
burnuplist.forEach(function(d)
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
);
var totalData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Total
);
var burnData = burnuplist.map(function(d)
return
date: d.Date,
value: d.Burn
);
var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d)
return d.Date
));
var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d)
return d.Total
) * 1.05]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.date)
)
.y(function(d)
return yScale(d.value)
);
var gX = g.append("g")
.attr("transform", `translate(0,$height)`)
.call(d3.axisBottom(xScale).tickFormat(function(d)
return formatTime(d)
).tickValues(burnuplist.map(function(d)
return d.Date
)))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");
var gY = g.append("g")
.call(d3.axisLeft(yScale));
var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i)
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");
var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d)
return xScale(d.date)
)
.attr("cy", function(d)
return yScale(d.value)
)
.style("fill", i ? "#3c763d" : "#000");
)
d3.selectAll("circle").on("mouseover", function(d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function(d)
div.transition()
.duration(500)
.style("opacity", 0);
);
div.tooltip
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>
edited Jan 26 at 2:55
answered Jan 26 at 0:16
Gerardo Furtado
1,1442420
1,1442420
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
For instance, every now and then I see people using loops (for
,forEach
,while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.
â Gerardo Furtado
Jan 26 at 13:36
add a comment |Â
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
For instance, every now and then I see people using loops (for
,forEach
,while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.
â Gerardo Furtado
Jan 26 at 13:36
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
This is really good! Actually this is my first encounter with the d3.js library. I was hoping for an answer like this, showing me where I did horribly wrong.
â Ludisposed
Jan 26 at 13:08
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
@Ludisposed You didn't do horribly wrong... it works. It's just not idiomatic. When you use a library/language, the idiomatic use not only facilitates understanding codes and tutorials but also allows you to improve the code, adding complexity without things breaking apart.
â Gerardo Furtado
Jan 26 at 13:15
For instance, every now and then I see people using loops (
for
, forEach
, while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.â Gerardo Furtado
Jan 26 at 13:36
For instance, every now and then I see people using loops (
for
, forEach
, while
...) to append elements in a D3 code. That may even work, but not only it can go wrong in several situations as also it hinders the proper learning of D3.â Gerardo Furtado
Jan 26 at 13:36
add a comment |Â
up vote
2
down vote
Since Gerardo has already given a great review and focused on d3 code, I would like to focus on the Javascript code.
Abstracting opacity setting code - D.R.Y.
Per the D.R.Y. principle, the code to set the opacity on the tooltip could be abstracted to a function which accepts the duration and opacity. Building on Gerardo's advice, the tooltip element could also have a more appropriate name, like tooltipContainer
(unless you're into that whole brevity thing... then maybe just tooltip
, though that could be confusing with the CSS class).
var tooltipContainer = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Gerardo pointed out the mix of const
and var
usage. If you wanted to keep the strict usage of const
then that container could be declared using const
.
Then after that, a function can be defined that sets the opacity
var transitionTooltipToOpacity = function(duration, opacity)
tooltipContainer.transition()
.duration(duration)
.style("opacity", opacity);
;
Then the mouseover and mouseout event handlers can utilize that function.
The following lines:
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
can be simplified to the following:
.on("mouseover", function (d)
transitionTooltipToOpacity(200, .9);
tooltipContainer.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", transitionTooltipToOpacity.bind(null, 500, 0));
Notice that the mouseout handler has a partial function, which is created using Function.bind(). That way, the function will be called with the duration and opacity values pre-set (and the other arguments that are passed to the event handlers like the current datum d
, the index i
, etc. are also passed to that function in this case).
CSS border
Bearing in mind that you mostly wanted feedback on the Javascript, I noticed that the stylesheet for the tooltip contains: border: 0px
. I don't believe that is necessary, unless there is some other style declaration that would need to be overridden. If it is required, then one can omit the units (i.e. px
) or use none
. While it is over 9 years old (and about ems), this SO question seems relevant.
1
Good advice regarding the names. I myself use pretty long names, likevar horizontalAxisTickValuesFiltered
and so on.... sometimes too long!
â Gerardo Furtado
Jan 26 at 9:21
add a comment |Â
up vote
2
down vote
Since Gerardo has already given a great review and focused on d3 code, I would like to focus on the Javascript code.
Abstracting opacity setting code - D.R.Y.
Per the D.R.Y. principle, the code to set the opacity on the tooltip could be abstracted to a function which accepts the duration and opacity. Building on Gerardo's advice, the tooltip element could also have a more appropriate name, like tooltipContainer
(unless you're into that whole brevity thing... then maybe just tooltip
, though that could be confusing with the CSS class).
var tooltipContainer = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Gerardo pointed out the mix of const
and var
usage. If you wanted to keep the strict usage of const
then that container could be declared using const
.
Then after that, a function can be defined that sets the opacity
var transitionTooltipToOpacity = function(duration, opacity)
tooltipContainer.transition()
.duration(duration)
.style("opacity", opacity);
;
Then the mouseover and mouseout event handlers can utilize that function.
The following lines:
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
can be simplified to the following:
.on("mouseover", function (d)
transitionTooltipToOpacity(200, .9);
tooltipContainer.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", transitionTooltipToOpacity.bind(null, 500, 0));
Notice that the mouseout handler has a partial function, which is created using Function.bind(). That way, the function will be called with the duration and opacity values pre-set (and the other arguments that are passed to the event handlers like the current datum d
, the index i
, etc. are also passed to that function in this case).
CSS border
Bearing in mind that you mostly wanted feedback on the Javascript, I noticed that the stylesheet for the tooltip contains: border: 0px
. I don't believe that is necessary, unless there is some other style declaration that would need to be overridden. If it is required, then one can omit the units (i.e. px
) or use none
. While it is over 9 years old (and about ems), this SO question seems relevant.
1
Good advice regarding the names. I myself use pretty long names, likevar horizontalAxisTickValuesFiltered
and so on.... sometimes too long!
â Gerardo Furtado
Jan 26 at 9:21
add a comment |Â
up vote
2
down vote
up vote
2
down vote
Since Gerardo has already given a great review and focused on d3 code, I would like to focus on the Javascript code.
Abstracting opacity setting code - D.R.Y.
Per the D.R.Y. principle, the code to set the opacity on the tooltip could be abstracted to a function which accepts the duration and opacity. Building on Gerardo's advice, the tooltip element could also have a more appropriate name, like tooltipContainer
(unless you're into that whole brevity thing... then maybe just tooltip
, though that could be confusing with the CSS class).
var tooltipContainer = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Gerardo pointed out the mix of const
and var
usage. If you wanted to keep the strict usage of const
then that container could be declared using const
.
Then after that, a function can be defined that sets the opacity
var transitionTooltipToOpacity = function(duration, opacity)
tooltipContainer.transition()
.duration(duration)
.style("opacity", opacity);
;
Then the mouseover and mouseout event handlers can utilize that function.
The following lines:
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
can be simplified to the following:
.on("mouseover", function (d)
transitionTooltipToOpacity(200, .9);
tooltipContainer.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", transitionTooltipToOpacity.bind(null, 500, 0));
Notice that the mouseout handler has a partial function, which is created using Function.bind(). That way, the function will be called with the duration and opacity values pre-set (and the other arguments that are passed to the event handlers like the current datum d
, the index i
, etc. are also passed to that function in this case).
CSS border
Bearing in mind that you mostly wanted feedback on the Javascript, I noticed that the stylesheet for the tooltip contains: border: 0px
. I don't believe that is necessary, unless there is some other style declaration that would need to be overridden. If it is required, then one can omit the units (i.e. px
) or use none
. While it is over 9 years old (and about ems), this SO question seems relevant.
Since Gerardo has already given a great review and focused on d3 code, I would like to focus on the Javascript code.
Abstracting opacity setting code - D.R.Y.
Per the D.R.Y. principle, the code to set the opacity on the tooltip could be abstracted to a function which accepts the duration and opacity. Building on Gerardo's advice, the tooltip element could also have a more appropriate name, like tooltipContainer
(unless you're into that whole brevity thing... then maybe just tooltip
, though that could be confusing with the CSS class).
var tooltipContainer = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Gerardo pointed out the mix of const
and var
usage. If you wanted to keep the strict usage of const
then that container could be declared using const
.
Then after that, a function can be defined that sets the opacity
var transitionTooltipToOpacity = function(duration, opacity)
tooltipContainer.transition()
.duration(duration)
.style("opacity", opacity);
;
Then the mouseover and mouseout event handlers can utilize that function.
The following lines:
.on("mouseover", function (d)
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", function (d)
div.transition()
.duration(500)
.style("opacity", 0);
);
can be simplified to the following:
.on("mouseover", function (d)
transitionTooltipToOpacity(200, .9);
tooltipContainer.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
)
.on("mouseout", transitionTooltipToOpacity.bind(null, 500, 0));
Notice that the mouseout handler has a partial function, which is created using Function.bind(). That way, the function will be called with the duration and opacity values pre-set (and the other arguments that are passed to the event handlers like the current datum d
, the index i
, etc. are also passed to that function in this case).
CSS border
Bearing in mind that you mostly wanted feedback on the Javascript, I noticed that the stylesheet for the tooltip contains: border: 0px
. I don't believe that is necessary, unless there is some other style declaration that would need to be overridden. If it is required, then one can omit the units (i.e. px
) or use none
. While it is over 9 years old (and about ems), this SO question seems relevant.
answered Jan 26 at 6:04
Sam Onela
5,88461545
5,88461545
1
Good advice regarding the names. I myself use pretty long names, likevar horizontalAxisTickValuesFiltered
and so on.... sometimes too long!
â Gerardo Furtado
Jan 26 at 9:21
add a comment |Â
1
Good advice regarding the names. I myself use pretty long names, likevar horizontalAxisTickValuesFiltered
and so on.... sometimes too long!
â Gerardo Furtado
Jan 26 at 9:21
1
1
Good advice regarding the names. I myself use pretty long names, like
var horizontalAxisTickValuesFiltered
and so on.... sometimes too long!â Gerardo Furtado
Jan 26 at 9:21
Good advice regarding the names. I myself use pretty long names, like
var horizontalAxisTickValuesFiltered
and so on.... sometimes too long!â Gerardo Furtado
Jan 26 at 9:21
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f185975%2fburnup-charts-in-d3-js%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password