// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { select } from "d3"; import { getChartContainer } from "../plugin/root"; import { getOrCreateElement, isElementOverflowing } from "../utils/utils"; import tooltipTemplate from "../../html/tooltip.html"; import { generateHtml } from "./generateHTML"; import { selectionEvent } from "./selectionEvent"; import { Settings } from "../types"; export interface ToolTip { (selection: any): void; // double check the return value is void. alwaysShow(): boolean; alwaysShow(alwaysShow: boolean): ToolTip; centered(): boolean; centered(centered: boolean): ToolTip; settings(): Settings; settings(settings: Settings): ToolTip; } export const tooltip = (): ToolTip => { let alwaysShow = false; let tooltipDiv = null; let settings: Settings | null = null; let centered = false; const _tooltip: any = (selection) => { const node = selection.node(); if (!node || !node.isConnected) { hideTooltip(tooltipDiv); return; } const container = select(getChartContainer(node)); tooltipDiv = getTooltipDiv(container); const showTip = function (_event, data) { generateHtml(tooltipDiv, data, settings); const nodes = selection.nodes(); const i = nodes.indexOf(this); showTooltip(container.node(), nodes[i], tooltipDiv, centered); select(nodes[i]).style("opacity", "0.7"); }; const hideTip = function (_event, _data) { hideTooltip(tooltipDiv); const nodes = selection.nodes(); const i = nodes.indexOf(this); if (nodes) select(nodes[i]).style("opacity", "1"); }; if (alwaysShow) { selection.each(function (data) { return showTip.call(this, undefined, data); }); } else { selection.on("mouseover", showTip).on("mouseout", hideTip); selectionEvent().settings(settings)(selection); } }; _tooltip.alwaysShow = (...args: boolean[]): any => { if (!args.length) { return alwaysShow; } alwaysShow = args[0]; return _tooltip; }; _tooltip.centered = (...args: boolean[]): any => { if (!args.length) { return centered; } centered = args[0]; return _tooltip; }; _tooltip.settings = (...args: Settings[]): any => { if (!args.length) { return settings; } settings = args[0]; return _tooltip; }; return _tooltip; }; function getTooltipDiv(container) { return getOrCreateElement(container, "div.tooltip", () => container .append("div") .attr("class", "tooltip") .style("z-index", 3) .style("opacity", 0) .html(tooltipTemplate), ); } function showTooltip(containerNode, node, tooltipDiv, centered) { const containerRect = containerNode.getBoundingClientRect(); const rect = node.getBoundingClientRect(); let left = rect.left + rect.width / 2 - containerRect.left; let top = rect.top - containerRect.top + containerNode.scrollTop; if (centered) top = rect.top + rect.height / 2 - containerRect.top + containerNode.scrollTop; tooltipDiv .style("left", `${left}px`) .style("top", `${top}px`) // .transition() // .duration(200) .style("opacity", 0.9); if (centered) [left, top] = centerTip(tooltipDiv, containerRect); shiftIfOverflowingChartArea(tooltipDiv, containerRect, left, top, centered); } function centerTip(tooltipDiv, containerRect) { const tooltipDivRect = tooltipDiv.node().getBoundingClientRect(); const leftAdjust = tooltipDivRect.width / 2; const newLeft = tooltipDivRect.left - leftAdjust - containerRect.left; tooltipDiv.style("left", `${newLeft}px`); const topAdjust = tooltipDivRect.height / 2; const newTop = tooltipDivRect.top - topAdjust - containerRect.top; tooltipDiv.style("top", `${newTop}px`); return [newLeft, newTop]; } function shiftIfOverflowingChartArea( tooltipDiv, containerRect, left, top, centered = false, ) { const tooltipDivRect = tooltipDiv.node().getBoundingClientRect(); if (isElementOverflowing(containerRect, tooltipDivRect)) { const leftAdjust = tooltipDivRect.right - containerRect.right; tooltipDiv.style("left", `${left - leftAdjust}px`); } if (isElementOverflowing(containerRect, tooltipDivRect, "bottom")) { const topAdjust = tooltipDivRect.bottom - containerRect.bottom; tooltipDiv.style("top", `${top - topAdjust}px`); } if (!centered) return; if (isElementOverflowing(containerRect, tooltipDivRect, "left")) { const leftAdjust = tooltipDivRect.left - containerRect.left; tooltipDiv.style("left", `${left - leftAdjust}px`); } if (isElementOverflowing(containerRect, tooltipDivRect, "top")) { const topAdjust = tooltipDivRect.top - containerRect.top; tooltipDiv.style("top", `${top - topAdjust}px`); } } function hideTooltip(tooltipDiv) { if (tooltipDiv) { tooltipDiv // .transition() // .duration(500) .style("opacity", 0); } }