Skip to content

Instantly share code, notes, and snippets.

@susielu
Last active April 9, 2020 14:22
Show Gist options
  • Save susielu/625aa4814098671290a8c6bb88a6301e to your computer and use it in GitHub Desktop.
Save susielu/625aa4814098671290a8c6bb88a6301e to your computer and use it in GitHub Desktop.
d3-annotation v2.0

d3-annotation v2.0

Updated features for d3-annotation, a full post here.

This block uses all three of the new features:

  • the nx/ny feature for placing line labels
  • default styling, no need to import an annotation css file
  • color attribute to color the annotations
  • new badge option for the HBO and Netflix nomination callouts
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
margin = { top: 30, right: 130, bottom: 50, left: 340 }
const x = d3
.scaleLinear()
.range([margin.left, width - margin.right])
.domain([2013, 2017])
const y = d3.scaleLinear().range([height - margin.bottom, margin.top])
d3.json("yearNetwork.json", function(error, json) {
if (error) throw error
y.domain([0, d3.max(json.networkLines, d => d.max)])
var line = d3
.line()
.x(function(d) {
return x(d.year)
})
.y(function(d) {
return y(d.value)
})
const networkLines = json.networkLines
const svg = d3.select("svg")
const colors = {
HBO: "black",
Netflix: "#D32F2F",
NBC: "#ffc107",
"FX Networks": "#0097a7",
ABC: "#00BFA5",
CBS: "#00BCD4",
FOX: "#3f51b5",
Showtime: "#C5CAE9",
AMC: "#D32F2F",
PBS: "#B39DDB",
Amazon: "#ffc107",
"Nat Geo": "#ff9800",
Hulu: "#00BFA5"
}
svg.append("g").attr("class", "lineChart")
const highlight = ["HBO", "Netflix"]
svg
.select("g.lineChart")
.selectAll("path.segment")
.data(networkLines.sort((a, b) => a.total - b.total))
.enter()
.append("path")
.attr("d", d => {
return line(d.line)
})
.style("stroke", (d, i) => {
return colors[d.network] || "grey"
})
.style("stroke-dasharray", (d, i) => {
return highlight.indexOf(d.network) !== -1 ? "none" : "2, 4"
})
/* Code below relevant for annotations */
let previousNY = 0
const labelAnnotations = networkLines
.sort(
//sort annotations by last data point for ordering
(a, b) =>
b.line[b.line.length - 1].value - a.line[a.line.length - 1].value
)
.reduce((p, c) => {
//push annotation down if it will overlap
const ypx = y(c.line[c.line.length - 1].value)
let ny
if (ypx - previousNY < 10) {
ny = previousNY + 15
}
p.push({
note: { label: c.network, orientation: "leftRight", align: "middle" },
y: ypx,
x: width - margin.right,
dx: highlight.indexOf(c.network) !== -1 ? 20 : 5,
id: c.network,
color: colors[c.network],
disable: ["connector"],
ny //use ny to directly place the note in xy space if needed
})
previousNY = ny || ypx
return p
}, [])
const axisAnnotations = json.networkLines
.filter(d => d.network === "HBO")[0]
.line.map(d => ({
note: { label: d.year, align: "middle", lineType: "none" },
type: d3.annotationXYThreshold,
ny: 190,
className: "axis",
y: 190,
x: x(d.year),
subject: {
y1: y(0),
y2: y(d.value)
}
}))
const labels = networkLines
.filter(d => highlight.indexOf(d.network) !== -1)
.reduce((p, c) => {
p = p.concat(
c.line.map(d => {
return {
network: c.network,
year: d.year,
value: d.value
}
})
)
return p
}, [])
const badgeAnnotations = labels.map(d => {
return {
subject: {
text: d.value,
radius: 12
},
color: colors[d.network],
type: d3.annotationBadge,
x: x(d.year),
y: y(d.value)
}
})
const makeAnnotations = d3
.annotation()
.type(d3.annotationLabel)
.annotations([...labelAnnotations, ...axisAnnotations, ...badgeAnnotations])
d3
.select("svg")
.append("g")
.attr("class", "annotation-group")
.call(makeAnnotations)
svg
.append("line")
.attr("class", "baseline axis")
.attr("x1", x(2013))
.attr("x2", x(2017))
.attr("y1", y(0))
.attr("y2", y(0))
.style("stroke", "lightgrey")
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href='https://fonts.googleapis.com/css?family=Lato:300,900' rel='stylesheet' type='text/css'>
<style>
body {
background-color: whitesmoke;
}
svg {
background-color: white;
font-family: 'Lato';
}
path.line {
stroke: lightgrey;
}
.annotation path.connector {
stroke-dasharray: 1, 1;
}
.annotation-note-title {
font-weight: bold;
}
.annotation.xythreshold {
cursor: move;
}
.annotation text {
font-size: .7em;
text-transform: uppercase;
font-weight: bold;
}
text.title {
font-size: 1.1em;
}
.lineChart path {
fill: none;
stroke-width: 2;
}
path.domain {
stroke: lightgrey;
}
.annotation.axis .annotation-note-bg {
fill: white;
stroke: white;
stroke-width: 10;
}
.annotation.axis path {
stroke: lightgrey;
}
.annotation.axis text {
fill: lightgrey;
}
.annotation.title text {
font-size: 2em;
font-weight: 100;
}
div.title {
font-size: 2em;
width: 200px;
position: absolute;
left: 70px;
top: 100px;
font-family: "Lato"
}
.annotation.badge text {
font-weight: normal;
font-size: 10px;
}
.subject-ring {
display: none;
}
</style>
</head>
<body>
<svg width=960 height=500>
</svg>
<div class="title">
<b style="color:#d32f2f">Netflix</b> Challenges <b>HBO</b> at the 2017 Emmys
</div>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdn.rawgit.com/susielu/d3-annotation/75ff6169/d3-annotation.js"></script>
<script src="index.js"></script>
</body>
</html>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
margin = { top: 30, right: 130, bottom: 50, left: 340 };
const x = d3.scaleLinear().range([margin.left, width - margin.right]).domain([2013, 2017]);
const y = d3.scaleLinear().range([height - margin.bottom, margin.top]);
d3.json("yearNetwork.json", function (error, json) {
if (error) throw error;
y.domain([0, d3.max(json.networkLines, d => d.max)]);
var line = d3.line().x(function (d) {
return x(d.year);
}).y(function (d) {
return y(d.value);
});
const networkLines = json.networkLines;
const svg = d3.select("svg");
const colors = {
HBO: "black",
Netflix: "#D32F2F",
NBC: "#ffc107",
"FX Networks": "#0097a7",
ABC: "#00BFA5",
CBS: "#00BCD4",
FOX: "#3f51b5",
Showtime: "#C5CAE9",
AMC: "#D32F2F",
PBS: "#B39DDB",
Amazon: "#ffc107",
"Nat Geo": "#ff9800",
Hulu: "#00BFA5"
};
svg.append("g").attr("class", "lineChart");
const highlight = ["HBO", "Netflix"];
svg.select("g.lineChart").selectAll("path.segment").data(networkLines.sort((a, b) => a.total - b.total)).enter().append("path").attr("d", d => {
return line(d.line);
}).style("stroke", (d, i) => {
return colors[d.network] || "grey";
}).style("stroke-dasharray", (d, i) => {
return highlight.indexOf(d.network) !== -1 ? "none" : "2, 4";
});
/* Code below relevant for annotations */
let previousNY = 0;
const labelAnnotations = networkLines.sort(
//sort annotations by last data point for ordering
(a, b) => b.line[b.line.length - 1].value - a.line[a.line.length - 1].value).reduce((p, c) => {
//push annotation down if it will overlap
const ypx = y(c.line[c.line.length - 1].value);
let ny;
if (ypx - previousNY < 10) {
ny = previousNY + 15;
}
p.push({
note: { label: c.network, orientation: "leftRight", align: "middle" },
y: ypx,
x: width - margin.right,
dx: highlight.indexOf(c.network) !== -1 ? 20 : 5,
id: c.network,
color: colors[c.network],
disable: ["connector"],
ny //use ny to directly place the note in xy space if needed
});
previousNY = ny || ypx;
return p;
}, []);
const axisAnnotations = json.networkLines.filter(d => d.network === "HBO")[0].line.map(d => ({
note: { label: d.year, align: "middle", lineType: "none" },
type: d3.annotationXYThreshold,
ny: 190,
className: "axis",
y: 190,
x: x(d.year),
subject: {
y1: y(0),
y2: y(d.value)
}
}));
const labels = networkLines.filter(d => highlight.indexOf(d.network) !== -1).reduce((p, c) => {
p = p.concat(c.line.map(d => {
return {
network: c.network,
year: d.year,
value: d.value
};
}));
return p;
}, []);
const badgeAnnotations = labels.map(d => {
return {
subject: {
text: d.value,
radius: 12
},
color: colors[d.network],
type: d3.annotationBadge,
x: x(d.year),
y: y(d.value)
};
});
const makeAnnotations = d3.annotation().type(d3.annotationLabel).annotations([...labelAnnotations, ...axisAnnotations, ...badgeAnnotations]);
d3.select("svg").append("g").attr("class", "annotation-group").call(makeAnnotations);
svg.append("line").attr("class", "baseline axis").attr("x1", x(2013)).attr("x2", x(2017)).attr("y1", y(0)).attr("y2", y(0)).style("stroke", "lightgrey");
});
{
"networkLines": [
{
"network": "AMC",
"line": [
{ "year": "2013", "value": 26 },
{ "year": "2014", "value": 26 },
{ "year": "2015", "value": 24 },
{ "year": "2016", "value": 24 },
{ "year": "2017", "value": 13 }
],
"total": 113,
"max": 26
},
{
"network": "Showtime",
"line": [
{ "year": "2013", "value": 32 },
{ "year": "2014", "value": 24 },
{ "year": "2015", "value": 18 },
{ "year": "2016", "value": 22 },
{ "year": "2017", "value": 15 }
],
"total": 111,
"max": 32
},
{
"network": "ABC",
"line": [
{ "year": "2013", "value": 45 },
{ "year": "2014", "value": 37 },
{ "year": "2015", "value": 42 },
{ "year": "2016", "value": 35 },
{ "year": "2017", "value": 33 }
],
"total": 192,
"max": 45
},
{
"network": "Netflix",
"line": [
{ "year": "2013", "value": 14 },
{ "year": "2014", "value": 31 },
{ "year": "2015", "value": 34 },
{ "year": "2016", "value": 54 },
{ "year": "2017", "value": 91 }
],
"total": 224,
"max": 91
},
{
"network": "HBO",
"line": [
{ "year": "2013", "value": 109 },
{ "year": "2014", "value": 99 },
{ "year": "2015", "value": 126 },
{ "year": "2016", "value": 94 },
{ "year": "2017", "value": 111 }
],
"total": 539,
"max": 126
},
{
"network": "CBS",
"line": [
{ "year": "2013", "value": 54 },
{ "year": "2014", "value": 47 },
{ "year": "2015", "value": 41 },
{ "year": "2016", "value": 35 },
{ "year": "2017", "value": 29 }
],
"total": 206,
"max": 54
},
{
"network": "FX Networks",
"line": [
{ "year": "2013", "value": 26 },
{ "year": "2014", "value": 45 },
{ "year": "2015", "value": 39 },
{ "year": "2016", "value": 57 },
{ "year": "2017", "value": 55 }
],
"total": 222,
"max": 57
},
{
"network": "NBC",
"line": [
{ "year": "2013", "value": 53 },
{ "year": "2014", "value": 47 },
{ "year": "2015", "value": 43 },
{ "year": "2016", "value": 41 },
{ "year": "2017", "value": 64 }
],
"total": 248,
"max": 64
},
{
"network": "FOX",
"line": [
{ "year": "2013", "value": 20 },
{ "year": "2014", "value": 21 },
{ "year": "2015", "value": 36 },
{ "year": "2016", "value": 30 },
{ "year": "2017", "value": 20 }
],
"total": 127,
"max": 36
},
{
"network": "PBS",
"line": [
{ "year": "2013", "value": 25 },
{ "year": "2014", "value": 34 },
{ "year": "2015", "value": 30 },
{ "year": "2016", "value": 26 },
{ "year": "2017", "value": 11 }
],
"total": 126,
"max": 34
},
{
"network": "Nat Geo",
"line": [
{ "year": "2014", "value": 4 },
{ "year": "2015", "value": 4 },
{ "year": "2016", "value": 10 },
{ "year": "2017", "value": 15 }
],
"total": 33,
"max": 10
},
{
"network": "Amazon",
"line": [
{ "year": "2015", "value": 12 },
{ "year": "2016", "value": 16 },
{ "year": "2017", "value": 16 }
],
"total": 44,
"max": 16
},
{
"network": "Hulu",
"line": [{ "year": "2016", "value": 2 }, { "year": "2017", "value": 18 }],
"total": 20,
"max": 18
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment