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 5f1ff7b  service dashboard: smaller widget height + per-metric color 
alignment
5f1ff7b is described below

commit 5f1ff7b4d5175658f41609b1a3ea63c74f7bbe95
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 12 21:29:17 2026 +0800

    service dashboard: smaller widget height + per-metric color alignment
    
    Widget rows shrink from 180px to 120px each — span-2 line widgets are
    240px tall now (was 360), so 2 rows of 3 line charts fit without
    dominating the page.
    
    Widget colors line up with the layer-header KPI strip:
      Traffic / RPM  → orange  (sw-accent)
      Apdex / SLA    → purple  (sw-purple)
      p99 / latency  → yellow  (sw-warn)
      err / failure  → red     (sw-err)
    
    Each widget exposes its metric color as a CSS custom property the card
    head consumes — left border + title color tinted, so the dashboard
    panes read at a glance with the same hue as the KPI summary above.
    
    TimeChart now actually honors its accent prop (was declared but
    unused — every line series rendered orange regardless). First series
    uses the resolved accent; secondary lines (relabels() multi-series)
    cycle through a distinguishable palette so percentile charts stay
    readable. TopList bars take the widget color too.
---
 apps/ui/src/components/charts/TimeChart.vue     | 66 ++++++++++++++++++-------
 apps/ui/src/views/layer/LayerDashboardsView.vue | 50 ++++++++++++++++---
 2 files changed, 90 insertions(+), 26 deletions(-)

diff --git a/apps/ui/src/components/charts/TimeChart.vue 
b/apps/ui/src/components/charts/TimeChart.vue
index b582f90..55d4546 100644
--- a/apps/ui/src/components/charts/TimeChart.vue
+++ b/apps/ui/src/components/charts/TimeChart.vue
@@ -54,13 +54,33 @@ const props = withDefaults(
   },
 );
 
-const PALETTE = [
-  '#f97316', // sw-accent (orange)
-  '#60a5fa', // info-ish
+/**
+ * Resolve a CSS variable like `var(--sw-accent)` to its computed RGB
+ * by querying the document root. ECharts doesn't honor CSS vars on
+ * canvas-rendered series, so we evaluate once and feed the hex. Falls
+ * back to the orange accent when the variable resolves empty.
+ */
+function cssVar(token: string): string {
+  if (!token.startsWith('var(')) return token;
+  if (typeof window === 'undefined') return '#f97316';
+  const name = token.replace(/^var\(\s*/, '').replace(/\s*\)\s*$/, '');
+  const v = 
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
+  return v || '#f97316';
+}
+
+/**
+ * Secondary palette for multi-line widgets (e.g., percentile relabels
+ * → 5 series). The first series uses the widget's accent (or the
+ * accent prop); subsequent lines pick from this palette so the lines
+ * are distinguishable. Picks deliberately don't reuse the accent.
+ */
+const SECONDARY = [
+  '#60a5fa', // info-ish (blue)
   '#a78bfa', // purple
   '#22d3ee', // cyan
   '#f472b6', // pink
-  '#34d399', // ok-ish
+  '#34d399', // ok-ish (green)
+  '#fbbf24', // amber
 ];
 
 const container = ref<HTMLDivElement | null>(null);
@@ -113,20 +133,26 @@ function buildOption(): echarts.EChartsCoreOption {
       axisLabel: { color: '#64748b', fontSize: 9 },
       splitLine: { lineStyle: { color: 'rgba(255,255,255,0.06)' } },
     },
-    series: props.series.map((s, i) => ({
-      name: s.label,
-      type: 'line',
-      smooth: true,
-      symbol: 'none',
-      lineStyle: { width: 1.5 },
-      data: s.data.map((v) => (v === null ? '-' : v)),
-      // Resolve CSS var for the first series; fall back to the palette.
-      itemStyle: { color: i === 0 ? PALETTE[0] : PALETTE[i % PALETTE.length] },
-      areaStyle:
-        props.series.length === 1
-          ? { color: PALETTE[0], opacity: 0.12 }
-          : undefined,
-    })),
+    series: props.series.map((s, i) => {
+      // First series uses the widget's accent color (resolved from a
+      // CSS var); secondary lines cycle through SECONDARY. Single-series
+      // widgets get a soft area fill in the accent tone.
+      const accentHex = cssVar(props.accent);
+      const color = i === 0 ? accentHex : SECONDARY[(i - 1) % 
SECONDARY.length];
+      return {
+        name: s.label,
+        type: 'line',
+        smooth: true,
+        symbol: 'none',
+        lineStyle: { width: 1.5 },
+        data: s.data.map((v) => (v === null ? '-' : v)),
+        itemStyle: { color },
+        areaStyle:
+          props.series.length === 1
+            ? { color: accentHex, opacity: 0.12 }
+            : undefined,
+      };
+    }),
   };
 }
 
@@ -152,6 +178,10 @@ watch(
   () => props.unit,
   () => chart?.setOption(buildOption()),
 );
+watch(
+  () => props.accent,
+  () => chart?.setOption(buildOption(), { replaceMerge: ['series'] }),
+);
 </script>
 
 <template>
