Skip to content

Commit

Permalink
feat: consistent colors in graph by sorting the data
Browse files Browse the repository at this point in the history
The Time Series visualisation chooses colors for each series based on
the order they are returned. The order previously relied on the API
requests, which meant that it was random every run, causing random
colors to be assigned every time the graph was refreshed.

We now sort the Frames by the resource ID first and then by the series
name. This should give a consistent order as long as no new resources
are added. And even then, new resources usually have a higher ID and do
not break the current sorting, so only new labels can cause "issues".

Closes #7
  • Loading branch information
apricote committed Apr 28, 2024
1 parent 9b6e9af commit bb4db25
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 1 deletion.
44 changes: 44 additions & 0 deletions pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"math"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -359,6 +360,9 @@ func (d *Datasource) queryMetrics(ctx context.Context, query backend.DataQuery)
}
}

// Keep colors in graph the same
sortFrames(resp.Frames)

return resp
}

Expand Down Expand Up @@ -417,6 +421,8 @@ func serverMetricsToFrames(id int64, serverName string, legendFormat string, met

frame.Fields = append(frame.Fields,
data.NewField("time", nil, timestamps),
// valuesField needs to be last, if this is changed,
// you also need to modify [sortFrames] for the new ordering.
valuesField,
)

Expand Down Expand Up @@ -465,6 +471,8 @@ func loadBalancerMetricsToFrames(id int64, loadBalancerMetrics string, legendFor

frame.Fields = append(frame.Fields,
data.NewField("time", nil, timestamps),
// valuesField needs to be last, if this is changed,
// you also need to modify [sortFrames] for the new ordering.
valuesField,
)

Expand All @@ -491,6 +499,42 @@ func getDisplayName(legendFormat string, labels data.Labels) string {
})
}

// sortFrames sorts frames by their [LabelID] and [LabelSeriesName]. This helps with the coloring in the
// Time Series panel, as they depend on the order of the results.
func sortFrames(frames []*data.Frame) {

slices.SortFunc(frames, func(a, b *data.Frame) int {
idA, okA := a.Fields[len(a.Fields)-1].Labels[LabelID]
idB, okB := b.Fields[len(b.Fields)-1].Labels[LabelID]

switch {
case !okA || !okB:
// Unknown ordering
return 0
case idA > idB:
return 1
case idA < idB:
return -1
}
// If IDs are equal, we compare by series name

seriesA, okA := a.Fields[len(a.Fields)-1].Labels[LabelSeriesName]
seriesB, okB := b.Fields[len(b.Fields)-1].Labels[LabelSeriesName]

switch {
case !okA || !okB:
// Unknown ordering
return 0
case seriesA > seriesB:
return 1
case seriesA < seriesB:
return -1
default:
return 0
}
})
}

// CheckHealth handles health checks sent from Grafana to the plugin.
// The main use case for these health checks is the test button on the
// datasource configuration page which allows users to verify that
Expand Down
61 changes: 60 additions & 1 deletion pkg/plugin/datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package plugin

import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/data"
"reflect"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
)

func TestQueryData(t *testing.T) {
Expand Down Expand Up @@ -80,3 +81,61 @@ func Test_getDisplayName(t *testing.T) {
})
}
}

func Test_sortFrames(t *testing.T) {
frame := func(id string, seriesName string) *data.Frame {
return &data.Frame{Fields: []*data.Field{{Labels: data.Labels{
LabelID: id,
LabelSeriesName: seriesName,
}}}}
}

type args struct {
frames []*data.Frame
}
tests := []struct {
name string
input []*data.Frame
expected []*data.Frame
}{
{
name: "Single Frame",
input: []*data.Frame{frame("Foo", "")},
expected: []*data.Frame{frame("Foo", "")},
},
{
name: "Two Frames Sorted",
input: []*data.Frame{frame("Bar", ""), frame("Foo", "")},
expected: []*data.Frame{frame("Bar", ""), frame("Foo", "")},
},
{
name: "Two Frames Unsorted",
input: []*data.Frame{frame("Foo", ""), frame("Bar", "")},
expected: []*data.Frame{frame("Bar", ""), frame("Foo", "")},
},
{
name: "Two Frames by Series Name Sorted",
input: []*data.Frame{frame("Foo", "A"), frame("Foo", "B")},
expected: []*data.Frame{frame("Foo", "A"), frame("Foo", "B")},
},
{
name: "Two Frames by Series Name Unsorted",
input: []*data.Frame{frame("Foo", "B"), frame("Foo", "A")},
expected: []*data.Frame{frame("Foo", "A"), frame("Foo", "B")},
},
{
name: "Two Frames by Series Name Equal",
input: []*data.Frame{frame("Foo", "A"), frame("Foo", "A")},
expected: []*data.Frame{frame("Foo", "A"), frame("Foo", "A")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sortFrames(tt.input)

if !reflect.DeepEqual(tt.input, tt.expected) {
t.Errorf("sortFrames() = %v, want: %v", tt.input, tt.expected)
}
})
}
}

0 comments on commit bb4db25

Please sign in to comment.