Files
el/ui/vessels/el-graph/src/view.el
T

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, "&", "&amp;"), "<", "&lt;"), ">", "&gt;")
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
}