Appearance
Ogma + Solid.js/Vite
Quick start with a template
You can scaffold a ready-to-use Solid.js + Ogma project in seconds using the official create script:
bash
npm create @linkurious/ogma my-app -- --template solidOr run npm create @linkurious/ogma my-app and pick "solid" from the interactive template dialog.
For the integration with Solid.js, we used the recommended Solid + TS + Vite template. It bootstraps a Solid application with TypeScript support out of the box.
Unfortunately, you cannot start right away from an online playground like StackBlitz, because the Ogma package is distributed through our private registry rather than the public npm. Follow the installation guide to set up access, then run npm install @linkurious/ogma inside the template. From there we add a sample Ogma.tsx component that reads a shared counter signal and adds nodes as you click on the "Increment" button, and a Tooltip.tsx component that is rendered inside an Ogma layer.
For that we will have to use some Solid-specific patterns:
- We need to implement lazy initialization of Ogma with
onMount/onCleanup. - We isolate the component so that you can control it from the outside using a shared signal.
- We reuse a regular Solid component as a tooltip by rendering it into an Ogma layer.
Ogma component
We add a new component in src/lib/Ogma.tsx, that would create the Ogma container and interact with the rest of the UI.
First, let's add the container and styling. Note the position: relative — it lets us position layers (like the tooltip below) inside the container:
tsx
export default function OgmaGraph() {
let container!: HTMLDivElement;
return <div id="container" ref={container}></div>;
}css
#container {
position: relative;
width: 400px;
height: 400px;
margin: auto;
border: 1px solid #ccc;
}That adds the <div> element to the DOM. Now we need to initialize Ogma inside the <div> element once it is mounted, using Solid's onMount lifecycle hook, and clean it up with onCleanup:
tsx
import Ogma from '@linkurious/ogma';
import { onMount, onCleanup } from 'solid-js';
export default function OgmaGraph() {
let container!: HTMLDivElement;
onMount(() => {
const ogma = new Ogma({
container,
graph: {
nodes: [{ id: 0 }],
edges: []
}
});
ogma.styles.addNodeRule({
text: {
content: n => `${n.getId()}`,
position: 'center',
color: 'white'
}
});
onCleanup(() => {
// kill the ogma instance when the component is removed from the DOM
ogma.destroy();
});
});
return <div id="container" ref={container}></div>;
}Using a ref on the container <div> guarantees the element exists in the DOM by the time onMount runs, so we can safely pass it as the container option.
Connecting the component to the state
For this example we will be using the simplest possible state, just a counter. Solid has a built-in way to share reactive state between components: a signal. Let's declare the state in the src/lib/state.ts file:
ts
import { createSignal } from 'solid-js';
// A tiny shared signal: the number of nodes currently in the graph.
export const [count, setCount] = createSignal(1);
export function increment() {
setCount(value => value + 1);
}Here we expose the counter signal and the function to increment it. We will use this counter to create nodes in the graph. A button in the UI will increment the counter and the Ogma component will react to it and create a new node.
In the button component, we will use the count signal to show how many nodes are in the graph and the increment function on click:
tsx
import { count, increment } from './state';
export default function Counter() {
return <button onClick={increment}>Nodes: {count()}</button>;
}Let's now use the counter signal in the Ogma component to create nodes. For that we use createEffect together with on so that we can skip the initial value and only react to further increments:
tsx
import Ogma from '@linkurious/ogma';
import { onMount, onCleanup, createEffect, on } from 'solid-js';
import { count } from './state';
export default function OgmaGraph() {
let container!: HTMLDivElement;
onMount(() => {
const ogma = new Ogma({
container,
graph: { nodes: [{ id: 0 }], edges: [] }
});
// react to the shared counter signal, skipping its initial value: each
// increment adds a node and re-runs the layout
createEffect(
on(
count,
nextCount => {
ogma
.addGraph({
nodes: [{ id: nextCount }],
edges: [{ source: 0, target: nextCount }]
})
.then(() => ogma.layouts.force({ locate: true }));
},
{ defer: true }
)
);
onCleanup(() => ogma.destroy());
});
return <div id="container" ref={container}></div>;
}At this point it is possible to click the button and see the graph animated with a new node added to it.
A tooltip rendered inside an Ogma layer
A great way to reuse the framework you already know is to render Solid components inside the graph. Ogma layers are plain DOM elements overlaid on top of the canvas, so we can render any Solid component into one and drive it with reactive signals.
Let's create a small src/lib/Tooltip.tsx component. It is a completely normal component — it does not know anything about Ogma, it just renders whatever props it is given:
tsx
import { Show } from 'solid-js';
interface TooltipProps {
id: string | number | null;
degree: number;
x: number;
y: number;
visible: boolean;
}
export default function Tooltip(props: TooltipProps) {
return (
<Show when={props.visible && props.id !== null}>
<div class="tooltip" style={{ left: `${props.x}px`, top: `${props.y}px` }}>
<strong>Node {props.id}</strong>
<span>
{props.degree} neighbour{props.degree === 1 ? '' : 's'}
</span>
</div>
</Show>
);
}css
.tooltip {
position: absolute;
transform: translate(-50%, calc(-100% - 12px));
padding: 6px 10px;
border-radius: 6px;
background: #1b1b1f;
color: #fff;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
}
.tooltip span {
display: block;
opacity: 0.7;
}Back in the Ogma component, we create a few signals, add an Ogma layer and render the tooltip into it using render from solid-js/web (the same function used to mount the whole app). Then we update the signals on the mouseover / mouseout graph events — Solid re-renders only the parts of the tooltip that read the changed signal:
tsx
import Ogma from '@linkurious/ogma';
import { onMount, onCleanup, createEffect, createSignal, on } from 'solid-js';
import { render } from 'solid-js/web';
import { count } from './state';
import Tooltip from './Tooltip';
export default function OgmaGraph() {
let container!: HTMLDivElement;
onMount(() => {
const ogma = new Ogma({
container,
graph: { nodes: [{ id: 0 }], edges: [] }
});
// signals shared with the mounted tooltip component
const [id, setId] = createSignal<string | number | null>(null);
const [degree, setDegree] = createSignal(0);
const [x, setX] = createSignal(0);
const [y, setY] = createSignal(0);
const [visible, setVisible] = createSignal(false);
// add a custom layer and render the Solid tooltip into it
const layer = ogma.layers.addLayer(document.createElement('div'));
const disposeTooltip = render(
() => (
<Tooltip id={id()} degree={degree()} x={x()} y={y()} visible={visible()} />
),
layer.element
);
ogma.events
.on('mouseover', ({ target }) => {
if (!target || !target.isNode) return;
const { x, y } = ogma.view.graphToScreenCoordinates(target.getPosition());
setId(target.getId());
setDegree(target.getDegree());
setX(x);
setY(y);
setVisible(true);
})
.on('mouseout', ({ target }) => {
if (target && target.isNode) setVisible(false);
});
createEffect(
on(
count,
nextCount => {
ogma
.addGraph({
nodes: [{ id: nextCount }],
edges: [{ source: 0, target: nextCount }]
})
.then(() => ogma.layouts.force({ locate: true }));
},
{ defer: true }
)
);
onCleanup(() => {
disposeTooltip();
ogma.destroy();
});
});
return <div id="container" ref={container}></div>;
}We convert the hovered node's graph coordinates to screen coordinates with ogma.view.graphToScreenCoordinates so the tooltip lines up with the node, and we call the disposeTooltip function returned by render inside onCleanup to clean the component up together with the Ogma instance.
And that's it: click the button to add nodes, and hover any node to see the Solid tooltip.
You can download the source code of the Solid.js app here.