diff --git a/color_test.go b/color_test.go index 1b7fade..757b16b 100644 --- a/color_test.go +++ b/color_test.go @@ -5,13 +5,6 @@ import ( "testing" ) -var ( - white = color.RGBA{0xff, 0xff, 0xff, 0xff} - red = color.RGBA{0xff, 0, 0, 0xff} - green = color.RGBA{0, 0xff, 0, 0xff} - blue = color.RGBA{0, 0, 0xff, 0xff} -) - func TestParseColor(t *testing.T) { white := color.RGBA{0xff, 0xff, 0xff, 0xff} diff --git a/misc.go b/misc.go new file mode 100644 index 0000000..8ccb375 --- /dev/null +++ b/misc.go @@ -0,0 +1,28 @@ +package main + +func coalesce(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} + +// provided a number and a bounding range for that number, normalizes that +// number within that bounds (linearly). that is, it returns a float between 0 +// and 1 representing i's position within the (min,max) range. Min and max are +// both inclusive. +func norm(i, min, max int) (n float64) { + if i < min { + return 0 + } + if i > max { + return 1 + } + + span := max - min + reach := i - min + + return float64(reach) / float64(span) +} diff --git a/misc_test.go b/misc_test.go new file mode 100644 index 0000000..c56f5b4 --- /dev/null +++ b/misc_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "testing" +) + +func TestNorm(t *testing.T) { + type test struct { + i int + min int + max int + n float64 + } + tests := []test{ + {1, 2, 3, 0}, + {3, 2, 1, 1}, + {1, 0, 10, 0.1}, + {5, 0, 10, 0.5}, + {125, 100, 200, 0.25}, + {-125, -200, -100, 0.75}, + } + + for _, test := range tests { + n := norm(test.i, test.min, test.max) + if n == test.n { + t.Logf("norm(%d, %d, %d) == %f", test.i, test.min, test.max, n) + } else { + t.Errorf("norm(%d, %d, %d) is %f, expected %f", test.i, test.min, test.max, n, test.n) + } + } +} diff --git a/server.go b/server.go index b32436c..3e75f9e 100644 --- a/server.go +++ b/server.go @@ -14,6 +14,9 @@ import ( var ( white = color.RGBA{0xff, 0xff, 0xff, 0xff} + red = color.RGBA{0xff, 0, 0, 0xff} + green = color.RGBA{0, 0xff, 0, 0xff} + blue = color.RGBA{0, 0, 0xff, 0xff} ) type server struct { @@ -27,6 +30,7 @@ func (s *server) logReceived(r *http.Request) { func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.logReceived(r) + encoder, err := getEncoding(r) if err != nil { s.writeError(w, err) @@ -36,9 +40,29 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { width := parseInt(r.URL.Query().Get("w"), 400) height := parseInt(r.URL.Query().Get("h"), 400) bg := parseColor(r.URL.Query().Get("bg"), white) + data := parseSeries(r.URL.Query().Get("s"), series{}) + // create a new canvas m := image.NewRGBA(image.Rect(0, 0, width, height)) + + // paint the background draw.Draw(m, m.Bounds(), &image.Uniform{bg}, m.Bounds().Min, draw.Src) + + // compute column dimensions + num_cols := data.len() + col_width := width + if num_cols > 0 { + col_width = width / num_cols // float here? + } + + fg := &image.Uniform{blue} + for idx, val := range data { + col_height_n := norm(val, data.min(), data.max()) + col_height := int(col_height_n * float64(height)) + x := idx * col_width + rect := image.Rect(x, height, x+col_width, height-col_height) + draw.Draw(m, rect, fg, image.ZP, draw.Src) + } encoder.WriteImage(w, m) } @@ -91,6 +115,62 @@ func parseColor(c_s string, d color.RGBA) color.RGBA { return c } +type series []int + +// minimum value in the series +func (s series) min() int { + switch len(s) { + case 0: + return 0 + case 1: + return s[0] + default: + } + m := s[0] + for _, i := range s[1:] { + if i < m { + m = i + } + } + return m +} + +// maximum value in the series +func (s series) max() int { + switch len(s) { + case 0: + return 0 + case 1: + return s[0] + default: + } + m := s[0] + for _, i := range s[1:] { + if i > m { + m = i + } + } + return m +} + +func (s series) len() int { + return len(s) +} + +// parses a user-supplied series +func parseSeries(s string, missing series) series { + parts := strings.Split(s, ",") + out := make(series, 0, len(parts)) + for _, p := range parts { + n, err := strconv.Atoi(p) + if err != nil { + return missing + } + out = append(out, n) + } + return out +} + type imageWriter interface { WriteImage(w http.ResponseWriter, m image.Image) }