diff --git a/crates/goose-mcp/src/autovisualiser/mod.rs b/crates/goose-mcp/src/autovisualiser/mod.rs
index 4f4b6efe8..e63469672 100644
--- a/crates/goose-mcp/src/autovisualiser/mod.rs
+++ b/crates/goose-mcp/src/autovisualiser/mod.rs
@@ -1720,6 +1720,64 @@ mod tests {
assert!(result.is_ok());
assert_mcp_apps_result(&result.unwrap(), "ui://autovisualiser/mermaid", "mermaid");
}
+
+ #[test]
+ fn donut_legend_renders_dynamic_values_as_text() {
+ let html = AutoVisualiserRouter::new()
+ .get_template_html("ui://autovisualiser/donut")
+ .unwrap();
+
+ assert!(!html.contains("legendEl.innerHTML"));
+ assert!(html.contains("labelEl.textContent = label"));
+ assert!(html.contains("valueEl.textContent = pct"));
+ }
+
+ #[test]
+ fn chord_tooltips_render_dynamic_values_as_text() {
+ let html = AutoVisualiserRouter::new()
+ .get_template_html("ui://autovisualiser/chord")
+ .unwrap();
+
+ assert!(!html.contains(".html("));
+ assert!(html.contains(".text(line)"));
+ assert!(html.contains("data.labels[d.source.index]"));
+ assert!(html.contains("data.labels[d.target.index]"));
+ }
+
+ #[test]
+ fn mermaid_errors_render_as_text_without_changing_svg_rendering() {
+ let html = AutoVisualiserRouter::new()
+ .get_template_html("ui://autovisualiser/mermaid")
+ .unwrap();
+
+ assert!(html.contains("errorEl.textContent = String(err.message || err)"));
+ assert!(html.contains("output.replaceChildren(errorEl)"));
+ assert!(html.contains("output.innerHTML = result.svg"));
+ assert!(!html.contains("'
' +"));
+ }
+
+ #[test]
+ fn sankey_tooltips_render_dynamic_values_as_text() {
+ let html = AutoVisualiserRouter::new()
+ .get_template_html("ui://autovisualiser/sankey")
+ .unwrap();
+
+ assert!(!html.contains(".html("));
+ assert!(html.contains("tooltip.append(index === 0 ? \"strong\" : \"span\").text(line)"));
+ assert!(html.contains("d.source.name + \" → \" + d.target.name"));
+ assert!(html.contains("var lines = [d.name]"));
+ }
+
+ #[test]
+ fn treemap_tooltips_render_dynamic_values_as_text() {
+ let html = AutoVisualiserRouter::new()
+ .get_template_html("ui://autovisualiser/treemap")
+ .unwrap();
+
+ assert!(!html.contains("tt.innerHTML"));
+ assert!(html.contains("name.textContent = d.data.name"));
+ assert!(html.contains("document.createTextNode(d.data.category)"));
+ }
}
#[cfg(test)]
diff --git a/crates/goose-mcp/src/autovisualiser/templates/chord_template.html b/crates/goose-mcp/src/autovisualiser/templates/chord_template.html
index 3ccf8c13a..1ad8dda28 100644
--- a/crates/goose-mcp/src/autovisualiser/templates/chord_template.html
+++ b/crates/goose-mcp/src/autovisualiser/templates/chord_template.html
@@ -87,8 +87,11 @@
.attr("d", arcGen)
.on("mouseover", function (event, d) {
fadeChords(g, d.index, 0.08);
- tooltipEl.style("opacity", 1)
- .html("
" + data.labels[d.index] + "Total: " + d.value.toLocaleString());
+ setTooltip([
+ data.labels[d.index],
+ "Total: " + d.value.toLocaleString(),
+ ], true);
+ tooltipEl.style("opacity", 1);
McpAppBridge.positionTooltip(tooltipEl, event);
})
.on("mouseout", function () {
@@ -114,9 +117,11 @@
d3.select(this).style("fill-opacity", 0.8);
var s2t = data.matrix[d.source.index][d.target.index];
var t2s = data.matrix[d.target.index][d.source.index];
- tooltipEl.style("opacity", 1)
- .html(data.labels[d.source.index] + " → " + data.labels[d.target.index] + ": " + s2t.toLocaleString() +
- "
" + data.labels[d.target.index] + " → " + data.labels[d.source.index] + ": " + t2s.toLocaleString());
+ setTooltip([
+ data.labels[d.source.index] + " → " + data.labels[d.target.index] + ": " + s2t.toLocaleString(),
+ data.labels[d.target.index] + " → " + data.labels[d.source.index] + ": " + t2s.toLocaleString(),
+ ], false);
+ tooltipEl.style("opacity", 1);
McpAppBridge.positionTooltip(tooltipEl, event);
})
.on("mouseout", function () {
@@ -125,6 +130,14 @@
});
}
+ function setTooltip(lines, boldFirst) {
+ tooltipEl.selectAll("*").remove();
+ lines.forEach(function (line, index) {
+ if (index > 0) tooltipEl.append("br");
+ tooltipEl.append(index === 0 && boldFirst ? "strong" : "span").text(line);
+ });
+ }
+
function fadeChords(g, focusIndex, opacity) {
g.selectAll(".chord-ribbon").style("opacity", function (d) {
if (focusIndex === null) return 1;
diff --git a/crates/goose-mcp/src/autovisualiser/templates/donut_template.html b/crates/goose-mcp/src/autovisualiser/templates/donut_template.html
index 803f75dde..217d1b4dc 100644
--- a/crates/goose-mcp/src/autovisualiser/templates/donut_template.html
+++ b/crates/goose-mcp/src/autovisualiser/templates/donut_template.html
@@ -134,9 +134,25 @@
var total = cd.values.reduce(function (a, b) { return a + b; }, 0);
cd.labels.forEach(function (label, i) {
var pct = total > 0 ? ((cd.values[i] / total) * 100).toFixed(1) : "0";
- legendEl.innerHTML += '
' +
- '
' + label + '' +
- '
' + pct + '%';
+ var item = document.createElement("div");
+ item.className = "legend-item";
+
+ var dot = document.createElement("div");
+ dot.className = "legend-dot";
+ dot.style.background = cd.colors[i];
+ item.appendChild(dot);
+
+ var labelEl = document.createElement("span");
+ labelEl.className = "legend-label";
+ labelEl.textContent = label;
+ item.appendChild(labelEl);
+
+ var valueEl = document.createElement("span");
+ valueEl.className = "legend-value";
+ valueEl.textContent = pct + "%";
+ item.appendChild(valueEl);
+
+ legendEl.appendChild(item);
});
card.appendChild(legendEl);
grid.appendChild(card);
diff --git a/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html b/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html
index 66ba61ca4..7257d33c8 100644
--- a/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html
+++ b/crates/goose-mcp/src/autovisualiser/templates/mermaid_template.html
@@ -135,7 +135,10 @@
}).catch(function (err) {
// Remove any error artefacts mermaid injected (bomb-icon SVGs, etc.)
document.querySelectorAll('[id^="d' + uid + '"]').forEach(function (el) { el.remove(); });
- output.innerHTML = '
' + (err.message || err) + "
";
+ var errorEl = document.createElement("div");
+ errorEl.className = "mermaid-error";
+ errorEl.textContent = String(err.message || err);
+ output.replaceChildren(errorEl);
setTimeout(McpAppBridge.reportSize, 80);
});
}
diff --git a/crates/goose-mcp/src/autovisualiser/templates/sankey_template.html b/crates/goose-mcp/src/autovisualiser/templates/sankey_template.html
index 3ca6db709..260fbfb79 100644
--- a/crates/goose-mcp/src/autovisualiser/templates/sankey_template.html
+++ b/crates/goose-mcp/src/autovisualiser/templates/sankey_template.html
@@ -172,8 +172,11 @@
.attr("stroke", function (d) { return nodeColor(d.source); })
.attr("stroke-width", function (d) { return Math.max(1, d.width); })
.on("mouseover", function (event, d) {
- tooltip.style("opacity", 1)
- .html("
" + d.source.name + " → " + d.target.name + "" + d.value.toLocaleString());
+ setTooltip([
+ d.source.name + " → " + d.target.name,
+ d.value.toLocaleString(),
+ ]);
+ tooltip.style("opacity", 1);
McpAppBridge.positionTooltip(tooltip, event);
})
.on("mouseout", function () { tooltip.style("opacity", 0); });
@@ -188,10 +191,11 @@
.on("mouseover", function (event, d) {
var totalIn = d.targetLinks.reduce(function (s, l) { return s + l.value; }, 0);
var totalOut = d.sourceLinks.reduce(function (s, l) { return s + l.value; }, 0);
- tooltip.style("opacity", 1)
- .html("
" + d.name + "" +
- (totalIn ? "
In: " + totalIn.toLocaleString() : "") +
- (totalOut ? "
Out: " + totalOut.toLocaleString() : ""));
+ var lines = [d.name];
+ if (totalIn) lines.push("In: " + totalIn.toLocaleString());
+ if (totalOut) lines.push("Out: " + totalOut.toLocaleString());
+ setTooltip(lines);
+ tooltip.style("opacity", 1);
McpAppBridge.positionTooltip(tooltip, event);
})
.on("mouseout", function () { tooltip.style("opacity", 0); });
@@ -205,6 +209,14 @@
.text(function (d) { return d.name; });
}
+ function setTooltip(lines) {
+ tooltip.selectAll("*").remove();
+ lines.forEach(function (line, index) {
+ if (index > 0) tooltip.append("br");
+ tooltip.append(index === 0 ? "strong" : "span").text(line);
+ });
+ }
+
function renderData(data) {
document.getElementById("loadingIndicator").classList.add("hidden");
initializeSVG();
diff --git a/crates/goose-mcp/src/autovisualiser/templates/treemap_template.html b/crates/goose-mcp/src/autovisualiser/templates/treemap_template.html
index 7bd592a68..9e2f8c84f 100644
--- a/crates/goose-mcp/src/autovisualiser/templates/treemap_template.html
+++ b/crates/goose-mcp/src/autovisualiser/templates/treemap_template.html
@@ -105,8 +105,15 @@
.on("mouseover", function (event, d) {
var pct = ((d.data.value / root.value) * 100).toFixed(1);
var tt = document.getElementById("tooltip");
- tt.innerHTML = "
" + d.data.name + "" + formatValue(d.data.value) + " (" + pct + "%)" +
- (d.data.category ? "
" + d.data.category : "");
+ var name = document.createElement("strong");
+ name.textContent = d.data.name;
+ tt.replaceChildren(name);
+ tt.appendChild(document.createElement("br"));
+ tt.appendChild(document.createTextNode(formatValue(d.data.value) + " (" + pct + "%)"));
+ if (d.data.category) {
+ tt.appendChild(document.createElement("br"));
+ tt.appendChild(document.createTextNode(d.data.category));
+ }
tt.style.opacity = 1;
McpAppBridge.positionTooltip(tt, event);
})