WEB: test
24
.github/workflows/tests.yml
vendored
@ -16,25 +16,29 @@ jobs:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.14
|
||||
go-version: ^1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
if [ -f Gopkg.toml ]; then
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
dep ensure
|
||||
fi
|
||||
run: go get -v -t -d ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
- name: Test that can build
|
||||
run: go build
|
||||
|
||||
- name: Test
|
||||
run: go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
run: go test -v -coverprofile=coverage.txt -covermode=atomic ./charts
|
||||
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v1.0.10
|
||||
|
||||
- name: Build web
|
||||
run: make build-web
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: web
|
23
Makefile
@ -2,17 +2,30 @@ build:
|
||||
go build
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
go test ./charts
|
||||
|
||||
docs: build
|
||||
cat charts/testdata/basic.json | ./calendarheatmap > docs/basic.png
|
||||
CALENDAR_HEATMAP_ASSETS_PATH=assets cat charts/testdata/basic.json | ./calendarheatmap -colorscale=purple-blue-9.csv > docs/colorscale-1.png
|
||||
CALENDAR_HEATMAP_ASSETS_PATH=assets cat charts/testdata/basic.json | ./calendarheatmap -colorscale=green-blue-9.csv > docs/colorscale-2.png
|
||||
CALENDAR_HEATMAP_ASSETS_PATH=assets cat charts/testdata/basic.json | ./calendarheatmap -colorscale=yellow-green-9.csv > docs/colorscale-3.png
|
||||
cat charts/testdata/basic.json | CALENDAR_HEATMAP_ASSETS_PATH=assets ./calendarheatmap -colorscale=purple-blue-9.csv > docs/colorscale-1.png
|
||||
cat charts/testdata/basic.json | CALENDAR_HEATMAP_ASSETS_PATH=assets ./calendarheatmap -colorscale=green-blue-9.csv > docs/colorscale-2.png
|
||||
cat charts/testdata/basic.json | CALENDAR_HEATMAP_ASSETS_PATH=assets ./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
|
||||
build-web:
|
||||
cp "$$(go env GOROOT)/misc/wasm/wasm_exec.js" web/
|
||||
cp -r assets web/assets
|
||||
cd web; GOARCH=wasm GOOS=js go build -o main.wasm main.go
|
||||
|
||||
run-web:
|
||||
cd web; python3 -m http.server 8000
|
||||
|
||||
clean:
|
||||
-rm web/wasm_exec.js
|
||||
-rm web/main.wasm
|
||||
-rm -rf web/assets
|
||||
|
||||
.PHONY: build test docs build-web run-web clean
|
@ -36,20 +36,22 @@ const (
|
||||
|
||||
// HeatmapConfig contains config of calendar heatmap image
|
||||
type HeatmapConfig struct {
|
||||
Counts map[string]int
|
||||
ColorScale ColorScale
|
||||
DrawMonthSeparator bool
|
||||
DrawLabels bool
|
||||
BoxSize int
|
||||
Margin int
|
||||
TextWidthLeft int
|
||||
TextHeightTop int
|
||||
TextColor color.RGBA
|
||||
BorderColor color.RGBA
|
||||
Locale string
|
||||
Format string
|
||||
FontFace font.Face
|
||||
ShowWeekdays map[time.Weekday]bool
|
||||
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
|
||||
}
|
||||
|
||||
// WriteHeatmap writes image with heatmap and additional elements
|
||||
@ -79,7 +81,7 @@ func WriteHeatmap(conf HeatmapConfig, w io.Writer) error {
|
||||
MaxY: height - conf.Margin,
|
||||
Margin: conf.Margin,
|
||||
BoxSize: conf.BoxSize,
|
||||
Width: 5,
|
||||
Width: conf.MonthSeparatorWidth,
|
||||
Color: conf.BorderColor,
|
||||
},
|
||||
)
|
||||
@ -88,20 +90,18 @@ func WriteHeatmap(conf HeatmapConfig, w io.Writer) error {
|
||||
labelsProvider := NewLabelsProvider(conf.Locale)
|
||||
|
||||
if conf.DrawLabels {
|
||||
visitors = append(visitors, &MonthLabelsVisitor{FontFace: conf.FontFace, Img: img, YOffset: 50, Color: conf.TextColor, LabelsProvider: labelsProvider})
|
||||
}
|
||||
visitors = append(visitors, &MonthLabelsVisitor{
|
||||
FontFace: conf.FontFace,
|
||||
Img: img,
|
||||
YOffset: conf.MonthLabelYOffset,
|
||||
Color: conf.TextColor,
|
||||
LabelsProvider: labelsProvider,
|
||||
})
|
||||
|
||||
for iter := NewDayIterator(conf.Counts, offset, conf.BoxSize, conf.Margin); !iter.Done(); iter.Next() {
|
||||
for _, v := range visitors {
|
||||
v.Visit(iter)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.DrawLabels {
|
||||
drawWeekdayLabels(
|
||||
conf.FontFace,
|
||||
img,
|
||||
offset,
|
||||
image.Point{X: 0, Y: conf.TextHeightTop},
|
||||
conf.ShowWeekdays,
|
||||
conf.BoxSize,
|
||||
conf.Margin,
|
||||
@ -110,6 +110,12 @@ func WriteHeatmap(conf HeatmapConfig, w io.Writer) error {
|
||||
)
|
||||
}
|
||||
|
||||
for iter := NewDayIterator(conf.Counts, offset, conf.BoxSize, conf.Margin); !iter.Done(); iter.Next() {
|
||||
for _, v := range visitors {
|
||||
v.Visit(iter)
|
||||
}
|
||||
}
|
||||
|
||||
switch conf.Format {
|
||||
case "png":
|
||||
if err := png.Encode(w, img); err != nil {
|
||||
@ -238,12 +244,10 @@ func (d *MonthLabelsVisitor) Visit(iter *DayIterator) {
|
||||
// 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(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
|
||||
y := offset.Y + boxSize - margin
|
||||
for _, w := range weekdayOrder {
|
||||
if weekdays[w] {
|
||||
drawText(fontFace, img, image.Point{X: offset.X - width, Y: y}, lp.GetWeekday(w), color)
|
||||
drawText(fontFace, img, image.Point{X: offset.X, Y: y}, lp.GetWeekday(w), color)
|
||||
}
|
||||
y += boxSize + margin
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
|
||||
"github.com/nikolaydubina/calendarheatmap/charts"
|
||||
)
|
||||
@ -28,7 +29,11 @@ func loadData(t *testing.T, filepath string) map[string]int {
|
||||
}
|
||||
|
||||
func loadFontFace(t *testing.T, filepath string) font.Face {
|
||||
fontFace, err := charts.LoadFontFaceFromFile(filepath)
|
||||
fontFace, err := charts.LoadFontFaceFromFile(filepath, opentype.FaceOptions{
|
||||
Size: 26,
|
||||
DPI: 280,
|
||||
Hinting: font.HintingNone,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -55,20 +60,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
Counts: loadData(t, path.Join("testdata", "basic.json")),
|
||||
ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")),
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
BoxSize: 150,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -76,20 +83,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
Counts: loadData(t, path.Join("testdata", "basic.json")),
|
||||
ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")),
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
BoxSize: 150,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -97,20 +106,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
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,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -118,20 +129,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
Counts: nil,
|
||||
ColorScale: loadColorscale(t, path.Join("..", "assets", "colorscales", "purple-blue-9.csv")),
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
BoxSize: 150,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -139,20 +152,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
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,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -160,20 +175,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
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,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -181,20 +198,22 @@ func TestCharts(t *testing.T) {
|
||||
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},
|
||||
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,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -202,20 +221,22 @@ func TestCharts(t *testing.T) {
|
||||
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,
|
||||
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,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -223,19 +244,21 @@ func TestCharts(t *testing.T) {
|
||||
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")),
|
||||
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,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
Margin: 30,
|
||||
TextWidthLeft: 300,
|
||||
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,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package charts
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
@ -8,6 +9,9 @@ import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed template.svg
|
||||
var svgTemplate string
|
||||
|
||||
// Day is SVG template day parameters
|
||||
type Day struct {
|
||||
Count int
|
||||
@ -44,7 +48,7 @@ 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 },
|
||||
}).Parse(fullyear))
|
||||
}).Parse(svgTemplate))
|
||||
|
||||
labelsProvider := NewLabelsProvider(conf.Locale)
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
package charts
|
||||
|
||||
const fullyear = `<svg width="752" height="112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(10, 20)">
|
||||
<svg viewBox="0 0 762 112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(20, 20)">
|
||||
{{range $w, $wo := $.Days}}<g transform="translate({{mul 14 $w}}, 0)">
|
||||
{{range $d, $do := $wo}}{{if $do.Show}}<rect class="day" width="11" height="11" x="0" y="{{mul 13 $d}}" fill="{{$do.Color}}" data-count="{{$do.Count}}" data-date="{{$do.Date}}"></rect>{{end}}
|
||||
{{end}}
|
||||
@ -11,7 +9,6 @@ const fullyear = `<svg width="752" height="112" xmlns="http://www.w3.org/2000/sv
|
||||
{{range $i, $label := $.LabelsMonths}}<text x="{{mul 14 $label.XOffset}}" y="-7" font-size="8px" fill="{{$.LabelsColor}}">{{$label.Label}}</text>
|
||||
{{end}}
|
||||
|
||||
{{range $i, $o := $.LabelsWeekdays}}<text text-anchor="start" font-size="8px" dx="-10" dy="{{add 8 (mul 13 $i)}}" fill="{{$.LabelsColor}}" {{if not $o.Show}}style="display: none;"{{end}}>{{$o.Label}}</text>
|
||||
{{range $i, $o := $.LabelsWeekdays}}<text text-anchor="start" font-size="8px" dx="-20" dy="{{add 8 (mul 13 $i)}}" fill="{{$.LabelsColor}}" {{if not $o.Show}}style="display: none;"{{end}}>{{$o.Label}}</text>
|
||||
{{end}}
|
||||
</g></svg>
|
||||
`
|
||||
</g></svg>
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 822 B |
BIN
charts/testdata/basic-all-weekdays-expected.png
vendored
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
BIN
charts/testdata/basic-all-weekdays-output.png
vendored
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
BIN
charts/testdata/basic-jpeg-expected.jpeg
vendored
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 367 KiB |
BIN
charts/testdata/basic-jpeg-output.jpeg
vendored
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 367 KiB |
BIN
charts/testdata/basic-korean-expected.png
vendored
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
BIN
charts/testdata/basic-korean-output.png
vendored
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
BIN
charts/testdata/basic-no-data-expected.png
vendored
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
BIN
charts/testdata/basic-no-data-output.png
vendored
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
BIN
charts/testdata/basic-no-labels-expected.png
vendored
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
BIN
charts/testdata/basic-no-labels-output.png
vendored
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
BIN
charts/testdata/basic-no-separator-expected.png
vendored
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 87 KiB |
BIN
charts/testdata/basic-no-separator-output.png
vendored
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 87 KiB |
BIN
charts/testdata/basic-no-weekdays-expected.png
vendored
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
BIN
charts/testdata/basic-no-weekdays-output.png
vendored
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
BIN
charts/testdata/basic-png-expected.png
vendored
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
BIN
charts/testdata/basic-png-output.png
vendored
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
20
charts/testdata/basic-svg-expected.svg
vendored
@ -1,5 +1,5 @@
|
||||
<svg width="752" height="112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(10, 20)">
|
||||
<svg viewBox="0 0 762 112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(20, 20)">
|
||||
<g transform="translate(0, 0)">
|
||||
|
||||
|
||||
@ -546,12 +546,12 @@
|
||||
<text x="686" y="-7" font-size="8px" fill="rgb(100,100,100)">Dec</text>
|
||||
|
||||
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="8" fill="rgb(100,100,100)" >Mon</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="21" fill="rgb(100,100,100)" style="display: none;">Tue</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="34" fill="rgb(100,100,100)" >Wed</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="47" fill="rgb(100,100,100)" style="display: none;">Thu</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="60" fill="rgb(100,100,100)" >Fri</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="73" fill="rgb(100,100,100)" style="display: none;">Sat</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="86" fill="rgb(100,100,100)" style="display: none;">Sun</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="8" fill="rgb(100,100,100)" >Mon</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="21" fill="rgb(100,100,100)" style="display: none;">Tue</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="34" fill="rgb(100,100,100)" >Wed</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="47" fill="rgb(100,100,100)" style="display: none;">Thu</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="60" fill="rgb(100,100,100)" >Fri</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="73" fill="rgb(100,100,100)" style="display: none;">Sat</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="86" fill="rgb(100,100,100)" style="display: none;">Sun</text>
|
||||
|
||||
</g></svg>
|
||||
</g></svg>
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
20
charts/testdata/basic-svg-output.svg
vendored
@ -1,5 +1,5 @@
|
||||
<svg width="752" height="112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(10, 20)">
|
||||
<svg viewBox="0 0 762 112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(20, 20)">
|
||||
<g transform="translate(0, 0)">
|
||||
|
||||
|
||||
@ -546,12 +546,12 @@
|
||||
<text x="686" y="-7" font-size="8px" fill="rgb(100,100,100)">Dec</text>
|
||||
|
||||
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="8" fill="rgb(100,100,100)" >Mon</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="21" fill="rgb(100,100,100)" style="display: none;">Tue</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="34" fill="rgb(100,100,100)" >Wed</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="47" fill="rgb(100,100,100)" style="display: none;">Thu</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="60" fill="rgb(100,100,100)" >Fri</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="73" fill="rgb(100,100,100)" style="display: none;">Sat</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="86" fill="rgb(100,100,100)" style="display: none;">Sun</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="8" fill="rgb(100,100,100)" >Mon</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="21" fill="rgb(100,100,100)" style="display: none;">Tue</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="34" fill="rgb(100,100,100)" >Wed</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="47" fill="rgb(100,100,100)" style="display: none;">Thu</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="60" fill="rgb(100,100,100)" >Fri</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="73" fill="rgb(100,100,100)" style="display: none;">Sat</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="86" fill="rgb(100,100,100)" style="display: none;">Sun</text>
|
||||
|
||||
</g></svg>
|
||||
</g></svg>
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@ -12,16 +12,12 @@ import (
|
||||
)
|
||||
|
||||
// LoadFontFace loads font face from bytes
|
||||
func LoadFontFace(fontBytes []byte) (font.Face, error) {
|
||||
func LoadFontFace(fontBytes []byte, options opentype.FaceOptions) (font.Face, error) {
|
||||
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,
|
||||
})
|
||||
face, err := opentype.NewFace(f, &options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not create font face: %w", err)
|
||||
}
|
||||
@ -29,12 +25,12 @@ func LoadFontFace(fontBytes []byte) (font.Face, error) {
|
||||
}
|
||||
|
||||
// LoadFontFaceFromFile loads font face from file
|
||||
func LoadFontFaceFromFile(fontPath string) (font.Face, error) {
|
||||
func LoadFontFaceFromFile(fontPath string, options opentype.FaceOptions) (font.Face, error) {
|
||||
fontBytes, err := ioutil.ReadFile(fontPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not open font file with error: %w", err)
|
||||
}
|
||||
return LoadFontFace(fontBytes)
|
||||
return LoadFontFace(fontBytes, options)
|
||||
}
|
||||
|
||||
// drawText inserts text into provided image at bottom left coordinate
|
||||
|
BIN
docs/basic.png
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
BIN
docs/korean.png
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
@ -1,5 +1,5 @@
|
||||
<svg width="752" height="112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(10, 20)">
|
||||
<svg viewBox="0 0 762 112" xmlns="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(20, 20)">
|
||||
<g transform="translate(0, 0)">
|
||||
|
||||
|
||||
@ -546,12 +546,12 @@
|
||||
<text x="686" y="-7" font-size="8px" fill="rgb(100,100,100)">12월</text>
|
||||
|
||||
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="8" fill="rgb(100,100,100)" >월</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="21" fill="rgb(100,100,100)" style="display: none;">화</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="34" fill="rgb(100,100,100)" >수</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="47" fill="rgb(100,100,100)" style="display: none;">목</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="60" fill="rgb(100,100,100)" >금</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="73" fill="rgb(100,100,100)" style="display: none;">토</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-10" dy="86" fill="rgb(100,100,100)" style="display: none;">일</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="8" fill="rgb(100,100,100)" >월</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="21" fill="rgb(100,100,100)" style="display: none;">화</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="34" fill="rgb(100,100,100)" >수</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="47" fill="rgb(100,100,100)" style="display: none;">목</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="60" fill="rgb(100,100,100)" >금</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="73" fill="rgb(100,100,100)" style="display: none;">토</text>
|
||||
<text text-anchor="start" font-size="8px" dx="-20" dy="86" fill="rgb(100,100,100)" style="display: none;">일</text>
|
||||
|
||||
</g></svg>
|
||||
</g></svg>
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
36
main.go
@ -14,6 +14,8 @@ import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/nikolaydubina/calendarheatmap/charts"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
)
|
||||
|
||||
//go:embed assets/fonts/Sunflower-Medium.ttf
|
||||
@ -56,7 +58,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
fontFace, err := charts.LoadFontFace(defaultFontFaceBytes)
|
||||
fontFace, err := charts.LoadFontFace(defaultFontFaceBytes, opentype.FaceOptions{
|
||||
Size: 26,
|
||||
DPI: 280,
|
||||
Hinting: font.HintingNone,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -72,19 +78,21 @@ func main() {
|
||||
}
|
||||
|
||||
conf := charts.HeatmapConfig{
|
||||
Counts: counts,
|
||||
ColorScale: colorscale,
|
||||
DrawMonthSeparator: monthSep,
|
||||
DrawLabels: labels,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
TextWidthLeft: 350,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
Locale: locale,
|
||||
Format: outputFormat,
|
||||
FontFace: fontFace,
|
||||
Counts: counts,
|
||||
ColorScale: colorscale,
|
||||
DrawMonthSeparator: monthSep,
|
||||
DrawLabels: labels,
|
||||
Margin: 30,
|
||||
BoxSize: 150,
|
||||
MonthSeparatorWidth: 5,
|
||||
MonthLabelYOffset: 50,
|
||||
TextWidthLeft: 300,
|
||||
TextHeightTop: 200,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
Locale: locale,
|
||||
Format: outputFormat,
|
||||
FontFace: fontFace,
|
||||
ShowWeekdays: map[time.Weekday]bool{
|
||||
time.Monday: true,
|
||||
time.Wednesday: true,
|
||||
|
3
web/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
wasm_exec.js
|
||||
main.wasm
|
||||
assets/
|
143
web/index.html
Normal file
@ -0,0 +1,143 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Bungee+Shade">
|
||||
|
||||
<style>
|
||||
#banner {
|
||||
font-family: 'Bungee Shade', serif;
|
||||
font-size: 64px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
|
||||
go.run(result.instance);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-D6NR6EJGD7"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-D6NR6EJGD7');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
|
||||
<div class="container">
|
||||
<div class="mx-auto" style="width: 700px;">
|
||||
<div class="d-grid gap-4">
|
||||
<div class="mb-3">
|
||||
<div style="padding-top: 25px;">
|
||||
<p id="banner" class="text-center">
|
||||
CALENDAR HEATMAP
|
||||
</p>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a class="github-button" href="https://github.com/nikolaydubina/calendarheatmap" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star nikolaydubina/calendarheatmap on GitHub">Star</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div id="output-container" style="height: 100px;"></div>
|
||||
</div>
|
||||
|
||||
<form id="inputConfig">
|
||||
<div class="d-grid gap-1">
|
||||
<div class="mb-3">
|
||||
<input type="radio" class="btn-check" name="formatOption" id="formatSVG" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary" for="formatSVG">SVG</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="formatOption" id="formatPNG" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary" for="formatPNG">PNG</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="formatOption" id="formatJPEG" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary" for="formatJPEG">JPEG</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" id="inputData" rows="10" style="resize: vertical; width: 100%; height: auto;">
|
||||
{
|
||||
"2020-05-16": 8,
|
||||
"2020-05-17": 13,
|
||||
"2020-05-18": 5,
|
||||
"2020-05-19": 8,
|
||||
"2020-05-20": 5,
|
||||
"2020-05-21": 10,
|
||||
"2020-05-23": 1
|
||||
}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-light" id="btnPrettifyJSON">Prettify JSON</button>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="switchMonthSeparator" checked>
|
||||
<label class="form-check-label" for="switchMonthSeparator">Months separator</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="switchLabels" checked>
|
||||
<label class="form-check-label" for="switchLabels">Labels</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchMon" checked>
|
||||
<label class="form-check-label" for="switchMon">Mon</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchTue">
|
||||
<label class="form-check-label" for="switchTue">Tue</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchWed" checked>
|
||||
<label class="form-check-label" for="switchWed">Wed</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchThu">
|
||||
<label class="form-check-label" for="switchThu">Thu</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchFri" checked>
|
||||
<label class="form-check-label" for="switchFri">Fri</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchSat">
|
||||
<label class="form-check-label" for="switchSat">Sat</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="switchSun">
|
||||
<label class="form-check-label" for="switchSun">Sun</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a id="downloadLink" download="" href="" class="btn btn-primary" role="button">
|
||||
<i class="bi bi-download"></i> Download
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
182
web/main.go
Normal file
@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/nikolaydubina/calendarheatmap/charts"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
)
|
||||
|
||||
//go:embed assets/fonts/Sunflower-Medium.ttf
|
||||
var defaultFontFaceBytes []byte
|
||||
|
||||
//go:embed assets/colorscales/green-blue-9.csv
|
||||
var defaultColorScaleBytes []byte
|
||||
|
||||
type Renderer struct {
|
||||
config charts.HeatmapConfig
|
||||
img []byte
|
||||
}
|
||||
|
||||
func (r *Renderer) PrettifyJSON(this js.Value, inputs []js.Value) interface{} {
|
||||
document := js.Global().Get("document")
|
||||
instr := document.Call("getElementById", "inputData").Get("value")
|
||||
data := map[string]interface{}{}
|
||||
if err := json.Unmarshal([]byte(instr.String()), &data); err == nil {
|
||||
if out, err := json.MarshalIndent(data, "", " "); err == nil {
|
||||
document.Call("getElementById", "inputData").Set("value", string(out))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) OnDataChange(this js.Value, inputs []js.Value) interface{} {
|
||||
document := js.Global().Get("document")
|
||||
instr := document.Call("getElementById", "inputData").Get("value")
|
||||
|
||||
data := map[string]int{}
|
||||
if err := json.Unmarshal([]byte(instr.String()), &data); err == nil {
|
||||
r.config.Counts = data
|
||||
r.Render()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) GetFormatUpdater(format string) func(this js.Value, inputs []js.Value) interface{} {
|
||||
return func(this js.Value, inputs []js.Value) interface{} {
|
||||
r.config.Format = format
|
||||
|
||||
if format == "svg" {
|
||||
js.Global().Get("document").Call("getElementById", "switchLabels").Set("disabled", "true")
|
||||
js.Global().Get("document").Call("getElementById", "switchMonthSeparator").Set("disabled", "true")
|
||||
} else {
|
||||
js.Global().Get("document").Call("getElementById", "switchLabels").Call("removeAttribute", "disabled")
|
||||
js.Global().Get("document").Call("getElementById", "switchMonthSeparator").Call("removeAttribute", "disabled")
|
||||
}
|
||||
|
||||
r.Render()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) GetWeekdaySwitchUpdater(weekday time.Weekday) func(this js.Value, inputs []js.Value) interface{} {
|
||||
return func(this js.Value, inputs []js.Value) interface{} {
|
||||
r.config.ShowWeekdays[weekday] = !r.config.ShowWeekdays[weekday]
|
||||
r.Render()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) ToggleLabels(this js.Value, inputs []js.Value) interface{} {
|
||||
r.config.DrawLabels = !r.config.DrawLabels
|
||||
r.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) ToggleMonthSeparator(this js.Value, inputs []js.Value) interface{} {
|
||||
r.config.DrawMonthSeparator = !r.config.DrawMonthSeparator
|
||||
r.Render()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) Render() {
|
||||
var output bytes.Buffer
|
||||
if err := charts.WriteHeatmap(r.config, &output); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.config.Format == "svg" {
|
||||
img := output.String()
|
||||
js.Global().Get("document").Call("getElementById", "output-container").Set("innerHTML", img)
|
||||
} else {
|
||||
img := js.Global().Get("document").Call("createElement", "img")
|
||||
src := fmt.Sprintf("data:image/%s;base64,%s", r.config.Format, base64.StdEncoding.EncodeToString(output.Bytes()))
|
||||
img.Set("src", src)
|
||||
img.Set("style", "width: 100%;")
|
||||
|
||||
container := js.Global().Get("document").Call("getElementById", "output-container")
|
||||
container.Set("innerHTML", "")
|
||||
container.Call("appendChild", img)
|
||||
}
|
||||
|
||||
// download file update button
|
||||
src := fmt.Sprintf("data:image/%s;base64,%s", r.config.Format, base64.StdEncoding.EncodeToString(output.Bytes()))
|
||||
link := js.Global().Get("document").Call("getElementById", "downloadLink")
|
||||
link.Set("href", src)
|
||||
link.Set("download", fmt.Sprintf("calendar-heatmap.%s", r.config.Format))
|
||||
}
|
||||
|
||||
func main() {
|
||||
c := make(chan bool)
|
||||
|
||||
colorscale, _ := charts.NewBasicColorscaleFromCSV(bytes.NewBuffer(defaultColorScaleBytes))
|
||||
fontFace, _ := charts.LoadFontFace(defaultFontFaceBytes, opentype.FaceOptions{
|
||||
Size: 13,
|
||||
DPI: 80,
|
||||
Hinting: font.HintingNone,
|
||||
})
|
||||
renderer := Renderer{
|
||||
config: charts.HeatmapConfig{
|
||||
Counts: nil,
|
||||
ColorScale: colorscale,
|
||||
DrawMonthSeparator: true,
|
||||
DrawLabels: true,
|
||||
Margin: 5,
|
||||
BoxSize: 24,
|
||||
MonthSeparatorWidth: 1,
|
||||
MonthLabelYOffset: 5,
|
||||
TextWidthLeft: 40,
|
||||
TextHeightTop: 15,
|
||||
TextColor: color.RGBA{100, 100, 100, 255},
|
||||
BorderColor: color.RGBA{200, 200, 200, 255},
|
||||
Locale: "en_US",
|
||||
Format: "png",
|
||||
FontFace: fontFace,
|
||||
ShowWeekdays: map[time.Weekday]bool{
|
||||
time.Monday: true,
|
||||
time.Wednesday: true,
|
||||
time.Friday: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
document := js.Global().Get("document")
|
||||
|
||||
document.Call("getElementById", "inputConfig").Call("reset")
|
||||
|
||||
document.Call("getElementById", "inputData").Set("onkeyup", js.FuncOf(renderer.OnDataChange))
|
||||
document.Call("getElementById", "btnPrettifyJSON").Set("onclick", js.FuncOf(renderer.PrettifyJSON))
|
||||
|
||||
document.Call("getElementById", "formatSVG").Set("onclick", js.FuncOf(renderer.GetFormatUpdater("svg")))
|
||||
document.Call("getElementById", "formatPNG").Set("onclick", js.FuncOf(renderer.GetFormatUpdater("png")))
|
||||
document.Call("getElementById", "formatJPEG").Set("onclick", js.FuncOf(renderer.GetFormatUpdater("jpeg")))
|
||||
|
||||
document.Call("getElementById", "switchMon").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Monday)))
|
||||
document.Call("getElementById", "switchTue").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Tuesday)))
|
||||
document.Call("getElementById", "switchWed").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Wednesday)))
|
||||
document.Call("getElementById", "switchThu").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Thursday)))
|
||||
document.Call("getElementById", "switchFri").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Friday)))
|
||||
document.Call("getElementById", "switchSat").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Saturday)))
|
||||
document.Call("getElementById", "switchSun").Set("onchange", js.FuncOf(renderer.GetWeekdaySwitchUpdater(time.Sunday)))
|
||||
|
||||
document.Call("getElementById", "switchLabels").Set("onchange", js.FuncOf(renderer.ToggleLabels))
|
||||
document.Call("getElementById", "switchMonthSeparator").Set("onchange", js.FuncOf(renderer.ToggleMonthSeparator))
|
||||
|
||||
renderer.OnDataChange(js.Value{}, nil)
|
||||
renderer.PrettifyJSON(js.Value{}, nil)
|
||||
renderer.Render()
|
||||
|
||||
<-c
|
||||
}
|