1
0
mirror of https://github.com/nikolaydubina/calendarheatmap.git synced 2025-03-04 16:15:45 +02:00
This commit is contained in:
Nikolay 2020-07-03 15:16:42 +08:00
parent 5cd1ef7340
commit 6c296eb900
17 changed files with 326 additions and 18 deletions

View File

@ -3,19 +3,21 @@
Self-contained, plain Go implementation of calendar heatmap inspired by Github contribution activity.
Basic
![basic](charts/testdata/basic.png)
Colorscales
![PuBu9](examples/chart_PuBu9.png)
![GnBu9](examples/chart_GnBu9.png)
![YlGn9](examples/chart_YlGn9.png)
![col1](charts/testdata/colorscale_1.png)
![col2](charts/testdata/colorscale_2.png)
Without month separator
![PuBu9_noseparator](examples/chart_PuBu9_noseparator.png)
![nosep](charts/testdata/noseparator.png)
Without labels
![PuBu9_noseparator](examples/chart_PuBu9_nolabels.png)
![nolab](charts/testdata/nolabels.png)
Without labels, without separator
![PuBu9_noseparator](examples/chart_PuBu9_noseparator_nolabels.png)
![nosep_nolab](charts/testdata/noseparator_nolabels.png)
Example:

171
charts/charts_test.go Normal file
View File

@ -0,0 +1,171 @@
package charts
import (
"fmt"
"image"
"image/color"
"image/png"
"os"
"testing"
"github.com/nikolaydubina/calendarheatmap/colorscales"
)
func savePNG(t *testing.T, img image.Image, filename string) {
f, err := os.Create(filename)
if err != nil {
t.Errorf(fmt.Errorf("can not save: %w", err).Error())
}
if err := png.Encode(f, img); err != nil {
t.Errorf(fmt.Errorf("can not encode png: %w", err).Error())
}
if err := f.Close(); err != nil {
t.Errorf(fmt.Errorf("can not close: %w", err).Error())
}
}
func TestBasicData(t *testing.T) {
countByDay := map[int]int{
137: 8, 138: 13, 139: 5, 140: 8, 141: 5, 142: 5, 143: 3, 144: 5,
145: 6, 146: 3, 147: 5, 148: 8, 149: 2, 150: 3, 151: 8, 152: 5,
153: 1, 154: 3, 155: 1, 156: 3, 157: 1, 158: 3, 159: 5, 161: 1,
162: 2, 164: 9, 165: 7, 166: 4, 167: 1, 169: 1, 172: 2, 173: 1,
175: 2, 176: 2, 177: 3, 178: 3, 179: 2, 180: 1, 181: 1, 182: 2,
}
t.Run("basic", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
ColorScale: colorscales.PuBu9,
DrawMonthSeparator: true,
DrawLabels: true,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/basic.png")
})
t.Run("colorscale_1", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
ColorScale: colorscales.GnBu9,
DrawMonthSeparator: true,
DrawLabels: true,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/colorscale_1.png")
})
t.Run("colorscale_2", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
ColorScale: colorscales.YlGn9,
DrawMonthSeparator: true,
DrawLabels: true,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/colorscale_2.png")
})
t.Run("no separator", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
ColorScale: colorscales.PuBu9,
DrawMonthSeparator: false,
DrawLabels: true,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/noseparator.png")
})
t.Run("no labels", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
ColorScale: colorscales.PuBu9,
DrawMonthSeparator: true,
DrawLabels: false,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/nolabels.png")
})
t.Run("no separator, no labels", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
ColorScale: colorscales.PuBu9,
DrawMonthSeparator: true,
DrawLabels: false,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/noseparator_nolabels.png")
})
t.Run("empty data", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: map[int]int{},
ColorScale: colorscales.PuBu9,
DrawMonthSeparator: true,
DrawLabels: false,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/empty_data.png")
})
t.Run("nil data", func(t *testing.T) {
img := NewHeatmap(HeatmapConfig{
Year: 2020,
CountByDay: nil,
ColorScale: colorscales.PuBu9,
DrawMonthSeparator: true,
DrawLabels: false,
Margin: 3,
BoxSize: 15,
TextWidthLeft: 35,
TextHightTop: 20,
TextColor: color.RGBA{100, 100, 100, 255},
BorderColor: color.RGBA{200, 200, 200, 255},
})
savePNG(t, img, "testdata/nil_data.png")
})
}

View File

