diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index b69b08c00a2..14d75b7a1ae 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -14,6 +14,7 @@ export enum ReducerID { variance = 'variance', stdDev = 'stdDev', last = 'last', + median = 'median', first = 'first', count = 'count', range = 'range', @@ -278,6 +279,14 @@ export const fieldReducers = new Registry(() => [ aliasIds: ['avg'], preservesUnits: true, }, + { + id: ReducerID.median, + name: 'Median', + description: 'Median Value', + standard: true, + aliasIds: ['median'], + preservesUnits: true, + }, { id: ReducerID.variance, name: 'Variance', diff --git a/pkg/expr/mathexp/reduce.go b/pkg/expr/mathexp/reduce.go index 76d74ca459f..26f2df42216 100644 --- a/pkg/expr/mathexp/reduce.go +++ b/pkg/expr/mathexp/reduce.go @@ -3,6 +3,7 @@ package mathexp import ( "fmt" "math" + "sort" "github.com/grafana/grafana-plugin-sdk-go/data" ) @@ -14,17 +15,18 @@ type ReducerFunc = func(fv *Float64Field) *float64 type ReducerID string const ( - ReducerSum ReducerID = "sum" - ReducerMean ReducerID = "mean" - ReducerMin ReducerID = "min" - ReducerMax ReducerID = "max" - ReducerCount ReducerID = "count" - ReducerLast ReducerID = "last" + ReducerSum ReducerID = "sum" + ReducerMean ReducerID = "mean" + ReducerMin ReducerID = "min" + ReducerMax ReducerID = "max" + ReducerCount ReducerID = "count" + ReducerLast ReducerID = "last" + ReducerMedian ReducerID = "median" ) // GetSupportedReduceFuncs returns collection of supported function names func GetSupportedReduceFuncs() []ReducerID { - return []ReducerID{ReducerSum, ReducerMean, ReducerMin, ReducerMax, ReducerCount, ReducerLast} + return []ReducerID{ReducerSum, ReducerMean, ReducerMin, ReducerMax, ReducerCount, ReducerLast, ReducerMedian} } func Sum(fv *Float64Field) *float64 { @@ -98,6 +100,32 @@ func Last(fv *Float64Field) *float64 { return fv.GetValue(fv.Len() - 1) } +func Median(fv *Float64Field) *float64 { + values := make([]float64, 0, fv.Len()) + for i := 0; i < fv.Len(); i++ { + v := fv.GetValue(i) + if v == nil || math.IsNaN(*v) { + nan := math.NaN() + return &nan + } + values = append(values, *v) + } + + if len(values) == 0 { + nan := math.NaN() + return &nan + } + + sort.Float64s(values) + mid := len(values) / 2 + if len(values)%2 == 0 { + v := (values[mid-1] + values[mid]) / 2 + return &v + } else { + return &values[mid] + } +} + func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) { switch rFunc { case ReducerSum: @@ -112,6 +140,8 @@ func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) { return Count, nil case ReducerLast: return Last, nil + case ReducerMedian: + return Median, nil default: return nil, fmt.Errorf("reduction %v not implemented", rFunc) } diff --git a/pkg/expr/mathexp/reduce_test.go b/pkg/expr/mathexp/reduce_test.go index c4d786cad8f..ab1d2046d5e 100644 --- a/pkg/expr/mathexp/reduce_test.go +++ b/pkg/expr/mathexp/reduce_test.go @@ -3,6 +3,7 @@ package mathexp import ( "math" "math/rand" + "sort" "testing" "time" @@ -90,6 +91,100 @@ func TestSeriesReduce(t *testing.T) { resultsIs: require.Equal, results: resultValuesNoErr(makeNumber("", nil, NaN)), }, + { + name: "median empty series", + red: "median", + varToReduce: "A", + vars: seriesEmpty, + errIs: require.NoError, + resultsIs: require.Equal, + results: resultValuesNoErr(makeNumber("", nil, NaN)), + }, + { + name: "median series with a nil value", + red: "median", + varToReduce: "A", + vars: seriesWithNil, + errIs: require.NoError, + resultsIs: require.Equal, + results: resultValuesNoErr(makeNumber("", nil, NaN)), + }, + { + name: "median series even number of elements", + red: "median", + varToReduce: "A", + vars: Vars{ + "A": resultValuesNoErr( + makeSeries("temp", nil, tp{ + time.Unix(5, 0), float64Pointer(20), + }, tp{ + time.Unix(10, 0), float64Pointer(10), + }), + ), + }, + errIs: require.NoError, + resultsIs: require.Equal, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(15))), + }, + { + name: "median series odd number of elements", + red: "median", + varToReduce: "A", + vars: Vars{ + "A": resultValuesNoErr( + makeSeries("temp", nil, tp{ + time.Unix(5, 0), float64Pointer(20), + }, tp{ + time.Unix(10, 0), float64Pointer(10), + }, tp{ + time.Unix(15, 0), float64Pointer(5), + }), + ), + }, + errIs: require.NoError, + resultsIs: require.Equal, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(10))), + }, + { + name: "median series with repeated values", + red: "median", + varToReduce: "A", + vars: Vars{ + "A": resultValuesNoErr( + makeSeries("temp", nil, tp{ + time.Unix(5, 0), float64Pointer(5), + }, tp{ + time.Unix(10, 0), float64Pointer(5), + }, tp{ + time.Unix(15, 0), float64Pointer(5), + }), + ), + }, + errIs: require.NoError, + resultsIs: require.Equal, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(5))), + }, + { + name: "median series with negative values", + red: "median", + varToReduce: "A", + vars: Vars{ + "A": resultValuesNoErr( + makeSeries("temp", nil, tp{ + time.Unix(5, 0), float64Pointer(-1), + }, tp{ + time.Unix(10, 0), float64Pointer(-3), + }, tp{ + time.Unix(10, 0), float64Pointer(-4), + }, tp{ + time.Unix(15, 0), float64Pointer(-2), + }), + ), + }, + errIs: require.NoError, + resultsIs: require.Equal, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(-2.5))), + }, { name: "min series with a nil value", red: "min", @@ -257,6 +352,27 @@ func TestSeriesReduceDropNN(t *testing.T) { vars: seriesEmpty, results: resultValuesNoErr(makeNumber("", nil, nil)), }, + { + name: "DropNN: median series with a nil value and real value", + red: "median", + varToReduce: "A", + vars: seriesWithNil, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(2))), + }, + { + name: "DropNN: median empty series", + red: "median", + varToReduce: "A", + vars: seriesEmpty, + results: resultValuesNoErr(makeNumber("", nil, nil)), + }, + { + name: "DropNN: median series that becomes empty after filtering non-number", + red: "median", + varToReduce: "A", + vars: seriesNonNumbers, + results: resultValuesNoErr(makeNumber("", nil, nil)), + }, { name: "DropNN: mean series that becomes empty after filtering non-number", red: "mean", @@ -351,6 +467,41 @@ func TestSeriesReduceReplaceNN(t *testing.T) { vars: seriesNonNumbers, results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))), }, + { + name: "replaceNN: median series with a nil value and real value", + red: "median", + varToReduce: "A", + vars: Vars{ + "A": resultValuesNoErr( + makeSeries("temp", nil, tp{ + time.Unix(5, 0), float64Pointer(2), + }, tp{ + time.Unix(10, 0), nil, + }, tp{ + time.Unix(15, 0), float64Pointer(5), + }), + ), + }, + results: resultValuesNoErr( + makeNumber("", nil, float64Pointer( + sortedFloat64([]float64{2, 5, replaceWith})[1]), + ), + ), + }, + { + name: "replaceNN: median empty series", + red: "median", + varToReduce: "A", + vars: seriesEmpty, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))), + }, + { + name: "replaceNN: median series that becomes empty after filtering non-number", + red: "median", + varToReduce: "A", + vars: seriesNonNumbers, + results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))), + }, { name: "replaceNN: count empty series", red: "count", @@ -386,3 +537,9 @@ func TestSeriesReduceReplaceNN(t *testing.T) { }) } } + +func sortedFloat64(f []float64) []float64 { + f = append([]float64(nil), f...) + sort.Float64s(f) + return f +} diff --git a/pkg/expr/query.panel.schema.json b/pkg/expr/query.panel.schema.json index 54a4bed521b..22c997a0f3c 100644 --- a/pkg/expr/query.panel.schema.json +++ b/pkg/expr/query.panel.schema.json @@ -187,7 +187,7 @@ "type": "string" }, "reducer": { - "description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ", + "description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ", "type": "string", "enum": [ "sum", @@ -195,7 +195,8 @@ "min", "max", "count", - "last" + "last", + "median" ], "x-enum-description": {} }, @@ -341,7 +342,7 @@ "additionalProperties": false }, "downsampler": { - "description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ", + "description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ", "type": "string", "enum": [ "sum", @@ -349,7 +350,8 @@ "min", "max", "count", - "last" + "last", + "median" ], "x-enum-description": {} }, diff --git a/pkg/expr/query.request.schema.json b/pkg/expr/query.request.schema.json index 3053aa7c859..d0691de1a64 100644 --- a/pkg/expr/query.request.schema.json +++ b/pkg/expr/query.request.schema.json @@ -213,7 +213,7 @@ "type": "string" }, "reducer": { - "description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ", + "description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ", "type": "string", "enum": [ "sum", @@ -221,7 +221,8 @@ "min", "max", "count", - "last" + "last", + "median" ], "x-enum-description": {} }, @@ -367,7 +368,7 @@ "additionalProperties": false }, "downsampler": { - "description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ", + "description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ", "type": "string", "enum": [ "sum", @@ -375,7 +376,8 @@ "min", "max", "count", - "last" + "last", + "median" ], "x-enum-description": {} }, diff --git a/pkg/expr/query.types.json b/pkg/expr/query.types.json index 4cad6ad630e..7e5d218010e 100644 --- a/pkg/expr/query.types.json +++ b/pkg/expr/query.types.json @@ -56,7 +56,7 @@ { "metadata": { "name": "reduce", - "resourceVersion": "1709915979242", + "resourceVersion": "1722250145266", "creationTimestamp": "2024-02-21T22:09:26Z" }, "spec": { @@ -79,14 +79,15 @@ "type": "string" }, "reducer": { - "description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ", + "description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ", "enum": [ "sum", "mean", "min", "max", "count", - "last" + "last", + "median" ], "type": "string", "x-enum-description": {} @@ -141,7 +142,7 @@ { "metadata": { "name": "resample", - "resourceVersion": "1709915973363", + "resourceVersion": "1722250145266", "creationTimestamp": "2024-02-21T22:09:26Z" }, "spec": { @@ -157,14 +158,15 @@ "description": "QueryType = resample", "properties": { "downsampler": { - "description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ", + "description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ", "enum": [ "sum", "mean", "min", "max", "count", - "last" + "last", + "median" ], "type": "string", "x-enum-description": {} diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index bb8233ab7a1..15c7b885ce8 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -4806,6 +4806,7 @@ "PENDING_PROCESSING", "PROCESSING", "FINISHED", + "CANCELED", "ERROR", "UNKNOWN" ] @@ -5490,7 +5491,7 @@ "type": "object", "title": "NavbarPreference defines model for NavbarPreference.", "properties": { - "savedItemIds": { + "bookmarkIds": { "type": "array", "items": { "type": "string" @@ -7217,6 +7218,7 @@ "PENDING_PROCESSING", "PROCESSING", "FINISHED", + "CANCELED", "ERROR", "UNKNOWN" ] @@ -7247,6 +7249,10 @@ "format": "int64" } }, + "total": { + "type": "integer", + "format": "int64" + }, "types": { "type": "object", "additionalProperties": { @@ -8380,6 +8386,23 @@ } } }, + "healthResponse": { + "type": "object", + "properties": { + "commit": { + "type": "string" + }, + "database": { + "type": "string" + }, + "enterpriseCommit": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "publicError": { "description": "PublicError is derived from Error and only contains information\navailable to the end user.", "type": "object", diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts index 499ef9fdd39..a1f88729c1e 100644 --- a/public/app/features/expressions/types.ts +++ b/public/app/features/expressions/types.ts @@ -79,6 +79,7 @@ export const reducerTypes: Array> = [ { value: ReducerID.min, label: 'Min', description: 'Get the minimum value' }, { value: ReducerID.max, label: 'Max', description: 'Get the maximum value' }, { value: ReducerID.mean, label: 'Mean', description: 'Get the average value' }, + { value: ReducerID.median, label: 'Median', description: 'Get the median value' }, { value: ReducerID.sum, label: 'Sum', description: 'Get the sum of all values' }, { value: ReducerID.count, label: 'Count', description: 'Get the number of values' }, { value: ReducerID.last, label: 'Last', description: 'Get the last value' },