godev/cmd/worker: refactor and simplify worker partitioning

Refactor the worker logic as follows:
- Rename 'nest' to 'group'.
- Use strongly typed strings (programName, graphName, etc) in more
  places
- Group data by raw bucket name, rather than "normalized counter name".
  This makes the data format simpler, and means we don't need to invoke
  normalizeCounterName very precisely in two places (action at a
  distance). A follow-up CL will lift name normalization to the caller,
  as noted in a TODO.
- Simplify normalizeCounterName to return a bucketName.

Notably, TestCharts did not need to change as a result of this change,
because these transformations only affect the intermediate
representation of the data, not the final output.

Change-Id: I4bfac774d40e665f8a6308936e15b7df219ab931
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/613075
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/godev/cmd/worker/main.go b/godev/cmd/worker/main.go
index b447aef..aebc41e 100644
--- a/godev/cmd/worker/main.go
+++ b/godev/cmd/worker/main.go
@@ -341,7 +341,7 @@
 			}
 		}
 
-		data := nest(reports)
+		data := group(reports)
 		charts := charts(cfg, start.Format(telemetry.DateOnly), end.Format(telemetry.DateOnly), data, xs)
 
 		obj := fileName(start, end)
@@ -399,17 +399,24 @@
 		prog := &program{ID: "charts:" + p.Name, Name: p.Name}
 		result.Programs = append(result.Programs, prog)
 		var charts []*chart
