From c02ee4b6f2bbac96562ac64601fdb77adce424bd Mon Sep 17 00:00:00 2001
From: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com>
Date: Sun, 1 Nov 2026 10:30:03 +0000
Subject: [PATCH] fix(traces): readable flame graph labels, unclipped chart
tooltips, outlined close button (#16119)
Co-authored-by: Ashish Kolhe <35160958+oasisk@users.noreply.github.com>
---
.../components/traces/FlameGraphView.spec.ts | 17 +++++++++++++++++
web/src/components/traces/FlameGraphView.vue | 6 +++++-
web/src/plugins/traces/TraceDetails.spec.ts | 8 ++++++++
web/src/plugins/traces/TraceDetails.vue | 8 ++++----
.../metrics/TracesComparisonField.spec.ts | 8 ++++++++
.../traces/metrics/TracesComparisonField.vue | 3 +++
.../traces/metrics/TracesLatencyHeatmap.spec.ts | 7 +++++++
.../traces/metrics/TracesLatencyHeatmap.vue | 3 +++
8 files changed, 55 insertions(+), 5 deletions(-)
diff --git a/web/src/components/traces/FlameGraphView.spec.ts b/web/src/components/traces/FlameGraphView.spec.ts
index 4f8b914925..0cce033f4e 100644
--- a/web/src/components/traces/FlameGraphView.spec.ts
+++ b/web/src/components/traces/FlameGraphView.spec.ts
@@ -70,6 +70,8 @@ vi.mock("@/utils/traces/serviceColorRegistry", () => ({
"service-1": "#4caf50",
"service-2": "#2196f3",
"service-3": "#ff9800",
+ "lime-service": "#84cc16",
+ "violet-service": "#7c3aed",
};
return colorMap[name] || "#9CA3AF";
}),
@@ -1483,6 +1485,21 @@ describe("FlameGraphView span event markers", () => {
expect(result.children.slice(1).every((c: any) => c.silent)).toBe(true);
});
+ it.each([
+ ["lime-service", "#000000"],
+ ["violet-service", "#ffffff"],
+ ])("picks a label colour that contrasts with the %s bar", (service, expected) => {
+ const wrapper = mount(FlameGraphView, {
+ props: {
+ spans: [{ ...spanWithEvents, resolvedIdentity: service, events: [] }],
+ traceDuration: 100,
+ selectedSpanId: null,
+ },
+ });
+
+ expect(renderBlock(wrapper, 100).textContent.style.fill).toBe(expected);
+ });
+
it("returns a bare rect for a span with no events", () => {
const wrapper = mount(FlameGraphView, {
props: {
diff --git a/web/src/components/traces/FlameGraphView.vue b/web/src/components/traces/FlameGraphView.vue
index a53e5d3a34..bd5e008cb0 100644
--- a/web/src/components/traces/FlameGraphView.vue
+++ b/web/src/components/traces/FlameGraphView.vue
@@ -163,6 +163,7 @@ import { type EnrichedSpan } from "@/ts/interfaces/traces/span.types";
import { formatDuration } from "@/composables/traces/useTraceProcessing";
import { maxOf } from "@/plugins/traces/threadView.utils";
import { getOrSetServiceColor } from "@/utils/traces/serviceColorRegistry";
+import { getContrastTextColor } from "@/utils/traces/traceColors";
import { escapeHtml } from "@/utils/html";
import {
normalizeSpanEvents,
@@ -649,7 +650,10 @@ const chartOptions = computed(() => {
type: "text",
style: {
text: data[params.dataIndex].spanData.operationName,
- fill: "#ffffff",
+ fill:
+ getContrastTextColor(data[params.dataIndex].itemStyle.color) === "black"
+ ? "#000000"
+ : "#ffffff",
font: "0.6875rem Inter, sans-serif",
overflow: "truncate",
width: rectWidth - 8,
diff --git a/web/src/plugins/traces/TraceDetails.spec.ts b/web/src/plugins/traces/TraceDetails.spec.ts
index c345fbb21d..0b79a9d413 100644
--- a/web/src/plugins/traces/TraceDetails.spec.ts
+++ b/web/src/plugins/traces/TraceDetails.spec.ts
@@ -1153,6 +1153,14 @@ describe("TraceDetails", () => {
expect(closeBtn.exists()).toBe(true);
});
+ it("renders the close button as outline with a Close tooltip", () => {
+ const closeBtn = wrapper.findComponent('[data-test="trace-details-close-btn"]');
+ expect(closeBtn.exists()).toBe(true);
+ expect(closeBtn.attributes("data-o2-variant")).toBe("outline");
+ const tooltip = closeBtn.findComponent({ name: "OTooltip" });
+ expect(tooltip.props("content")).toBe(i18n.global.t("common.close"));
+ });
+
it("should show share button when showShareButton is true in standalone mode", () => {
const shareBtn = wrapper.find('[data-test="trace-details-share-link-btn"]');
expect(shareBtn.exists()).toBe(true);
diff --git a/web/src/plugins/traces/TraceDetails.vue b/web/src/plugins/traces/TraceDetails.vue
index 0fae8b8ba7..b6f0753bbd 100644
--- a/web/src/plugins/traces/TraceDetails.vue
+++ b/web/src/plugins/traces/TraceDetails.vue
@@ -215,12 +215,12 @@ along with this program. If not, see .
-
+
@@ -392,13 +392,13 @@ along with this program. If not, see .
-
+
diff --git a/web/src/plugins/traces/metrics/TracesComparisonField.spec.ts b/web/src/plugins/traces/metrics/TracesComparisonField.spec.ts
index 84b28af2cf..933d8ed96f 100644
--- a/web/src/plugins/traces/metrics/TracesComparisonField.spec.ts
+++ b/web/src/plugins/traces/metrics/TracesComparisonField.spec.ts
@@ -164,6 +164,14 @@ describe("TracesComparisonField", () => {
expect(o.series[1].itemStyle.color).toBe("#0a4ce8");
});
+ it("renders the histogram tooltip on the body, so the card does not clip it", async () => {
+ wrapper = mountField(numeric());
+ await settle();
+ const o = chartOptions(wrapper);
+ expect(o.tooltip.appendToBody).toBe(true);
+ expect(o.tooltip.className).toBe("o2-echarts-tooltip");
+ });
+
it("pre-selects the most different bin and marks it", async () => {
const field = numeric();
wrapper = mountField(field);
diff --git a/web/src/plugins/traces/metrics/TracesComparisonField.vue b/web/src/plugins/traces/metrics/TracesComparisonField.vue
index bd7b21ec0d..c2e825036a 100644
--- a/web/src/plugins/traces/metrics/TracesComparisonField.vue
+++ b/web/src/plugins/traces/metrics/TracesComparisonField.vue
@@ -210,6 +210,9 @@ const options = computed(() => {
grid: { left: 4, right: 4, top: 6, bottom: 20 },
tooltip: {
trigger: "axis",
+ // An overflow-hidden card ancestor clips a tooltip rendered inside the chart.
+ appendToBody: true,
+ className: "o2-echarts-tooltip",
axisPointer: { type: "shadow" },
backgroundColor: chartColor("--color-tooltip-bg"),
borderWidth: 0,
diff --git a/web/src/plugins/traces/metrics/TracesLatencyHeatmap.spec.ts b/web/src/plugins/traces/metrics/TracesLatencyHeatmap.spec.ts
index 8b184a66e0..7cdc53b401 100644
--- a/web/src/plugins/traces/metrics/TracesLatencyHeatmap.spec.ts
+++ b/web/src/plugins/traces/metrics/TracesLatencyHeatmap.spec.ts
@@ -276,6 +276,13 @@ describe("TracesLatencyHeatmap", () => {
expect(o.series[0].type).toBe("heatmap");
});
+ it("renders the tooltip on the body, so the card does not clip it", async () => {
+ wrapper = await mountHeatmap();
+ const o = options(wrapper);
+ expect(o.tooltip.appendToBody).toBe(true);
+ expect(o.tooltip.className).toBe("o2-echarts-tooltip");
+ });
+
it("draws cells without borders, so a dense grid is not striped", async () => {
wrapper = await mountHeatmap();
expect(options(wrapper).series[0].itemStyle.borderWidth).toBe(0);
diff --git a/web/src/plugins/traces/metrics/TracesLatencyHeatmap.vue b/web/src/plugins/traces/metrics/TracesLatencyHeatmap.vue
index c5b27b0faf..b1b1fa5c48 100644
--- a/web/src/plugins/traces/metrics/TracesLatencyHeatmap.vue
+++ b/web/src/plugins/traces/metrics/TracesLatencyHeatmap.vue
@@ -238,6 +238,9 @@ const options = computed(() => {
grid: { left: 52, right: 8, top: 8, bottom: 22 },
tooltip: {
trigger: "item",
+ // An overflow-hidden card ancestor clips a tooltip rendered inside the chart.
+ appendToBody: true,
+ className: "o2-echarts-tooltip",
backgroundColor: chartColor("--color-tooltip-bg"),
borderWidth: 0,
textStyle: { color: chartColor("--color-tooltip-text"), fontSize: 12 },