Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cmd/telemetry/telemetry_renderers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,38 @@ import (
"strings"
)

// computeAxisMax examines all datasets and returns a Y-axis hard max string.
// If outliers are detected (actual max > P99 * 1.5), it returns a value slightly
// above P99. Otherwise it returns "" (no constraint, use auto-scale).
func computeAxisMax(data [][]float64) string {
var all []float64
for _, dataset := range data {
all = append(all, dataset...)
}
if len(all) < 4 {
return ""
}
sorted := make([]float64, len(all))
copy(sorted, all)
slices.Sort(sorted)
p99Idx := int(float64(len(sorted)-1) * 0.99)
p99 := sorted[p99Idx]
actualMax := sorted[len(sorted)-1]
if p99 > 0 && actualMax > p99*1.5 {
return fmt.Sprintf("%f", p99*1.1)
}
return ""
}

func telemetryTableHTMLRenderer(tableValues table.TableValues, data [][]float64, datasetNames []string, chartConfig report.ChartTemplateStruct, datasetHiddenFlags []bool) string {
if len(tableValues.Fields) == 0 {
slog.Error("no fields in table", slog.String("table", tableValues.Name))
return ""
}
// Auto-detect outliers and set hard Y-axis max for auto-scaled charts
if chartConfig.YaxisMax == "" && chartConfig.SuggestedMax == "0" {
chartConfig.YaxisMax = computeAxisMax(data)
}
tsFieldIdx := 0
var timestamps []string
for i := range tableValues.Fields[0].Values {
Expand Down
92 changes: 92 additions & 0 deletions cmd/telemetry/telemetry_renderers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (C) 2021-2025 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause

package telemetry

import (
"fmt"
"testing"
)

func TestComputeAxisMax(t *testing.T) {
tests := []struct {
name string
data [][]float64
wantEmpty bool // true means expect "" (no constraint)
wantAbove float64
wantBelow float64
}{
{
name: "normal data, no outliers",
data: [][]float64{{10, 12, 11, 13, 10, 12, 11, 14, 10, 13}},
wantEmpty: true,
},
{
name: "single extreme outlier",
data: [][]float64{{10, 12, 11, 13, 10, 12, 11, 14, 10, 10000}},
wantEmpty: false,
wantAbove: 13,
wantBelow: 10000,
},
{
name: "all identical values",
data: [][]float64{{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}},
wantEmpty: true,
},
{
name: "too few data points",
data: [][]float64{{10, 20, 30}},
wantEmpty: true,
},
{
name: "empty data",
data: [][]float64{},
wantEmpty: true,
},
{
name: "multiple datasets, one with outlier",
data: [][]float64{{10, 12, 11, 13, 10}, {11, 14, 10, 13, 50000}},
wantEmpty: false,
wantAbove: 13,
wantBelow: 50000,
},
{
name: "gradual increase, no outlier",
data: [][]float64{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
wantEmpty: true,
},
{
name: "all zeros",
data: [][]float64{{0, 0, 0, 0, 0}},
wantEmpty: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := computeAxisMax(tt.data)
if tt.wantEmpty {
if got != "" {
t.Errorf("computeAxisMax() = %q, want empty string", got)
}
return
}
if got == "" {
t.Errorf("computeAxisMax() = empty string, want a constraining value")
return
}
// Parse and check bounds
var val float64
n, err := fmt.Sscanf(got, "%f", &val)
if err != nil || n != 1 {
t.Errorf("computeAxisMax() = %q, could not parse as float", got)
return
}
if val <= tt.wantAbove {
t.Errorf("computeAxisMax() = %f, want > %f", val, tt.wantAbove)
}
if val >= tt.wantBelow {
t.Errorf("computeAxisMax() = %f, want < %f", val, tt.wantBelow)
}
})
}
}
7 changes: 5 additions & 2 deletions internal/report/render_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ new Chart(document.getElementById('{{.ID}}'), {
display: true
},
suggestedMin: {{.SuggestedMin}},
suggestedMax: {{.SuggestedMax}},
suggestedMax: {{.SuggestedMax}},{{if .YaxisMax}}
max: {{.YaxisMax}},{{end}}
}
},
plugins: {
Expand Down Expand Up @@ -525,7 +526,8 @@ new Chart(document.getElementById('{{.ID}}'), {
display: true
},
suggestedMin: {{.SuggestedMin}},
suggestedMax: {{.SuggestedMax}},
suggestedMax: {{.SuggestedMax}},{{if .YaxisMax}}
max: {{.YaxisMax}},{{end}}
}
},
plugins: {
Expand Down Expand Up @@ -590,6 +592,7 @@ type ChartTemplateStruct struct {
AspectRatio string
SuggestedMin string
SuggestedMax string
YaxisMax string // hard max for Y-axis; "" means no constraint
}

// CreateFieldNameWithDescription creates HTML for a field name with optional description tooltip
Expand Down
Loading