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"

Reply via email to