Skip to content

Commit 7ff5bd0

Browse files
committed
feature: heatmap
1 parent 4207ca1 commit 7ff5bd0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2040
-272
lines changed

cmd/heatmap/main.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package main
2+
3+
import (
4+
"github.com/johnfercher/maroto/v2"
5+
"github.com/johnfercher/maroto/v2/pkg/components/chart"
6+
"github.com/johnfercher/maroto/v2/pkg/components/row"
7+
"github.com/johnfercher/maroto/v2/pkg/config"
8+
"github.com/johnfercher/maroto/v2/pkg/consts/pagesize"
9+
"github.com/johnfercher/maroto/v2/pkg/props"
10+
"log"
11+
)
12+
13+
func main() {
14+
xMax := 620
15+
yMax := 200
16+
heat := buildHeat(xMax, yMax)
17+
18+
cfg := config.NewBuilder().
19+
WithDebug(true).
20+
WithPageSize(pagesize.A4).
21+
Build()
22+
23+
m := maroto.New(cfg)
24+
25+
m.AddRows(
26+
row.New(200).Add(
27+
chart.NewHeatMapCol(12, "Efficiency", heat, props.HeatMap{
28+
TransparentValues: []int{0},
29+
InvertScale: false,
30+
HalfColor: false,
31+
}),
32+
),
33+
)
34+
35+
document, err := m.Generate()
36+
if err != nil {
37+
log.Fatal(err.Error())
38+
}
39+
40+
err = document.Save("docs/assets/pdf/heatmap.pdf")
41+
if err != nil {
42+
log.Fatal(err.Error())
43+
}
44+
}
45+
46+
func buildHeat(x, y int) [][]int {
47+
var heat [][]int
48+
for i := 0; i < x; i++ {
49+
var line []int
50+
for j := 0; j < y; j++ {
51+
w := i + j
52+
wp := float64(w) / 100
53+
line = append(line, int(wp))
54+
}
55+
heat = append(heat, line)
56+
}
57+
return heat
58+
}

docs/assets/examples/textgrid/v2/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package main
22

