This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch feat/explore in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 78df6242bd51bdc655b48f9878ac94754a6c5edb Author: Wu Sheng <[email protected]> AuthorDate: Tue Jun 23 21:10:06 2026 +0800 fix(explore): clickable distribution dots + Time on its own conditions row - The scatter dots render ~1-2px once the 1000-unit viewBox is squashed into the card, so clicking one to open its trace almost never landed (native + zipkin both). Add a fat transparent hit-target circle per dot; the visible dot is unchanged. Fixes the per-layer Traces scatter too, since the widget is shared. - Force the Time field to start its own grid row so it no longer gets crammed next to the wide Tags/Annotation field (most visible in the Zipkin layout). --- .../src/features/operate/explore/ExploreView.vue | 4 +- apps/ui/src/render/widgets/TraceDistribution.vue | 46 ++++++++++++++-------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/apps/ui/src/features/operate/explore/ExploreView.vue b/apps/ui/src/features/operate/explore/ExploreView.vue index d35824c..269df27 100644 --- a/apps/ui/src/features/operate/explore/ExploreView.vue +++ b/apps/ui/src/features/operate/explore/ExploreView.vue @@ -624,7 +624,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, true) <span>{{ t('Annotation query') }}</span> <input v-model="cond.annotationQuery" class="cf-input mono" type="text" :placeholder="t('error or key=value, AND-joined')" /> </label> - <label class="cf" :class="{ 'cf-wide': isCustomRange }"> + <label class="cf iq-time" :class="{ 'cf-wide': isCustomRange }"> <span>{{ t('Time') }}</span> <template v-if="isCustomRange"> <span class="cf-range"> @@ -768,6 +768,8 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onPageKeyDown, true) @media (max-width: 560px) { .iq-grid { grid-template-columns: 1fr; } } .cf { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: var(--sw-fg-3); font-weight: 500; min-width: 0; } .cf.cf-wide { grid-column: span 2; } +.cf.iq-time { grid-column-start: 1; } +.cf.iq-time.cf-wide { grid-column: 1 / span 2; } .cf.cf-chk { justify-content: flex-end; } .cf.cf-disabled > span { color: var(--sw-fg-3); opacity: 0.7; } .cf small { font-weight: 400; font-size: 9.5px; margin-left: 4px; font-style: italic; } diff --git a/apps/ui/src/render/widgets/TraceDistribution.vue b/apps/ui/src/render/widgets/TraceDistribution.vue index 4a6458e..8a7b364 100644 --- a/apps/ui/src/render/widgets/TraceDistribution.vue +++ b/apps/ui/src/render/widgets/TraceDistribution.vue @@ -106,6 +106,18 @@ const scatterBounds = computed(() => { const yMax = rawYMax || 1; return { xMin: Math.min(...xs), xMax: Math.max(...xs), yMin: 0, yMax }; }); +// Pre-place each point in viewBox space so the template (and its +// transparent hit-target) share one cx/cy instead of recomputing it. +const placedPoints = computed(() => { + const b = scatterBounds.value; + if (!b) return []; + const xSpan = b.xMax - b.xMin; + return scatterPoints.value.map((p) => ({ + ...p, + cx: xSpan === 0 ? 500 : ((p.x - b.xMin) / xSpan) * 1000, + cy: 1000 - ((p.y - b.yMin) / (b.yMax - b.yMin || 1)) * 990, + })); +}); const scatterXTicks = computed(() => { const b = scatterBounds.value; if (!b) return []; @@ -206,22 +218,24 @@ function onScatterDot(p: ScatterPoint, ev: MouseEvent): void { @pointercancel="onScatterUp" > <line x1="0" y1="998" x2="1000" y2="998" stroke="var(--sw-line-2)" stroke-width="1" vector-effect="non-scaling-stroke" /> - <circle - v-for="p in scatterPoints" - :key="p.id" - :cx="scatterBounds.xMax === scatterBounds.xMin ? 500 : ((p.x - scatterBounds.xMin) / (scatterBounds.xMax - scatterBounds.xMin)) * 1000" - :cy="1000 - ((p.y - scatterBounds.yMin) / (scatterBounds.yMax - scatterBounds.yMin || 1)) * 990" - :r="isHot(p.rowKey) ? 6 : 3.2" - :fill="p.isError ? 'var(--sw-err)' : 'var(--sw-accent)'" - :fill-opacity="isHot(p.rowKey) ? 1 : (hasHot ? 0.35 : 0.9)" - :stroke="isHot(p.rowKey) ? 'var(--sw-fg-0)' : (p.isError ? 'var(--sw-err)' : 'var(--sw-accent-2)')" - :stroke-width="isHot(p.rowKey) ? 1.8 : 0.8" - vector-effect="non-scaling-stroke" - class="scatter-dot" - @click="onScatterDot(p, $event)" - > - <title>{{ p.label }} · {{ fmtMs(p.y) }}{{ isHot(p.rowKey) ? ` · ${t('selected')}` : '' }}</title> - </circle> + <g v-for="p in placedPoints" :key="p.id"> + <circle + :cx="p.cx" + :cy="p.cy" + :r="isHot(p.rowKey) ? 6 : 3.2" + :fill="p.isError ? 'var(--sw-err)' : 'var(--sw-accent)'" + :fill-opacity="isHot(p.rowKey) ? 1 : (hasHot ? 0.35 : 0.9)" + :stroke="isHot(p.rowKey) ? 'var(--sw-fg-0)' : (p.isError ? 'var(--sw-err)' : 'var(--sw-accent-2)')" + :stroke-width="isHot(p.rowKey) ? 1.8 : 0.8" + vector-effect="non-scaling-stroke" + pointer-events="none" + /> + <!-- A fat transparent hit-target: the visible dot is ~1-2px once the + 1000-unit viewBox is squashed to the card, far too small to click. --> + <circle :cx="p.cx" :cy="p.cy" r="18" fill="transparent" class="scatter-dot" @click="onScatterDot(p, $event)"> + <title>{{ p.label }} · {{ fmtMs(p.y) }}{{ isHot(p.rowKey) ? ` · ${t('selected')}` : '' }}</title> + </circle> + </g> <!-- Drag-select rectangle. --> <rect v-if="dragRect"
