Skip to content

Instantly share code, notes, and snippets.

@alex-r-bigelow
Last active May 20, 2019 16:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alex-r-bigelow/b7fce6bf42ff4e910f6695bd48e8a132 to your computer and use it in GitHub Desktop.
Save alex-r-bigelow/b7fce6bf42ff4e910f6695bd48e8a132 to your computer and use it in GitHub Desktop.
Tooltip demo
license: MIT
height: 300
scrolling: no
border: yes
node_modules
package-lock.json

A semi-smart function for displaying tooltips with D3.js

Functions

showTooltip([parameters])

Shows a tooltip; assumes the existence of a div with id="tooltip", that is positioned absolutely

hideTooltip()

Shorthand for hiding the tooltip

showTooltip([parameters])

Shows a tooltip; assumes the existence of a div with id="tooltip", that is positioned absolutely

Kind: global function

Param Type Default Description
[parameters] Object {} Parameter object
[parameters.content] String '' The message that will be displayed. Can be an empty string (hides the tooltip), a non-empty HTML string, or a function (which is called with the d3-selected tooltip div as the first argument)
[parameters.targetBounds] Object Specifies a bounding box that the tooltip should NOT occlude (typically, the results of a getBoundingClientRect() call)
[parameters.anchor] Object Specifies -1 to 1 positioning of the tooltip relative to targetBounds; for example, { x: -1 } right-aligns the tooltip to the left edge of targetBounds, { x: 0 } centers the tooltip horizontally, and { x: 1 } left-aligns the tooltip to the right edge of targetBounds
[parameters.hideAfterMs] Number 1000 Hides the tooltip after a certain delay; set this to zero to disable hiding

hideTooltip()

Shorthand for hiding the tooltip

Kind: global function

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tooltip demo</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<script type="module" src="script.js"></script>
</head>
<body>
Page contents go here
<div id="htmlTooltip" class="hoverable">HTML tooltip</div>
<div id="jsTooltip" class="hoverable">JS tooltip</div>
<!-- the gl_keep class is important for goldenlayout popouts, if you use them -->
<div id="tooltip" class="gl_keep" style="display:none"></div>
</body>
</html>
/* globals d3 */
import { showTooltip, hideTooltip } from './tooltip.js';
// An example of an HTML-based tooltip:
d3.select('#htmlTooltip').on('mouseenter', () => {
showTooltip({
content: 'This is an <strong>HTML-based</strong> tooltip',
targetBounds: d3.select('#htmlTooltip').node().getBoundingClientRect(),
anchor: { x: 0, y: 1 }
});
}).on('mouseleave', hideTooltip);
// An example of a JS-based tooltip:
d3.select('#jsTooltip').on('mouseenter', () => {
showTooltip({
content: tooltip => {
tooltip.html('This is a <span class="makeMeBold">Javascript-based</span> tooltip');
tooltip.select('.makeMeBold').style('font-weight', 'bold');
},
targetBounds: d3.select('#jsTooltip').node().getBoundingClientRect()
});
}).on('mouseleave', hideTooltip);
#tooltip {
position: absolute;
padding: 0.5em;
border-radius: 0.5em;
background: #333;
color: #fff;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.25);
}
.hoverable {
display: inline-block;
cursor: pointer;
padding: 1em;
}
/* globals d3 */
/**
* Shows a tooltip; assumes the existence of a div with `id="tooltip"`, that is
* positioned absolutely
* @param {Object} [parameters={}]
* Parameter object
* @param {String} [parameters.content='']
* The message that will be displayed. Can be an empty string (hides the
* tooltip), a non-empty HTML string, or a function (which is called with the
* d3-selected tooltip div as the first argument)
* @param {Object} [parameters.targetBounds=null]
* Specifies a bounding box that the tooltip should NOT occlude (typically, the
* results of a getBoundingClientRect() call)
* @param {Object} [parameters.anchor=null]
* Specifies -1 to 1 positioning of the tooltip relative to targetBounds; for
* example, { x: -1 } right-aligns the tooltip to the left edge of
* targetBounds, { x: 0 } centers the tooltip horizontally, and { x: 1 }
* left-aligns the tooltip to the right edge of targetBounds
* @param {Number} [parameters.hideAfterMs=1000]
* Hides the tooltip after a certain delay; set this to zero to disable hiding
*/
function showTooltip ({ content = '', targetBounds = null, anchor = null, hideAfterMs = 1000 } = {}) {
window.clearTimeout(window._tooltipTimeout);
const showEvent = d3.event;
d3.select('body').on('click.tooltip', () => {
if (showEvent !== d3.event) {
hideTooltip();
} else {
d3.event.stopPropagation();
}
});
let tooltip = d3.select('#tooltip')
.style('left', null)
.style('top', null)
.style('width', null)
.style('display', content ? null : 'none');
if (content) {
if (typeof content === 'function') {
content(tooltip);
} else {
tooltip.html(content);
}
let tooltipBounds = tooltip.node().getBoundingClientRect();
let left;
let top;
if (targetBounds === null) {
// todo: position the tooltip WITHIN the window, based on anchor,
// instead of outside the targetBounds
throw new Error('tooltips without targetBounds are not yet supported');
} else {
anchor = anchor || {};
if (anchor.x === undefined) {
if (anchor.y !== undefined) {
// with y defined, default is to center x
anchor.x = 0;
} else {
if (targetBounds.left > window.innerWidth - targetBounds.right) {
// there's more space on the left; try to put it there
anchor.x = -1;
} else {
// more space on the right; try to put it there
anchor.x = 1;
}
}
}
if (anchor.y === undefined) {
if (anchor.x !== undefined) {
// with x defined, default is to center y
anchor.y = 0;
} else {
if (targetBounds.top > window.innerHeight - targetBounds.bottom) {
// more space above; try to put it there
anchor.y = -1;
} else {
// more space below; try to put it there
anchor.y = 1;
}
}
}
left = (targetBounds.left + targetBounds.right) / 2 +
anchor.x * targetBounds.width / 2 -
tooltipBounds.width / 2 +
anchor.x * tooltipBounds.width / 2;
top = (targetBounds.top + targetBounds.bottom) / 2 +
anchor.y * targetBounds.height / 2 -
tooltipBounds.height / 2 +
anchor.y * tooltipBounds.height / 2;
}
// Clamp the tooltip so that it stays on screen
if (left + tooltipBounds.width > window.innerWidth) {
left = window.innerWidth - tooltipBounds.width;
}
if (left < 0) {
left = 0;
}
if (top + tooltipBounds.height > window.innerHeight) {
top = window.innerHeight - tooltipBounds.height;
}
if (top < 0) {
top = 0;
}
tooltip.style('left', left + 'px')
.style('top', top + 'px');
window.clearTimeout(window._tooltipTimeout);
if (hideAfterMs > 0) {
window._tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, hideAfterMs);
}
}
}
/**
* Shorthand for hiding the tooltip
*/
function hideTooltip () {
showTooltip();
}
export { showTooltip, hideTooltip };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment