charts/shared/tooltip.mjs (178 lines of code) (raw):
import helpers from "./helpers"
import { mustache } from "./toolbelt"
/****** Example tooltip template */
// `
// <b>{{#formatDate}}date{{/formatDate}}</b><br/>
// <b>Australia</b>: {{Australia}}<br/>
// <b>France</b>: {{France}}<br/>
// <b>Germany</b>: {{Germany}}<br/>
// <b>Italy</b>: {{Italy}}<br/>
// <b>Sweden</b>: {{Sweden}}<br/>
// <b>United Kingdom</b>: {{United Kingdom}}<br/>
// `
/****** end tooltip template */
class Tooltip {
/***
Tooltip constructor
- parentSelector: provide where the tooltip element is going to be appended
- className (optional): provide additional css class names for more style control
-------------*/
constructor(template) {
this.settings = {}
this.settings.template = template
const parentSelector = "#graphicContainer"
this.$el = d3
.select(parentSelector)
.append("div")
.attr("class", `tooltip`)
.attr("width", "100px")
.attr("id", "tooltip")
.style("position", "absolute")
.style("background-color", "white")
.style("opacity", 0)
this.parentOffset = d3
.select(parentSelector)
.node()
.getBoundingClientRect().top
this.templateRender = (d) => {
return mustache(this.settings.template, { ...helpers, ...d })
}
}
updateTemplate(template) {
this.settings.template = template
}
/***
Show tooltip.
- html: HTML string to display
- containerWidth: width of area where hover events should trigger
- pos (optional): {
left: Number,
top: Number,
leftOffset: Number,
topOffset: Number
} - Provide overrides for left/top positions
-------------*/
show(html, containerWidth, containerHeight, pos) {
this.$el.html(html)
const tipWidth = this.$el.node().getBoundingClientRect().width
const tipHeight = this.$el.node().getBoundingClientRect().height
const left = pos && pos.left ? pos.left : d3.pointer(event)[0]
const top = pos && pos.top ? pos.top : d3.pointer(event)[1] - this.parentOffset
const leftOffset = pos && pos.leftOffset ? pos.leftOffset : 0
const topOffset = pos && pos.topOffset ? pos.topOffset : 0
// console.log("containerWidth:", containerWidth, "containerHeight:", containerHeight, "pageX", d3.event.pageX, "pageY", d3.event.pageY, "top", top, "parentOffset", this.parentOffset)
if (d3.pointer(event)[0] < containerWidth / 2) {
this.$el.style("left", `${d3.pointer(event)[0] + tipWidth/2 + 10}px`)
} else if (d3.pointer(event)[0] >= containerWidth / 2) {
this.$el.style("left", `${left - tipWidth - 10}px`)
}
// this.$el.style("top", `${top + topOffset}px`)
if (top < containerHeight - tipHeight) {
this.$el.style("top", `${top + topOffset}px`)
} else if (top >= containerHeight - tipHeight) {
this.$el.style("top", `${top + topOffset - tipHeight}px`)
}
this.$el.transition().duration(200).style("opacity", 0.9)
}
/***
Hide tooltip
-------------*/
hide() {
this.$el.transition().duration(500).style("opacity", 0)
}
/***
Bind events to target element.
- $bindEls: Elements that trigger the mouse events
- containerWidth: width of area where hover events should trigger
- templateRender: accepts function, string or number. Function to return the tooltip text.
(Usually this passes in the data and the mustache template will render the output)
- pos (optional): {
left: Number,
top: Number,
leftOffset: Number,
topOffset: Number
} - Provide overrides for left/top positions
-------------*/
bindEvents($bindEls, containerWidth, containerHeight, pos) {
//console.log(pos)
const self = this
$bindEls
.on("mouseover", function (d) {
let data = (d3.select(this).datum().data == undefined) ? d.target.__data__ : d3.select(this).datum().data ;
//console.log(data)
const html = self.templateRender(data)
self.show(html, containerWidth, containerHeight, pos)
})
.on("mouseout", () => {
this.hide()
})
}
drawHoverFeature(features, height, width, xVar, marginleft, chartKeyData, x, datum, spareKeys) {
let tooltipData
let templateRender = this.templateRender
let $el = d3.select("#tooltip")
var tooltip = $el.node();
const $hoverLine = features
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", height)
.style("opacity", 0)
.style("stroke", "#333")
.style("stroke-dasharray", 4)
const $hoverLayerRect = features
.append("rect")
.attr("width", width)
.attr("height", height)
.style("opacity", 0)
// Handle mouse hover event
// Find the datum based on mouse position
const getTooltipData = (event, d) => {
const bisectX = d3.bisector((d) => d[xVar]).left,
x0 = x.invert(event.clientX - marginleft),
i = bisectX(datum, x0, 1),
tooltipData = {}
spareKeys.forEach((key) => {
const datum = chartKeyData[key],
d0 = datum[i - 1],
d1 = datum[i]
if (d0 && d1) {
d = (d0 && d1) ? x0 - d0[xVar] > d1[xVar] - x0 ? d1 : d0 : d0
tooltipData[xVar] = d[xVar]
tooltipData[key] = d[key]
}
})
return tooltipData
}
$hoverLayerRect
.on("mousemove touchmove", function (event, d) {
const tooltipData = getTooltipData(event, this)
const tooltipText = templateRender(tooltipData)
$el.html(tooltipText)
let tipWidth = 200
let tipHeight = 100
let topOffset = 100
if (event.clientX < width / 2) {
$el.style("left", `${x(tooltipData[xVar]) + marginleft + 10}px`)
} else {
$el.style("left", `${x(tooltipData[xVar]) + marginleft - ( tipWidth) + 20}px`)
}
if (event.clientY > height / 2) {
$el.style("top", `${event.clientY - ( tooltip.getBoundingClientRect().height * 2 ) + 20}px`)
} else {
$el.style("top", `${event.clientY - ( tooltip.getBoundingClientRect().height ) - 20}px`)
}
$el.transition().duration(200).style("opacity", 0.9)
$hoverLine
.attr("x1", x(tooltipData[xVar]))
.attr("x2", x(tooltipData[xVar]))
.style("opacity", 0.5)
})
.on("mouseout touchend", function () {
$el.transition().duration(500).style("opacity", 0)
$hoverLine.style("opacity", 0)
})
}
}
export default Tooltip