Skip to content

Data model ​

Two arrays describe everything on screen: entities and links. The map only reads them: no field is added, reordered or normalised in place.

AssetEntity ​

ts
interface AssetEntity<M = unknown> {
  id: EntityId
  type: string
  label: string
  sublabels?: string[]
  status?: string
  badges?: Badge[]
  parentId?: EntityId | null
  position?: XY
  meta?: M
}
FieldPurpose
idUnique across the dataset. Everything else refers to an entity by it.
typeSelects the icon and the render component. An open string union — any value is valid.
labelThe name on the plate under the circle.
sublabelsExtra lines under the label: IP, FQDN, domain. The first is drawn in the muted color, the second in the overlay color.
statusMapped to a color through themeConfig.statusColors. The package ships no status names.
badgesSmall markers under the plate.
parentIdHierarchy, used by grouping and collapsing. null or absent means a root.
positionA pinned position in canvas px. Outranks the layout engine.
metaYour own payload. Marked markRaw internally and never made reactive.

meta is what the details panel renders and what filter groups read through valuesOf. Give it a type and it flows through to the slots:

ts
interface AssetMeta {
  owner?: string
  subnet?: 'internet' | 'vpn' | 'docker'
  severity?: 'critical' | 'high' | 'medium' | 'low'
}

const entities: AssetEntity<AssetMeta>[] = [
  { id: 'ws-1', type: 'workstation', label: 'Workstation', meta: { owner: 'Dmitry', severity: 'low' } },
]
ts
interface AssetLink<M = unknown> {
  id: string
  source: EntityId
  target: EntityId
  layers: string[]
  type?: string
  label?: string
  directed?: boolean
  sourceHandle?: string
  targetHandle?: string
  meta?: M
}
FieldPurpose
layersWhich projections the link belongs to. See Views.
typeSelects the edge style through a CSS class, and a custom edge component if one is registered.
directedDraws an arrow, and constrains direction-aware traversals such as blastRadius.
labelDrawn on the line. Aggregated edges fall back to a count instead.
sourceHandle / targetHandleWhich handle on each end the line attaches to. Only for nodes that draw more than one — see Edges.
metaYour own payload, the same way as on an entity.

Parallel links between the same pair are kept: two links with different ids are two edges, and the count survives into aggregation when a group collapses.

Handling invalid data ​

buildGraph never throws on bad data. It reports through onIssue and carries on:

SituationWhat happens
Duplicate entity or link idThe first occurrence wins.
Link to a missing entityThe link is dropped.
parentId pointing at a missing entityThe node becomes a root.
A cycle in the hierarchyThe parent link is cut at the node that closed the cycle.
parentId equal to the entity's own idTreated as a root.

That matters because asset data usually arrives from inventory systems, where a dangling reference is ordinary, and the map needs to stay usable with that type of data:

ts
import { buildGraph } from 'vue-asset-map/core'

const graph = buildGraph(entities, links, {
  onIssue: (issue) => console.warn(`[asset-map] ${issue.code}: ${issue.id}`, issue.detail),
})

The indexed graph ​

buildGraph returns an AssetGraph: the same entities and links plus the indexes everything downstream reads.

ts
interface AssetGraph<E = unknown, L = unknown> {
  entities: ReadonlyMap<EntityId, AssetEntity<E>>
  links: ReadonlyMap<string, AssetLink<L>>
  adjacency: ReadonlyMap<EntityId, readonly string[]>
  childrenOf: ReadonlyMap<EntityId, readonly EntityId[]>
  roots: readonly EntityId[]
  parentOf: ReadonlyMap<EntityId, EntityId | null>
  outgoing: ReadonlyMap<EntityId, readonly string[]>
  incoming: ReadonlyMap<EntityId, readonly string[]>
}

Two details matter here. A self-loop appears in adjacency once, not twice, so a doubled loop stays distinguishable from two parallel links. parentOf holds the effective parent, already post-repair, so a cycle in the hierarchy cannot make a traversal hang.

Inside a component you rarely build the graph yourself; the map does it and exposes the result:

ts
const map = useTemplateRef<AssetMapExposed>('map')
const graph = map.value?.getGraph()

Recomputation is tied to the array references. Editing a field inside an existing entity does not rebuild the indexes. Replace the array to signal a change, the way an immutable store would.

MIT licensed