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); })