Back to Blog
HTML5 CanvasReact hooksnetwork graph visualizationcybersecurity dashboardrequestAnimationFramethreat intelligence UILahorePakistan

Visualizing Cyber Threat Vectors: Real-Time Network Graphs Using HTML5 Canvas and React Hooks

Anber Aziz•Lahore, Pakistan•
Canvas network graph of threat nodes and edges updating in a React dashboard

For live cyber threat maps, HTML5 Canvas plus a React hook that owns requestAnimationFrame outperforms SVG and DOM graphs. You draw nodes and edges into one bitmap, update that scene from streaming telemetry, and keep React out of the per-frame loop. SVG is the right tool for a dozen icons. It is the wrong tool for hundreds of moving attack paths.

As corporate security platforms move to AI-driven threat hunting, displaying massive network logs becomes a UX problem. When an agent intercepts multiple compromised nodes at once, an operator needs the relationship map immediately.

Standard DOM or SVG graphs become sluggish with hundreds of live connections because animating thousands of vectors forces continuous layout recalculation. The remainder of this article is the rendering contract I use: Canvas for the scene, React for the application shell, sockets for the feed.

Who this is for

This is for product teams building security and ops dashboards, for startups whose differentiator is "see the blast radius now," and for engineering teams in Lahore, across Pakistan, and working remote-from-Pakistan who ship browser UIs to analysts on ordinary laptops.

If you are a product manager, a frozen graph during an incident is a failed feature, not a polish bug. If you are a founder, Canvas is cheaper than licensing a heavy WebGL engine for the first version. If you are an engineer, this is refs, animation frames, and hit-testing—not another chart library wrapper.

Why does Canvas beat DOM and SVG for threat graphs?

SVG is excellent for icons because each shape is a document node. Hundreds of malware edges, though, each need their own memory slice. Animating them at 60fps overloads the CPU.

The HTML5 Canvas API is a single raster buffer. The engine draws immediately onto that buffer instead of tracking separate elements. Combined with React hook dependencies, you can clear and redraw threat maps without dropping frames.

I still use SVG for the chrome around the map: legends, buttons, severity filters. Mixing is allowed. The rule is that anything that moves at frame rate should not be a DOM node. A static "critical / medium / low" key can be HTML. The pulsing red edge from a beaconing host should be Canvas.

WebGL is the next step when you are in the thousands of nodes with GPU instancing. Most product threat maps I see are hundreds of nodes with labels. 2D Canvas is the honest layer. Jumping to Three.js for a flat graph is extra complexity and extra failure modes (context loss, shader bugs) you do not need on day one.

How do I build an interactive network graph with React hooks?

"use client";

import React, { useEffect, useRef } from "react";

interface ThreatNode {
  id: string;
  x: number;
  y: number;
  severity: "low" | "medium" | "critical";
  label: string;
}

interface ThreatLink {
  sourceId: string;
  targetId: string;
}

interface ThreatMapProps {
  nodes: ThreatNode[];
  links: ThreatLink[];
}

export const InteractiveThreatMap = ({ nodes, links }: ThreatMapProps) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    let animationFrameId = 0;

    const renderLoop = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      links.forEach((link) => {
        const source = nodes.find((n) => n.id === link.sourceId);
        const target = nodes.find((n) => n.id === link.targetId);
        if (!source || !target) return;
        ctx.beginPath();
        ctx.moveTo(source.x, source.y);
        ctx.lineTo(target.x, target.y);
        ctx.strokeStyle = "rgba(239, 68, 68, 0.2)";
        ctx.lineWidth = 1.5;
        ctx.stroke();
      });

      nodes.forEach((node) => {
        ctx.beginPath();
        ctx.arc(node.x, node.y, 8, 0, 2 * Math.PI);
        ctx.fillStyle =
          node.severity === "critical"
            ? "#ef4444"
            : node.severity === "medium"
              ? "#f59e0b"
              : "#10b981";
        ctx.fill();
        ctx.fillStyle = "#6b7280";
        ctx.font = "11px monospace";
        ctx.fillText(node.label, node.x + 12, node.y + 4);
      });

      animationFrameId = requestAnimationFrame(renderLoop);
    };

    renderLoop();
    return () => cancelAnimationFrame(animationFrameId);
  }, [nodes, links]);

  return <canvas ref={canvasRef} width={960} height={540} />;
};

Optimization rules

  • requestAnimationFrame syncs draws to the monitor refresh rate.
  • Putting nodes and links in the dependency list redraws only when telemetry changes.
  • Drawing labels on the canvas avoids extra HTML overlay collision work.

Two upgrades I make before this ships. First, do not nodes.find inside the link loop. Build a Map<id, node> once per frame. Second, if positions are static between socket events, you do not need a continuous rAF loop at all—draw once when nodes or links change. I keep the loop when I pulse critical nodes or animate edge flow. I drop it when the graph is a snapshot.

Device pixel ratio matters on retina laptops used in SOCs. Set canvas.width = cssWidth * dpr, scale the context, and size the element with CSS. Otherwise the graph looks soft and operators lose trust in the picture.

How should the map consume a multi-agent WebSocket feed?

