Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SVG support #13

Merged
merged 1 commit into from Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
`