33
import (
4-
"github.com/johnfercher/maroto/v2/pkg/consts/fontstyle"
54
"log"
65

6+
"github.com/johnfercher/maroto/v2/pkg/consts/fontstyle"
7+
78
"github.com/johnfercher/maroto/v2/pkg/core"
89

910
"github.com/johnfercher/maroto/v2"

docs/assets/pdf/heatmap.pdf

6.64 MB
Binary file not shown.

docs/assets/pdf/v2.pdf

14 KB
Binary file not shown.

docs/assets/text/heatmap.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
generate -> avg: 128.18ms, executions: [128.18ms]
2+
add_rows -> avg: 5083.50ns, executions: [10.00μs, 0.17μs]
3+
file_size -> 6.96Mb

docs/assets/text/v2.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
generate -> avg: 18.06ms, executions: [18.06ms]
2-
header -> avg: 356.00ns, executions: [356.00ns]
3-
footer -> avg: 71.00ns, executions: [71.00ns]
4-
add_row -> avg: 112.55ns, executions: [0.11μs, 0.15μs, 0.05μs, 0.08μs, 0.02μs, 0.02μs, 0.53μs, 0.06μs, 0.02μs, 0.07μs, 0.02μs, 0.02μs, 0.02μs, 0.06μs, 0.02μs, 0.02μs, 0.03μs, 1.41μs, 0.06μs, 0.02μs, 0.06μs, 0.02μs, 0.02μs, 0.01μs, 0.07μs, 0.02μs, 0.02μs, 0.03μs, 0.29μs, 1.74μs, 0.02μs, 0.07μs, 0.02μs, 0.02μs, 0.02μs, 0.06μs, 0.02μs, 0.02μs, 0.02μs, 0.24μs, 0.05μs, 0.02μs, 0.07μs, 0.02μs, 0.01μs, 0.02μs, 0.06μs, 0.03μs, 0.01μs, 0.01μs, 0.24μs, 0.05μs, 0.01μs, 0.05μs, 0.02μs]
5-
file_size -> 267.93Kb
1+
generate -> avg: 26.67ms, executions: [26.67ms]
2+
header -> avg: 21.00μs, executions: [21.00μs]
3+
footer -> avg: 333.00ns, executions: [333.00ns]
4+
add_rows -> avg: 157.56ns, executions: [0.29μs, 0.50μs, 0.29μs, 0.50μs, 0.08μs, 0.12μs, 1.21μs, 0.17μs, 0.04μs, 0.08μs, 0.04μs, 0.04μs, 0.04μs, 0.21μs, 0.04μs, 0.04μs, 0.04μs, 0.67μs, 0.17μs, 0.04μs, 0.12μs, 0.04μs, 0.04μs, 0.04μs, 0.17μs, 0.04μs, 0.04μs, 0.04μs, 0.50μs, 0.12μs, 0.04μs, 0.12μs, 0.04μs, 0.21μs, 0.04μs, 0.12μs, 0.04μs, 0.04μs, 0.08μs, 0.50μs, 0.17μs, 0.04μs, 0.08μs, 0.04μs, 0.04μs, 0.04μs, 0.33μs, 0.04μs, 0.04μs, 0.04μs, 0.46μs, 0.08μs, 0.04μs, 0.12μs, 0.04μs]
5+
file_size -> 282.23Kb

internal/providers/gofpdf/builder.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Dependencies struct {
2020
Code core.Code
2121
Image core.Image
2222
Line core.Line
23+
HeatMap core.HeatMap
2324
Cache cache.Cache
2425
CellWriter cellwriter.CellWriter
2526
Cfg *entity.Config
@@ -68,6 +69,8 @@ func (b *builder) Build(cfg *entity.Config, cache cache.Cache) *Dependencies {
6869
text := NewText(fpdf, math, font)
6970
image := NewImage(fpdf, math)
7071
line := NewLine(fpdf)
72+
chart := NewChart(fpdf, line, text)
73+
heatMap := NewHeatMap(fpdf, chart)
7174
cellWriter := cellwriter.NewBuilder().
7275
Build(fpdf)
7376

@@ -78,6 +81,7 @@ func (b *builder) Build(cfg *entity.Config, cache cache.Cache) *Dependencies {
7881
Code: code,
7982
Image: image,
8083
Line: line,
84+
HeatMap: heatMap,
8185
CellWriter: cellWriter,
8286
Cfg: cfg,
8387
Cache: cache,

internal/providers/gofpdf/chart.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package gofpdf
2+
3+
import (
4+
"github.com/johnfercher/maroto/v2/internal/providers/gofpdf/gofpdfwrapper"
5+
"github.com/johnfercher/maroto/v2/pkg/consts/linestyle"
6+
"github.com/johnfercher/maroto/v2/pkg/consts/orientation"
7+
"github.com/johnfercher/maroto/v2/pkg/core"
8+
"github.com/johnfercher/maroto/v2/pkg/core/entity"
9+
"github.com/johnfercher/maroto/v2/pkg/props"
10+
)
11+
12+
type chart struct {
13+
pdf gofpdfwrapper.Fpdf
14+
line core.Line
15+
text core.Text
16+
}
17+
18+
func NewChart(pdf gofpdfwrapper.Fpdf, line core.Line, text core.Text) *chart {
19+
return &chart{
20+
pdf: pdf,
21+
line: line,
22+
text: text,
23+
}
24+
}
25+
26+
func (c *chart) Add(cell *entity.Cell, margins *entity.Margins, prop *props.Chart) {
27+
// X
28+
c.line.Add(cell, &props.Line{
29+
Orientation: orientation.Horizontal,
30+
SizePercent: 88,
31+
OffsetPercent: 94,
32+
Style: linestyle.Solid,
33+
Thickness: 0.5,
34+
})
35+
36+
// Y
37+
c.line.Add(cell, &props.Line{
38+
Orientation: orientation.Vertical,
39+
SizePercent: 88,
40+
OffsetPercent: 6,
41+
Style: linestyle.Solid,
42+
Thickness: 0.5,
43+
})
44+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package gofpdf
2+
3+
import (
4+
"errors"
5+
"math"
6+
7+
"github.com/johnfercher/maroto/v2/pkg/props"
8+
9+
"github.com/johnfercher/maroto/v2/internal/providers/gofpdf/gofpdfwrapper"
10+
"github.com/johnfercher/maroto/v2/pkg/core"
11+
"github.com/johnfercher/maroto/v2/pkg/core/entity"
12+
)
13+
14+
var ErrOutOfRange = errors.New("out of range")
15+
16+
type heatMap struct {
17+
pdf gofpdfwrapper.Fpdf
18+
defaultFillColor *props.Color
19+
chart core.Chart
20+
padding float64
21+
}
22+
23+
func NewHeatMap(pdf gofpdfwrapper.Fpdf, chart core.Chart) *heatMap {
24+
return &heatMap{
25+
pdf: pdf,
26+
chart: chart,
27+
defaultFillColor: &props.WhiteColor,
28+
padding: 0,
29+
}
30+
}
31+
32+
func (s heatMap) Add(heatMap [][]int, cell *entity.Cell, margins *entity.Margins, prop *props.HeatMap) {
33+
if heatMap == nil || len(heatMap) == 0 || len(heatMap[0]) == 0 {
34+
return
35+
}
36+
37+
max := s.getMax(heatMap)
38+
transparent := s.getTransparent(prop)
39+
stepX, stepY := s.getSteps(heatMap, cell, prop)
40+
41+
for i := 0; i < len(heatMap)-1; i++ {
42+
for j := 0; j < len(heatMap[i])-1; j++ {
43+
if !transparent[heatMap[i][j]] {
44+
r, g, b := GetHeatColor(heatMap[i][j], max, prop.InvertScale, prop.HalfColor)
45+
46+
x := float64(i)*stepX + cell.X + margins.Left
47+
y := float64(j) * stepY
48+
49+
// Invert to draw from bottom to up
50+
y = cell.Height + margins.Top + cell.Y - y - stepY
51+
52+
s.pdf.SetFillColor(r, g, b)
53+
s.pdf.Rect(x, y, stepX, stepY, "F")
54+
s.pdf.SetFillColor(s.defaultFillColor.Red, s.defaultFillColor.Green, s.defaultFillColor.Blue)
55+
}
56+
}
57+
}
58+
59+
if prop.Chart != nil {
60+
s.chart.Add(cell, margins, prop.Chart)
61+
}
62+
}
63+
64+
func (s heatMap) getSteps(heatMap [][]int, cell *entity.Cell, prop *props.HeatMap) (float64, float64) {
65+
xSize := len(heatMap)
66+
stepX := (cell.Width) / float64(xSize-1)
67+
68+
ySize := len(heatMap[0])
69+
stepY := (cell.Height) / float64(ySize-1)
70+
71+
return stepX, stepY
72+
}
73+
74+
func GetHeatColor(i int, total int, invertScale bool, halfColor bool) (int, int, int) {
75+
offset := 360.0 / 7.0 / 2.0
76+
iStep := GetStepWithOffset(360, float64(total), float64(i), -offset)
77+
78+
if iStep < 0 {
79+
iStep = 360 + iStep
80+
}
81+
82+
r, g, b, _ := HSVToRGB(iStep, 1.0, 1.0)
83+
return int(r), int(g), int(b)
84+
}
85+
86+
func (s heatMap) getMax(matrix [][]int) int {
87+
var max = 0
88+
for _, row := range matrix {
89+
for _, cell := range row {
90+
if cell > max {
91+
max = cell
92+
}
93+
}
94+
}
95+
96+
return max
97+
}
98+
99+
func (s heatMap) getTransparent(p *props.HeatMap) map[int]bool {
100+
m := make(map[int]bool)
101+
for _, t := range p.TransparentValues {
102+
m[t] = true
103+
}
104+
return m
105+
}
106+
107+
// HSVToRGB converts an HSV triple to an RGB triple.
108+
// Source: https://github.com/Crazy3lf/colorconv/blob/master/colorconv.go
109+
func HSVToRGB(h, s, v float64) (r, g, b uint8, err error) {
110+
if h < 0 || h >= 360 ||
111+
s < 0 || s > 1 ||
112+
v < 0 || v > 1 {
113+
return 0, 0, 0, ErrOutOfRange
114+
}
115+
// When 0 ≤ h < 360, 0 ≤ s ≤ 1 and 0 ≤ v ≤ 1:
116+
C := v * s
117+
X := C * (1 - math.Abs(math.Mod(h/60, 2)-1))
118+
m := v - C
119+
var Rnot, Gnot, Bnot float64
120+
switch {
121+
case 0 <= h && h < 60:
122+
Rnot, Gnot, Bnot = C, X, 0
123+
case 60 <= h && h < 120:
124+
Rnot, Gnot, Bnot = X, C, 0
125+
case 120 <= h && h < 180:
126+
Rnot, Gnot, Bnot = 0, C, X
127+
case 180 <= h && h < 240:
128+
Rnot, Gnot, Bnot = 0, X, C
129+
case 240 <= h && h < 300:
130+
Rnot, Gnot, Bnot = X, 0, C
131+
case 300 <= h && h < 360:
132+
Rnot, Gnot, Bnot = C, 0, X
133+
}
134+
r = uint8(math.Round((Rnot + m) * 255))
135+
g = uint8(math.Round((Gnot + m) * 255))
136+
b = uint8(math.Round((Bnot + m) * 255))
137+
return r, g, b, nil
138+
}
139+
140+
func GetStep(scaleMax float64, valueMax float64) float64 {
141+
return scaleMax / valueMax
142+
}
143+
144+
func GetStepWithOffset(scaleMax float64, valueMax float64, i float64, offset float64) float64 {
145+
scaleStep := GetStep(scaleMax, valueMax)
146+
iStep := i * scaleStep
147+
return iStep + offset
148+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package gofpdf
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"testing"
6+
)
7+
8+
func TestGetStep(t *testing.T) {
9+
t.Run("should calc rate correctly", func(t *testing.T) {
10+
// Act & Assert
11+
assert.Equal(t, 10.0, GetStep(360, 36))
12+
})
13+
}
14+
15+
func TestGetStepWithOffset(t *testing.T) {
16+
t.Run("should calc rate correctly", func(t *testing.T) {
17+
// Arrange
18+
scaleMax := 100.0
19+
valueMax := 10.0
20+
value := 10.0
21+
offset := 10.0
22+
23+
// Act
24+
valueWithOffset := GetStepWithOffset(scaleMax, valueMax, value, offset)
25+
26+
// Assert
27+
assert.Equal(t, 2.0, valueWithOffset)
28+
})
29+
}
30+
31+
func TestGetHeatColor(t *testing.T) {
32+
t.Run("should calc color correctly", func(t *testing.T) {
33+
// Arrange
34+
i := 10
35+
total := 100
36+
37+
// Act
38+
r, g, b := GetHeatColor(i, total, false, false)
39+
40+
// Act
41+
assert.Equal(t, 255, r)
42+
assert.Equal(t, 83, g)
43+
assert.Equal(t, 0, b)
44+
})
45+
}

0 commit comments

Comments
 (0)