The map becomes useful when a CrewAI-style backend streams exploits over a Node.js socket. The client updates the state array, React notices the mutation, and the canvas hook recalculates coordinates on the next sweep. That pattern keeps defense dashboards responsive without dropped frames.

Batch the socket. If the agent emits 200 events a second, applying each one with setNodes will thrash React and rebuild the effect. I coalesce into a 50–100ms window, then replace the arrays once. The Canvas loop can read a sceneRef if I need sub-batch animation, but the React state remains the committed topology.

useEffect(() => {
  const socket = new WebSocket(url);
  const buffer: ThreatEvent[] = [];
  let flush = 0;

  socket.onmessage = (event) => {
    buffer.push(JSON.parse(event.data));
    if (!flush) {
      flush = window.setTimeout(() => {
        applyThreatEvents(buffer.splice(0));
        flush = 0;
      }, 75);
    }
  };

  return () => {
    socket.close();
    window.clearTimeout(flush);
  };
}, [url]);

applyThreatEvents should be a pure merge: upsert nodes by id, drop edges whose endpoints vanished, cap the graph so a noisy scanner cannot paint ten thousand circles. A visualization without a cap is a denial-of-service against the analyst's browser.

Canvas vs SVG vs WebGL for security UIs

ApproachStrengthWeaknessI pick it when
SVG / DOMHit-testing, CSS, accessibilityLayout cost at scale< ~50 static nodes
Canvas 2DPredictable redraw, simple pipelineManual picking, no DOM a11y for nodesLive maps, hundreds of edges
WebGLHuge node countsTooling and context lossEstate-wide graphs, GPU already in stack

Accessibility is the Canvas tax. A canvas is one element. I expose a side list of critical nodes as real buttons, keep keyboard focus there, and treat the bitmap as a spatial view of the same data. Screen readers do not walk your arcs.

Click handling is similarly manual. On pointerdown, convert CSS coordinates to canvas space (account for DPR), then test nodes from last-drawn to first so overlapping critical hosts win. For a few hundred nodes, a linear scan is fine. Beyond that, a simple grid hash is enough; I do not start with a quadtree.

function hitTest(nodes: ThreatNode[], x: number, y: number, radius = 10) {
  for (let i = nodes.length - 1; i >= 0; i -= 1) {
    const node = nodes[i];
    const dx = x - node.x;
    const dy = y - node.y;
    if (dx * dx + dy * dy <= radius * radius) return node;
  }
  return null;
}

function toCanvasPoint(canvas: HTMLCanvasElement, event: PointerEvent) {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  return {
    x: (event.clientX - rect.left) * scaleX,
    y: (event.clientY - rect.top) * scaleY,
  };
}

If you set CSS size independently of the bitmap size for retina, divide by DPR after scaling, or keep canvas.width equal to CSS pixels times devicePixelRatio and scale the context once at startup. Mixed conventions here are the usual reason clicks miss by a few pixels and operators stop trusting the map.

What product details make the graph usable in an incident?

Color is not enough. Critical should encode size or a ring, not only red, because a non-trivial share of analysts work in environments where red/green is a poor sole channel. Labels should collide less: hide low-severity names until hover, always show the node the operator selected.

Pause the animation loop when the tab is hidden (document.hidden) so a background SOC tab does not burn CPU. Resume on visibility. This is polite on a laptop that is also running the VPN and the ticket tool.

I build production AI and full-stack systems from Lahore, and the threat UIs I ship treat Canvas as a renderer, not as a place to hide business logic. Anber Aziz is the author because the same React-plus-socket pattern shows up whether the graph is malware edges or agent task flow.

If you are building a live security or ops visualization and want the rendering path to stay honest under stream load, services is how I work with product teams, and contact is the shortest route to a review.

FAQ

Why is HTML5 Canvas better than SVG for live threat graphs?

SVG gives every node and edge a DOM identity, so hundreds of moving links force layout and style work every frame. Canvas is one bitmap you redraw. For operator dashboards with streaming telemetry, that difference is the gap between a smooth map and a locked tab.

How do I animate a Canvas graph from React without dropping frames?

Hold a ref to the canvas, drive drawing from requestAnimationFrame, and keep the scene graph in a ref or in state that you replace in batches. Do not create or destroy the 2D context on every React render. Sync React state to the loop; do not let JSX own each particle.

Should node positions live in React state?

Authoritative topology can live in state. Per-frame physics and hover interpolation should live in a ref so React does not reconcile 60 times a second. Push React updates when the socket sends a new node or severity, not on every tick of the layout solver.

How do I make a Canvas map clickable?

Hit-test in the click handler by walking nodes in reverse draw order and testing point-in-circle (or a spatial index if the set is large). Canvas has no built-in pick. Keep a parallel structure of ids and radii; do not parse pixels.

What visualization approach fits security products built in Pakistan?

Start with Canvas, a modest node cap, and WebSocket diffs rather than a D3 SVG force graph of the whole estate. Product teams in Lahore and remote-from-Pakistan engineering groups usually ship SOC-style UIs to browsers on mixed hardware; rasterizing the graph is the reliable path.

Ada
AdaOnline
Anber's AI Assistant

Before we begin

So Anber can follow up with you if needed.