Live Service Map
A diagram that shows the state of a system as it changes: each link between services is colored, dashed and animated according to its health, and labeled with its latency. New data arrives, the lines change, and the diagram is never rebuilt by hand.
What it uses:
- Connection styling — a
styleper connection carries its color, dashes and opacity. - Animated flow — a moving pattern whose speed reflects the state.
- Connection labels — the latency, in a label drawn by the library.
- Themes —
darkThemefor a dark dashboard.
Plain JavaScript
<div id="diagram">
<div class="service" id="gateway" style="left: 0; top: 110px">Gateway</div>
<div class="service" id="api" style="left: 260px; top: 110px">API</div>
<div class="service" id="db" style="left: 540px; top: 30px">Database</div>
<div class="service" id="cache" style="left: 540px; top: 190px">Cache</div>
</div>body {
margin: 0;
padding: 16px;
font:
14px system-ui,
sans-serif;
background: #0f172a;
color: #e2e8f0;
}
#diagram {
position: relative;
height: 300px;
}
.service {
position: absolute;
width: 120px;
padding: 12px 14px;
background: #1e293b;
border: 1px solid #475569;
border-radius: 8px;
text-align: center;
}The data lives in status. One function turns it into connections, and the timer stands in for your real source, which may be polling or a WebSocket.
import {
createVisualLinker,
darkTheme,
type ConnectionDescriptor,
} from '@macrulez/visual-linker-core'
type Status = 'ok' | 'slow' | 'down'
const links = [
{ id: 'gateway>api', from: 'gateway', to: 'api' },
{ id: 'api>db', from: 'api', to: 'db' },
{ id: 'api>cache', from: 'api', to: 'cache' },
]
const status: Record<string, { status: Status; latency: number }> = {
'gateway>api': { status: 'ok', latency: 12 },
'api>db': { status: 'ok', latency: 48 },
'api>cache': { status: 'ok', latency: 3 },
}
const look = {
ok: { color: '#4ade80', dashed: false, animated: { shape: 'dots', speed: 50 } },
slow: { color: '#fbbf24', dashed: false, animated: { shape: 'dashes', speed: 20 } },
down: { color: '#f87171', dashed: true, animated: false, opacity: 0.7 },
} as const
function describe(): ConnectionDescriptor[] {
return links.map(({ id, from, to }) => {
const { status: s, latency } = status[id]
return {
id,
from: { blockId: from },
to: { blockId: to },
style: { ...look[s], markers: { end: { shape: 'arrow' } } },
labels: [
{ id: 'latency', position: 'middle', text: s === 'down' ? 'down' : `${latency} ms` },
],
}
})
}
const diagram = document.querySelector<HTMLElement>('#diagram')!
const linker = createVisualLinker(diagram, {
theme: darkTheme,
lines: { curve: 'smoothstep', width: 2, highlight: { width: 3 } },
interaction: { highlight: true },
})
linker.setBlocks(
['gateway', 'api', 'db', 'cache'].map((id) => ({
id,
el: diagram.querySelector<HTMLElement>(`#${id}`)!,
})),
)
linker.setConnections(describe())
setInterval(() => {
const link = links[Math.floor(Math.random() * links.length)]
const next: Status[] = ['ok', 'slow', 'down']
status[link.id] = {
status: next[Math.floor(Math.random() * 3)],
latency: Math.round(Math.random() * 400),
}
linker.setConnections(describe())
}, 2000)Vue
The health data is reactive and the connections are computed from it, so any change to the data redraws the diagram. The styles are the same as above, in a <style scoped> block.
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive } from 'vue'
import { VisualLinker, darkTheme, vVlBlock } from '@macrulez/visual-linker-vue'
import type { ConnectionDescriptor, VisualLinkerConfig } from '@macrulez/visual-linker-vue'
type Status = 'ok' | 'slow' | 'down'
const services = [
{ id: 'gateway', title: 'Gateway', x: 0, y: 110 },
{ id: 'api', title: 'API', x: 260, y: 110 },
{ id: 'db', title: 'Database', x: 540, y: 30 },
{ id: 'cache', title: 'Cache', x: 540, y: 190 },
]
const links = [
{ id: 'gateway>api', from: 'gateway', to: 'api' },
{ id: 'api>db', from: 'api', to: 'db' },
{ id: 'api>cache', from: 'api', to: 'cache' },
]
const health = reactive<Record<string, { status: Status; latency: number }>>({
'gateway>api': { status: 'ok', latency: 12 },
'api>db': { status: 'ok', latency: 48 },
'api>cache': { status: 'ok', latency: 3 },
})
const look = {
ok: { color: '#4ade80', dashed: false, animated: { shape: 'dots', speed: 50 } },
slow: { color: '#fbbf24', dashed: false, animated: { shape: 'dashes', speed: 20 } },
down: { color: '#f87171', dashed: true, animated: false, opacity: 0.7 },
} as const
const connections = computed<ConnectionDescriptor[]>(() =>
links.map(({ id, from, to }) => {
const { status, latency } = health[id]
return {
id,
from: { blockId: from },
to: { blockId: to },
style: { ...look[status], markers: { end: { shape: 'arrow' } } },
labels: [
{ id: 'latency', position: 'middle', text: status === 'down' ? 'down' : `${latency} ms` },
],
}
}),
)
const config: VisualLinkerConfig = {
theme: darkTheme,
lines: { curve: 'smoothstep', width: 2, highlight: { width: 3 } },
interaction: { highlight: true },
}
let timer: ReturnType<typeof setInterval>
onMounted(() => {
timer = setInterval(() => {
const link = links[Math.floor(Math.random() * links.length)]
const statuses: Status[] = ['ok', 'slow', 'down']
health[link.id] = {
status: statuses[Math.floor(Math.random() * 3)],
latency: Math.round(Math.random() * 400),
}
}, 2000)
})
onBeforeUnmount(() => clearInterval(timer))
</script>
<template>
<VisualLinker class="diagram" :connections="connections" :config="config">
<div
v-for="service in services"
:key="service.id"
v-vl-block="service.id"
class="service"
:style="{ left: `${service.x}px`, top: `${service.y}px` }"
>
{{ service.title }}
</div>
</VisualLinker>
</template>Nuxt
The component works unchanged. The timer starts in onMounted, which the server never runs, so nothing is polled during server rendering and the first client render matches the server's. The import for VisualLinker and vVlBlock can be dropped.
How it works
- One function from data to connections.
describe()returns the full list for the current data: the endpoints, astylefor the status and a label with the latency. Passing the new list tosetConnectionsredraws the diagram. - Status as a style.
okis a green line with fast dots,slowan amber one with slow dashes,downa red dashed line without animation, dimmed withopacity. The speed of the animation tells the state without reading the label. - Vue needs no calls.
computedtrackshealth, so assigning a new status is the whole update. - Theme.
darkThemesets the colors of ports and labels for a dark page. The colors of the lines come from the status. - Highlight.
interaction: { highlight: true }turns it on, andhighlight: { width: 3 }thickens the lines of a service while the pointer is over it.
Variations
- Real data. Replace the timer with a WebSocket or a polling request that writes the new
statusandlatencyinto the same structure. - Speed from load. Compute
animated.speedfrom throughput, so that a busier link visibly flows faster. - Thresholds. Derive the status from the latency, for example
slowabove 200 ms, instead of receiving it.