mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
SSE: Mode to drop NaN/Inf/Null in Reduction operations (#43583)
Co-authored-by: Yuriy Tseretyan <yuriy.tseretyan@grafana.com> Co-authored-by: gillesdemey <gilles.de.mey@gmail.com>
This commit is contained in:
co-authored by
Yuriy Tseretyan
gillesdemey
parent
0cb3037b55
commit
040ce40113
+18
-5
@@ -148,31 +148,44 @@ Reduce takes one or more time series returned from a query or an expression and
|
|||||||
|
|
||||||
- **Function -** The reduction function to use
|
- **Function -** The reduction function to use
|
||||||
- **Input -** The variable (refID (such as `A`)) to resample
|
- **Input -** The variable (refID (such as `A`)) to resample
|
||||||
|
- **Mode -** Allows control behavior of reduction function when a series contains non-numerical values (null, NaN, +\-Inf)
|
||||||
|
|
||||||
#### Reduction Functions
|
#### Reduction Functions
|
||||||
|
|
||||||
> **Note:** In the future we plan to add options to control empty, NaN, and null behavior for reduction functions.
|
|
||||||
|
|
||||||
##### Count
|
##### Count
|
||||||
|
|
||||||
Count returns the number of points in each series.
|
Count returns the number of points in each series.
|
||||||
|
|
||||||
##### Mean
|
##### Mean
|
||||||
|
|
||||||
Mean returns the total of all values in each series divided by the number of points in that series. If any values in the series are null or nan, or if the series is empty, NaN is returned.
|
Mean returns the total of all values in each series divided by the number of points in that series. In `strict` mode if any values in the series are null or nan, or if the series is empty, NaN is returned.
|
||||||
|
|
||||||
##### Min and Max
|
##### Min and Max
|
||||||
|
|
||||||
Min and Max return the smallest or largest value in the series respectively. If any values in the series are null or nan, or if the series is empty, NaN is returned.
|
Min and Max return the smallest or largest value in the series respectively. In `strict` mode if any values in the series are null or nan, or if the series is empty, NaN is returned.
|
||||||
|
|
||||||
##### Sum
|
##### Sum
|
||||||
|
|
||||||
Sum returns the total of all values in the series. If series is of zero length, the sum will be 0. If there are any NaN or Null values in the series, NaN is returned.
|
Sum returns the total of all values in the series. If series is of zero length, the sum will be 0. In `strict` mode if there are any NaN or Null values in the series, NaN is returned.
|
||||||
|
|
||||||
#### Last
|
#### Last
|
||||||
|
|
||||||
Last returns the last number in the series. If the series has no values then returns NaN.
|
Last returns the last number in the series. If the series has no values then returns NaN.
|
||||||
|
|
||||||
|
#### Reduction Modes
|
||||||
|
|
||||||
|
##### Strict
|
||||||
|
|
||||||
|
In Strict mode the input series is processed as is. If any values in the series are non-numeric (null, NaN or +\-Inf), NaN is returned.
|
||||||
|
|
||||||
|
##### Drop Non-Numeric
|
||||||
|
|
||||||
|
In this mode all non-numeric values (null, NaN or +\-Inf) in the input series are filtered out before executing the reduction function.
|
||||||
|
|
||||||
|
##### Replace Non-Numeric
|
||||||
|
|
||||||
|
In this mode all non-numeric values are replaced by a pre-defined value.
|
||||||
|
|
||||||
### Resample
|
### Resample
|
||||||
|
|
||||||
Resample changes the time stamps in each time series to have a consistent time interval. The main use case is so you can resample time series that do not share the same timestamps so math can be performed between them. This can be done by resample each of the two series, and then in a Math operation referencing the resampled variables.
|
Resample changes the time stamps in each time series to have a consistent time interval. The main use case is so you can resample time series that do not share the same timestamps so math can be performed between them. This can be done by resample each of the two series, and then in a Math operation referencing the resampled variables.
|
||||||
|
|||||||
@@ -402,10 +402,7 @@ func TestPercentDiffAbsReducer(t *testing.T) {
|
|||||||
func valBasedSeries(vals ...*float64) mathexp.Series {
|
func valBasedSeries(vals ...*float64) mathexp.Series {
|
||||||
newSeries := mathexp.NewSeries("", nil, len(vals))
|
newSeries := mathexp.NewSeries("", nil, len(vals))
|
||||||
for idx, f := range vals {
|
for idx, f := range vals {
|
||||||
err := newSeries.SetPoint(idx, time.Unix(int64(idx), 0), f)
|
newSeries.SetPoint(idx, time.Unix(int64(idx), 0), f)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newSeries
|
return newSeries
|
||||||
}
|
}
|
||||||
@@ -413,10 +410,7 @@ func valBasedSeries(vals ...*float64) mathexp.Series {
|
|||||||
func valBasedSeriesWithLabels(l data.Labels, vals ...*float64) mathexp.Series {
|
func valBasedSeriesWithLabels(l data.Labels, vals ...*float64) mathexp.Series {
|
||||||
newSeries := mathexp.NewSeries("", l, len(vals))
|
newSeries := mathexp.NewSeries("", l, len(vals))
|
||||||
for idx, f := range vals {
|
for idx, f := range vals {
|
||||||
err := newSeries.SetPoint(idx, time.Unix(int64(idx), 0), f)
|
newSeries.SetPoint(idx, time.Unix(int64(idx), 0), f)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newSeries
|
return newSeries
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-5
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/expr/mathexp"
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,16 +73,22 @@ type ReduceCommand struct {
|
|||||||
Reducer string
|
Reducer string
|
||||||
VarToReduce string
|
VarToReduce string
|
||||||
refID string
|
refID string
|
||||||
|
seriesMapper mathexp.ReduceMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReduceCommand creates a new ReduceCMD.
|
// NewReduceCommand creates a new ReduceCMD.
|
||||||
func NewReduceCommand(refID, reducer, varToReduce string) *ReduceCommand {
|
func NewReduceCommand(refID, reducer, varToReduce string, mapper mathexp.ReduceMapper) (*ReduceCommand, error) {
|
||||||
// TODO: validate reducer here, before execution
|
_, err := mathexp.GetReduceFunc(reducer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &ReduceCommand{
|
return &ReduceCommand{
|
||||||
Reducer: reducer,
|
Reducer: reducer,
|
||||||
VarToReduce: varToReduce,
|
VarToReduce: varToReduce,
|
||||||
refID: refID,
|
refID: refID,
|
||||||
}
|
seriesMapper: mapper,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query.
|
// UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query.
|
||||||
@@ -105,7 +112,36 @@ func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error) {
|
|||||||
return nil, fmt.Errorf("expected reducer to be a string, got %T for refId %v", rawReducer, rn.RefID)
|
return nil, fmt.Errorf("expected reducer to be a string, got %T for refId %v", rawReducer, rn.RefID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewReduceCommand(rn.RefID, redFunc, varToReduce), nil
|
var mapper mathexp.ReduceMapper = nil
|
||||||
|
settings, ok := rn.Query["settings"]
|
||||||
|
if ok {
|
||||||
|
switch s := settings.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
mode, ok := s["mode"]
|
||||||
|
if ok && mode != "" {
|
||||||
|
switch mode {
|
||||||
|
case "dropNN":
|
||||||
|
mapper = mathexp.DropNonNumber{}
|
||||||
|
case "replaceNN":
|
||||||
|
valueStr, ok := s["replaceWithValue"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("expected settings.replaceWithValue to be specified when mode is 'replaceNN' for refId %v", rn.RefID)
|
||||||
|
}
|
||||||
|
switch value := valueStr.(type) {
|
||||||
|
case float64:
|
||||||
|
mapper = mathexp.ReplaceNonNumberWithValue{Value: value}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("expected settings.replaceWithValue to be a number, got %T for refId %v", value, rn.RefID)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("reducer mode %s is not supported for refId %v. Supported only: [dropNN,replaceNN]", mode, rn.RefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("expected settings to be an object, got %T for refId %v", s, rn.RefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewReduceCommand(rn.RefID, redFunc, varToReduce, mapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedsVars returns the variable names (refIds) that are dependencies
|
// NeedsVars returns the variable names (refIds) that are dependencies
|
||||||
@@ -123,7 +159,7 @@ func (gr *ReduceCommand) Execute(ctx context.Context, vars mathexp.Vars) (mathex
|
|||||||
if !ok {
|
if !ok {
|
||||||
return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type())
|
return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type())
|
||||||
}
|
}
|
||||||
num, err := series.Reduce(gr.refID, gr.Reducer)
|
num, err := series.Reduce(gr.refID, gr.Reducer, gr.seriesMapper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newRes, err
|
return newRes, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_UnmarshalReduceCommand_Settings(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
querySettings string
|
||||||
|
isError bool
|
||||||
|
expectedMapper mathexp.ReduceMapper
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no mapper function when settings is not specified",
|
||||||
|
querySettings: ``,
|
||||||
|
expectedMapper: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no mapper function when mode is not specified",
|
||||||
|
querySettings: `, "settings" : { }`,
|
||||||
|
expectedMapper: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when settings is not object",
|
||||||
|
querySettings: `, "settings" : "drop-nan"`,
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no mapper function when mode is empty",
|
||||||
|
querySettings: `, "settings" : { "mode": "" }`,
|
||||||
|
expectedMapper: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when mode is not known",
|
||||||
|
querySettings: `, "settings" : { "mode": "test" }`,
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filterNonNumber function when mode is 'dropNN'",
|
||||||
|
querySettings: `, "settings" : { "mode": "dropNN" }`,
|
||||||
|
expectedMapper: mathexp.DropNonNumber{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNanWithValue function when mode is 'dropNN'",
|
||||||
|
querySettings: `, "settings" : { "mode": "replaceNN" , "replaceWithValue": -12 }`,
|
||||||
|
expectedMapper: mathexp.ReplaceNonNumberWithValue{Value: -12},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error if mode is 'replaceNN' but field replaceWithValue is not specified",
|
||||||
|
querySettings: `, "settings" : { "mode": "replaceNN" }`,
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error if mode is 'replaceNN' but field replaceWithValue is not a number",
|
||||||
|
querySettings: `, "settings" : { "mode": "replaceNN", "replaceWithValue" : "-12" }`,
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
q := fmt.Sprintf(`{ "expression" : "$A", "reducer": "sum"%s }`, test.querySettings)
|
||||||
|
var qmap = make(map[string]interface{})
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(q), &qmap))
|
||||||
|
|
||||||
|
cmd, err := UnmarshalReduceCommand(&rawNode{
|
||||||
|
RefID: "A",
|
||||||
|
Query: qmap,
|
||||||
|
QueryType: "",
|
||||||
|
TimeRange: TimeRange{},
|
||||||
|
DataSource: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
if test.isError {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, cmd)
|
||||||
|
|
||||||
|
require.Equal(t, test.expectedMapper, cmd.seriesMapper)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-18
@@ -133,18 +133,14 @@ func (e *State) unarySeries(s Series, op string) (Series, error) {
|
|||||||
for i := 0; i < s.Len(); i++ {
|
for i := 0; i < s.Len(); i++ {
|
||||||
t, f := s.GetPoint(i)
|
t, f := s.GetPoint(i)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
if err := newSeries.SetPoint(i, t, nil); err != nil {
|
newSeries.SetPoint(i, t, nil)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newF, err := unaryOp(op, *f)
|
newF, err := unaryOp(op, *f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newSeries, err
|
return newSeries, err
|
||||||
}
|
}
|
||||||
if err := newSeries.SetPoint(i, t, &newF); err != nil {
|
newSeries.SetPoint(i, t, &newF)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newSeries, nil
|
return newSeries, nil
|
||||||
}
|
}
|
||||||
@@ -437,9 +433,7 @@ func (e *State) biSeriesNumber(labels data.Labels, op string, s Series, scalarVa
|
|||||||
nF := math.NaN()
|
nF := math.NaN()
|
||||||
t, f := s.GetPoint(i)
|
t, f := s.GetPoint(i)
|
||||||
if f == nil || scalarVal == nil {
|
if f == nil || scalarVal == nil {
|
||||||
if err := newSeries.SetPoint(i, t, nil); err != nil {
|
newSeries.SetPoint(i, t, nil)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if seriesFirst {
|
if seriesFirst {
|
||||||
@@ -450,9 +444,7 @@ func (e *State) biSeriesNumber(labels data.Labels, op string, s Series, scalarVa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return newSeries, err
|
return newSeries, err
|
||||||
}
|
}
|
||||||
if err := newSeries.SetPoint(i, t, &nF); err != nil {
|
newSeries.SetPoint(i, t, &nF)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newSeries, nil
|
return newSeries, nil
|
||||||
}
|
}
|
||||||
@@ -475,18 +467,14 @@ func (e *State) biSeriesSeries(labels data.Labels, op string, aSeries, bSeries S
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if aF == nil || bF == nil {
|
if aF == nil || bF == nil {
|
||||||
if err := newSeries.AppendPoint(aIdx, aTime, nil); err != nil {
|
newSeries.AppendPoint(aTime, nil)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nF, err := binaryOp(op, *aF, *bF)
|
nF, err := binaryOp(op, *aF, *bF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newSeries, err
|
return newSeries, err
|
||||||
}
|
}
|
||||||
if err := newSeries.AppendPoint(aIdx, aTime, &nF); err != nil {
|
newSeries.AppendPoint(aTime, &nF)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newSeries, nil
|
return newSeries, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,16 +116,18 @@ func TestNumberExpr(t *testing.T) {
|
|||||||
results: Results{[]Value{makeNumber("", nil, float64Pointer(-2.0))}},
|
results: Results{[]Value{makeNumber("", nil, float64Pointer(-2.0))}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "binary: Scalar Op Number (Number will nil val) - currently Panics",
|
name: "binary: Scalar Op Number (Number will nil val) returns nil",
|
||||||
expr: "1 + $A",
|
expr: "1 + $A",
|
||||||
|
newErrIs: assert.NoError,
|
||||||
|
execErrIs: assert.NoError,
|
||||||
|
resultIs: assert.Equal,
|
||||||
vars: Vars{"A": Results{[]Value{makeNumber("", nil, nil)}}},
|
vars: Vars{"A": Results{[]Value{makeNumber("", nil, nil)}}},
|
||||||
willPanic: true,
|
results: Results{[]Value{makeNumber("", nil, nil)}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
testBlock := func() {
|
|
||||||
e, err := New(tt.expr)
|
e, err := New(tt.expr)
|
||||||
tt.newErrIs(t, err)
|
tt.newErrIs(t, err)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
@@ -133,12 +135,6 @@ func TestNumberExpr(t *testing.T) {
|
|||||||
tt.execErrIs(t, err)
|
tt.execErrIs(t, err)
|
||||||
tt.resultIs(t, tt.results, res)
|
tt.resultIs(t, tt.results, res)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if tt.willPanic {
|
|
||||||
assert.Panics(t, testBlock)
|
|
||||||
} else {
|
|
||||||
assert.NotPanics(t, testBlock)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ type tp struct {
|
|||||||
func makeSeries(name string, labels data.Labels, points ...tp) Series {
|
func makeSeries(name string, labels data.Labels, points ...tp) Series {
|
||||||
newSeries := NewSeries(name, labels, len(points))
|
newSeries := NewSeries(name, labels, len(points))
|
||||||
for idx, p := range points {
|
for idx, p := range points {
|
||||||
err := newSeries.SetPoint(idx, p.t, p.f)
|
newSeries.SetPoint(idx, p.t, p.f)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newSeries
|
return newSeries
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,9 +226,7 @@ func perFloat(e *State, val Value, floatF func(x float64) float64) (Value, error
|
|||||||
if f != nil {
|
if f != nil {
|
||||||
nF = floatF(*f)
|
nF = floatF(*f)
|
||||||
}
|
}
|
||||||
if err := newSeries.SetPoint(i, t, &nF); err != nil {
|
newSeries.SetPoint(i, t, &nF)
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
newVal = newSeries
|
newVal = newSeries
|
||||||
default:
|
default:
|
||||||
@@ -257,9 +255,7 @@ func perNullableFloat(e *State, val Value, floatF func(x *float64) *float64) (Va
|
|||||||
newSeries := NewSeries(e.RefID, resSeries.GetLabels(), resSeries.Len())
|
newSeries := NewSeries(e.RefID, resSeries.GetLabels(), resSeries.Len())
|
||||||
for i := 0; i < resSeries.Len(); i++ {
|
for i := 0; i < resSeries.Len(); i++ {
|
||||||
t, f := resSeries.GetPoint(i)
|
t, f := resSeries.GetPoint(i)
|
||||||
if err := newSeries.SetPoint(i, t, floatF(f)); err != nil {
|
newSeries.SetPoint(i, t, floatF(f))
|
||||||
return newSeries, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
newVal = newSeries
|
newVal = newSeries
|
||||||
default:
|
default:
|
||||||
|
|||||||
+100
-21
@@ -3,10 +3,13 @@ package mathexp
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ReducerFunc = func(fv *Float64Field) *float64
|
||||||
|
|
||||||
func Sum(fv *Float64Field) *float64 {
|
func Sum(fv *Float64Field) *float64 {
|
||||||
var sum float64
|
var sum float64
|
||||||
for i := 0; i < fv.Len(); i++ {
|
for i := 0; i < fv.Len(); i++ {
|
||||||
@@ -75,38 +78,114 @@ func Last(fv *Float64Field) *float64 {
|
|||||||
f = math.NaN()
|
f = math.NaN()
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
v := fv.GetValue(fv.Len() - 1)
|
return fv.GetValue(fv.Len() - 1)
|
||||||
f = *v
|
}
|
||||||
return &f
|
|
||||||
|
func GetReduceFunc(rFunc string) (ReducerFunc, error) {
|
||||||
|
switch strings.ToLower(rFunc) {
|
||||||
|
case "sum":
|
||||||
|
return Sum, nil
|
||||||
|
case "mean":
|
||||||
|
return Avg, nil
|
||||||
|
case "min":
|
||||||
|
return Min, nil
|
||||||
|
case "max":
|
||||||
|
return Max, nil
|
||||||
|
case "count":
|
||||||
|
return Count, nil
|
||||||
|
case "last":
|
||||||
|
return Last, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("reduction %v not implemented", rFunc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce turns the Series into a Number based on the given reduction function
|
// Reduce turns the Series into a Number based on the given reduction function
|
||||||
func (s Series) Reduce(refID, rFunc string) (Number, error) {
|
// if ReduceMapper is defined it applies it to the provided series and performs reduction of the resulting series.
|
||||||
|
// Otherwise, the reduction operation is done against the original series.
|
||||||
|
func (s Series) Reduce(refID, rFunc string, mapper ReduceMapper) (Number, error) {
|
||||||
var l data.Labels
|
var l data.Labels
|
||||||
if s.GetLabels() != nil {
|
if s.GetLabels() != nil {
|
||||||
l = s.GetLabels().Copy()
|
l = s.GetLabels().Copy()
|
||||||
}
|
}
|
||||||
number := NewNumber(refID, l)
|
number := NewNumber(refID, l)
|
||||||
var f *float64
|
var f *float64
|
||||||
fVec := s.Frame.Fields[seriesTypeValIdx]
|
series := s
|
||||||
|
if mapper != nil {
|
||||||
|
series = mapSeries(s, mapper)
|
||||||
|
}
|
||||||
|
fVec := series.Frame.Fields[seriesTypeValIdx]
|
||||||
floatField := Float64Field(*fVec)
|
floatField := Float64Field(*fVec)
|
||||||
switch rFunc {
|
reduceFunc, err := GetReduceFunc(rFunc)
|
||||||
case "sum":
|
if err != nil {
|
||||||
f = Sum(&floatField)
|
return number, err
|
||||||
case "mean":
|
}
|
||||||
f = Avg(&floatField)
|
f = reduceFunc(&floatField)
|
||||||
case "min":
|
if f != nil && mapper != nil {
|
||||||
f = Min(&floatField)
|
f = mapper.MapOutput(f)
|
||||||
case "max":
|
|
||||||
f = Max(&floatField)
|
|
||||||
case "count":
|
|
||||||
f = Count(&floatField)
|
|
||||||
case "last":
|
|
||||||
f = Last(&floatField)
|
|
||||||
default:
|
|
||||||
return number, fmt.Errorf("reduction %v not implemented", rFunc)
|
|
||||||
}
|
}
|
||||||
number.SetValue(f)
|
number.SetValue(f)
|
||||||
|
|
||||||
return number, nil
|
return number, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReduceMapper interface {
|
||||||
|
MapInput(s *float64) *float64
|
||||||
|
MapOutput(v *float64) *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapSeries creates a series where all points are mapped using the provided map function ReduceMapper.MapInput
|
||||||
|
func mapSeries(s Series, mapper ReduceMapper) Series {
|
||||||
|
newSeries := NewSeries(s.Frame.RefID, s.GetLabels(), 0)
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
f := s.GetValue(i)
|
||||||
|
f = mapper.MapInput(f)
|
||||||
|
if f == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newFloat := *f
|
||||||
|
newSeries.AppendPoint(s.GetTime(i), &newFloat)
|
||||||
|
}
|
||||||
|
return newSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropNonNumber struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapInput returns nil if the input parameter is nil or point to either a NaN or a Inf
|
||||||
|
func (d DropNonNumber) MapInput(s *float64) *float64 {
|
||||||
|
if s == nil || math.IsNaN(*s) || math.IsInf(*s, 0) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapOutput returns nil if the input parameter is nil or point to either a NaN or a Inf
|
||||||
|
func (d DropNonNumber) MapOutput(s *float64) *float64 {
|
||||||
|
if s != nil && math.IsNaN(*s) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplaceNonNumberWithValue struct {
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapInput returns a pointer to ReplaceNonNumberWithValue.Value if input parameter is nil or points to either a NaN or an Inf.
|
||||||
|
// Otherwise, returns the input pointer as is.
|
||||||
|
func (r ReplaceNonNumberWithValue) MapInput(v *float64) *float64 {
|
||||||
|
if v == nil || math.IsNaN(*v) || math.IsInf(*v, 0) {
|
||||||
|
return &r.Value
|
||||||
|
} else {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapOutput returns a pointer to ReplaceNonNumberWithValue.Value if input parameter is nil or points to either a NaN or an Inf.
|
||||||
|
// Otherwise, returns the input pointer as is.
|
||||||
|
func (r ReplaceNonNumberWithValue) MapOutput(s *float64) *float64 {
|
||||||
|
if s != nil && math.IsNaN(*s) {
|
||||||
|
return &r.Value
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package mathexp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -227,6 +228,19 @@ func TestSeriesReduce(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "last null series",
|
||||||
|
red: "last",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
errIs: require.NoError,
|
||||||
|
resultsIs: require.Equal,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -234,7 +248,7 @@ func TestSeriesReduce(t *testing.T) {
|
|||||||
results := Results{}
|
results := Results{}
|
||||||
seriesSet := tt.vars[tt.varToReduce]
|
seriesSet := tt.vars[tt.varToReduce]
|
||||||
for _, series := range seriesSet.Values {
|
for _, series := range seriesSet.Values {
|
||||||
ns, err := series.Value().(*Series).Reduce("", tt.red)
|
ns, err := series.Value().(*Series).Reduce("", tt.red, nil)
|
||||||
tt.errIs(t, err)
|
tt.errIs(t, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -251,3 +265,252 @@ func TestSeriesReduce(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var seriesNonNumbers = Vars{
|
||||||
|
"A": Results{
|
||||||
|
[]Value{
|
||||||
|
makeSeries("temp", nil,
|
||||||
|
tp{time.Unix(5, 0), NaN},
|
||||||
|
tp{time.Unix(10, 0), float64Pointer(math.Inf(-1))},
|
||||||
|
tp{time.Unix(15, 0), float64Pointer(math.Inf(1))},
|
||||||
|
tp{time.Unix(15, 0), nil}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeriesReduceDropNN(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
red string
|
||||||
|
vars Vars
|
||||||
|
varToReduce string
|
||||||
|
results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dropNN: sum series",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: aSeries,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(3)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dropNN: sum series with a nil value",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(2)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dropNN: sum empty series",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dropNN: mean series with a nil value and real value",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(2)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DropNN: mean empty series",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DropNN: mean series that becomes empty after filtering non-number",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesNonNumbers,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DropNN: count empty series",
|
||||||
|
red: "count",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DropNN: count series with nil and value should only count real numbers",
|
||||||
|
red: "count",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(1)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
results := Results{}
|
||||||
|
seriesSet := tt.vars[tt.varToReduce]
|
||||||
|
for _, series := range seriesSet.Values {
|
||||||
|
ns, err := series.Value().(*Series).Reduce("", tt.red, DropNonNumber{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
results.Values = append(results.Values, ns)
|
||||||
|
}
|
||||||
|
opt := cmp.Comparer(func(x, y float64) bool {
|
||||||
|
return (math.IsNaN(x) && math.IsNaN(y)) || x == y
|
||||||
|
})
|
||||||
|
options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)
|
||||||
|
if diff := cmp.Diff(tt.results, results, options...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeriesReduceReplaceNN(t *testing.T) {
|
||||||
|
replaceWith := rand.Float64()
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
red string
|
||||||
|
vars Vars
|
||||||
|
varToReduce string
|
||||||
|
results Results
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "replaceNN: sum series",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: aSeries,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(3)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: sum series with a nil value",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(replaceWith+2)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: sum empty series",
|
||||||
|
red: "sum",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: mean series with a nil value and real value",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer((2+replaceWith)/2e0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: mean empty series",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(replaceWith)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: mean series that becomes empty after filtering non-number",
|
||||||
|
red: "mean",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesNonNumbers,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(replaceWith)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: count empty series",
|
||||||
|
red: "count",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesEmpty,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(0)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replaceNN: count series with nil and value should only count real numbers",
|
||||||
|
red: "count",
|
||||||
|
varToReduce: "A",
|
||||||
|
vars: seriesWithNil,
|
||||||
|
results: Results{
|
||||||
|
[]Value{
|
||||||
|
makeNumber("", nil, float64Pointer(2)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
results := Results{}
|
||||||
|
seriesSet := tt.vars[tt.varToReduce]
|
||||||
|
for _, series := range seriesSet.Values {
|
||||||
|
ns, err := series.Value().(*Series).Reduce("", tt.red, ReplaceNonNumberWithValue{Value: replaceWith})
|
||||||
|
require.NoError(t, err)
|
||||||
|
results.Values = append(results.Values, ns)
|
||||||
|
}
|
||||||
|
opt := cmp.Comparer(func(x, y float64) bool {
|
||||||
|
return (math.IsNaN(x) && math.IsNaN(y)) || x == y
|
||||||
|
})
|
||||||
|
options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)
|
||||||
|
if diff := cmp.Diff(tt.results, results, options...); diff != "" {
|
||||||
|
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ func (s Series) Resample(refID string, interval time.Duration, downsampler strin
|
|||||||
}
|
}
|
||||||
value = tmp
|
value = tmp
|
||||||
}
|
}
|
||||||
if err := resampled.SetPoint(idx, t, value); err != nil {
|
resampled.SetPoint(idx, t, value)
|
||||||
return resampled, err
|
|
||||||
}
|
|
||||||
t = t.Add(interval)
|
t = t.Add(interval)
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,17 +166,15 @@ func (s Series) GetPoint(pointIdx int) (time.Time, *float64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetPoint sets the time and value on the corresponding vectors at the specified index.
|
// SetPoint sets the time and value on the corresponding vectors at the specified index.
|
||||||
func (s Series) SetPoint(pointIdx int, t time.Time, f *float64) (err error) {
|
func (s Series) SetPoint(pointIdx int, t time.Time, f *float64) {
|
||||||
s.Frame.Fields[seriesTypeTimeIdx].Set(pointIdx, t)
|
s.Frame.Fields[seriesTypeTimeIdx].Set(pointIdx, t)
|
||||||
s.Frame.Fields[seriesTypeValIdx].Set(pointIdx, f)
|
s.Frame.Fields[seriesTypeValIdx].Set(pointIdx, f)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendPoint appends a point (time/value).
|
// AppendPoint appends a point (time/value).
|
||||||
func (s Series) AppendPoint(pointIdx int, t time.Time, f *float64) (err error) {
|
func (s Series) AppendPoint(t time.Time, f *float64) {
|
||||||
s.Frame.Fields[seriesTypeTimeIdx].Append(t)
|
s.Frame.Fields[seriesTypeTimeIdx].Append(t)
|
||||||
s.Frame.Fields[seriesTypeValIdx].Append(f)
|
s.Frame.Fields[seriesTypeValIdx].Append(f)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Len returns the length of the series.
|
// Len returns the length of the series.
|
||||||
@@ -214,8 +212,8 @@ func (ss SortSeriesByTime) Len() int { return Series(ss).Len() }
|
|||||||
func (ss SortSeriesByTime) Swap(i, j int) {
|
func (ss SortSeriesByTime) Swap(i, j int) {
|
||||||
iTimeVal, iFVal := Series(ss).GetPoint(i)
|
iTimeVal, iFVal := Series(ss).GetPoint(i)
|
||||||
jTimeVal, jFVal := Series(ss).GetPoint(j)
|
jTimeVal, jFVal := Series(ss).GetPoint(j)
|
||||||
_ = Series(ss).SetPoint(j, iTimeVal, iFVal)
|
Series(ss).SetPoint(j, iTimeVal, iFVal)
|
||||||
_ = Series(ss).SetPoint(i, jTimeVal, jFVal)
|
Series(ss).SetPoint(i, jTimeVal, jFVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss SortSeriesByTime) Less(i, j int) bool {
|
func (ss SortSeriesByTime) Less(i, j int) bool {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
|
import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
||||||
import { ExpressionQuery, reducerTypes } from '../types';
|
import { ExpressionQuery, ExpressionQuerySettings, ReducerMode, reducerMode, reducerTypes } from '../types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
labelWidth: number;
|
labelWidth: number;
|
||||||
@@ -21,6 +21,49 @@ export const Reduce: FC<Props> = ({ labelWidth, onChange, refIds, query }) => {
|
|||||||
onChange({ ...query, reducer: value.value });
|
onChange({ ...query, reducer: value.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSettingsChanged = (settings: ExpressionQuerySettings) => {
|
||||||
|
onChange({ ...query, settings: settings });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModeChanged = (value: SelectableValue<ReducerMode>) => {
|
||||||
|
let newSettings: ExpressionQuerySettings;
|
||||||
|
switch (value.value) {
|
||||||
|
case ReducerMode.ReplaceNonNumbers:
|
||||||
|
let replaceWithNumber = 0;
|
||||||
|
if (query.settings?.mode === ReducerMode.ReplaceNonNumbers) {
|
||||||
|
replaceWithNumber = query.settings?.replaceWithValue ?? 0;
|
||||||
|
}
|
||||||
|
newSettings = {
|
||||||
|
mode: ReducerMode.ReplaceNonNumbers,
|
||||||
|
replaceWithValue: replaceWithNumber,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
newSettings = {
|
||||||
|
mode: value.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
onSettingsChanged(newSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReplaceWithChanged = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.currentTarget.valueAsNumber;
|
||||||
|
onSettingsChanged({ mode: ReducerMode.ReplaceNonNumbers, replaceWithValue: value ?? 0 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const mode = query.settings?.mode ?? ReducerMode.Strict;
|
||||||
|
|
||||||
|
const replaceWithNumber = () => {
|
||||||
|
if (mode !== ReducerMode.ReplaceNonNumbers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<InlineField label="Replace With" labelWidth={labelWidth}>
|
||||||
|
<Input type="number" width={10} onChange={onReplaceWithChanged} value={query.settings?.replaceWithValue ?? 0} />
|
||||||
|
</InlineField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Function" labelWidth={labelWidth}>
|
<InlineField label="Function" labelWidth={labelWidth}>
|
||||||
@@ -29,6 +72,10 @@ export const Reduce: FC<Props> = ({ labelWidth, onChange, refIds, query }) => {
|
|||||||
<InlineField label="Input" labelWidth={labelWidth}>
|
<InlineField label="Input" labelWidth={labelWidth}>
|
||||||
<Select menuShouldPortal onChange={onRefIdChange} options={refIds} value={query.expression} width={20} />
|
<Select menuShouldPortal onChange={onRefIdChange} options={refIds} value={query.expression} width={20} />
|
||||||
</InlineField>
|
</InlineField>
|
||||||
|
<InlineField label="Mode" labelWidth={labelWidth}>
|
||||||
|
<Select menuShouldPortal onChange={onModeChanged} options={reducerMode} value={mode} width={25} />
|
||||||
|
</InlineField>
|
||||||
|
{replaceWithNumber()}
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,30 @@ export const reducerTypes: Array<SelectableValue<string>> = [
|
|||||||
{ value: ReducerID.last, label: 'Last', description: 'Get the last value' },
|
{ value: ReducerID.last, label: 'Last', description: 'Get the last value' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export enum ReducerMode {
|
||||||
|
Strict = '', // backend API wants an empty string to support "strict" mode
|
||||||
|
ReplaceNonNumbers = 'replaceNN',
|
||||||
|
DropNonNumbers = 'dropNN',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducerMode: Array<SelectableValue<ReducerMode>> = [
|
||||||
|
{
|
||||||
|
value: ReducerMode.Strict,
|
||||||
|
label: 'Strict',
|
||||||
|
description: 'Result can be NaN if series contains non-numeric data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ReducerMode.DropNonNumbers,
|
||||||
|
label: 'Drop Non-numeric Values',
|
||||||
|
description: 'Drop NaN, +/-Inf and null from input series before reducing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ReducerMode.ReplaceNonNumbers,
|
||||||
|
label: 'Replace Non-numeric Values',
|
||||||
|
description: 'Replace NaN, +/-Inf and null with a constant value before reducing',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const downsamplingTypes: Array<SelectableValue<string>> = [
|
export const downsamplingTypes: Array<SelectableValue<string>> = [
|
||||||
{ value: ReducerID.min, label: 'Min', description: 'Fill with the minimum value' },
|
{ value: ReducerID.min, label: 'Min', description: 'Fill with the minimum value' },
|
||||||
{ value: ReducerID.max, label: 'Max', description: 'Fill with the maximum value' },
|
{ value: ReducerID.max, label: 'Max', description: 'Fill with the maximum value' },
|
||||||
@@ -49,7 +73,14 @@ export interface ExpressionQuery extends DataQuery {
|
|||||||
downsampler?: string;
|
downsampler?: string;
|
||||||
upsampler?: string;
|
upsampler?: string;
|
||||||
conditions?: ClassicCondition[];
|
conditions?: ClassicCondition[];
|
||||||
|
settings?: ExpressionQuerySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpressionQuerySettings {
|
||||||
|
mode?: ReducerMode;
|
||||||
|
replaceWithValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClassicCondition {
|
export interface ClassicCondition {
|
||||||
evaluator: {
|
evaluator: {
|
||||||
params: number[];
|
params: number[];
|
||||||
|
|||||||
Reference in New Issue
Block a user