Made with d3.annotation. An example showing how you can dynamically change the annotation type and add hover events.
Last active
April 2, 2024 07:16
-
-
Save susielu/974e41473737320f8db5ae711ded8542 to your computer and use it in GitHub Desktop.
d3-annotation: Responsive Types and Hover
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// line chart code: https://bl.ocks.org/d3noob/402dd382a51a4f6eea487f9a35566de0 | |
// time series from: http://bl.ocks.org/mbostock/3883245 | |
// set the dimensions and margins of the graph | |
const margin = {top: 20, right: 20, bottom: 30, left: 50}, | |
height = 500 - margin.top - margin.bottom; | |
const maxWidth = 860 - margin.left - margin.right; | |
let width = 860 - margin.left - margin.right; | |
const parseTime = d3.timeParse("%d-%b-%y"); | |
const x = d3.scaleTime().range([0, width]); | |
const y = d3.scaleLinear().range([height, 0]); | |
const valueline = d3.line() | |
.x(d => x(d.date)) | |
.y(d => y(d.close)); | |
const svg = d3.select("svg") | |
.attr("width", 960) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", | |
"translate(" + margin.left + "," + margin.top + ")"); | |
d3.tsv("data.hidden.tsv", function(error, data) { | |
if (error) throw error; | |
data.forEach(function(d) { | |
d.date = parseTime(d.date); | |
d.close = +d.close; | |
}); | |
x.domain(d3.extent(data, d => d.date)); | |
y.domain([0, d3.max(data, d => d.close)]); | |
svg.append("path") | |
.data([data]) | |
.attr("class", "line") | |
.attr("d", valueline); | |
svg.append("g") | |
.attr("class", "x-axis") | |
.attr("transform", "translate(0," + height + ")") | |
.call(d3.axisBottom(x)); | |
svg.append("g") | |
.call(d3.axisLeft(y)); | |
//Add annotations | |
const labels = [ | |
{ | |
data: {date: "9-Apr-12", close: 636.23}, | |
dy: 37, | |
dx: -142, | |
subject: { text: 'C', y:"bottom" }, | |
id: "minimize-badge" | |
}, | |
{ | |
data: {date: "26-Feb-08", close: 119.15}, | |
dy: -137, | |
dx: 0, | |
note: { align: "middle"}, | |
subject: { text: 'A', y:"bottom" }, | |
id: "minimize-badge" | |
}, | |
{ | |
data: {date: "18-Sep-09", close: 185.02}, | |
dy: 37, | |
dx: 42, | |
subject: { text: 'B'}, | |
id: "minimize-badge" | |
} | |
].map(l => { | |
l.note = Object.assign({}, l.note, { title: `Close: ${l.data.close}`, | |
label: `${l.data.date}`}) | |
return l | |
}) | |
//using a separate type of annotation to control the resize functionality | |
const resize = [{ | |
subject: { | |
y1: 0, | |
y2: height | |
}, | |
x: width, | |
dx: 10, | |
dy: height/2, | |
disable: ["connector"], | |
note: { | |
title: "< >", | |
label: "drag to resize", | |
lineType: "none" | |
} | |
}] | |
const timeFormat = d3.timeFormat("%d-%b-%y") | |
window.makeAnnotations = d3.annotation() | |
.annotations(labels) | |
.type(d3.annotationCalloutElbow) | |
.accessors({ x: d => x(parseTime(d.date)), | |
y: d => y(d.close) | |
}) | |
.accessorsInverse({ | |
date: d => timeFormat(x.invert(d.x)), | |
close: d => y.invert(d.y) | |
}) | |
.on('subjectover', function(annotation) { | |
//cannot reference this if you are using es6 function syntax | |
this.append('text') | |
.attr('class', 'hover') | |
.text(annotation.note.title) | |
.attr('text-anchor', 'middle') | |
.attr('y', (annotation.subject.y && annotation.subject.y == "bottom") ? 50 : -40) | |
.attr('x', -15) | |
this.append('text') | |
.attr('class', 'hover') | |
.text(annotation.note.label) | |
.attr('text-anchor', 'middle') | |
.attr('y', (annotation.subject.y && annotation.subject.y == "bottom") ? 70 : -60) | |
.attr('x', -15) | |
}) | |
.on('subjectout', function(annotation) { | |
this.selectAll('text.hover') | |
.remove() | |
}) | |
//Isn't using data for placement so accessors and accessorsInverse | |
//are not necessary | |
window.makeResize = d3.annotation() | |
.annotations(resize) | |
.type(d3.annotationXYThreshold) | |
svg.append("g") | |
.attr("class", "annotation-test") | |
.call(makeAnnotations) | |
svg.append("g") | |
.attr("class", "annotation-resize") | |
.call(makeResize) | |
svg.select(".annotation.xythreshold") | |
.call(d3.drag() | |
.on("drag", function(d) { | |
const newWidth = Math.max(0, Math.min(maxWidth, d.x + d3.event.dx)) | |
d.x = newWidth | |
const threshold = 400 | |
if (newWidth < threshold && width >= threshold){ | |
makeAnnotations.type(d3.annotationBadge) | |
svg.select("g.annotation-test").call(makeAnnotations) | |
} else if (newWidth > threshold && width <= threshold) { | |
makeAnnotations.type(d3.annotationCalloutElbow) | |
svg.select("g.annotation-test").call(makeAnnotations) | |
} | |
width = newWidth | |
x.range([0, width]) | |
makeAnnotations.updatedAccessors() | |
makeResize.updatedAccessors() | |
svg.select("g.x-axis") | |
.call(d3.axisBottom(x)); | |
svg.select("path.line") | |
.attr("d", valueline); | |
}) | |
) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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; | |
} | |
:root { | |
--accent-color: #E8336D; | |
} | |
svg { | |
background-color: white; | |
font-family: 'Lato'; | |
} | |
path.line { | |
fill: none; | |
stroke: black; | |
stroke-width: 1px; | |
} | |
.annotation path { | |
stroke: var(--accent-color); | |
fill: none; | |
} | |
.annotation path.connector-arrow{ | |
fill: var(--accent-color); | |
} | |
.annotation text { | |
fill: var(--accent-color); | |
} | |
.annotation-note-title { | |
font-weight: bold; | |
} | |
.annotation.xythreshold { | |
cursor: move; | |
} | |
.annotation.xythreshold .annotation-subject{ | |
stroke-width: 3px; | |
} | |
.annotation.badge path.subject-pointer, .annotation.badge path.subject { | |
fill: var(--accent-color); | |
stroke-width: 3px; | |
stroke-linecap: round; | |
} | |
.annotation.badge path.subject-ring { | |
fill: white; | |
stroke-width: 3px; | |
} | |
.annotation.badge .badge-text { | |
fill: white; | |
font-size: .7em; | |
} | |
rect.annotation-note-bg { | |
fill: rgba(255, 255, 255, 0); | |
} | |
text.hover { | |
font-size: .7em; | |
} | |
</style> | |
</head> | |
<body> | |
<svg width=960 height=500></svg> | |
<script src="https://d3js.org/d3.v4.js"></script> | |
<script src="https://rawgit.com/susielu/d3-annotation/master/d3-annotation.min.js"></script> | |
<script src="index.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
// line chart code: https://bl.ocks.org/d3noob/402dd382a51a4f6eea487f9a35566de0 | |
// time series from: http://bl.ocks.org/mbostock/3883245 | |
// set the dimensions and margins of the graph | |
var margin = { top: 20, right: 20, bottom: 30, left: 50 }, | |
height = 500 - margin.top - margin.bottom; | |
var maxWidth = 860 - margin.left - margin.right; | |
var width = 860 - margin.left - margin.right; | |
var parseTime = d3.timeParse("%d-%b-%y"); | |
var _x = d3.scaleTime().range([0, width]); | |
var _y = d3.scaleLinear().range([height, 0]); | |
var valueline = d3.line().x(function (d) { | |
return _x(d.date); | |
}).y(function (d) { | |
return _y(d.close); | |
}); | |
var svg = d3.select("svg").attr("width", 960).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
d3.tsv("data.hidden.tsv", function (error, data) { | |
if (error) throw error; | |
data.forEach(function (d) { | |
d.date = parseTime(d.date); | |
d.close = +d.close; | |
}); | |
_x.domain(d3.extent(data, function (d) { | |
return d.date; | |
})); | |
_y.domain([0, d3.max(data, function (d) { | |
return d.close; | |
})]); | |
svg.append("path").data([data]).attr("class", "line").attr("d", valueline); | |
svg.append("g").attr("class", "x-axis").attr("transform", "translate(0," + height + ")").call(d3.axisBottom(_x)); | |
svg.append("g").call(d3.axisLeft(_y)); | |
//Add annotations | |
var labels = [{ | |
data: { date: "9-Apr-12", close: 636.23 }, | |
dy: 37, | |
dx: -142, | |
subject: { text: 'C', y: "bottom" }, | |
id: "minimize-badge" | |
}, { | |
data: { date: "26-Feb-08", close: 119.15 }, | |
dy: -137, | |
dx: 0, | |
note: { align: "middle" }, | |
subject: { text: 'A', y: "bottom" }, | |
id: "minimize-badge" | |
}, { | |
data: { date: "18-Sep-09", close: 185.02 }, | |
dy: 37, | |
dx: 42, | |
subject: { text: 'B' }, | |
id: "minimize-badge" | |
}].map(function (l) { | |
l.note = Object.assign({}, l.note, { title: "Close: " + l.data.close, | |
label: "" + l.data.date }); | |
return l; | |
}); | |
//using a separate type of annotation to control the resize functionality | |
var resize = [{ | |
subject: { | |
y1: 0, | |
y2: height | |
}, | |
x: width, | |
dx: 10, | |
dy: height / 2, | |
disable: ["connector"], | |
note: { | |
title: "< >", | |
label: "drag to resize", | |
lineType: "none" | |
} | |
}]; | |
var timeFormat = d3.timeFormat("%d-%b-%y"); | |
window.makeAnnotations = d3.annotation().annotations(labels).type(d3.annotationCalloutElbow).accessors({ x: function x(d) { | |
return _x(parseTime(d.date)); | |
}, | |
y: function y(d) { | |
return _y(d.close); | |
} | |
}).accessorsInverse({ | |
date: function date(d) { | |
return timeFormat(_x.invert(d.x)); | |
}, | |
close: function close(d) { | |
return _y.invert(d.y); | |
} | |
}).on('subjectover', function (annotation) { | |
//cannot reference this if you are using es6 function syntax | |
this.append('text').attr('class', 'hover').text(annotation.note.title).attr('text-anchor', 'middle').attr('y', annotation.subject.y && annotation.subject.y == "bottom" ? 50 : -40).attr('x', -15); | |
this.append('text').attr('class', 'hover').text(annotation.note.label).attr('text-anchor', 'middle').attr('y', annotation.subject.y && annotation.subject.y == "bottom" ? 70 : -60).attr('x', -15); | |
}).on('subjectout', function (annotation) { | |
this.selectAll('text.hover').remove(); | |
}); | |
//Isn't using data for placement so accessors and accessorsInverse | |
//are not necessary | |
window.makeResize = d3.annotation().annotations(resize).type(d3.annotationXYThreshold); | |
svg.append("g").attr("class", "annotation-test").call(makeAnnotations); | |
svg.append("g").attr("class", "annotation-resize").call(makeResize); | |
svg.select(".annotation.xythreshold").call(d3.drag().on("drag", function (d) { | |
var newWidth = Math.max(0, Math.min(maxWidth, d.x + d3.event.dx)); | |
d.x = newWidth; | |
var threshold = 400; | |
if (newWidth < threshold && width >= threshold) { | |
makeAnnotations.type(d3.annotationBadge); | |
svg.select("g.annotation-test").call(makeAnnotations); | |
} else if (newWidth > threshold && width <= threshold) { | |
makeAnnotations.type(d3.annotationCalloutElbow); | |
svg.select("g.annotation-test").call(makeAnnotations); | |
} | |
width = newWidth; | |
_x.range([0, width]); | |
makeAnnotations.updatedAccessors(); | |
makeResize.updatedAccessors(); | |
svg.select("g.x-axis").call(d3.axisBottom(_x)); | |
svg.select("path.line").attr("d", valueline); | |
})); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment