Skip to content

Commit 8f96fd3

Browse files
ui: type-matched glyphs in the expanded series rows too (#40)
Every metric in practice is multi-series, so the collapsed rows show '—' and the data the user actually sees is the expanded per-series sub-rows — which were still plain line sparklines for all types. Now those match the type: counters render rate bars, histograms render their per-series latency distribution (hist_facet returns each series' latest bucket_counts + shared bounds), gauges keep the value line. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 825b95f commit 8f96fd3

3 files changed

Lines changed: 39 additions & 15 deletions

File tree

server/src/api.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -701,11 +701,16 @@ pub struct HistFacetPoint {
701701
pub struct HistFacetSeries {
702702
attrs: serde_json::Value,
703703
points: Vec<HistFacetPoint>,
704+
/// This series' most recent per-bucket counts, so the row can draw the
705+
/// distribution shape (bars) rather than just a percentile line.
706+
dist: Vec<i64>,
704707
}
705708

706709
#[derive(Serialize)]
707710
pub struct HistFacetResponse {
708711
unit: Option<String>,
712+
/// Bucket upper bounds shared by the series' `dist` arrays.
713+
bounds: Vec<f64>,
709714
series: Vec<HistFacetSeries>,
710715
truncated: i64,
711716
}
@@ -743,8 +748,14 @@ pub async fn metric_hist_facet(
743748
.map_err(internal)?;
744749

745750
let mut unit: Option<String> = None;
746-
let mut map: std::collections::BTreeMap<String, (serde_json::Value, Vec<HistFacetPoint>)> =
747-
std::collections::BTreeMap::new();
751+
let mut bounds: Vec<f64> = Vec::new();
752+
// Rows are ordered t ASC, so the last counts seen per series is the latest
753+
// distribution.
754+
#[allow(clippy::type_complexity)]
755+
let mut map: std::collections::BTreeMap<
756+
String,
757+
(serde_json::Value, Vec<HistFacetPoint>, Vec<i64>),
758+
> = std::collections::BTreeMap::new();
748759
for r in rows {
749760
if unit.is_none() {
750761
unit = r.unit;
@@ -753,28 +764,37 @@ pub async fn metric_hist_facet(
753764
(Some(b), Some(c)) if c.len() == b.len() + 1 => (b, c),
754765
_ => continue,
755766
};
767+
if bounds.is_empty() {
768+
bounds = b.clone();
769+
}
756770
let point = HistFacetPoint {
757771
t: r.t,
758772
p50: hist_quantile(&b, &c, 0.50),
759773
p95: hist_quantile(&b, &c, 0.95),
760774
p99: hist_quantile(&b, &c, 0.99),
761775
};
762-
map.entry(r.attrs.to_string())
763-
.or_insert_with(|| (r.attrs, Vec::new()))
764-
.1
765-
.push(point);
776+
let entry = map
777+
.entry(r.attrs.to_string())
778+
.or_insert_with(|| (r.attrs, Vec::new(), Vec::new()));
779+
entry.1.push(point);
780+
entry.2 = c;
766781
}
767782

768783
let mut series: Vec<HistFacetSeries> = map
769784
.into_values()
770-
.map(|(attrs, points)| HistFacetSeries { attrs, points })
785+
.map(|(attrs, points, dist)| HistFacetSeries {
786+
attrs,
787+
points,
788+
dist,
789+
})
771790
.collect();
772791
series.sort_by(|a, b| b.points.len().cmp(&a.points.len()));
773792
let truncated = series.len().saturating_sub(FACET_MAX_SERIES) as i64;
774793
series.truncate(FACET_MAX_SERIES);
775794

776795
Ok(Json(HistFacetResponse {
777796
unit,
797+
bounds,
778798
series,
779799
truncated,
780800
}))

ui/src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,11 @@ export interface HistFacetPoint {
172172
export interface HistFacetSeries {
173173
attrs: Record<string, string>;
174174
points: HistFacetPoint[];
175+
dist: number[];
175176
}
176177
export interface HistFacetResponse {
177178
unit: string | null;
179+
bounds: number[];
178180
series: HistFacetSeries[];
179181
truncated: number;
180182
}

ui/src/components/MetricList.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,13 @@ function FacetSeriesRows({ name, unit }: { name: string; unit: string | null })
7575
<tr key={i}>
7676
<td className="mono sub-label">{labels[i]}</td>
7777
<td>
78-
{vals.length >= 2 ? (
79-
<Sparkline values={[...vals].reverse()} />
80-
) : (
78+
{vals.length < 2 ? (
8179
<span className="muted"></span>
80+
) : data.rated ? (
81+
// counter: per-interval rate as bars (points are oldest→newest)
82+
<Bars values={vals} />
83+
) : (
84+
<Sparkline values={[...vals].reverse()} />
8285
)}
8386
</td>
8487
<td className="num">{formatValue(latest, u)}</td>
@@ -97,7 +100,7 @@ function FacetSeriesRows({ name, unit }: { name: string; unit: string | null })
97100
);
98101
}
99102

100-
// Histogram series rows: p95 trend sparkline + latest p50/p95/p99.
103+
// Histogram series rows: latency distribution bars + latest p50/p95/p99.
101104
function HistSeriesRows({ name, unit }: { name: string; unit: string | null }) {
102105
const [data, setData] = useState<HistFacetResponse | null>(null);
103106
const [error, setError] = useState<string | null>(null);
@@ -119,14 +122,13 @@ function HistSeriesRows({ name, unit }: { name: string; unit: string | null }) {
119122
<caption className="muted small subseries-count">{data.series.length} series</caption>
120123
<tbody>
121124
{data.series.map((s, i) => {
122-
const p95s = s.points.map((p) => p.p95).filter((v): v is number => v != null);
123125
const last = s.points.length ? s.points[s.points.length - 1] : null;
124126
return (
125127
<tr key={i}>
126128
<td className="mono sub-label">{labels[i]}</td>
127-
<td title="p95 trend">
128-
{p95s.length >= 2 ? (
129-
<Sparkline values={[...p95s].reverse()} />
129+
<td title="latency distribution">
130+
{s.dist && s.dist.length > 1 ? (
131+
<Bars values={s.dist} />
130132
) : (
131133
<span className="muted"></span>
132134
)}

0 commit comments

Comments
 (0)