156 lines
6.6 KiB
EmacsLisp
156 lines
6.6 KiB
EmacsLisp
// 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 <svg>...</svg> 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 {
|
|
"<svg xmlns=\"http://www.w3.org/2000/svg\" " +
|
|
"width=\"" + int_to_str(width) + "\" " +
|
|
"height=\"" + int_to_str(height) + "\" " +
|
|
"viewBox=\"0 0 " + int_to_str(width) + " " + int_to_str(height) + "\" " +
|
|
"style=\"background:#0d1117;font-family:'IBM Plex Mono',monospace\">"
|
|
}
|
|
|
|
fn svg_close() -> String { "</svg>" }
|
|
|
|
fn svg_defs() -> String {
|
|
"<defs>" +
|
|
"<filter id=\"glow\"><feGaussianBlur stdDeviation=\"2\" result=\"blur\"/>" +
|
|
"<feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>" +
|
|
"</defs>"
|
|
}
|
|
|
|
// ── 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)
|
|
"<line " +
|
|
"x1=\"" + x1s + "\" y1=\"" + y1s + "\" " +
|
|
"x2=\"" + x2s + "\" y2=\"" + y2s + "\" " +
|
|
"stroke=\"" + edge_stroke_color() + "\" " +
|
|
"stroke-width=\"" + sw_str + "\" " +
|
|
"stroke-opacity=\"0.7\"/>"
|
|
}
|
|
|
|
// ── 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)
|
|
"<circle cx=\"" + xs + "\" cy=\"" + ys + "\" r=\"" + rs + "\" " +
|
|
"fill=\"" + color + "\" fill-opacity=\"0.85\" " +
|
|
"stroke=\"" + color + "\" stroke-width=\"1.5\" filter=\"url(#glow)\"/>" +
|
|
"<text x=\"" + xs + "\" y=\"" + label_y + "\" " +
|
|
"text-anchor=\"middle\" font-size=\"9\" fill=\"#8b9aaa\" " +
|
|
"font-family=\"IBM Plex Mono,monospace\">" + label_safe + "</text>"
|
|
}
|
|
|
|
// ── Position lookup ───────────────────────────────────────────────────────────
|
|
//
|
|
// Build a flat map from node_id -> position JSON in process state.
|
|
// Key: "pos_<id>" -> "{\"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
|
|
}
|