Merge pull request #11 from nikolaydubina/feature/korean
Feature/korean
93
charts/assets/fonts/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright 2008 The Sunflower Project Authors
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
charts/assets/fonts/Sunflower-Medium.ttf
Normal file
@ -9,31 +9,6 @@ import (
|
||||
"github.com/nikolaydubina/calendarheatmap/colorscales"
|
||||
)
|
||||
|
||||
var monthLabel = map[time.Month]string{
|
||||
time.January: "Jan",
|
||||
time.February: "Feb",
|
||||
time.March: "Mar",
|
||||
time.April: "Apr",
|
||||
time.May: "May",
|
||||
time.June: "Jun",
|
||||
time.July: "Jul",
|
||||
time.August: "Aug",
|
||||
time.September: "Sep",
|
||||
time.October: "Oct",
|
||||
time.November: "Nov",
|
||||
time.December: "Dec",
|
||||
}
|
||||
|
||||
var weekdayLabel = map[time.Weekday]string{
|
||||
time.Monday: "Mon",
|
||||
time.Tuesday: "Tue",
|
||||
time.Wednesday: "Wed",
|
||||
time.Thursday: "Thu",
|
||||
time.Friday: "Fri",
|
||||
time.Saturday: "Sat",
|
||||
time.Sunday: "Sun",
|
||||
}
|
||||
|
||||
var weekdayOrder = [7]time.Weekday{
|
||||
time.Monday,
|
||||
time.Tuesday,
|
||||
@ -62,6 +37,7 @@ type HeatmapConfig struct {
|
||||
TextHeightTop int
|
||||
TextColor color.RGBA
|
||||
BorderColor color.RGBA
|
||||
Locale string
|
||||
}
|
||||
|
||||
// NewHeatmap creates image with heatmap and additional elements
|
||||
@ -86,14 +62,20 @@ func NewHeatmap(conf HeatmapConfig) image.Image {
|
||||
MaxY: height - conf.Margin,
|
||||
Margin: conf.Margin,
|
||||
BoxSize: conf.BoxSize,
|
||||
Width: 1,
|
||||
Width: 5,
|
||||
Color: conf.BorderColor,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
locale := "en_US"
|
||||
if conf.Locale != "" {
|
||||
locale = conf.Locale
|
||||
}
|
||||
labelsProvider := NewLabelsProvider(locale)
|
||||
|
||||
if conf.DrawLabels {
|
||||
visitors = append(visitors, &MonthLabelsVisitor{Img: img, YOffset: 5, Color: conf.TextColor})
|
||||
visitors = append(visitors, &MonthLabelsVisitor{Img: img, YOffset: 50, Color: conf.TextColor, LabelsProvider: labelsProvider})
|
||||
}
|
||||
|
||||
for iter := NewDayIterator(conf.Year, offset, conf.CountByDay, conf.BoxSize, conf.Margin); !iter.Done(); iter.Next() {
|
||||
@ -114,6 +96,7 @@ func NewHeatmap(conf HeatmapConfig) image.Image {
|
||||
conf.BoxSize,
|
||||
conf.Margin,
|
||||
conf.TextColor,
|
||||
labelsProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@ -159,37 +142,41 @@ func (d *MonthSeparatorVisitor) Visit(iter *DayIterator) {
|
||||
|
||||
marginSep := d.Margin / 2
|
||||
|
||||
xL := p.X - marginSep - d.Width
|
||||
xL := p.X - marginSep - d.Width/2
|
||||
xR := p.X + d.BoxSize + marginSep
|
||||
|
||||
// left vertical line
|
||||
drawLineAxis(
|
||||
draw.Draw(
|
||||
d.Img,
|
||||
image.Point{X: xL, Y: p.Y},
|
||||
image.Point{X: xL, Y: d.MaxY - d.Width},
|
||||
d.Color,
|
||||
image.Rect(xL, p.Y, xL+d.Width, d.MaxY),
|
||||
&image.Uniform{d.Color},
|
||||
image.ZP,
|
||||
draw.Src,
|
||||
)
|
||||
if day.Weekday() != weekdayOrder[0] {
|
||||
// right vertical line
|
||||
drawLineAxis(
|
||||
draw.Draw(
|
||||
d.Img,
|
||||
image.Point{X: xR, Y: d.MinY},
|
||||
image.Point{X: xR, Y: p.Y - marginSep - d.Width},
|
||||
d.Color,
|
||||
image.Rect(xR, d.MinY, xR+d.Width, p.Y-marginSep),
|
||||
&image.Uniform{d.Color},
|
||||
image.ZP,
|
||||
draw.Src,
|
||||
)
|
||||
// right vertical line
|
||||
drawLineAxis(
|
||||
// horizontal line
|
||||
draw.Draw(
|
||||
d.Img,
|
||||
image.Point{X: xL, Y: p.Y - marginSep - d.Width},
|
||||
image.Point{X: xR, Y: p.Y - marginSep - d.Width},
|
||||
d.Color,
|
||||
image.Rect(xL, p.Y-marginSep, xR+d.Width, p.Y-marginSep-d.Width),
|
||||
&image.Uniform{d.Color},
|
||||
image.ZP,
|
||||
draw.Src,
|
||||
)
|
||||
// connect left vertical line and horizontal one
|
||||
drawLineAxis(
|
||||
draw.Draw(
|
||||
d.Img,
|
||||
image.Point{X: xL, Y: p.Y - marginSep - d.Width},
|
||||
image.Point{X: xL, Y: p.Y},
|
||||
d.Color,
|
||||
image.Rect(xL, p.Y-marginSep-d.Width, xL+d.Width, p.Y),
|
||||
&image.Uniform{d.Color},
|
||||
image.ZP,
|
||||
draw.Src,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -197,9 +184,10 @@ func (d *MonthSeparatorVisitor) Visit(iter *DayIterator) {
|
||||
|
||||
// MonthLabelsVisitor draws month label on top of first row 0 of month
|
||||
type MonthLabelsVisitor struct {
|
||||
Img *image.RGBA
|
||||
YOffset int
|
||||
Color color.RGBA
|
||||
Img *image.RGBA
|
||||
YOffset int
|
||||
Color color.RGBA
|
||||
LabelsProvider LabelsProvider
|
||||
}
|
||||
|
||||
// Visit on every iteration
|
||||
@ -211,7 +199,7 @@ func (d *MonthLabelsVisitor) Visit(iter *DayIterator) {
|
||||
drawText(
|
||||
d.Img,
|
||||
image.Point{X: p.X, Y: p.Y - d.YOffset},
|
||||
monthLabel[day.Month()],
|
||||
d.LabelsProvider.GetMonth(day.Month()),
|
||||
d.Color,
|
||||
)
|
||||
}
|
||||
@ -220,13 +208,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) {
|
||||
width := 25
|
||||
height := 10
|
||||
func drawWeekdayLabels(img *image.RGBA, offset image.Point, weekdays map[time.Weekday]bool, boxSize int, margin int, color color.RGBA, lp LabelsProvider) {
|
||||
width := 300
|
||||
height := 100
|
||||
y := offset.Y + height
|
||||
for _, w := range weekdayOrder {
|
||||
if weekdays[w] {
|
||||
drawText(img, image.Point{X: offset.X - width, Y: y}, weekdayLabel[w], color)
|
||||
drawText(img, image.Point{X: offset.X - width, Y: y}, lp.GetWeekday(w), color)
|
||||
}
|
||||
y += boxSize + margin
|
||||
}
|
||||
|
@ -25,12 +25,13 @@ func savePNG(t *testing.T, img image.Image, filename string) {
|
||||
}
|
||||
|
||||
func TestBasicData(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: 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,
|
||||
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) {
|
||||
@ -40,10 +41,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.PuBu9,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
@ -57,10 +58,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.GnBu9,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
@ -74,16 +75,34 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.YlGn9,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
savePNG(t, img, "testdata/colorscale_2.png")
|
||||
})
|
||||
|
||||
t.Run("korean", func(t *testing.T) {
|
||||
img := NewHeatmap(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",
|
||||
})
|
||||
savePNG(t, img, "testdata/korean.png")
|
||||
})
|
||||
|
||||
t.Run("no separator", func(t *testing.T) {
|
||||
img := NewHeatmap(HeatmapConfig{
|
||||
Year: 2020,
|
||||
@ -91,10 +110,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.PuBu9,
|
||||
DrawMonthSeparator: false,
|
||||
DrawLabels: true,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
@ -108,10 +127,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.PuBu9,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: false,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
@ -125,10 +144,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.PuBu9,
|
||||
DrawMonthSeparator: false,
|
||||
DrawLabels: false,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
@ -142,10 +161,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.PuBu9,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: false,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
@ -159,10 +178,10 @@ func TestBasicData(t *testing.T) {
|
||||
ColorScale: colorscales.PuBu9,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: false,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
})
|
||||
|
79
charts/labels.go
Normal file
@ -0,0 +1,79 @@
|
||||
package charts
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var localeConfig = map[string]LabelsProvider{
|
||||
"en_US": LabelsProvider{
|
||||
months: map[time.Month]string{
|
||||
time.January: "Jan",
|
||||
time.February: "Feb",
|
||||
time.March: "Mar",
|
||||
time.April: "Apr",
|
||||
time.May: "May",
|
||||
time.June: "Jun",
|
||||
time.July: "Jul",
|
||||
time.August: "Aug",
|
||||
time.September: "Sep",
|
||||
time.October: "Oct",
|
||||
time.November: "Nov",
|
||||
time.December: "Dec",
|
||||
},
|
||||
weekdays: map[time.Weekday]string{
|
||||
time.Monday: "Mon",
|
||||
time.Tuesday: "Tue",
|
||||
time.Wednesday: "Wed",
|
||||
time.Thursday: "Thu",
|
||||
time.Friday: "Fri",
|
||||
time.Saturday: "Sat",
|
||||
time.Sunday: "Sun",
|
||||
},
|
||||
},
|
||||
"ko_KR": LabelsProvider{
|
||||
months: map[time.Month]string{
|
||||
time.January: "일월",
|
||||
time.February: "이월",
|
||||
time.March: "삼월",
|
||||
time.April: "사월",
|
||||
time.May: "오월",
|
||||
time.June: "유월",
|
||||
time.July: "칠월",
|
||||
time.August: "팔월",
|
||||
time.September: "구월",
|
||||
time.October: "시월",
|
||||
time.November: "십일월",
|
||||
time.December: "십이월",
|
||||
},
|
||||
weekdays: map[time.Weekday]string{
|
||||
time.Monday: "월요일",
|
||||
time.Tuesday: "화요일",
|
||||
time.Wednesday: "수요일",
|
||||
time.Thursday: "목요일",
|
||||
time.Friday: "금요일",
|
||||
time.Saturday: "토요일",
|
||||
time.Sunday: "일요일",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// LabelsProvider provides labels for locale
|
||||
type LabelsProvider struct {
|
||||
months map[time.Month]string
|
||||
weekdays map[time.Weekday]string
|
||||
}
|
||||
|
||||
// NewLabelsProvider initializes labels provider for locale
|
||||
func NewLabelsProvider(locale string) LabelsProvider {
|
||||
return localeConfig[locale]
|
||||
}
|
||||
|
||||
// GetMonth returns month label
|
||||
func (p LabelsProvider) GetMonth(month time.Month) string {
|
||||
return p.months[month]
|
||||
}
|
||||
|
||||
// GetWeekday returns weekday label
|
||||
func (p LabelsProvider) GetWeekday(weekday time.Weekday) string {
|
||||
return p.weekdays[weekday]
|
||||
}
|
BIN
charts/testdata/basic.png
vendored
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 88 KiB |
BIN
charts/testdata/colorscale_1.png
vendored
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 88 KiB |
BIN
charts/testdata/colorscale_2.png
vendored
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 88 KiB |
BIN
charts/testdata/empty_data.png
vendored
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 46 KiB |
BIN
charts/testdata/korean.png
vendored
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
charts/testdata/nil_data.png
vendored
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 46 KiB |
BIN
charts/testdata/nolabels.png
vendored
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 47 KiB |
BIN
charts/testdata/noseparator.png
vendored
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 87 KiB |
BIN
charts/testdata/noseparator_nolabels.png
vendored
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 46 KiB |
BIN
charts/testdata/nosepartor.png
vendored
Before Width: | Height: | Size: 2.1 KiB |
@ -1,46 +1,41 @@
|
||||
package charts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// drawLineAxis draws line parallel to X or Y axis
|
||||
func drawLineAxis(img draw.Image, a image.Point, b image.Point, col color.Color) {
|
||||
switch {
|
||||
// do not attempt to draw dot
|
||||
case a == b:
|
||||
return
|
||||
// vertical
|
||||
case a.X == b.X:
|
||||
y1, y2 := a.Y, b.Y
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
for q := y1; q <= y2; q++ {
|
||||
img.Set(a.X, q, col)
|
||||
}
|
||||
// horizontal
|
||||
case a.Y == b.Y:
|
||||
x1, x2 := a.X, b.X
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
for q := x1; q <= x2; q++ {
|
||||
img.Set(q, a.Y, col)
|
||||
}
|
||||
default:
|
||||
panic("input line is not parallel to axis. not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// 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),
|
||||
@ -48,7 +43,7 @@ func drawText(img *image.RGBA, offset image.Point, text string, color color.RGBA
|
||||
d := &font.Drawer{
|
||||
Dst: img,
|
||||
Src: image.NewUniform(color),
|
||||
Face: basicfont.Face7x13,
|
||||
Face: face,
|
||||
Dot: point,
|
||||
}
|
||||
d.DrawString(text)
|
||||
|
@ -1,31 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
2
go.mod
@ -2,4 +2,4 @@ module github.com/nikolaydubina/calendarheatmap
|
||||
|
||||
go 1.14
|
||||
|
||||
require golang.org/x/image v0.0.0-20200618115811-c13761719519
|
||||
require golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
||||
|
5
go.sum
@ -1,3 +1,4 @@
|
||||
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
|
||||
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
11
main.go
@ -24,6 +24,7 @@ func main() {
|
||||
labels := flag.Bool("labels", true, "labels for weekday and months")
|
||||
monthSep := flag.Bool("monthsep", true, "render month separator")
|
||||
outputFormat := flag.String("output", "png", "output format (png, jpeg, gif)")
|
||||
locale := flag.String("locale", "en_US", "locale of labels (default en_US)")
|
||||
flag.Parse()
|
||||
|
||||
data, err := ioutil.ReadAll(os.Stdin)
|
||||
@ -46,18 +47,20 @@ func main() {
|
||||
log.Fatal("error parsing data: %w", err)
|
||||
}
|
||||
|
||||
os.Setenv("CALENDAR_HEATMAP_ASSETS_PATH", "charts/assets")
|
||||
img := charts.NewHeatmap(charts.HeatmapConfig{
|
||||
Year: year,
|
||||
CountByDay: countByDay,
|
||||
ColorScale: colorscales.LoadColorScale(*colorScale),
|
||||
DrawMonthSeparator: *monthSep,
|
||||
DrawLabels: *labels,
|
||||
Margin: 3,
|
||||
BoxSize: 15,
|
||||
TextWidthLeft: 35,
|
||||
TextHeightTop: 20,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
Locale: *locale,
|
||||
})
|
||||
|
||||
outWriter := os.Stdout
|
||||
|