This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new 328f861 ui: process topology — straight edges, compact honeycomb
(ui-18), alt labels; edge modal full-width + ESC + mm:ss axis
328f861 is described below
commit 328f861bdc1bc35674d371268447eaba208fbfa6
Author: Wu Sheng <[email protected]>
AuthorDate: Wed May 20 13:09:44 2026 +0800
ui: process topology — straight edges, compact honeycomb (ui-18), alt
labels; edge modal full-width + ESC + mm:ss axis
Topology graph:
- Edges are straight lines now (no bezier arcs / spiral look); a
reverse-direction edge of a pair shifts onto a parallel track so both
stay visible. Arrow + animated dashes + protocol pill unchanged.
- Inside processes pack a compact honeycomb cluster (cells touch),
matching ui-18, instead of wide spread rows. Bigger cells
(26–50px) that scale down as the cluster grows.
- Inside labels alternate BELOW / ABOVE by index (cell 0 below, 1
above, 2 below, …) so they don't collide in the tight cluster;
external peers label below. Label font 12px mono / 600, same on
inside + outside nodes.
Edge dashboard modal:
- Width now `.dlg.edge-dlg` (beats the base `.dlg` 560px regardless of
rule order) → 92vw × 92vh; body flex-fills + scrolls. Charts 150px.
- ESC closes the modal (window keydown).
- X-axis labels are elapsed mm:ss across the profiling window instead
of relative `-Nm` markers (which blow up for long tasks). New
optional `xLabels` prop on TimeChart, fed from the task window +
series length.
---
apps/ui/src/components/charts/TimeChart.vue | 9 +-
.../layer/profiling/LayerNetworkProfilingView.vue | 43 +++++++--
.../src/layer/profiling/ProcessTopologyGraph.vue | 101 ++++++++++++++-------
3 files changed, 115 insertions(+), 38 deletions(-)
diff --git a/apps/ui/src/components/charts/TimeChart.vue
b/apps/ui/src/components/charts/TimeChart.vue
index 9ef8b16..3c835f6 100644
--- a/apps/ui/src/components/charts/TimeChart.vue
+++ b/apps/ui/src/components/charts/TimeChart.vue
@@ -54,6 +54,10 @@ const props = withDefaults(
* values to integers (pod counts, replica counts, error counts).
* Defaults to the compact-readable rule. */
format?: 'int' | 'decimal' | 'compact';
+ /** Explicit x-axis bucket labels. When provided (and length-matched)
+ * these replace the default relative `-Nm` markers — e.g. a caller
+ * with a known window can pass `mm:ss` elapsed labels. */
+ xLabels?: string[];
}>(),
{
height: 180,
@@ -112,7 +116,10 @@ function buildOption(): echarts.EChartsCoreOption {
// implied to be MINUTE-stepped over the last 15m), so we label the
// axis with relative "-Nm" markers.
const length = props.series[0]?.data.length ?? 0;
- const xLabels = Array.from({ length }, (_, i) => `-${length - i - 1}m`);
+ const xLabels =
+ props.xLabels && props.xLabels.length === length
+ ? props.xLabels
+ : Array.from({ length }, (_, i) => `-${length - i - 1}m`);
return {
backgroundColor: 'transparent',
tooltip: {
diff --git a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
index 1652309..7b330ed 100644
--- a/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
+++ b/apps/ui/src/layer/profiling/LayerNetworkProfilingView.vue
@@ -31,7 +31,7 @@
└──────────────┴─────────────────────────────────────────────┘
-->
<script setup lang="ts">
-import { computed, ref, watch } from 'vue';
+import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useLayerInstances } from '@/layer/useLayerInstances';
import { useSelectedService } from '@/layer/useSelectedService';
@@ -225,6 +225,34 @@ const targetProcessName = computed(
() => (selectedCall.value && nodeById(selectedCall.value.target)?.name) ||
'—',
);
+/** Edge-chart x-axis labels as elapsed `mm:ss` across the profiling
+ * window — compact and meaningful regardless of how long the task ran
+ * (relative `-Nm` markers blow up for multi-hour tasks). Derived from
+ * the metric series length + the task's run window. */
+const edgeXLabels = computed<string[]>(() => {
+ const m = relationMetrics.value;
+ const n = m?.client[0]?.values.length ?? m?.server[0]?.values.length ?? 0;
+ if (n <= 0) return [];
+ const task = currentTask.value;
+ const spanMs =
+ task?.taskStartTime && task.fixedTriggerDuration
+ ? task.fixedTriggerDuration * 1000
+ : windowMinutes.value * 60_000;
+ const stepMs = n > 1 ? spanMs / (n - 1) : spanMs;
+ return Array.from({ length: n }, (_, i) => {
+ const sec = Math.round((i * stepMs) / 1000);
+ const mm = Math.floor(sec / 60);
+ const ss = sec % 60;
+ return `${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
+ });
+});
+
+function onEdgeKeydown(ev: KeyboardEvent): void {
+ if (ev.key === 'Escape' && selectedCall.value) closeEdge();
+}
+onMounted(() => window.addEventListener('keydown', onEdgeKeydown));
+onBeforeUnmount(() => window.removeEventListener('keydown', onEdgeKeydown));
+
async function keepAlive(): Promise<void> {
if (!currentTask.value) return;
aliveStatus.value = null;
@@ -426,7 +454,8 @@ function fmtTime(ms: number): string {
</div>
<TimeChart
:series="[{ label: m.label, data: m.values, unit: m.unit }]"
- :height="120"
+ :x-labels="edgeXLabels"
+ :height="150"
:unit="m.unit"
/>
</div>
@@ -759,9 +788,11 @@ function fmtTime(ms: number): string {
font-size: 10.5px;
color: var(--sw-fg-1);
}
-/* Edge dashboard modal — wide; client | server side-by-side, each side
- a 2-up widget grid → 4 widgets per row (2 client + 2 server). */
-.edge-dlg { width: 1600px; max-width: 96vw; max-height: 90vh; }
+/* Edge dashboard modal — near-fullscreen; client | server side-by-side,
+ each side a 2-up widget grid → 4 widgets per row (2 client + 2 server). */
+/* `.dlg.edge-dlg` (not `.edge-dlg`) so this beats the base `.dlg`
+ * width:560px regardless of rule order. */
+.dlg.edge-dlg { width: 92vw; max-width: 92vw; height: 92vh; max-height: 92vh; }
.edge-dlg-title {
display: flex;
align-items: center;
@@ -779,7 +810,7 @@ function fmtTime(ms: number): string {
border-radius: 10px;
padding: 1px 8px;
}
-.edge-dlg-body { overflow-y: auto; padding: 12px 14px; }
+.edge-dlg-body { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 12px
14px; }
.edge-cols {
display: grid;
grid-template-columns: 1fr 1fr;
diff --git a/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
b/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
index 60d2fb9..1ca3ddd 100644
--- a/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
+++ b/apps/ui/src/layer/profiling/ProcessTopologyGraph.vue
@@ -53,6 +53,32 @@ const host = ref<HTMLDivElement | null>(null);
const selectedCallId = ref<string | null>(null);
// ── Hex geometry (flat-top) ─────────────────────────────────────────
+const SQRT3 = Math.sqrt(3);
+/** Flat-top axial → pixel. Neighbour centres land √3·r apart, so a cell
+ * hexagon of radius r tiles (touches) its neighbours — a honeycomb. */
+function axialToPixel(ax: number, ay: number, r: number, o: Pt): Pt {
+ return { x: 1.5 * ax * r + o.x, y: ((SQRT3 / 2) * ax + SQRT3 * ay) * r + o.y
};
+}
+/** Axial coords in spiral order from the centre — packs a compact
+ * honeycomb cluster (centre cell, then rings around it). */
+function spiralHex(n: number): Array<{ x: number; y: number }> {
+ const dirs = [[1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1]];
+ const out = [{ x: 0, y: 0 }];
+ let k = 1;
+ while (out.length < n) {
+ let cx = dirs[4][0] * k;
+ let cy = dirs[4][1] * k;
+ for (let side = 0; side < 6 && out.length < n + 6; side++) {
+ for (let step = 0; step < k; step++) {
+ out.push({ x: cx, y: cy });
+ cx += dirs[side][0];
+ cy += dirs[side][1];
+ }
+ }
+ k++;
+ }
+ return out.slice(0, n);
+}
function hexCellPath(cx: number, cy: number, R: number): string {
const v: [number, number][] = [];
for (let i = 0; i < 6; i++) {
@@ -80,21 +106,17 @@ function layout(o: Pt, w: number, h: number):
PositionedNode[] {
const inside = props.nodes.filter(isInside).map((n) => ({ ...n }) as
PositionedNode);
const outside = props.nodes.filter((n) => !isInside(n)).map((n) => ({ ...n
}) as PositionedNode);
- // Inside processes: centred LEFT-RIGHT rows (not an arc). Columns bias
- // slightly wide so the block reads horizontally; the last row is
- // centred on its own count.
+ // Inside processes: a compact honeycomb cluster (cells touch), packed
+ // spiral-from-centre. Cell size scales down as the cluster grows.
const n = inside.length;
- const cols = Math.max(1, Math.min(n, Math.ceil(Math.sqrt(n * 1.7))));
- const rows = Math.max(1, Math.ceil(n / cols));
- cellRadius = Math.max(16, Math.min(34, 240 / (Math.max(cols, rows) + 1)));
- const dx = cellRadius * 1.95;
- const dy = cellRadius * 2.0;
+ const rings = Math.max(1, Math.ceil((-3 + Math.sqrt(9 + 12 * Math.max(1, n -
1))) / 6));
+ cellRadius = Math.max(26, Math.min(50, 130 / (rings + 0.5)));
+ const cells = spiralHex(n);
inside.forEach((node, i) => {
- const r = Math.floor(i / cols);
- const c = i % cols;
- const rowCount = r < rows - 1 ? cols : n - cols * (rows - 1);
- node.x = o.x + (c - (rowCount - 1) / 2) * dx;
- node.y = o.y + (r - (rows - 1) / 2) * dy;
+ const c = cells[i] ?? { x: 0, y: 0 };
+ const p = axialToPixel(c.x, c.y, cellRadius, o);
+ node.x = p.x;
+ node.y = p.y;
});
// External peers spread on a canvas-filling ellipse (skipping the
@@ -143,26 +165,34 @@ function buildCalls(byId: Map<string, PositionedNode>):
PositionedCall[] {
}
return out;
}
-function controlPoint(s: Pt, t: Pt, lowerArc: boolean): Pt {
- const dx = t.x - s.x;
- const dy = t.y - s.y;
- const theta = Math.atan2(dy, dx) - Math.PI / 2;
- const len = (Math.sqrt(dx * dx + dy * dy) / 2) * 0.5;
- const cx = (s.x + t.x) / 2 + len * Math.cos(theta);
- let cy = (s.y + t.y) / 2 + len * Math.sin(theta);
- if (lowerArc) cy = s.y + t.y - cy;
- return { x: cx, y: cy };
+/** Straight-line endpoints. A reverse-direction edge of the same pair is
+ * shifted onto a parallel track so the two directed lines stay visible
+ * instead of overlapping. */
+function edgeEnds(c: PositionedCall): { sx: number; sy: number; tx: number;
ty: number } {
+ let sx = c.source.x;
+ let sy = c.source.y;
+ let tx = c.target.x;
+ let ty = c.target.y;
+ if (c.lowerArc) {
+ const dx = tx - sx;
+ const dy = ty - sy;
+ const len = Math.hypot(dx, dy) || 1;
+ const nx = (-dy / len) * 7;
+ const ny = (dx / len) * 7;
+ sx += nx;
+ sy += ny;
+ tx += nx;
+ ty += ny;
+ }
+ return { sx, sy, tx, ty };
}
function edgePath(c: PositionedCall): string {
- const cp = controlPoint(c.source, c.target, c.lowerArc);
- return `M ${c.source.x} ${c.source.y} Q ${cp.x} ${cp.y} ${c.target.x}
${c.target.y}`;
+ const e = edgeEnds(c);
+ return `M ${e.sx} ${e.sy} L ${e.tx} ${e.ty}`;
}
function edgeMid(c: PositionedCall): Pt {
- const cp = controlPoint(c.source, c.target, c.lowerArc);
- return {
- x: 0.25 * c.source.x + 0.5 * cp.x + 0.25 * c.target.x,
- y: 0.25 * c.source.y + 0.5 * cp.y + 0.25 * c.target.y,
- };
+ const e = edgeEnds(c);
+ return { x: (e.sx + e.tx) / 2, y: (e.sy + e.ty) / 2 };
}
// ── Node info popover (floating window) ─────────────────────────────
@@ -364,14 +394,23 @@ function render(): void {
.attr('fill-opacity', (d) => (isInside(d) ? 0.85 : 0.75))
.attr('stroke', 'var(--sw-bg-0, #0d0f14)')
.attr('stroke-width', 1.5);
+ // Inside cells touch, so their labels alternate BELOW / ABOVE the
+ // honeycomb by index (cell 0 below, cell 1 above, cell 2 below, …) to
+ // avoid colliding with neighbours. External peers always label below
+ // their (isolated) cell. `positioned` is [inside…, outside…] so the
+ // datum index `i` is the inside index for inside cells.
+ function labelY(d: PositionedNode, i: number): number {
+ if (!isInside(d)) return 30;
+ return i % 2 === 1 ? -(cellRadius + 8) : cellRadius + 16;
+ }
nodeSel
.append('text')
.attr('x', 0)
- .attr('y', (d) => (isInside(d) ? cellRadius + 11 : 30))
+ .attr('y', labelY)
.attr('text-anchor', 'middle')
.attr('fill', 'var(--sw-fg-1, #d4d6dd)')
.style('font-family', 'var(--sw-mono, monospace)')
- .style('font-size', '16px')
+ .style('font-size', '12px')
.style('font-weight', '600')
.text((d) => {
const base = d.name.split('.')[0];