diff --git a/.gitignore b/.gitignore index 66fd13c..98e8a77 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# build artefacts +app.debug +calendarheatmap \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1170035 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +build: + go build + +test: + go test ./... + +docs: build + cat charts/testdata/basic.json | ./calendarheatmap > docs/basic.png + cat charts/testdata/basic.json | ./calendarheatmap -colorscale=purple-blue-9.csv > docs/colorscale-1.png + cat charts/testdata/basic.json | ./calendarheatmap -colorscale=green-blue-9.csv > docs/colorscale-2.png + cat charts/testdata/basic.json | ./calendarheatmap -colorscale=yellow-green-9.csv > docs/colorscale-3.png + cat charts/testdata/basic.json | ./calendarheatmap -locale=ko_KR > docs/korean.png + cat charts/testdata/basic.json | ./calendarheatmap -locale=ko_KR -output=svg > docs/korean.svg + cat charts/testdata/basic.json | ./calendarheatmap -labels=false > docs/nolabels.png + cat charts/testdata/basic.json | ./calendarheatmap -monthsep=false > docs/noseparator.png + cat charts/testdata/basic.json | ./calendarheatmap -labels=false -monthsep=false > docs/noseparator_nolabels.png + +.PHONY: build test docs \ No newline at end of file diff --git a/README.md b/README.md index adfbcc3..eb25380 100644 --- a/README.md +++ b/README.md @@ -18,27 +18,27 @@ $ echo '{ Basic -![basic](charts/testdata/basic.png) +![basic](docs/basic.png) Colorscales -![col1](charts/testdata/colorscale_1.png) -![col2](charts/testdata/colorscale_2.png) +![col1](docs/colorscale-1.png) +![col2](docs/colorscale-2.png) +![col2](docs/colorscale-3.png) UTF-8 -![col1](charts/testdata/korean.png) +![col1](docs/korean.png) SVG - -![svg](charts/testdata/korean.svg) +![svg](docs/korean.svg) Without month separator -![nosep](charts/testdata/noseparator.png) +![nosep](docs/noseparator.png) Without labels -![nolab](charts/testdata/nolabels.png) +![nolab](docs/nolabels.png) Without labels, without separator -![nosep_nolab](charts/testdata/noseparator_nolabels.png) +![nosep_nolab](docs/noseparator_nolabels.png) ## GitHub stars over time diff --git a/assets/colorscales/green-blue-9.csv b/assets/colorscales/green-blue-9.csv new file mode 100644 index 0000000..a61404e --- /dev/null +++ b/assets/colorscales/green-blue-9.csv @@ -0,0 +1,10 @@ +R,G,B +247,252,240 +224,243,219 +204,235,197 +168,221,181 +123,204,196 +78,179,211 +43,140,190 +8,104,172 +8,64,129 \ No newline at end of file diff --git a/assets/colorscales/purple-blue-9.csv b/assets/colorscales/purple-blue-9.csv new file mode 100644 index 0000000..2e03e3a --- /dev/null +++ b/assets/colorscales/purple-blue-9.csv @@ -0,0 +1,10 @@ +R,G,B +255,247,251 +236,231,242 +208,209,230 +166,189,219 +116,169,207 +54,144,192 +5,112,176 +4,90,141 +2,56,88 \ No newline at end of file diff --git a/assets/colorscales/yellow-green-9.csv b/assets/colorscales/yellow-green-9.csv new file mode 100644 index 0000000..87ab516 --- /dev/null +++ b/assets/colorscales/yellow-green-9.csv @@ -0,0 +1,10 @@ +R,G,B +255,255,229 +247,252,185 +217,240,163 +173,221,142 +120,198,121 +65,171,93 +35,132,67 +0,104,55 +0,69,41 \ No newline at end of file diff --git a/charts/assets/fonts/OFL.txt b/assets/fonts/OFL.txt similarity index 100% rename from charts/assets/fonts/OFL.txt rename to assets/fonts/OFL.txt diff --git a/charts/assets/fonts/Sunflower-Medium.ttf b/assets/fonts/Sunflower-Medium.ttf similarity index 100% rename from charts/assets/fonts/Sunflower-Medium.ttf rename to assets/fonts/Sunflower-Medium.ttf diff --git a/charts/charts.go b/charts/charts.go index 246a1a0..307a743 100644 --- a/charts/charts.go +++ b/charts/charts.go @@ -11,9 +11,14 @@ import ( "io" "time" - "github.com/nikolaydubina/calendarheatmap/colorscales" + "golang.org/x/image/font" ) +// ColorScale is interface for extracting color based on value from 0 to 1 +type ColorScale interface { + GetColor(val float64) color.RGBA +} + var weekdayOrder = [7]time.Weekday{ time.Monday, time.Tuesday, @@ -32,7 +37,7 @@ const ( // HeatmapConfig contains config of calendar heatmap image type HeatmapConfig struct { Counts map[string]int - ColorScale colorscales.ColorScale + ColorScale ColorScale DrawMonthSeparator bool DrawLabels bool BoxSize int @@ -43,12 +48,15 @@ type HeatmapConfig struct { BorderColor color.RGBA Locale string Format string + FontFace font.Face + ShowWeekdays map[time.Weekday]bool } // WriteHeatmap writes image with heatmap and additional elements func WriteHeatmap(conf HeatmapConfig, w io.Writer) error { if conf.Format == "svg" { - return writeSVG(conf, w) + writeSVG(conf, w) + return nil } width := conf.TextWidthLeft + numWeekCols*(conf.BoxSize+conf.Margin) @@ -77,14 +85,10 @@ func WriteHeatmap(conf HeatmapConfig, w io.Writer) error { ) } - locale := "en_US" - if conf.Locale != "" { - locale = conf.Locale - } - labelsProvider := NewLabelsProvider(locale) + labelsProvider := NewLabelsProvider(conf.Locale) if conf.DrawLabels { - visitors = append(visitors, &MonthLabelsVisitor{Img: img, YOffset: 50, Color: conf.TextColor, LabelsProvider: labelsProvider}) + visitors = append(visitors, &MonthLabelsVisitor{FontFace: conf.FontFace, Img: img, YOffset: 50, Color: conf.TextColor, LabelsProvider: labelsProvider}) } for iter := NewDayIterator(conf.Counts, offset, conf.BoxSize, conf.Margin); !iter.Done(); iter.Next() { @@ -95,13 +99,10 @@ func WriteHeatmap(conf HeatmapConfig, w io.Writer) error { if conf.DrawLabels { drawWeekdayLabels( + conf.FontFace, img, offset, - map[time.Weekday]bool{ - time.Monday: true, - time.Wednesday: true, - time.Friday: true, - }, + conf.ShowWeekdays, conf.BoxSize, conf.Margin, conf.TextColor, @@ -137,7 +138,7 @@ type DayVisitor interface { // DayBoxVisitor draws signle heatbox type DayBoxVisitor struct { Img *image.RGBA - ColorScale colorscales.ColorScale + ColorScale ColorScale BoxSize int } @@ -214,6 +215,7 @@ type MonthLabelsVisitor struct { YOffset int Color color.RGBA LabelsProvider LabelsProvider + FontFace font.Face } // Visit on every iteration @@ -223,6 +225,7 @@ func (d *MonthLabelsVisitor) Visit(iter *DayIterator) { if iter.Row == 0 && day.Day() <= 7 { p := iter.Point() drawText( + d.FontFace, d.Img, image.Point{X: p.X, Y: p.Y - d.YOffset}, d.LabelsProvider.GetMonth(day.Month()), @@ -234,13 +237,13 @@ func (d *MonthLabelsVisitor) Visit(iter *DayIterator) { // drawWeekdayLabel draws column of same width labels for weekdays // All weekday labels assumed to have same width, which really depends on font. // offset argument is top right corner of where to insert column of weekday labels. -func drawWeekdayLabels(img *image.RGBA, offset image.Point, weekdays map[time.Weekday]bool, boxSize int, margin int, color color.RGBA, lp LabelsProvider) { +func drawWeekdayLabels(fontFace font.Face, img *image.RGBA, offset image.Point, weekdays map[time.Weekday]bool, boxSize int, margin int, color color.RGBA, lp LabelsProvider) { width := 250 height := 100 y := offset.Y + height for _, w := range weekdayOrder { if weekdays[w] { - drawText(img, image.Point{X: offset.X - width, Y: y}, lp.GetWeekday(w), color) + drawText(fontFace, img, image.Point{X: offset.X - width, Y: y}, lp.GetWeekday(w), color) } y += boxSize + margin } diff --git a/charts/charts_test.go b/charts/charts_test.go index 84b3a74..d037cbd 100644 --- a/charts/charts_test.go +++ b/charts/charts_test.go @@ -1,223 +1,279 @@ package charts_test import ( + "bytes" + "encoding/json" "image/color" + "io/ioutil" "os" + "path" "testing" + "time" + + "golang.org/x/image/font" "github.com/nikolaydubina/calendarheatmap/charts" - "github.com/nikolaydubina/calendarheatmap/colorscales" ) -var counts map[string]int = map[string]int{ - "2020-05-17": 13, - "2020-05-18": 5, - "2020-05-19": 8, - "2020-05-20": 5, - "2020-05-21": 5, - "2020-05-22": 3, - "2020-05-23": 5, - "2020-05-24": 6, - "2020-05-25": 3, - "2020-05-26": 5, - "2020-05-27": 8, - "2020-05-28": 2, - "2020-05-29": 2, - "2020-05-30": 8, - "2020-05-31": 5, - "2020-06-01": 1, - "2020-06-02": 3, - "2020-06-03": 1, - "2020-06-04": 3, - "2020-06-05": 1, - "2020-06-06": 3, - "2020-06-07": 5, - "2020-06-09": 1, - "2020-06-10": 2, - "2020-06-12": 9, - "2020-06-13": 7, - "2020-06-14": 4, - "2020-06-15": 1, - "2020-06-17": 1, - "2020-06-20": 2, - "2020-06-21": 1, - "2020-06-23": 2, - "2020-06-24": 2, - "2020-06-25": 3, - "2020-06-26": 3, - "2020-06-27": 2, - "2020-06-28": 1, - "2020-06-29": 1, - "2020-06-30": 2, -} - -func save(t *testing.T, conf charts.HeatmapConfig, filename string) { - f, err := os.Create(filename) +func loadData(t *testing.T, filepath string) map[string]int { + var counts map[string]int + data, err := ioutil.ReadFile(filepath) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } - if err := charts.WriteHeatmap(conf, f); err != nil { - t.Errorf(err.Error()) - } - if err := f.Close(); err != nil { - t.Errorf(err.Error()) + if err := json.Unmarshal(data, &counts); err != nil { + t.Error(err) } + return counts } -func TestBasicData(t *testing.T) { - os.Setenv("CALENDAR_HEATMAP_ASSETS_PATH", "assets") - - t.Run("basic", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: true, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/basic.png") - }) - - t.Run("colorscale_1", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.GnBu9, - DrawMonthSeparator: true, - DrawLabels: true, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/colorscale_1.png") - }) - - t.Run("colorscale_2", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.YlGn9, - DrawMonthSeparator: true, - DrawLabels: true, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/colorscale_2.png") - }) - - t.Run("korean", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: true, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Locale: "ko_KR", - Format: "png", - } - save(t, conf, "testdata/korean.png") - }) - - t.Run("no separator", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: false, - DrawLabels: true, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/noseparator.png") - }) - - t.Run("no labels", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: false, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/nolabels.png") - }) - - t.Run("no separator, no labels", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: false, - DrawLabels: false, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/noseparator_nolabels.png") - }) - - t.Run("empty data", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: false, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/empty_data.png") - }) - - t.Run("nil data", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: false, - Margin: 30, - BoxSize: 150, - TextWidthLeft: 350, - TextHeightTop: 200, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "png", - } - save(t, conf, "testdata/nil_data.png") - }) +func loadFontFace(t *testing.T, filepath string) font.Face { + fontFace, err := charts.LoadFontFaceFromFile(filepath) + if err != nil { + t.Error(err) + } + return fontFace +} + +func loadColorscale(t *testing.T, filepath string) charts.ColorScale { + colorscale, err := charts.NewBasicColorscaleFromCSVFile(filepath) + if err != nil { + t.Fail() + } + return colorscale +} + +func TestCharts(t *testing.T) { + tests := []struct { + name string + outputPath string + expectedPath string + conf charts.HeatmapConfig + }{ + { + name: "basic-png", + outputPath: path.Join("testdata", "basic-png-output.png"), + expectedPath: path.Join("testdata", "basic-png-expected.png"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "basic-jpeg", + outputPath: path.Join("testdata", "basic-jpeg-output.jpeg"), + expectedPath: path.Join("testdata", "basic-jpeg-expected.jpeg"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "jpeg", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "basic-svg", + outputPath: path.Join("testdata", "basic-svg-output.svg"), + expectedPath: path.Join("testdata", "basic-svg-expected.svg"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "svg", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "no-data", + outputPath: path.Join("testdata", "basic-no-data-output.png"), + expectedPath: path.Join("testdata", "basic-no-data-expected.png"), + conf: charts.HeatmapConfig{ + Counts: nil, + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "no-labels", + outputPath: path.Join("testdata", "basic-no-labels-output.png"), + expectedPath: path.Join("testdata", "basic-no-labels-expected.png"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: false, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "no-separator", + outputPath: path.Join("testdata", "basic-no-separator-output.png"), + expectedPath: path.Join("testdata", "basic-no-separator-expected.png"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: false, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "korean", + outputPath: path.Join("testdata", "basic-korean-output.png"), + expectedPath: path.Join("testdata", "basic-korean-expected.png"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "ko_KR", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{time.Monday: true, time.Wednesday: true, time.Friday: true}, + }, + }, + { + name: "no-weekdays", + outputPath: path.Join("testdata", "basic-no-weekdays-output.png"), + expectedPath: path.Join("testdata", "basic-no-weekdays-expected.png"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: nil, + }, + }, + { + name: "all-weekdays", + outputPath: path.Join("testdata", "basic-all-weekdays-output.png"), + expectedPath: path.Join("testdata", "basic-all-weekdays-expected.png"), + conf: charts.HeatmapConfig{ + Counts: loadData(t, path.Join("testdata", "basic.json")), + ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")), + DrawMonthSeparator: true, + DrawLabels: true, + BoxSize: 150, + Margin: 30, + TextWidthLeft: 350, + TextHeightTop: 200, + TextColor: color.RGBA{100, 100, 100, 255}, + BorderColor: color.RGBA{200, 200, 200, 255}, + Locale: "en_US", + Format: "png", + FontFace: loadFontFace(t, path.Join("..", "assets", "fonts", "Sunflower-Medium.ttf")), + ShowWeekdays: map[time.Weekday]bool{ + time.Monday: true, + time.Tuesday: true, + time.Wednesday: true, + time.Thursday: true, + time.Friday: true, + time.Saturday: true, + time.Sunday: true, + }, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // output + outputfile, err := os.Create(tc.outputPath) + if err != nil { + t.Error(err) + } + if err := charts.WriteHeatmap(tc.conf, outputfile); err != nil { + t.Error(err) + } + if err := outputfile.Close(); err != nil { + t.Error(err) + } + + // compare to expected + expected, err := ioutil.ReadFile(tc.expectedPath) + if err != nil { + t.Error(err) + } + actual, err := ioutil.ReadFile(tc.outputPath) + if err != nil { + t.Error(err) + } + if !bytes.Equal(expected, actual) { + t.Fail() + } + }) + } } diff --git a/charts/colorscale.go b/charts/colorscale.go new file mode 100644 index 0000000..6897c28 --- /dev/null +++ b/charts/colorscale.go @@ -0,0 +1,85 @@ +package charts + +import ( + "encoding/csv" + "errors" + "fmt" + "image/color" + "math" + "os" + "strconv" +) + +// BasicColorScale is color scale with variable number of colors +type BasicColorScale []color.RGBA + +// GetColor returns color based on float value from 0 to 1 +func (c BasicColorScale) GetColor(val float64) color.RGBA { + if len(c) == 0 { + return color.RGBA{} + } + maxIdx := len(c) - 1 + idx := int(math.Round(float64(maxIdx) * val)) + return c[idx] +} + +func uint8FromStr(s string) (uint8, error) { + v, err := strconv.Atoi(s) + if err != nil { + return 0, errors.New("can not convert to int") + } + if v < 0 { + return 0, errors.New("less than 0") + } + if v > math.MaxUint8 { + return 0, errors.New("higher than max") + } + return uint8(v), nil +} + +// NewBasicColorscaleFromCSVFile loads basic colorscale from CSV file +func NewBasicColorscaleFromCSVFile(path string) (BasicColorScale, error) { + colorscaleReader, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("can not open file: %w", err) + } + rows, err := csv.NewReader(colorscaleReader).ReadAll() + if err != nil { + return nil, fmt.Errorf("can not read CSV: %w", err) + } + if len(rows) == 0 { + return nil, errors.New("empty colorscales file") + } + + colmap := make(map[string]int, 3) + for i, name := range rows[0] { + colmap[name] = i + } + if _, ok := colmap["R"]; !ok { + return nil, errors.New("missing R column") + } + if _, ok := colmap["G"]; !ok { + return nil, errors.New("missing G column") + } + if _, ok := colmap["B"]; !ok { + return nil, errors.New("missing B column") + } + + colorscale := make(BasicColorScale, len(rows)-1) + for i, vals := range rows[1:] { + r, err := uint8FromStr(vals[colmap["R"]]) + if err != nil { + return nil, fmt.Errorf("bad value for color: %w", err) + } + g, err := uint8FromStr(vals[colmap["G"]]) + if err != nil { + return nil, fmt.Errorf("bad value for color: %w", err) + } + b, err := uint8FromStr(vals[colmap["B"]]) + if err != nil { + return nil, fmt.Errorf("bad value for color: %w", err) + } + colorscale[i] = color.RGBA{r, g, b, 255} + } + return colorscale, nil +} diff --git a/charts/dayiter.go b/charts/dayiter.go index 7bce11f..292a327 100644 --- a/charts/dayiter.go +++ b/charts/dayiter.go @@ -21,7 +21,7 @@ type DayIterator struct { // NewDayIterator initializes iterator for a year func NewDayIterator(counts map[string]int, offset image.Point, boxSize int, margin int) *DayIterator { year := 1972 - for dateStr, _ := range counts { + for dateStr := range counts { date, err := time.Parse("2006-01-02", dateStr) if err != nil { panic(err) diff --git a/charts/labels.go b/charts/labels.go index 57705c8..ccae56a 100644 --- a/charts/labels.go +++ b/charts/labels.go @@ -65,7 +65,11 @@ type LabelsProvider struct { // NewLabelsProvider initializes labels provider for locale func NewLabelsProvider(locale string) LabelsProvider { - return localeConfig[locale] + lp, ok := localeConfig[locale] + if !ok { + return localeConfig["en_US"] + } + return lp } // GetMonth returns month label diff --git a/charts/svg.go b/charts/svg.go index b77ba6c..a81eaec 100644 --- a/charts/svg.go +++ b/charts/svg.go @@ -8,6 +8,7 @@ import ( "text/template" ) +// Day is SVG template day parameters type Day struct { Count int Date string @@ -15,64 +16,69 @@ type Day struct { Show bool } +// WeekdayLabel is SVG template weekday label parameters type WeekdayLabel struct { Label string Show bool } +// MonthLabel is SVG template month label parameters +type MonthLabel struct { + Label string + XOffset int +} + +// Params is total SVG template parameters type Params struct { Days [53][7]Day - LabelsMonths [12]string + LabelsMonths [12]MonthLabel LabelsWeekdays [7]WeekdayLabel LabelsColor string } -func writeColor(c color.RGBA) string { +func writeSVGColor(c color.RGBA) string { return fmt.Sprintf("rgb(%d,%d,%d)", c.R, c.G, c.B) } -func writeSVG(conf HeatmapConfig, w io.Writer) error { +func writeSVG(conf HeatmapConfig, w io.Writer) { fullYearTemplate := template.Must(template.New("fullyear").Funcs(template.FuncMap{ "mul": func(a int, b int) int { return a * b }, "add": func(a int, b int) int { return a + b }, - "sub": func(a int, b int) int { return a - b }, }).Parse(fullyear)) - days := [53][7]Day{} + labelsProvider := NewLabelsProvider(conf.Locale) + labelsWeekdays := [7]WeekdayLabel{} + for i, w := range weekdayOrder { + labelsWeekdays[i] = WeekdayLabel{labelsProvider.GetWeekday(w), conf.ShowWeekdays[w]} + } + + labelsMonths := [12]MonthLabel{} + for i, v := range labelsProvider.months { + labelsMonths[i-1].Label = v + } + + month := 0 + days := [53][7]Day{} for iter := NewDayIterator(conf.Counts, image.Point{}, 0, 0); !iter.Done(); iter.Next() { days[iter.Col][iter.Row] = Day{ Count: iter.Count(), Date: iter.Time().Format("2006-01-02"), - Color: writeColor(conf.ColorScale.GetColor(iter.Value())), + Color: writeSVGColor(conf.ColorScale.GetColor(iter.Value())), Show: true, } + + // Note, day is from 1~31 + if iter.Row == 0 && iter.Time().Day() <= 7 { + labelsMonths[month].XOffset = iter.Col + month++ + } } - locale := "en_US" - if conf.Locale != "" { - locale = conf.Locale - } - labelsProvider := NewLabelsProvider(locale) - - labelsMonths := [12]string{} - for i, v := range labelsProvider.months { - labelsMonths[i-1] = v - } - - labelsWeekdays := [7]WeekdayLabel{} - for i, v := range labelsProvider.weekdays { - labelsWeekdays[i] = WeekdayLabel{v, true} - } - - params := Params{ + fullYearTemplate.Execute(w, Params{ Days: days, LabelsMonths: labelsMonths, LabelsWeekdays: labelsWeekdays, - LabelsColor: writeColor(conf.TextColor), - } - - fullYearTemplate.Execute(w, params) - - return nil + LabelsColor: writeSVGColor(conf.TextColor), + }) } diff --git a/charts/svg_test.go b/charts/svg_test.go deleted file mode 100644 index e3f1653..0000000 --- a/charts/svg_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package charts_test - -import ( - "image/color" - "testing" - - "github.com/nikolaydubina/calendarheatmap/charts" - "github.com/nikolaydubina/calendarheatmap/colorscales" -) - -func TestBasicDataSVG(t *testing.T) { - t.Run("basic", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: true, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "svg", - } - save(t, conf, "testdata/basic.svg") - }) - - t.Run("korean", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: counts, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: true, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Locale: "ko_KR", - Format: "svg", - } - save(t, conf, "testdata/korean.svg") - }) - - t.Run("empty data", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: map[string]int{}, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: false, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "svg", - } - save(t, conf, "testdata/empty_data.svg") - }) - - t.Run("nil data", func(t *testing.T) { - conf := charts.HeatmapConfig{ - Counts: nil, - ColorScale: colorscales.PuBu9, - DrawMonthSeparator: true, - DrawLabels: false, - TextColor: color.RGBA{100, 100, 100, 255}, - BorderColor: color.RGBA{200, 200, 200, 255}, - Format: "svg", - } - save(t, conf, "testdata/nil_data.svg") - }) -} diff --git a/charts/templates.go b/charts/templates.go index dc2f63f..7b4f934 100644 --- a/charts/templates.go +++ b/charts/templates.go @@ -1,17 +1,17 @@ package charts -const fullyear = ` +const fullyear = ` {{range $w, $wo := $.Days}} - {{range $d, $do := $wo}}{{if $do.Show}}{{end}} + {{range $d, $do := $wo}}{{if $do.Show}}{{end}} {{end}} {{end}} - - {{range $i, $label := $.LabelsMonths}}{{$label}} + + {{range $i, $label := $.LabelsMonths}}{{$label.Label}} {{end}} - {{range $i, $o := $.LabelsWeekdays}}{{$o.Label}} + {{range $i, $o := $.LabelsWeekdays}}{{$o.Label}} {{end}} ` diff --git a/charts/testdata/basic-all-weekdays-expected.png b/charts/testdata/basic-all-weekdays-expected.png new file mode 100644 index 0000000..3f74712 Binary files /dev/null and b/charts/testdata/basic-all-weekdays-expected.png differ diff --git a/charts/testdata/basic-all-weekdays-output.png b/charts/testdata/basic-all-weekdays-output.png new file mode 100644 index 0000000..3f74712 Binary files /dev/null and b/charts/testdata/basic-all-weekdays-output.png differ diff --git a/charts/testdata/basic-jpeg-expected.jpeg b/charts/testdata/basic-jpeg-expected.jpeg new file mode 100644 index 0000000..7b4e150 Binary files /dev/null and b/charts/testdata/basic-jpeg-expected.jpeg differ diff --git a/charts/testdata/basic-jpeg-output.jpeg b/charts/testdata/basic-jpeg-output.jpeg new file mode 100644 index 0000000..7b4e150 Binary files /dev/null and b/charts/testdata/basic-jpeg-output.jpeg differ diff --git a/charts/testdata/korean.png b/charts/testdata/basic-korean-expected.png similarity index 87% rename from charts/testdata/korean.png rename to charts/testdata/basic-korean-expected.png index 61e596c..fa1a651 100644 Binary files a/charts/testdata/korean.png and b/charts/testdata/basic-korean-expected.png differ diff --git a/charts/testdata/basic-korean-output.png b/charts/testdata/basic-korean-output.png new file mode 100644 index 0000000..fa1a651 Binary files /dev/null and b/charts/testdata/basic-korean-output.png differ diff --git a/charts/testdata/basic-no-data-expected.png b/charts/testdata/basic-no-data-expected.png new file mode 100644 index 0000000..07d9537 Binary files /dev/null and b/charts/testdata/basic-no-data-expected.png differ diff --git a/charts/testdata/basic-no-data-output.png b/charts/testdata/basic-no-data-output.png new file mode 100644 index 0000000..07d9537 Binary files /dev/null and b/charts/testdata/basic-no-data-output.png differ diff --git a/charts/testdata/nil_data.png b/charts/testdata/basic-no-labels-expected.png similarity index 80% rename from charts/testdata/nil_data.png rename to charts/testdata/basic-no-labels-expected.png index 084bc6b..521f1e4 100644 Binary files a/charts/testdata/nil_data.png and b/charts/testdata/basic-no-labels-expected.png differ diff --git a/charts/testdata/empty_data.png b/charts/testdata/basic-no-labels-output.png similarity index 80% rename from charts/testdata/empty_data.png rename to charts/testdata/basic-no-labels-output.png index 084bc6b..521f1e4 100644 Binary files a/charts/testdata/empty_data.png and b/charts/testdata/basic-no-labels-output.png differ diff --git a/charts/testdata/noseparator.png b/charts/testdata/basic-no-separator-expected.png similarity index 93% rename from charts/testdata/noseparator.png rename to charts/testdata/basic-no-separator-expected.png index e438e80..0720fc6 100644 Binary files a/charts/testdata/noseparator.png and b/charts/testdata/basic-no-separator-expected.png differ diff --git a/charts/testdata/basic-no-separator-output.png b/charts/testdata/basic-no-separator-output.png new file mode 100644 index 0000000..0720fc6 Binary files /dev/null and b/charts/testdata/basic-no-separator-output.png differ diff --git a/charts/testdata/basic-no-weekdays-expected.png b/charts/testdata/basic-no-weekdays-expected.png new file mode 100644 index 0000000..943024c Binary files /dev/null and b/charts/testdata/basic-no-weekdays-expected.png differ diff --git a/charts/testdata/basic-no-weekdays-output.png b/charts/testdata/basic-no-weekdays-output.png new file mode 100644 index 0000000..943024c Binary files /dev/null and b/charts/testdata/basic-no-weekdays-output.png differ diff --git a/charts/testdata/basic.png b/charts/testdata/basic-png-expected.png similarity index 84% rename from charts/testdata/basic.png rename to charts/testdata/basic-png-expected.png index 944bad5..be4b3e7 100644 Binary files a/charts/testdata/basic.png and b/charts/testdata/basic-png-expected.png differ diff --git a/charts/testdata/basic-png-output.png b/charts/testdata/basic-png-output.png new file mode 100644 index 0000000..be4b3e7 Binary files /dev/null and b/charts/testdata/basic-png-output.png differ diff --git a/charts/testdata/basic-svg-expected.svg b/charts/testdata/basic-svg-expected.svg new file mode 100644 index 0000000..8057d56 --- /dev/null +++ b/charts/testdata/basic-svg-expected.svg @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec + + + Mon + Tue + Wed + Thu + Fri + Sat + Sun + + diff --git a/charts/testdata/basic-svg-output.svg b/charts/testdata/basic-svg-output.svg new file mode 100644 index 0000000..8057d56 --- /dev/null +++ b/charts/testdata/basic-svg-output.svg @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec + + + Mon + Tue + Wed + Thu + Fri + Sat + Sun + + diff --git a/charts/testdata/basic.svg b/charts/testdata/basic.svg deleted file mode 100644 index 26a7f09..0000000 --- a/charts/testdata/basic.svg +++ /dev/null @@ -1,557 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Jan - Feb - Mar - Apr - May - Jun - Jul - Aug - Sep - Oct - Nov - Dec - - - Sun - Mon - Tue - Wed - Thu - Fri - Sat - - diff --git a/charts/testdata/empty_data.svg b/charts/testdata/empty_data.svg deleted file mode 100644 index 05276f0..0000000 --- a/charts/testdata/empty_data.svg +++ /dev/null @@ -1,557 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Jan - Feb - Mar - Apr - May - Jun - Jul - Aug - Sep - Oct - Nov - Dec - - - Sun - Mon - Tue - Wed - Thu - Fri - Sat - - diff --git a/charts/testdata/korean.svg b/charts/testdata/korean.svg deleted file mode 100644 index 286eef6..0000000 --- a/charts/testdata/korean.svg +++ /dev/null @@ -1,557 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1월 - 2월 - 3월 - 4월 - 5월 - 6월 - 7월 - 8월 - 9월 - 10월 - 11월 - 12월 - - - - - - - - - - - diff --git a/charts/testdata/nil_data.svg b/charts/testdata/nil_data.svg deleted file mode 100644 index 05276f0..0000000 --- a/charts/testdata/nil_data.svg +++ /dev/null @@ -1,557 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Jan - Feb - Mar - Apr - May - Jun - Jul - Aug - Sep - Oct - Nov - Dec - - - Sun - Mon - Tue - Wed - Thu - Fri - Sat - - diff --git a/charts/testdata/nolabels.png b/charts/testdata/nolabels.png deleted file mode 100644 index 084bc6b..0000000 Binary files a/charts/testdata/nolabels.png and /dev/null differ diff --git a/charts/text.go b/charts/text.go new file mode 100644 index 0000000..d5ef0eb --- /dev/null +++ b/charts/text.go @@ -0,0 +1,50 @@ +package charts + +import ( + "fmt" + "image" + "image/color" + "io/ioutil" + + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/math/fixed" +) + +// LoadFontFaceFromFile loads font face from file +func LoadFontFaceFromFile(fontPath string) (font.Face, error) { + fontBytes, err := ioutil.ReadFile(fontPath) + if err != nil { + return nil, fmt.Errorf("can not open font file with error: %w", err) + } + f, err := opentype.Parse(fontBytes) + if err != nil { + return nil, fmt.Errorf("can not parse font file: %w", err) + } + face, err := opentype.NewFace(f, &opentype.FaceOptions{ + Size: 26, + DPI: 280, + Hinting: font.HintingNone, + }) + if err != nil { + return nil, fmt.Errorf("can not create font face: %w", err) + } + return face, nil +} + +// drawText inserts text into provided image at bottom left coordinate +func drawText(fontFace font.Face, img *image.RGBA, offset image.Point, text string, color color.RGBA) { + if fontFace == nil { + return + } + d := &font.Drawer{ + Dst: img, + Src: image.NewUniform(color), + Face: fontFace, + Dot: fixed.Point26_6{ + X: fixed.Int26_6(offset.X * 64), + Y: fixed.Int26_6(offset.Y * 64), + }, + } + d.DrawString(text) +} diff --git a/charts/utils.go b/charts/utils.go deleted file mode 100644 index 4632043..0000000 --- a/charts/utils.go +++ /dev/null @@ -1,50 +0,0 @@ -package charts - -import ( - "fmt" - "image" - "image/color" - "io/ioutil" - "log" - "os" - - "golang.org/x/image/font" - "golang.org/x/image/font/opentype" - "golang.org/x/image/math/fixed" -) - -// drawText inserts text into provided image at bottom left coordinate -func drawText(img *image.RGBA, offset image.Point, text string, color color.RGBA) { - assetsPath := os.Getenv("CALENDAR_HEATMAP_ASSETS_PATH") - if assetsPath == "" { - log.Fatalf("assets path is not set") - } - fontBytes, err := ioutil.ReadFile(fmt.Sprintf("%s/fonts/Sunflower-Medium.ttf", assetsPath)) - if err != nil { - log.Fatalf("can not open font file with error: %#v", err) - } - f, err := opentype.Parse(fontBytes) - if err != nil { - log.Fatalf("can not parse font file: %v", err) - } - face, err := opentype.NewFace(f, &opentype.FaceOptions{ - Size: 26, - DPI: 280, - Hinting: font.HintingNone, - }) - if err != nil { - log.Fatalf("can not create font face: %v", err) - } - - point := fixed.Point26_6{ - X: fixed.Int26_6(offset.X * 64), - Y: fixed.Int26_6(offset.Y * 64), - } - d := &font.Drawer{ - Dst: img, - Src: image.NewUniform(color), - Face: face, - Dot: point, - } - d.DrawString(text) -} diff --git a/colorscales/colorscales.go b/colorscales/colorscales.go deleted file mode 100644 index 4bb0a21..0000000 --- a/colorscales/colorscales.go +++ /dev/null @@ -1,24 +0,0 @@ -package colorscales - -import ( - "image/color" -) - -// ColorScale is interface for extracting color from float -type ColorScale interface { - GetColor(val float64) color.RGBA -} - -// LoadColorScale loads colorscale struct based on associated string name -func LoadColorScale(name string) ColorScale { - switch name { - case "PuBu9": - return PuBu9 - case "GnBu9": - return GnBu9 - case "YlGn9": - return YlGn9 - default: - panic("unknown colorscale " + name) - } -} diff --git a/colorscales/colorscales9.go b/colorscales/colorscales9.go deleted file mode 100644 index 9bd1f75..0000000 --- a/colorscales/colorscales9.go +++ /dev/null @@ -1,55 +0,0 @@ -package colorscales - -import ( - "image/color" - "math" -) - -// ColorScale9 is color scale with 9 colors -type ColorScale9 [9]color.RGBA - -// GetColor returns color based on float value from 0 to 1 -func (c ColorScale9) GetColor(val float64) color.RGBA { - maxIdx := 8 - idx := int(math.Round(float64(maxIdx) * val)) - return c[idx] -} - -// PuBu9 is Purple-Blue colorscale 9 colors -var PuBu9 = ColorScale9{ - color.RGBA{255, 247, 251, 255}, - color.RGBA{236, 231, 242, 255}, - color.RGBA{208, 209, 230, 255}, - color.RGBA{166, 189, 219, 255}, - color.RGBA{116, 169, 207, 255}, - color.RGBA{54, 144, 192, 255}, - color.RGBA{5, 112, 176, 255}, - color.RGBA{4, 90, 141, 255}, - color.RGBA{2, 56, 88, 255}, -} - -// GnBu9 is Green-Blue colorscale 9 colors -var GnBu9 = ColorScale9{ - color.RGBA{247, 252, 240, 255}, - color.RGBA{224, 243, 219, 255}, - color.RGBA{204, 235, 197, 255}, - color.RGBA{168, 221, 181, 255}, - color.RGBA{123, 204, 196, 255}, - color.RGBA{78, 179, 211, 255}, - color.RGBA{43, 140, 190, 255}, - color.RGBA{8, 104, 172, 255}, - color.RGBA{8, 64, 129, 255}, -} - -// YlGn9 is Yellow-Green colorscale 9 colors -var YlGn9 = ColorScale9{ - color.RGBA{255, 255, 229, 255}, - color.RGBA{247, 252, 185, 255}, - color.RGBA{217, 240, 163, 255}, - color.RGBA{173, 221, 142, 255}, - color.RGBA{120, 198, 121, 255}, - color.RGBA{65, 171, 93, 255}, - color.RGBA{35, 132, 67, 255}, - color.RGBA{0, 104, 55, 255}, - color.RGBA{0, 69, 41, 255}, -} diff --git a/charts/testdata/colorscale_1.png b/docs/basic.png similarity index 91% rename from charts/testdata/colorscale_1.png rename to docs/basic.png index def4a4b..68744af 100644 Binary files a/charts/testdata/colorscale_1.png and b/docs/basic.png differ diff --git a/docs/colorscale-1.png b/docs/colorscale-1.png new file mode 100644 index 0000000..be4b3e7 Binary files /dev/null and b/docs/colorscale-1.png differ diff --git a/docs/colorscale-2.png b/docs/colorscale-2.png new file mode 100644 index 0000000..68744af Binary files /dev/null and b/docs/colorscale-2.png differ diff --git a/charts/testdata/colorscale_2.png b/docs/colorscale-3.png similarity index 85% rename from charts/testdata/colorscale_2.png rename to docs/colorscale-3.png index be2d5c0..4fdc86d 100644 Binary files a/charts/testdata/colorscale_2.png and b/docs/colorscale-3.png differ diff --git a/docs/korean.png b/docs/korean.png new file mode 100644 index 0000000..97b16c5 Binary files /dev/null and b/docs/korean.png differ diff --git a/docs/korean.svg b/docs/korean.svg new file mode 100644 index 0000000..7dcc9f3 --- /dev/null +++ b/docs/korean.svg @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1월 + 2월 + 3월 + 4월 + 5월 + 6월 + 7월 + 8월 + 9월 + 10월 + 11월 + 12월 + + + + + + + + + + + diff --git a/docs/nolabels.png b/docs/nolabels.png new file mode 100644 index 0000000..27cb96e Binary files /dev/null and b/docs/nolabels.png differ diff --git a/docs/noseparator.png b/docs/noseparator.png new file mode 100644 index 0000000..abbd82e Binary files /dev/null and b/docs/noseparator.png differ diff --git a/charts/testdata/noseparator_nolabels.png b/docs/noseparator_nolabels.png similarity index 58% rename from charts/testdata/noseparator_nolabels.png rename to docs/noseparator_nolabels.png index 16763de..9f90ad1 100644 Binary files a/charts/testdata/noseparator_nolabels.png and b/docs/noseparator_nolabels.png differ diff --git a/main.go b/main.go index 3769f09..610226b 100644 --- a/main.go +++ b/main.go @@ -7,29 +7,37 @@ import ( "io/ioutil" "log" "os" + "path" + "time" "github.com/nikolaydubina/calendarheatmap/charts" - "github.com/nikolaydubina/calendarheatmap/colorscales" ) func main() { - os.Setenv("CALENDAR_HEATMAP_ASSETS_PATH", "charts/assets") - var ( colorScale string labels bool locale string monthSep bool outputFormat string + assetsPath string ) flag.BoolVar(&labels, "labels", true, "labels for weekday and months") flag.BoolVar(&monthSep, "monthsep", true, "render month separator") - flag.StringVar(&colorScale, "colorscale", "PuBu9", "refer to colorscales for examples") + flag.StringVar(&colorScale, "colorscale", "green-blue-9.csv", "filename of colorscale") flag.StringVar(&locale, "locale", "en_US", "locale of labels (en_US, ko_KR)") flag.StringVar(&outputFormat, "output", "png", "output format (png, jpeg, gif, svg)") + flag.StringVar(&assetsPath, "assetspath", "", "absolute path, or relative path for executable, of calendarheatmap repo assets, if not set will try CALENDARHEATMAP_ASSETS env variable, if not will try 'assets'") flag.Parse() + if assetsPath == "" { + assetsPath = os.Getenv("CALENDAR_HEATMAP_ASSETS_PATH") + if assetsPath == "" { + assetsPath = "assets" + } + } + data, err := ioutil.ReadAll(os.Stdin) if err != nil { log.Fatal(err) @@ -40,9 +48,19 @@ func main() { log.Fatal(err) } + colorscale, err := charts.NewBasicColorscaleFromCSVFile(path.Join(assetsPath, "colorscales", colorScale)) + if err != nil { + log.Fatal(err) + } + + fontFace, err := charts.LoadFontFaceFromFile(path.Join(assetsPath, "fonts", "Sunflower-Medium.ttf")) + if err != nil { + log.Fatal(err) + } + conf := charts.HeatmapConfig{ Counts: counts, - ColorScale: colorscales.LoadColorScale(colorScale), + ColorScale: colorscale, DrawMonthSeparator: monthSep, DrawLabels: labels, Margin: 30, @@ -53,6 +71,12 @@ func main() { BorderColor: color.RGBA{200, 200, 200, 255}, Locale: locale, Format: outputFormat, + FontFace: fontFace, + ShowWeekdays: map[time.Weekday]bool{ + time.Monday: true, + time.Wednesday: true, + time.Friday: true, + }, } charts.WriteHeatmap(conf, os.Stdout) }