Skip to content

Commit

Permalink
Merge pull request #13 from nikolaydubina/feature/svg
Browse files Browse the repository at this point in the history
SVG support
  • Loading branch information
nikolaydubina committed Dec 7, 2020
2 parents ec04dc4 + e68737a commit e8ca0a3
Show file tree
Hide file tree
Showing 11 changed files with 2,438 additions and 4 deletions.
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -27,6 +27,9 @@ Colorscales
UTF-8
![col1](charts/testdata/korean.png)

SVG
![svg](charts/testdata/korean.svg)

Without month separator
![nosep](charts/testdata/noseparator.png)

Expand Down
Binary file added calendarheatmap
Binary file not shown.
5 changes: 5 additions & 0 deletions charts/dayiter.go
Expand Up @@ -80,3 +80,8 @@ func (d *DayIterator) Time() time.Time {
func (d *DayIterator) Value() float64 {
return float64(d.countByDay[d.time.YearDay()]) / float64(d.maxCount)
}

// Count returns count value
func (d *DayIterator) Count() int {
return d.countByDay[d.time.YearDay()]
}
72 changes: 72 additions & 0 deletions charts/svg.go
@@ -0,0 +1,72 @@
package charts

import (
"fmt"
"image"
"io"
"text/template"
)

type Day struct {
Count int
Date string
Color string
Show bool
}

type WeekdayLabel struct {
Label string
Show bool
}

type Params struct {
Days [53][7]Day
LabelsMonths [12]string
LabelsWeekdays [7]WeekdayLabel
LabelColor string
}

func NewHeatmapSVG(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{}

for iter := NewDayIterator(conf.Year, image.Point{}, conf.CountByDay, 0, 0); !iter.Done(); iter.Next() {
color := conf.ColorScale.GetColor(iter.Value())
days[iter.Col][iter.Row] = Day{
Count: iter.Count(),
Date: iter.Time().Format("2006-01-02"),
Color: fmt.Sprintf("rgb(%d,%d,%d)", color.R, color.G, color.B),
Show: true,
}
}

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{
Days: days,
LabelsMonths: labelsMonths,
LabelsWeekdays: labelsWeekdays,
LabelColor: fmt.Sprintf("rgb(%d,%d,%d)", conf.TextColor.R, conf.TextColor.G, conf.TextColor.B),
}

fullYearTemplate.Execute(w, params)
}
101 changes: 101 additions & 0 deletions charts/svg_test.go
@@ -0,0 +1,101 @@
package charts

import (
"fmt"
"image/color"
"os"
"testing"

"github.com/nikolaydubina/calendarheatmap/colorscales"
)

func saveSVG(t *testing.T, conf HeatmapConfig, filename string) {
f, err := os.Create(filename)
if err != nil {
t.Errorf(fmt.Errorf("can not save: %w", err).Error())
}
NewHeatmapSVG(conf, f)
if err := f.Close(); err != nil {
t.Errorf(fmt.Errorf("can not close: %w", err).Error())
}
}

func TestBasicDataSVG(t *testing.T) {
os.Setenv("CALENDAR_HEATMAP_ASSETS_PATH", "assets")
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: 2, 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) {
conf := HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
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},
}
saveSVG(t, conf, "testdata/basic.svg")
})

t.Run("korean", func(t *testing.T) {
conf := HeatmapConfig{
Year: 2020,
CountByDay: countByDay,
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",
}
saveSVG(t, conf, "testdata/korean.svg")
})

t.Run("empty data", func(t *testing.T) {
conf := HeatmapConfig{
Year: 2020,
CountByDay: map[int]int{},
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},
}
saveSVG(t, conf, "testdata/empty_data.svg")
})

t.Run("nil data", func(t *testing.T) {
conf := HeatmapConfig{
Year: 2020,
CountByDay: nil,
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},
}
saveSVG(t, conf, "testdata/nil_data.svg")
})
}
17 changes: 17 additions & 0 deletions charts/templates.go
@@ -0,0 +1,17 @@
package charts

const fullyear = `<svg width="722" height="112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
<g transform="translate(10, 20)">
{{range $w, $wo := $.Days}}<g transform="translate({{mul 14 $w}}, 0)">
{{range $d, $do := $wo}}{{if $do.Show}}<rect class="day" width="10" height="10" x="{{sub 14 $w}}" y="{{mul 13 $d}}" fill="{{$do.Color}}" data-count="{{$do.Count}}" data-date="{{$do.Date}}"></rect>{{end}}
{{end}}
</g>
{{end}}
{{range $i, $label := $.LabelsMonths}}<text x="{{add 14 (mul 52 $i)}}" y="-7" font-size="10px" fill="{{$.LabelColor}}">{{$label}}</text>
{{end}}
{{range $i, $o := $.LabelsWeekdays}}<text text-anchor="start" font-size="9px" dx="-10" dy="{{add 8 (mul 13 $i)}}" fill="{{$.LabelColor}}" {{if not $o.Show}}style="display: none;"{{end}}>{{$o.Label}}</text>
{{end}}
</g></svg>
`

0 comments on commit e8ca0a3

Please sign in to comment.