@ -27,7 +27,8 @@ func NewDayIterator(year int, offset image.Point, countByDay map[int]int, boxSiz
row = i
}
}
maxCount := 0
// in case CountByDay is empty, we need to make Value 0/1 -> 0
maxCount := 1
for _, q := range countByDay {
if q > maxCount {
maxCount = q

114
charts/dayiter_test.go Normal file
View File

@ -0,0 +1,114 @@
package charts
import (
"image"
"testing"
)
func TestBasicDayIter(t *testing.T) {
t.Run("num days correct", func(t *testing.T) {
iter := NewDayIterator(
2019,
image.Point{X: 0, Y: 0},
map[int]int{},
5,
3,
)
if iter == nil {
t.Errorf("should not be nil")
}
if iter.Done() {
t.Errorf("should not be done on start")
}
cnt := 1
for ; !iter.Done(); iter.Next() {
cnt++
}
cnt = cnt - 1
if cnt != 365 {
t.Errorf("2019 has 365 days, got %d", cnt)
}
if iter.Time().YearDay() != 1 || iter.Time().Year() != 2020 {
t.Errorf("has to be day 1 of next year")
}
})
t.Run("num days correct, leap year", func(t *testing.T) {
iter := NewDayIterator(
2000,
image.Point{X: 0, Y: 0},
map[int]int{},
5,
3,
)
if iter == nil {
t.Errorf("should not be nil")
}
if iter.Done() {
t.Errorf("should not be done on start")
}
cnt := 1
for ; !iter.Done(); iter.Next() {
cnt++
}
cnt = cnt - 1
if cnt != 366 {
t.Errorf("2000 has 366 days, got %d", cnt)
}
if iter.Time().YearDay() != 1 || iter.Time().Year() != 2001 {
t.Errorf("has to be day 1 of next year")
}
})
t.Run("value check, float", func(t *testing.T) {
iter := NewDayIterator(
2000,
image.Point{X: 0, Y: 0},
map[int]int{2: 2, 5: 1},
5,
3,
)
for ; !iter.Done(); iter.Next() {
var exp float64
switch iter.Time().YearDay() {
case 2:
exp = 1
case 5:
exp = 0.5
}
if iter.Value() != exp {
t.Errorf("wrong day value")
}
}
})
t.Run("value check, empty counters", func(t *testing.T) {
iter := NewDayIterator(
2000,
image.Point{X: 0, Y: 0},
map[int]int{},
5,
3,
)
for ; !iter.Done(); iter.Next() {
if iter.Value() != 0 {
t.Errorf("wrong day value")
}
}
})
t.Run("value check, nil counters", func(t *testing.T) {
iter := NewDayIterator(
2000,
image.Point{X: 0, Y: 0},
nil,
5,
3,
)
for ; !iter.Done(); iter.Next() {
if iter.Value() != 0 {
t.Errorf("wrong day value")
}
}
})
}

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
charts/testdata/empty_data.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
charts/testdata/nil_data.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
charts/testdata/noseparator_nolabels.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
charts/testdata/nosepartor.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

31
charts/utils_test.go Normal file
View File

@ -0,0 +1,31 @@
package charts
import (
"image"
"image/color"
"testing"
)
func TestBasicDrawAxis(t *testing.T) {
img := image.NewRGBA(image.Rect(0, 0, 10, 10))
t.Run("along x", func(t *testing.T) {
drawLineAxis(img, image.Point{X: 0, Y: 0}, image.Point{X: 0, Y: 10}, color.Black)
})
t.Run("along y", func(t *testing.T) {
drawLineAxis(img, image.Point{X: 0, Y: 0}, image.Point{X: 10, Y: 0}, color.Black)
})
t.Run("reverse x", func(t *testing.T) {
drawLineAxis(img, image.Point{X: 0, Y: 10}, image.Point{X: 0, Y: 0}, color.Black)
})
t.Run("reverse y", func(t *testing.T) {
drawLineAxis(img, image.Point{X: 10, Y: 0}, image.Point{X: 0, Y: 0}, color.Black)
})
t.Run("dot", func(t *testing.T) {
drawLineAxis(img, image.Point{X: 0, Y: 0}, image.Point{X: 0, Y: 0}, color.Black)
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,11 +0,0 @@
#!/bin/sh
set -x
go run ../main.go -colorscale=PuBu9 -output=chart_PuBu9.png
go run ../main.go -colorscale=GnBu9 -output=chart_GnBu9.png
go run ../main.go -colorscale=YlGn9 -output=chart_YlGn9.png
go run ../main.go -colorscale=PuBu9 -output=chart_PuBu9_nolabels.png -labels=false
go run ../main.go -colorscale=PuBu9 -output=chart_PuBu9_noseparator.png -monthsep=false
go run ../main.go -colorscale=PuBu9 -output=chart_PuBu9_noseparator_nolabels.png -monthsep=false -labels=false