1
0
mirror of https://github.com/nikolaydubina/calendarheatmap.git synced 2024-12-12 21:29:53 +02:00
calendarheatmap/charts/charts.go

255 lines
5.8 KiB
Go
Raw Normal View History

2020-07-01 20:48:34 +02:00
package charts
import (
2020-12-08 01:23:11 +02:00
"fmt"
2020-07-01 20:48:34 +02:00
"image"
"image/color"
"image/draw"
2020-12-08 01:23:11 +02:00
"image/gif"
"image/jpeg"
"image/png"
"io"
2020-07-01 20:48:34 +02:00
"time"
2021-02-27 17:38:13 +02:00
"golang.org/x/image/font"
2020-07-01 20:48:34 +02:00
)
2021-02-27 17:38:13 +02:00
// ColorScale is interface for extracting color based on value from 0 to 1
type ColorScale interface {
GetColor(val float64) color.RGBA
}
2020-07-03 03:44:39 +02:00
var weekdayOrder = [7]time.Weekday{
time.Monday,
time.Tuesday,
time.Wednesday,
time.Thursday,
time.Friday,
time.Saturday,
time.Sunday,
}
2020-07-01 20:48:34 +02:00
const (
2020-07-03 03:44:39 +02:00
numWeeksYear = 52
numWeekCols = numWeeksYear + 1 // 53 * 7 = 371 > 366
2020-07-01 20:48:34 +02:00
)
2020-07-02 14:07:08 +02:00
// HeatmapConfig contains config of calendar heatmap image
type HeatmapConfig struct {
2021-08-10 12:42:20 +02:00
Counts map[string]int
ColorScale ColorScale
DrawMonthSeparator bool
DrawLabels bool
BoxSize int
Margin int
MonthSeparatorWidth int
MonthLabelYOffset int
TextWidthLeft int
TextHeightTop int
TextColor color.RGBA
BorderColor color.RGBA
Locale string
Format string
FontFace font.Face
ShowWeekdays map[time.Weekday]bool
2020-07-02 14:07:08 +02:00
}
2020-12-08 01:23:11 +02:00
// WriteHeatmap writes image with heatmap and additional elements
func WriteHeatmap(conf HeatmapConfig, w io.Writer) error {
if conf.Format == "svg" {
2021-02-27 18:16:51 +02:00
writeSVG(conf, w)
2021-02-28 03:48:04 +02:00
return nil
2020-12-08 01:23:11 +02:00
}
2020-07-03 03:44:39 +02:00
width := conf.TextWidthLeft + numWeekCols*(conf.BoxSize+conf.Margin)
2020-07-11 11:31:58 +02:00
height := conf.TextHeightTop + 7*(conf.BoxSize+conf.Margin)
offset := image.Point{X: conf.TextWidthLeft, Y: conf.TextHeightTop}
2020-07-01 20:48:34 +02:00
img := image.NewRGBA(image.Rect(0, 0, width, height))
draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.ZP, draw.Src)
2020-07-03 03:44:39 +02:00
visitors := []DayVisitor{
&DayBoxVisitor{img, conf.ColorScale, conf.BoxSize},
}
2020-07-01 20:48:34 +02:00
2020-07-03 03:44:39 +02:00
if conf.DrawMonthSeparator {
visitors = append(
visitors,
&MonthSeparatorVisitor{
Img: img,
2020-07-11 11:31:58 +02:00
MinY: conf.TextHeightTop,
2020-07-03 03:44:39 +02:00
MaxY: height - conf.Margin,
Margin: conf.Margin,
BoxSize: conf.BoxSize,
2021-08-10 12:42:20 +02:00
Width: conf.MonthSeparatorWidth,
2020-07-03 03:44:39 +02:00
Color: conf.BorderColor,
},
)
}
2020-07-01 20:48:34 +02:00
2021-02-28 03:53:47 +02:00
labelsProvider := NewLabelsProvider(conf.Locale)
2020-09-29 14:02:08 +02:00
2020-07-03 03:44:39 +02:00
if conf.DrawLabels {
2021-08-10 12:42:20 +02:00
visitors = append(visitors, &MonthLabelsVisitor{
FontFace: conf.FontFace,
Img: img,
YOffset: conf.MonthLabelYOffset,
Color: conf.TextColor,
LabelsProvider: labelsProvider,
})
2020-07-01 20:48:34 +02:00
2020-07-02 14:54:02 +02:00
drawWeekdayLabels(
2021-02-27 17:38:13 +02:00
conf.FontFace,
2020-07-02 14:54:02 +02:00
img,
2021-08-10 12:42:20 +02:00
image.Point{X: 0, Y: conf.TextHeightTop},
2021-02-27 19:21:46 +02:00
conf.ShowWeekdays,
2020-07-03 03:44:39 +02:00
conf.BoxSize,
conf.Margin,
conf.TextColor,
2020-09-29 14:02:08 +02:00
labelsProvider,
2020-07-02 14:54:02 +02:00
)
}
2021-08-10 12:42:20 +02:00
for iter := NewDayIterator(conf.Counts, offset, conf.BoxSize, conf.Margin); !iter.Done(); iter.Next() {
for _, v := range visitors {
v.Visit(iter)
}
}
2020-12-08 01:23:11 +02:00
switch conf.Format {
case "png":
if err := png.Encode(w, img); err != nil {
return err
}
case "jpeg":
if err := jpeg.Encode(w, img, nil); err != nil {
return err
}
case "gif":
if err := gif.Encode(w, img, nil); err != nil {
return err
}
default:
return fmt.Errorf("unexpected format")
}
return nil
2020-07-01 20:48:34 +02:00
}
2020-07-03 03:44:39 +02:00
// DayVisitor is interface to update image based on current box
type DayVisitor interface {
Visit(iter *DayIterator)
}
// DayBoxVisitor draws signle heatbox
type DayBoxVisitor struct {
Img *image.RGBA
2021-02-27 17:38:13 +02:00
ColorScale ColorScale
2020-07-03 03:44:39 +02:00
BoxSize int
}
// Visit called on every iteration
func (d *DayBoxVisitor) Visit(iter *DayIterator) {
p := iter.Point()
r := image.Rect(p.X, p.Y, p.X+d.BoxSize, p.Y+d.BoxSize)
color := d.ColorScale.GetColor(iter.Value())
draw.Draw(d.Img, r, &image.Uniform{color}, image.ZP, draw.Src)
}
// MonthSeparatorVisitor draws month separator
type MonthSeparatorVisitor struct {
Img *image.RGBA
MinY int
MaxY int
Margin int
BoxSize int
Width int
Color color.RGBA
}
// Visit called on every iteration
func (d *MonthSeparatorVisitor) Visit(iter *DayIterator) {
day := iter.Time()
2020-07-02 14:54:02 +02:00
if day.Day() == 1 && day.Month() != time.January {
2020-07-03 03:44:39 +02:00
p := iter.Point()
2020-07-02 14:54:02 +02:00
2020-07-03 03:44:39 +02:00
marginSep := d.Margin / 2
2020-07-02 14:54:02 +02:00
2020-09-29 14:02:08 +02:00
xL := p.X - marginSep - d.Width/2
2020-07-03 03:44:39 +02:00
xR := p.X + d.BoxSize + marginSep
// left vertical line
2020-09-29 14:02:08 +02:00
draw.Draw(
2020-07-03 03:44:39 +02:00
d.Img,
2020-09-29 14:02:08 +02:00
image.Rect(xL, p.Y, xL+d.Width, d.MaxY),
&image.Uniform{d.Color},
image.ZP,
draw.Src,
2020-07-03 03:44:39 +02:00
)
if day.Weekday() != weekdayOrder[0] {
// right vertical line
2020-09-29 14:02:08 +02:00
draw.Draw(
2020-07-03 03:44:39 +02:00
d.Img,
2020-09-29 14:02:08 +02:00
image.Rect(xR, d.MinY, xR+d.Width, p.Y-marginSep),
&image.Uniform{d.Color},
image.ZP,
draw.Src,
2020-07-03 03:44:39 +02:00
)
2020-09-29 14:02:08 +02:00
// horizontal line
draw.Draw(
2020-07-03 03:44:39 +02:00
d.Img,
2020-09-29 14:02:08 +02:00
image.Rect(xL, p.Y-marginSep, xR+d.Width, p.Y-marginSep-d.Width),
&image.Uniform{d.Color},
image.ZP,
draw.Src,
2020-07-03 03:44:39 +02:00
)
// connect left vertical line and horizontal one
2020-09-29 14:02:08 +02:00
draw.Draw(
2020-07-03 03:44:39 +02:00
d.Img,
2020-09-29 14:02:08 +02:00
image.Rect(xL, p.Y-marginSep-d.Width, xL+d.Width, p.Y),
&image.Uniform{d.Color},
image.ZP,
draw.Src,
2020-07-03 03:44:39 +02:00
)
2020-07-01 20:48:34 +02:00
}
}
}
2020-07-02 14:07:08 +02:00
2020-07-03 03:44:39 +02:00
// MonthLabelsVisitor draws month label on top of first row 0 of month
type MonthLabelsVisitor struct {
2020-09-29 14:02:08 +02:00
Img *image.RGBA
YOffset int
Color color.RGBA
LabelsProvider LabelsProvider
2021-02-27 17:38:13 +02:00
FontFace font.Face
2020-07-03 03:44:39 +02:00
}
// Visit on every iteration
func (d *MonthLabelsVisitor) Visit(iter *DayIterator) {
day := iter.Time()
2020-07-03 03:47:43 +02:00
// Note, day is from 1~31
2020-07-03 03:44:39 +02:00
if iter.Row == 0 && day.Day() <= 7 {
p := iter.Point()
drawText(
2021-02-27 17:38:13 +02:00
d.FontFace,
2020-07-03 03:44:39 +02:00
d.Img,
image.Point{X: p.X, Y: p.Y - d.YOffset},
2020-09-29 14:02:08 +02:00
d.LabelsProvider.GetMonth(day.Month()),
2020-07-03 03:44:39 +02:00
d.Color,
)
2020-07-02 14:07:08 +02:00
}
2020-07-02 14:54:02 +02:00
}
// 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.
2021-02-27 17:38:13 +02:00
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) {
2021-08-10 12:42:20 +02:00
y := offset.Y + boxSize - margin
2020-07-02 14:54:02 +02:00
for _, w := range weekdayOrder {
if weekdays[w] {
2021-08-10 12:42:20 +02:00
drawText(fontFace, img, image.Point{X: offset.X, Y: y}, lp.GetWeekday(w), color)
2020-07-02 14:54:02 +02:00
}
y += boxSize + margin
2020-07-02 14:07:08 +02:00
}
}