diff --git a/apps/ui/src/views/layer/LayerDashboardsView.vue 
b/apps/ui/src/views/layer/LayerDashboardsView.vue
index 0ec6164..b925386 100644
--- a/apps/ui/src/views/layer/LayerDashboardsView.vue
+++ b/apps/ui/src/views/layer/LayerDashboardsView.vue
@@ -31,6 +31,7 @@ import { useRoute } from 'vue-router';
 import type { LayerDef } from '@skywalking-horizon-ui/api-client';
 import TimeChart from '@/components/charts/TimeChart.vue';
 import TopList from '@/components/charts/TopList.vue';
+import { colorForMetric } from '@/composables/metricColor';
 import { useLayerDashboard, useLayerDashboardConfig } from 
'@/composables/useLayerDashboard';
 import { useLayerLanding } from '@/composables/useLayerLanding';
 import { useLayers } from '@/composables/useLayers';
@@ -95,6 +96,34 @@ function gridStyle(w: { span?: number; rowSpan?: number; w?: 
number; h?: number
   };
 }
 
+/**
+ * Resolve a widget's primary metric color from its title / id / first
+ * expression. Same color scheme as the layer-header KPI strip so
+ * Apdex shows purple, Traffic orange, p99 yellow, err red across both
+ * surfaces — the operator builds one mental color map.
+ */
+function widgetColor(w: { id?: string; title?: string; expressions?: string[] 
}): string {
+  // Try a few sources in priority order; the colorForMetric helper
+  // pattern-matches on the metric key (cpm / sla / apdex / err /
+  // p50/p75/p95/p99 / etc.) — the title or id usually contains one.
+  const candidates: string[] = [];
+  if (w.id) candidates.push(w.id);
+  if (w.title) candidates.push(w.title);
+  if (w.expressions?.[0]) candidates.push(w.expressions[0]);
+  // Lower-case + flatten so patterns like 'service_cpm' / 'Traffic'
+  // both hit the right band.
+  for (const c of candidates) {
+    const c2 = c.toLowerCase();
+    if (/(^|[^a-z])cpm([^a-z]|$)/.test(c2) || c2.includes('traffic') || 
c2.includes('rpm')) return 'var(--sw-accent)';
+    if (c2.includes('apdex')) return 'var(--sw-purple)';
+    if (c2.includes('sla') || c2.includes('success')) return 
'var(--sw-purple)';
+    if (/p\d{2,3}/.test(c2) || c2.includes('percentile') || 
c2.includes('resp_time') || c2.includes('response time') || 
c2.includes('latency')) return 'var(--sw-warn)';
+    if (c2.includes('err') || c2.includes('error') || c2.includes('failure')) 
return 'var(--sw-err)';
+  }
+  // Fall back to the metric catalog helper.
+  return colorForMetric(w.id || w.title || w.expressions?.[0] || '');
+}
+
 /**
  * Evaluate a widget's `visibleWhen` predicate.
  *   - `<metric_name> has value`  → the widget's result has a non-null
@@ -149,7 +178,7 @@ function isVisible(
         v-for="w in widgets.filter((wi) => isVisible(wi, 
resultsById.get(wi.id)))"
         :key="w.id"
         class="widget sw-card"
-        :style="gridStyle(w)"
+        :style="{ ...gridStyle(w), '--widget-accent': widgetColor(w) }"
       >
         <div class="w-head" :title="w.tip">
           <h4>{{ w.title }}</h4>
@@ -172,7 +201,8 @@ function isVisible(
               v-if="resultsById.get(w.id)?.series?.length"
               :series="resultsById.get(w.id)!.series!"
               :unit="w.unit"
-              :height="(w.rowSpan ?? 1) * 160 - 60"
+              :height="(w.rowSpan ?? 1) * 110 - 50"
+              :accent="widgetColor(w)"
             />
             <span v-else class="muted">no data</span>
           </template>
@@ -181,6 +211,7 @@ function isVisible(
               v-if="resultsById.get(w.id)?.topList?.length"
               :items="resultsById.get(w.id)!.topList!"
               :unit="w.unit"
+              :color="widgetColor(w)"
             />
             <span v-else class="muted">no data</span>
           </template>
@@ -238,13 +269,13 @@ function isVisible(
 .grid {
   /* 12-col flow grid with fixed row height. `grid-auto-flow: dense`
    * back-fills gaps so a span-12 widget after several span-4s doesn't
-   * leave a hole. Widget heights are deliberately uniform — operators
-   * vary span (width) more than rowSpan. */
+   * leave a hole. Row height tuned smaller so 2-row line widgets fit
+   * comfortably without dwarfing the rest of the page. */
   display: grid;
   grid-template-columns: repeat(12, minmax(0, 1fr));
-  grid-auto-rows: 180px;
+  grid-auto-rows: 120px;
   grid-auto-flow: row dense;
-  gap: 12px;
+  gap: 10px;
 }
 .widget {
   display: flex;
@@ -257,14 +288,17 @@ function isVisible(
   align-items: baseline;
   justify-content: space-between;
   gap: 8px;
-  padding: 8px 12px;
+  padding: 7px 12px;
   border-bottom: 1px solid var(--sw-line);
+  /* Subtle left-edge accent tinted to the widget's primary metric
+   * color — ties each card to the matching KPI in the layer header. */
+  border-left: 3px solid var(--widget-accent, var(--sw-accent));
 }
 .w-head h4 {
   margin: 0;
   font-size: 11.5px;
   font-weight: 600;
-  color: var(--sw-fg-0);
+  color: var(--widget-accent, var(--sw-fg-0));
   letter-spacing: -0.01em;
   white-space: nowrap;
   overflow: hidden;

Reply via email to