+		program := programName(p.Name)
 		if !telemetry.IsToolchainProgram(p.Name) {
-			charts = append(charts, d.partition(p.Name, "Version", p.Versions, compareSemver))
+			charts = append(charts, d.partition(program, "Version", toSliceOf[bucketName](p.Versions), compareSemver))
 		}
 		charts = append(charts,
-			d.partition(p.Name, "GOOS", cfg.GOOS, nil),
-			d.partition(p.Name, "GOARCH", cfg.GOARCH, nil),
-			d.partition(p.Name, "GoVersion", cfg.GoVersion, version.Compare))
+			d.partition(program, "GOOS", toSliceOf[bucketName](cfg.GOOS), nil),
+			d.partition(program, "GOARCH", toSliceOf[bucketName](cfg.GOARCH), nil),
+			d.partition(program, "GoVersion", toSliceOf[bucketName](cfg.GoVersion), version.Compare))
 		for _, c := range p.Counters {
 			// TODO: add support for histogram counters by getting the counter type
 			// from the chart config.
-			charts = append(charts, d.partition(p.Name, c.Name, tconfig.Expand(c.Name), nil))
+			chart, _ := splitCounterName(c.Name)
+			var buckets []bucketName
+			for _, counter := range tconfig.Expand(c.Name) {
+				_, bucket := splitCounterName(counter)
+				buckets = append(buckets, bucket)
+			}
+			charts = append(charts, d.partition(program, chart, buckets, nil))
 		}
 		for _, p := range charts {
 			if p != nil {
@@ -420,6 +427,15 @@
 	return result
 }
 
+// toSliceOf converts a slice of once string type to another.
+func toSliceOf[To, From ~string](s []From) []To {
+	var s2 []To
+	for _, v := range s {
+		s2 = append(s2, To(v))
+	}
+	return s2
+}
+
 // compareSemver wraps semver.Compare, to differentiate equivalent semver
 // lexically, as we want all sorting to be stable.
 func compareSemver(x, y string) int {
@@ -447,20 +463,18 @@
 // if compareBuckets is provided, it is used to sort the buckets, where
 // compareBuckets returns -1, 0, or +1 if x < y, x == y, or x > y.
 // Otherwise, buckets are sorted lexically.
-func (d data) partition(program, counter string, counters []string, compareBuckets func(x, y string) int) *chart {
-	prefix, _ := splitCounterName(counter)
+func (d data) partition(program programName, chartName graphName, buckets []bucketName, compareBuckets func(x, y string) int) *chart {
 	chart := &chart{
-		ID:   "charts:" + program + ":" + prefix,
-		Name: prefix,
+		ID:   fmt.Sprintf("charts:%s:%s", program, chartName),
+		Name: string(chartName),
 		Type: "partition",
 	}
 	pk := programName(program)
-	gk := graphName(prefix)
 
 	var (
-		counts = make(map[string]float64) // bucket name -> total count
-		empty  = true                     // keep track of empty reports, so they can be skipped
-		end    weekName                   // latest week observed
+		merged = make(map[bucketName]map[reportID]struct{}) // normalized bucket name -> merged report IDs
+		empty  = true                                       // keep track of empty reports, so they can be skipped
+		end    weekName                                     // latest week observed
 	)
 	for wk := range d {
 		if wk >= end {
@@ -468,22 +482,20 @@
 		}
 		// We group versions into major minor buckets, we must skip
 		// major minor versions we've already added to the dataset.
-		seen := make(map[string]bool)
-		for _, b := range counters {
-			// TODO(hyangah): let caller normalize names in counters.
-			counter := normalizeCounterName(counter, b)
-			if seen[counter] {
+		seen := make(map[bucketName]bool)
+		for _, bucket := range buckets {
+			if seen[bucket] {
 				continue
 			}
-			seen[counter] = true
-			ck := counterName(counter)
-			// number of reports where count prefix:bucket > 0
-			n := len(d[wk][pk][gk][ck])
-			_, bucket := splitCounterName(counter)
-
-			counts[bucket] += float64(n)
-			if n > 0 {
+			seen[bucket] = true
+			// TODO(hyangah): let caller normalize names in counters.
+			normal := normalizeCounterName(chartName, bucket)
+			if _, ok := merged[normal]; !ok {
+				merged[normal] = make(map[reportID]struct{})
+			}
+			for id := range d[wk][pk][chartName][bucket] {
 				empty = false
+				merged[normal][id] = struct{}{}
 			}
 		}
 	}
@@ -493,11 +505,11 @@
 	}
 
 	// datum.Week always points to the end date
-	for bucket, v := range counts {
+	for bucket, v := range merged {
 		d := &datum{
 			Week:  string(end),
-			Key:   bucket,
-			Value: v,
+			Key:   string(bucket),
+			Value: float64(len(v)),
 		}
 		chart.Data = append(chart.Data, d)
 	}
@@ -522,18 +534,23 @@
 
 // graphName is the graph name.
 // A graph plots distribution or timeseries of related counters.
+//
+// TODO(rfindley): rename to chartName.
 type graphName string
 
-// counterName is the counter name.
+// counterName is the counter name. counterName is graphName:bucketName.
 type counterName string
 
+// bucketName is the bucket name.
+type bucketName string
+
 // reportID is the upload report ID.
 // The current implementation uses telemetry.Report.X,
 // a random number, computed by the uploader when creating a Report object.
 // See x/telemetry/internal/upload.(*uploader).createReport.
 type reportID float64
 
-type data map[weekName]map[programName]map[graphName]map[counterName]map[reportID]int64
+type data map[weekName]map[programName]map[graphName]map[bucketName]map[reportID]int64
 
 // Names of special counters.
 // Unlike other counters, these are constructed from the metadata in the report.
@@ -544,19 +561,31 @@
 	goversionCounter = "GoVersion"
 )
 
-// nest groups the report data by week, program, prefix, counter, and x value
+// group groups the report data by week, program, prefix, counter, and x value
 // summing together counter values for each program report in a report.
-func nest(reports []telemetry.Report) data {
+func group(reports []telemetry.Report) data {
 	result := make(data)
 	for _, r := range reports {
+		var (
+			week = weekName(r.Week)
+			// x is a random number sent with each upload report.
+			// Since there is no identifier for the uploader, we use x as the uploader ID
+			// to approximate the number of unique uploader.
+			//
+			// Multiple uploads with the same x will overwrite each other, so we set the
+			// value, rather than add it to the existing value.
+			id = reportID(r.X)
+		)
 		for _, p := range r.Programs {
-			result.writeCount(r.Week, p.Program, versionCounter, p.Version, r.X, 1)
-			result.writeCount(r.Week, p.Program, goosCounter, p.GOOS, r.X, 1)
-			result.writeCount(r.Week, p.Program, goarchCounter, p.GOARCH, r.X, 1)
-			result.writeCount(r.Week, p.Program, goversionCounter, p.GoVersion, r.X, 1)
+			program := programName(p.Program)
+
+			result.writeCount(week, program, versionCounter, bucketName(p.Version), id, 1)
+			result.writeCount(week, program, goosCounter, bucketName(p.GOOS), id, 1)
+			result.writeCount(week, program, goarchCounter, bucketName(p.GOARCH), id, 1)
+			result.writeCount(week, program, goversionCounter, bucketName(p.GoVersion), id, 1)
 			for c, value := range p.Counters {
-				prefix, _ := splitCounterName(c)
-				result.writeCount(r.Week, p.Program, prefix, c, r.X, value)
+				chart, bucket := splitCounterName(c)
+				result.writeCount(week, program, chart, bucket, id, value)
 			}
 		}
 	}
@@ -566,36 +595,20 @@
 // writeCount writes the counter values to the result. When a report contains
 // multiple program reports for the same program, the value of the counters
 // in that report are summed together.
-func (d data) writeCount(week, program, prefix, counter string, x float64, value int64) {
-	wk := weekName(week)
-	if _, ok := d[wk]; !ok {
-		d[wk] = make(map[programName]map[graphName]map[counterName]map[reportID]int64)
+func (d data) writeCount(week weekName, program programName, chart graphName, bucket bucketName, id reportID, value int64) {
+	if _, ok := d[week]; !ok {
+		d[week] = make(map[programName]map[graphName]map[bucketName]map[reportID]int64)
 	}
-	pk := programName(program)
-	if _, ok := d[wk][pk]; !ok {
-		d[wk][pk] = make(map[graphName]map[counterName]map[reportID]int64)
+	if _, ok := d[week][program]; !ok {
+		d[week][program] = make(map[graphName]map[bucketName]map[reportID]int64)
 	}
-	// We want to group and plot bucket/histogram counters with the same prefix.
-	// Use the prefix as the graph name.
-	gk := graphName(prefix)
-	if _, ok := d[wk][pk][gk]; !ok {
-		d[wk][pk][gk] = make(map[counterName]map[reportID]int64)
+	if _, ok := d[week][program][chart]; !ok {
+		d[week][program][chart] = make(map[bucketName]map[reportID]int64)
 	}
-	// TODO(hyangah): let caller pass the normalized counter name.
-	counter = normalizeCounterName(prefix, counter)
-	ck := counterName(counter)
-	if _, ok := d[wk][pk][gk][ck]; !ok {
-		d[wk][pk][gk][ck] = make(map[reportID]int64)
+	if _, ok := d[week][program][chart][bucket]; !ok {
+		d[week][program][chart][bucket] = make(map[reportID]int64)
 	}
-
-	// x is a random number sent with each upload report.
-	// Since there is no identifier for the uploader, we use x as the uploader ID
-	// to approximate the number of unique uploader.
-	//
-	// Multiple uploads with the same x will overwrite each other, so we set the
-	// value, rather than add it to the existing value.
-	id := reportID(x)
-	d[wk][pk][gk][ck][id] = value
+	d[week][program][chart][bucket][id] = value
 }
 
 // normalizeCounterName normalizes the counter name.
@@ -606,35 +619,31 @@
 // To limit the cardinality of version and goVersion, this function
 // uses only major and minor version numbers in the pseudo-counter names.
 // If the counter is a normal counter name, it is returned as is.
-func normalizeCounterName(prefix, counter string) string {
-	switch prefix {
+func normalizeCounterName(chart graphName, bucket bucketName) bucketName {
+	switch chart {
 	case versionCounter:
-		if counter == "devel" {
-			return prefix + ":" + counter
+		if bucket == "devel" {
+			return bucket
 		}
-		if strings.HasPrefix(counter, "go") {
-			return prefix + ":" + goMajorMinor(counter)
+		if strings.HasPrefix(string(bucket), "go") {
+			return bucketName(goMajorMinor(string(bucket)))
 		}
-		return prefix + ":" + semver.MajorMinor(counter)
-	case goosCounter:
-		return prefix + ":" + counter
-	case goarchCounter:
-		return prefix + ":" + counter
+		return bucketName(semver.MajorMinor(string(bucket)))
 	case goversionCounter:
-		return prefix + ":" + goMajorMinor(counter)
+		return bucketName(goMajorMinor(string(bucket)))
 	}
-	return counter
+	return bucket
 }
 
 // splitCounterName gets splits the prefix and bucket splitCounterName of a counter name
 // or a bucket name. For an input with no bucket part prefix and bucket
 // are the same.
-func splitCounterName(name string) (prefix, bucket string) {
+func splitCounterName(name string) (graphName, bucketName) {
 	prefix, bucket, found := strings.Cut(name, ":")
 	if !found {
 		bucket = prefix
 	}
-	return prefix, bucket
+	return graphName(prefix), bucketName(bucket)
 }
 
 // goMajorMinor gets the go<Maj>,<Min> version for a given go version.
diff --git a/godev/cmd/worker/main_test.go b/godev/cmd/worker/main_test.go
index 3ec9be0..4ee1bde 100644
--- a/godev/cmd/worker/main_test.go
+++ b/godev/cmd/worker/main_test.go
@@ -130,7 +130,7 @@
 	},
 }
 
-func TestNest(t *testing.T) {
+func TestGroup(t *testing.T) {
 	type args struct {
 		reports []telemetry.Report
 	}
@@ -173,35 +173,35 @@
 				weekName("2999-01-01"): {
 					programName("example.com/mod/pkg"): {
 						graphName("Version"): {
-							counterName("Version:v1.2"): {
+							bucketName("v1.2.3"): {
 								reportID(0.1234567890): 1,
 							},
 						},
 						graphName("GOOS"): {
-							counterName("GOOS:darwin"): {
+							bucketName("darwin"): {
 								reportID(0.1234567890): 1,
 							},
 						},
 						graphName("GOARCH"): {
-							counterName("GOARCH:arm64"): {
+							bucketName("arm64"): {
 								reportID(0.1234567890): 1,
 							},
 						},
 						graphName("GoVersion"): {
-							counterName("GoVersion:go1.2"): {
+							bucketName("go1.2.3"): {
 								reportID(0.1234567890): 1,
 							},
 						},
 						graphName("main"): {
-							counterName("main"): {
+							bucketName("main"): {
 								reportID(0.1234567890): 1,
 							},
 						},
 						graphName("flag"): {
-							counterName("flag:a"): {
+							bucketName("a"): {
 								reportID(0.1234567890): 2,
 							},
-							counterName("flag:b"): {
+							bucketName("b"): {
 								reportID(0.1234567890): 3,
 							},
 						},
@@ -212,7 +212,7 @@
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := nest(tt.args.reports)
+			got := group(tt.args.reports)
 			if diff := cmp.Diff(tt.want, got); diff != "" {
 				t.Errorf("nest() mismatch (-want +got):\n%s", diff)
 			}
@@ -221,11 +221,11 @@
 }
 
 func TestPartition(t *testing.T) {
-	exampleData := nest(exampleReports)
+	exampleData := group(exampleReports)
 	type args struct {
-		program string
-		name    string
-		buckets []string
+		program programName
+		name    graphName
+		buckets []bucketName
 	}
 	tests := []struct {
 		name string
@@ -239,7 +239,7 @@
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "Version",
-				buckets: []string{"v1.2.3", "v2.3.4"},
+				buckets: []bucketName{"v1.2.3", "v2.3.4"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:Version",
@@ -265,7 +265,7 @@
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "Version",
-				buckets: []string{"v1.2", "v2.3"},
+				buckets: []bucketName{"v1.2.3", "v2.3.4"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:Version",
@@ -291,7 +291,7 @@
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "Version",
-				buckets: []string{"v1.2.3", "v2.3.4", "v1.2.3"},
+				buckets: []bucketName{"v1.2.3", "v2.3.4", "v1.2.3"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:Version",
@@ -317,7 +317,7 @@
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "GOOS",
-				buckets: []string{"darwin", "linux"},
+				buckets: []bucketName{"darwin", "linux"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:GOOS",
@@ -341,26 +341,23 @@
 			name: "three days, multiple versions",
 			data: data{
 				"2999-01-01": {"example.com/mod/pkg": {"Version": {
-					"Version":      {0.1: 5},
-					"Version:v1.2": {0.1: 2},
-					"Version:v2.3": {0.1: 3},
+					"v1.2.3": {0.1: 2},
+					"v2.3.4": {0.1: 3},
 				},
 				}},
 				"2999-01-04": {"example.com/mod/pkg": {"Version": {
-					"Version":      {0.3: 2, 0.4: 5},
-					"Version:v1.2": {0.3: 2},
-					"Version:v2.3": {0.4: 5},
+					"v1.2.3": {0.3: 2},
+					"v2.3.4": {0.4: 5},
 				},
 				}},
 				"2999-01-05": {"example.com/mod/pkg": {"Version": {
-					"Version":      {0.5: 6},
-					"Version:v2.3": {0.5: 6},
+					"v2.3.4": {0.5: 6},
 				}}},
 			},
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "Version",
-				buckets: []string{"v1.2.3", "v2.3.4"},
+				buckets: []bucketName{"v1.2.3", "v2.3.4"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:Version",
@@ -384,27 +381,24 @@
 			name: "three days, multiple GOOS",
 			data: data{
 				"2999-01-01": {"example.com/mod/pkg": {"GOOS": {
-					"GOOS":        {0.1: 4, 0.2: 4, 0.3: 2},
-					"GOOS:darwin": {0.1: 2, 0.2: 2, 0.3: 2},
-					"GOOS:linux":  {0.1: 2, 0.2: 2},
+					"darwin": {0.1: 2, 0.2: 2, 0.3: 2},
+					"linux":  {0.1: 2, 0.2: 2},
 				},
 				}},
 				"2999-01-02": {"example.com/mod/pkg": {"GOOS": {
-					"GOOS":        {0.4: 2, 0.5: 2, 0.6: 5},
-					"GOOS:darwin": {0.4: 2, 0.5: 2},
-					"GOOS:linux":  {0.6: 5},
+					"darwin": {0.4: 2, 0.5: 2},
+					"linux":  {0.6: 5},
 				},
 				}},
 				"2999-01-03": {"example.com/mod/pkg": {"GOOS": {
-					"GOOS":        {0.6: 3},
-					"GOOS:darwin": {0.6: 3},
+					"darwin": {0.6: 3},
 				},
 				}},
 			},
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "GOOS",
-				buckets: []string{"darwin", "linux"},
+				buckets: []bucketName{"darwin", "linux"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:GOOS",
@@ -428,21 +422,19 @@
 			name: "two days data, missing GOOS in first day",
 			data: data{
 				"2999-01-01": {"example.com/mod/pkg": {"Version": {
-					"Version":      {0.1: 2},
-					"Version:v1.2": {0.1: 2},
+					"v1.2": {0.1: 2},
 				},
 				}},
 				"2999-01-02": {"example.com/mod/pkg": {"GOOS": {
-					"GOOS":        {0.3: 4},
-					"GOOS:darwin": {0.3: 2},
-					"GOOS:linux":  {0.3: 2},
+					"darwin": {0.3: 2},
+					"linux":  {0.3: 2},
 				},
 				}},
 			},
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "GOOS",
-				buckets: []string{"darwin", "linux"},
+				buckets: []bucketName{"darwin", "linux"},
 			},
 			want: &chart{
 				ID:   "charts:example.com/mod/pkg:GOOS",
@@ -484,7 +476,7 @@
 			args: args{
 				program: "example.com/mod/pkg",
 				name:    "Version",
-				buckets: []string{"v1.2.3", "v2.3.4"},
+				buckets: []bucketName{"v1.2.3", "v2.3.4"},
 			},
 			want: nil,
 		},
@@ -500,7 +492,7 @@
 }
 
 func TestCharts(t *testing.T) {
-	exampleData := nest(exampleReports)
+	exampleData := group(exampleReports)
 	cfg := &config.Config{
 		UploadConfig: &telemetry.UploadConfig{
 			GOOS:       []string{"darwin"},
@@ -638,54 +630,54 @@
 
 func TestNormalizeCounterName(t *testing.T) {
 	testcases := []struct {
-		name    string
-		prefix  string
-		counter string
-		want    string
+		name   string
+		chart  graphName
+		bucket bucketName
+		want   bucketName
 	}{
 		{
-			name:    "strip patch version for Version",
-			prefix:  "Version",
-			counter: "v0.15.3",
-			want:    "Version:v0.15",
+			name:   "strip patch version for Version",
+			chart:  "Version",
+			bucket: "v0.15.3",
+			want:   "v0.15",
 		},
 		{
-			name:    "strip patch go version for Version",
-			prefix:  "Version",
-			counter: "go1.12.3",
-			want:    "Version:go1.12",
+			name:   "strip patch go version for Version",
+			chart:  "Version",
+			bucket: "go1.12.3",
+			want:   "go1.12",
 		},
 		{
-			name:    "concatenate devel for Version",
-			prefix:  "Version",
-			counter: "devel",
-			want:    "Version:devel",
+			name:   "concatenate devel for Version",
+			chart:  "Version",
+			bucket: "devel",
+			want:   "devel",
 		},
 		{
-			name:    "concatenate for GOOS",
-			prefix:  "GOOS",
-			counter: "darwin",
-			want:    "GOOS:darwin",
+			name:   "concatenate for GOOS",
+			chart:  "GOOS",
+			bucket: "darwin",
+			want:   "darwin",
 		},
 		{
-			name:    "concatenate for GOARCH",
-			prefix:  "GOARCH",
-			counter: "amd64",
-			want:    "GOARCH:amd64",
+			name:   "concatenate for GOARCH",
+			chart:  "GOARCH",
+			bucket: "amd64",
+			want:   "amd64",
 		},
 		{
-			name:    "strip patch version for GoVersion",
-			prefix:  "GoVersion",
-			counter: "go1.12.3",
-			want:    "GoVersion:go1.12",
+			name:   "strip patch version for GoVersion",
+			chart:  "GoVersion",
+			bucket: "go1.12.3",
+			want:   "go1.12",
 		},
 	}
 
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
-			got := normalizeCounterName(tc.prefix, tc.counter)
+			got := normalizeCounterName(tc.chart, tc.bucket)
 			if tc.want != got {
-				t.Errorf("normalizeCounterName(%q, %q) = %q, want %q", tc.prefix, tc.counter, got, tc.want)
+				t.Errorf("normalizeCounterName(%q, %q) = %q, want %q", tc.chart, tc.bucket, got, tc.want)
 			}
 		})
 	}
@@ -693,22 +685,25 @@
 
 func TestWriteCount(t *testing.T) {
 	type keyValue struct {
-		week, program, prefix, counter string
-		x                              float64
-		value                          int64
+		week    weekName
+		program programName
+		chart   graphName
+		bucket  bucketName
+		x       reportID
+		value   int64
 	}
 	testcases := []struct {
 		name   string
 		inputs []keyValue
-		wants  []keyValue
+		want   []keyValue
 	}{
 		{
 			name: "program version counter should have value",
 			inputs: []keyValue{
 				{"2987-07-01", "golang.org/x/tools/gopls", "Version", "v0.15.3", 0.00009, 1},
 			},
-			wants: []keyValue{
-				{"2987-07-01", "golang.org/x/tools/gopls", "Version", "Version:v0.15", 0.00009, 1},
+			want: []keyValue{
+				{"2987-07-01", "golang.org/x/tools/gopls", "Version", "v0.15.3", 0.00009, 1},
 			},
 		},
 		{
@@ -716,7 +711,7 @@
 			inputs: []keyValue{
 				{"2987-06-30", "cmd/go", "go/invocations", "go/invocations", 0.86995, 84},
 			},
-			wants: []keyValue{
+			want: []keyValue{
 				{"2987-06-30", "cmd/go", "go/invocations", "go/invocations", 0.86995, 84},
 			},
 		},
@@ -727,8 +722,8 @@
 				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2},
 				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 3},
 			},
-			wants: []keyValue{
-				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "GOOS:windows", 0.86018, 3},
+			want: []keyValue{
+				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 3},
 			},
 		},
 		{
@@ -737,9 +732,9 @@
 				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2},
 				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "linux", 0.86018, 4},
 			},
-			wants: []keyValue{
-				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "GOOS:windows", 0.86018, 2},
-				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "GOOS:linux", 0.86018, 4},
+			want: []keyValue{
+				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2},
+				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "linux", 0.86018, 4},
 			},
 		},
 	}
@@ -748,13 +743,13 @@
 		t.Run(tc.name, func(t *testing.T) {
 			d := make(data)
 			for _, input := range tc.inputs {
-				d.writeCount(input.week, input.program, input.prefix, input.counter, input.x, input.value)
+				d.writeCount(input.week, input.program, input.chart, input.bucket, input.x, input.value)
 			}
 
-			for _, want := range tc.wants {
-				got := d[weekName(want.week)][programName(want.program)][graphName(want.prefix)][counterName(want.counter)][reportID(want.x)]
+			for _, want := range tc.want {
+				got := d[want.week][want.program][want.chart][want.bucket][want.x]
 				if want.value != got {
-					t.Errorf("d[%q][%q][%q][%q][%v] = %v, want %v", want.week, want.program, want.prefix, want.counter, want.x, got, want.value)
+					t.Errorf("d[%q][%q][%q][%q][%v] = %v, want %v", want.week, want.program, want.chart, want.bucket, want.x, got, want.value)
 				}
 			}
 		})