Skip to content

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:

Plain JavaScript ​

html
<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>
css
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.

ts
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.

vue
<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, a style for the status and a label with the latency. Passing the new list to setConnections redraws the diagram.
  • Status as a style. ok is a green line with fast dots, slow an amber one with slow dashes, down a red dashed line without animation, dimmed with opacity. The speed of the animation tells the state without reading the label.
  • Vue needs no calls. computed tracks health, so assigning a new status is the whole update.
  • Theme. darkTheme sets 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, and highlight: { 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 status and latency into the same structure.
  • Speed from load. Compute animated.speed from throughput, so that a busier link visibly flows faster.
  • Thresholds. Derive the status from the latency, for example slow above 200 ms, instead of receiving it.