Appearance
Tooltip
This example shows how to build a dynamic tooltip using Ogma.layers.addOverlay
and an external tooltip component. You can see how you can handle the tooltip positioning inside of the viewport.
ts
import Ogma from '@linkurious/ogma';
import { Tooltip } from './tooltip';
import './styles.css';
const ogma = new Ogma({
graph: {
nodes: [{ id: 0 }, { id: 1 }, { id: 3 }],
edges: [
{ source: 0, target: 1, id: 0 },
{ source: 1, target: 3, id: 1 }
]
},
container: 'graph-container'
});
await ogma.layouts.force({ locate: true });
const tooltip = new Tooltip(ogma, {
placement: 'left',
// here you can use your HTML templates
content: ogma => {
const { target } = ogma.getPointerInformation();
if (target)
return `${
target.isNode ? 'node' : 'edge'
}: <span class="info">${target.getId()}</span>`;
return '';
}
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<script type="module" src="index.ts"></script>
</body>
</html>
css
html,
body {
margin: 0;
padding: 0;
font-family: Georgia, 'Times New Roman', Times, serif;
}
:root {
--overlay-background-color: #282c34;
--overlay-text-color: #61dafb;
}
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
.ogma-tooltip {
z-index: 401;
box-sizing: border-box;
}
.ogma-tooltip--content {
transform: translate(-50%, 0);
background-color: var(--overlay-background-color);
color: var(--overlay-text-color);
border-radius: 5px;
padding: 5px;
box-sizing: border-box;
box-shadow: 0 8px 30px rgb(0 0 0 / 12%);
width: auto;
height: auto;
position: relative;
}
.ogma-tooltip {
/* transition: linear;
transition-property: transform;
transition-duration: 50ms; */
pointer-events: none;
}
.ogma-tooltip--content:after {
content: '';
width: 0;
height: 0;
border-style: solid;
border-width: 6px 7px 6px 0;
border-color: transparent var(--overlay-background-color) transparent
transparent;
position: absolute;
left: 50%;
top: auto;
bottom: 3px;
right: auto;
transform: translate(-50%, 100%) rotate(270deg);
}
.ogma-tooltip--top .ogma-tooltip--content {
bottom: 6px;
transform: translate(-50%, -100%);
}
.ogma-tooltip--bottom .ogma-tooltip--content {
transform: translate(-50%, 0%);
top: 3px;
}
.ogma-tooltip--bottom .ogma-tooltip--content:after {
top: 3px;
bottom: auto;
transform: translate(-50%, -100%) rotate(90deg);
}
.ogma-tooltip--right .ogma-tooltip--content {
transform: translate(0, -50%);
left: 6px;
}
.ogma-tooltip--right .ogma-tooltip--content:after {
left: 0%;
top: 50%;
transform: translate(-100%, -50%) rotate(0deg);
}
.ogma-tooltip--left .ogma-tooltip--content {
transform: translate(-100%, -50%);
right: 6px;
}
.ogma-tooltip--left .ogma-tooltip--content:after {
right: 0%;
left: auto;
top: 50%;
transform: translate(100%, -50%) rotate(180deg);
}
.info {
color: white;
}
ts
import Ogma, { Size, Overlay } from '@linkurious/ogma';
type Point = { x: number; y: number };
type PositionGetter = (ogma: Ogma) => Point | null;
export type Content = string | ((ogma: Ogma, position: Point | null) => string);
export type Placement = 'top' | 'bottom' | 'left' | 'right' | 'center';
interface Options {
position: Point | PositionGetter;
content?: Content;
size?: Size;
visible?: boolean;
placement?: Placement;
tooltipClass?: string;
}
const defaultOptions: Required<Options> = {
tooltipClass: 'ogma-tooltip',
placement: 'right',
size: { width: 'auto', height: 'auto' } as any as Size,
visible: true,
content: '',
position: ogma =>
ogma.view.screenToGraphCoordinates(ogma.getPointerInformation())
};
export class Tooltip {
private ogma: Ogma;
private options: Required<Options>;
private timer = 0;
public size: Size = { width: 0, height: 0 };
public layer: Overlay;
constructor(ogma: Ogma, options: Partial<Options>) {
this.ogma = ogma;
this.options = { ...defaultOptions, ...options };
this.createLayer();
}
private getPosition() {
if (typeof this.options.position === 'function')
return this.options.position(this.ogma);
return this.options.position;
}
private getContent(position: Point | null) {
const content = this.options.content;
if (typeof content === 'string') return content;
else if (typeof content === 'function') return content(this.ogma, position);
return '';
}
private createLayer() {
const { tooltipClass, placement, size, visible } = this.options;
const className = getContainerClass(tooltipClass, placement);
const wrapperHtml = `<div class="${className}"><div class="${tooltipClass}--content" /></div>`;
const newCoords = this.getPosition();
this.layer = this.ogma.layers.addOverlay({
position: newCoords || { x: -9999, y: -9999 },
element: wrapperHtml,
scaled: false,
size
});
this.frame();
}
private frame = () => {
this.update();
this.timer = requestAnimationFrame(this.frame);
};
private updateSize() {
this.size = {
width: this.layer.element.offsetWidth,
height: this.layer.element.offsetHeight
};
}
private update() {
const coords = this.getPosition();
const newContent = this.getContent(coords);
this.layer.element.firstElementChild!.innerHTML = newContent;
if (!newContent) {
this.layer.hide();
return;
} else this.layer.show();
this.updateSize();
this.layer.element.className = getContainerClass(
this.options.tooltipClass,
getAdjustedPlacement(
coords as Point,
this.options.placement,
this.size,
this.ogma
)
);
this.layer.setPosition(coords);
}
setPosition(position: Point | PositionGetter) {
this.options.position = position;
}
setContent(content: Content) {
this.options.content = content;
}
getSize() {
return { ...this.size };
}
destroy() {
this.layer.destroy();
}
}
function getAdjustedPlacement(
coords: Point,
placement: Placement,
dimensions: Size,
ogma: Ogma
): Placement {
const { width: screenWidth, height: screenHeight } = ogma.view.getSize();
const { x, y } = ogma.view.graphToScreenCoordinates(coords);
let res = placement;
const { width, height } = dimensions;
if (placement === 'left' && x - width < 0) res = 'right';
else if (placement === 'right' && x + width > screenWidth) res = 'left';
else if (placement === 'bottom' && y + height > screenHeight) res = 'top';
else if (placement === 'top' && y - height < 0) res = 'bottom';
if (res === 'right' || res === 'left') {
if (y + height / 2 > screenHeight) res = 'top';
else if (y - height / 2 < 0) res = 'bottom';
} else {
if (x + width / 2 > screenWidth) res = 'left';
else if (x - width / 2 < 0) res = 'right';
}
return res;
}
function getContainerClass(popupClass: string, placement: Placement) {
return `${popupClass} ${popupClass}--${placement}`;
}