// view.el — SVG renderer for the force-directed graph.
//
// Takes layout positions + node/edge data and produces a complete SVG string.
// Rendering is purely server-side — no DOM, no JavaScript.
//
// Public API:
// graph_render_svg(nodes_json, edges_json, positions_json, width, height) -> String
// Returns a complete string ready for embedding or serving.
//
// Visual conventions:
// - Background: #0d1117 (dark, matching Studio theme)
// - Edges drawn first (below nodes)
// - Nodes: filled circle with stroke, radius by salience
// - Labels: truncated to 30 chars, below node, 10px IBM Plex Mono
// ── SVG helpers ───────────────────────────────────────────────────────────────
fn svg_open(width: Int, height: Int) -> String {
"" }
fn svg_defs() -> String {
"" +
"" +
"" +
""
}
// ── Edge rendering ────────────────────────────────────────────────────────────
fn svg_edge(x1: Float, y1: Float, x2: Float, y2: Float, weight: Float) -> String {
let sw: Float = edge_stroke_width(weight)
let sw_str: String = format_float(sw, 1)
let x1s: String = format_float(x1, 1)
let y1s: String = format_float(y1, 1)
let x2s: String = format_float(x2, 1)
let y2s: String = format_float(y2, 1)
""
}
// ── Node rendering ────────────────────────────────────────────────────────────
fn svg_node(x: Float, y: Float, radius: Int, color: String, label: String) -> String {
let xs: String = format_float(x, 1)
let ys: String = format_float(y, 1)
let rs: String = int_to_str(radius)
let label_trunc: String = node_label_truncate(label)
// Escape XML special chars in label
let label_safe: String = str_replace(str_replace(str_replace(label_trunc, "&", "&"), "<", "<"), ">", ">")
let label_y: String = format_float(y + int_to_float(radius) + int_to_float(12), 1)
"" +
"" + label_safe + ""
}
// ── Position lookup ───────────────────────────────────────────────────────────
//
// Build a flat map from node_id -> position JSON in process state.
// Key: "pos_" -> "{\"x\":...,\"y\":...}"
fn build_position_index(positions_json: String) -> Bool {
let count: Int = json_array_len(positions_json)
let i: Int = 0
while i < count {
let pos: String = json_array_get(positions_json, i)
let id: String = json_get_string(pos, "id")
if !str_eq(id, "") {
state_set("pos_" + id, pos)
}
let i = i + 1
}
true
}
fn get_pos_x(node_id: String) -> Float {
let pos: String = state_get("pos_" + node_id)
if str_eq(pos, "") { return int_to_float(0) }
json_get_float(pos, "x")
}
fn get_pos_y(node_id: String) -> Float {
let pos: String = state_get("pos_" + node_id)
if str_eq(pos, "") { return int_to_float(0) }
json_get_float(pos, "y")
}
// ── Public: graph_render_svg ──────────────────────────────────────────────────
fn graph_render_svg(nodes_json: String, edges_json: String, positions_json: String, width: Int, height: Int) -> String {
// Index positions by node id
build_position_index(positions_json)
let out: String = svg_open(width, height)
let out = out + svg_defs()
// ── Draw edges (behind nodes) ─────────────────────────────────────────────
let edge_count: Int = json_array_len(edges_json)
let i: Int = 0
while i < edge_count {
let e: String = json_array_get(edges_json, i)
let src: String = edge_source(e)
let tgt: String = edge_target(e)
let w: Float = edge_weight(e)
// Only draw if both endpoints have positions
let src_pos: String = state_get("pos_" + src)
let tgt_pos: String = state_get("pos_" + tgt)
if !str_eq(src_pos, "") {
if !str_eq(tgt_pos, "") {
let x1: Float = get_pos_x(src)
let y1: Float = get_pos_y(src)
let x2: Float = get_pos_x(tgt)
let y2: Float = get_pos_y(tgt)
let out = out + svg_edge(x1, y1, x2, y2, w)
}
}
let i = i + 1
}
// ── Draw nodes (over edges) ───────────────────────────────────────────────
let node_count: Int = json_array_len(nodes_json)
let j: Int = 0
while j < node_count {
let n: String = json_array_get(nodes_json, j)
let id: String = node_id(n)
let lbl: String = node_label(n)
let ntype: String = node_type_field(n)
let sal: Float = node_salience(n)
let color: String = node_color(ntype)
let radius: Int = node_radius_int(sal)
let pos: String = state_get("pos_" + id)
if !str_eq(pos, "") {
let x: Float = get_pos_x(id)
let y: Float = get_pos_y(id)
let out = out + svg_node(x, y, radius, color, lbl)
}
let j = j + 1
}
let out = out + svg_close()
out
}