Add response time badge and chart
This commit is contained in:
parent
bab69478dd
commit
470e3a3ebc
@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -11,19 +12,27 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// badgeHandler handles the automatic generation of badge based on the group name and service name passed.
|
const (
|
||||||
|
badgeColorHexAwesome = "#40cc11"
|
||||||
|
badgeColorHexGreat = "#94cc11"
|
||||||
|
badgeColorHexGood = "#ccd311"
|
||||||
|
badgeColorHexPassable = "#ccb311"
|
||||||
|
badgeColorHexBad = "#cc8111"
|
||||||
|
badgeColorHexVeryBad = "#c7130a"
|
||||||
|
)
|
||||||
|
|
||||||
|
// uptimeBadgeHandler handles the automatic generation of badge based on the group name and service name passed.
|
||||||
//
|
//
|
||||||
// Valid values for {duration}: 7d, 24h, 1h
|
// Valid values for {duration}: 7d, 24h, 1h
|
||||||
// Pattern for {identifier}: <KEY>.svg
|
func uptimeBadgeHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
func badgeHandler(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
variables := mux.Vars(request)
|
variables := mux.Vars(request)
|
||||||
duration := variables["duration"]
|
duration := variables["duration"]
|
||||||
var from time.Time
|
var from time.Time
|
||||||
switch duration {
|
switch duration {
|
||||||
case "7d":
|
case "7d":
|
||||||
from = time.Now().Add(-time.Hour * 24 * 7)
|
from = time.Now().Add(-7 * 24 * time.Hour)
|
||||||
case "24h":
|
case "24h":
|
||||||
from = time.Now().Add(-time.Hour * 24)
|
from = time.Now().Add(-24 * time.Hour)
|
||||||
case "1h":
|
case "1h":
|
||||||
from = time.Now().Add(-time.Hour)
|
from = time.Now().Add(-time.Hour)
|
||||||
default:
|
default:
|
||||||
@ -31,8 +40,13 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) {
|
|||||||
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
|
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
identifier := variables["identifier"]
|
var key string
|
||||||
key := strings.TrimSuffix(identifier, ".svg")
|
if identifier := variables["identifier"]; len(identifier) > 0 {
|
||||||
|
// XXX: Remove this conditional statement in v3.0.0 and rely on variables["key"] instead
|
||||||
|
key = strings.TrimSuffix(identifier, ".svg")
|
||||||
|
} else {
|
||||||
|
key = variables["key"]
|
||||||
|
}
|
||||||
uptime, err := storage.Get().GetUptimeByKey(key, from, time.Now())
|
uptime, err := storage.Get().GetUptimeByKey(key, from, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrServiceNotFound {
|
if err == common.ErrServiceNotFound {
|
||||||
@ -50,10 +64,10 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) {
|
|||||||
writer.Header().Set("Date", formattedDate)
|
writer.Header().Set("Date", formattedDate)
|
||||||
writer.Header().Set("Expires", formattedDate)
|
writer.Header().Set("Expires", formattedDate)
|
||||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||||
_, _ = writer.Write(generateSVG(duration, uptime))
|
_, _ = writer.Write(generateUptimeBadgeSVG(duration, uptime))
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSVG(duration string, uptime float64) []byte {
|
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
||||||
var labelWidth, valueWidth, valueWidthAdjustment int
|
var labelWidth, valueWidth, valueWidthAdjustment int
|
||||||
switch duration {
|
switch duration {
|
||||||
case "7d":
|
case "7d":
|
||||||
@ -106,15 +120,118 @@ func generateSVG(duration string, uptime float64) []byte {
|
|||||||
|
|
||||||
func getBadgeColorFromUptime(uptime float64) string {
|
func getBadgeColorFromUptime(uptime float64) string {
|
||||||
if uptime >= 0.975 {
|
if uptime >= 0.975 {
|
||||||
return "#40cc11"
|
return badgeColorHexAwesome
|
||||||
} else if uptime >= 0.95 {
|
} else if uptime >= 0.95 {
|
||||||
return "#94cc11"
|
return badgeColorHexGreat
|
||||||
} else if uptime >= 0.9 {
|
} else if uptime >= 0.9 {
|
||||||
return "#ccc311"
|
return badgeColorHexGood
|
||||||
} else if uptime >= 0.8 {
|
} else if uptime >= 0.8 {
|
||||||
return "#ccb311"
|
return badgeColorHexPassable
|
||||||
} else if uptime >= 0.65 {
|
} else if uptime >= 0.65 {
|
||||||
return "#cc8111"
|
return badgeColorHexBad
|
||||||
}
|
}
|
||||||
return "#c7130a"
|
return badgeColorHexVeryBad
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseTimeBadgeHandler handles the automatic generation of badge based on the group name and service name passed.
|
||||||
|
//
|
||||||
|
// Valid values for {duration}: 7d, 24h, 1h
|
||||||
|
func responseTimeBadgeHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
variables := mux.Vars(request)
|
||||||
|
duration := variables["duration"]
|
||||||
|
var from time.Time
|
||||||
|
switch duration {
|
||||||
|
case "7d":
|
||||||
|
from = time.Now().Add(-7 * 24 * time.Hour)
|
||||||
|
case "24h":
|
||||||
|
from = time.Now().Add(-24 * time.Hour)
|
||||||
|
case "1h":
|
||||||
|
from = time.Now().Add(-time.Hour)
|
||||||
|
default:
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := variables["key"]
|
||||||
|
averageResponseTime, err := storage.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
if err == common.ErrServiceNotFound {
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
} else if err == common.ErrInvalidTimeRange {
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
} else {
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
_, _ = writer.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formattedDate := time.Now().Format(http.TimeFormat)
|
||||||
|
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
writer.Header().Set("Date", formattedDate)
|
||||||
|
writer.Header().Set("Expires", formattedDate)
|
||||||
|
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
_, _ = writer.Write(generateResponseTimeBadgeSVG(duration, averageResponseTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []byte {
|
||||||
|
var labelWidth, valueWidth int
|
||||||
|
switch duration {
|
||||||
|
case "7d":
|
||||||
|
labelWidth = 105
|
||||||
|
case "24h":
|
||||||
|
labelWidth = 110
|
||||||
|
case "1h":
|
||||||
|
labelWidth = 105
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
color := getBadgeColorFromResponseTime(averageResponseTime)
|
||||||
|
sanitizedValue := strconv.Itoa(averageResponseTime) + "ms"
|
||||||
|
valueWidth = len(sanitizedValue) * 11
|
||||||
|
width := labelWidth + valueWidth
|
||||||
|
labelX := labelWidth / 2
|
||||||
|
valueX := labelWidth + (valueWidth / 2)
|
||||||
|
svg := []byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
|
||||||
|
<linearGradient id="b" x2="0" y2="100%%">
|
||||||
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<mask id="a">
|
||||||
|
<rect width="%d" height="20" rx="3" fill="#fff"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#a)">
|
||||||
|
<path fill="#555" d="M0 0h%dv20H0z"/>
|
||||||
|
<path fill="%s" d="M%d 0h%dv20H%dz"/>
|
||||||
|
<path fill="url(#b)" d="M0 0h%dv20H0z"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||||
|
<text x="%d" y="15" fill="#010101" fill-opacity=".3">
|
||||||
|
response time %s
|
||||||
|
</text>
|
||||||
|
<text x="%d" y="14">
|
||||||
|
response time %s
|
||||||
|
</text>
|
||||||
|
<text x="%d" y="15" fill="#010101" fill-opacity=".3">
|
||||||
|
%s
|
||||||
|
</text>
|
||||||
|
<text x="%d" y="14">
|
||||||
|
%s
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</svg>`, width, width, labelWidth, color, labelWidth, valueWidth, labelWidth, width, labelX, duration, labelX, duration, valueX, sanitizedValue, valueX, sanitizedValue))
|
||||||
|
return svg
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBadgeColorFromResponseTime(responseTime int) string {
|
||||||
|
if responseTime <= 50 {
|
||||||
|
return badgeColorHexAwesome
|
||||||
|
} else if responseTime <= 200 {
|
||||||
|
return badgeColorHexGreat
|
||||||
|
} else if responseTime <= 300 {
|
||||||
|
return badgeColorHexGood
|
||||||
|
} else if responseTime <= 500 {
|
||||||
|
return badgeColorHexPassable
|
||||||
|
} else if responseTime <= 750 {
|
||||||
|
return badgeColorHexBad
|
||||||
|
}
|
||||||
|
return badgeColorHexVeryBad
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,116 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetBadgeColorFromUptime(t *testing.T) {
|
func TestGetBadgeColorFromUptime(t *testing.T) {
|
||||||
if getBadgeColorFromUptime(1) != "#40cc11" {
|
scenarios := []struct {
|
||||||
t.Error("expected #40cc11 from an uptime of 1, got", getBadgeColorFromUptime(1))
|
Uptime float64
|
||||||
|
ExpectedColor string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Uptime: 1,
|
||||||
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.99,
|
||||||
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.97,
|
||||||
|
ExpectedColor: badgeColorHexGreat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.95,
|
||||||
|
ExpectedColor: badgeColorHexGreat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.93,
|
||||||
|
ExpectedColor: badgeColorHexGood,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.9,
|
||||||
|
ExpectedColor: badgeColorHexGood,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.85,
|
||||||
|
ExpectedColor: badgeColorHexPassable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.7,
|
||||||
|
ExpectedColor: badgeColorHexBad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.65,
|
||||||
|
ExpectedColor: badgeColorHexBad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Uptime: 0.6,
|
||||||
|
ExpectedColor: badgeColorHexVeryBad,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if getBadgeColorFromUptime(0.95) != "#94cc11" {
|
for _, scenario := range scenarios {
|
||||||
t.Error("expected #94cc11 from an uptime of 0.95, got", getBadgeColorFromUptime(0.95))
|
t.Run("uptime-"+strconv.Itoa(int(scenario.Uptime*100)), func(t *testing.T) {
|
||||||
}
|
if getBadgeColorFromUptime(scenario.Uptime) != scenario.ExpectedColor {
|
||||||
if getBadgeColorFromUptime(0.9) != "#ccc311" {
|
t.Errorf("expected %s from %f, got %v", scenario.ExpectedColor, scenario.Uptime, getBadgeColorFromUptime(scenario.Uptime))
|
||||||
t.Error("expected #c9cc11 from an uptime of 0.9, got", getBadgeColorFromUptime(0.9))
|
}
|
||||||
}
|
})
|
||||||
if getBadgeColorFromUptime(0.85) != "#ccb311" {
|
}
|
||||||
t.Error("expected #ccb311 from an uptime of 0.85, got", getBadgeColorFromUptime(0.85))
|
}
|
||||||
}
|
|
||||||
if getBadgeColorFromUptime(0.75) != "#cc8111" {
|
func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||||
t.Error("expected #cc8111 from an uptime of 0.75, got", getBadgeColorFromUptime(0.75))
|
scenarios := []struct {
|
||||||
}
|
ResponseTime int
|
||||||
if getBadgeColorFromUptime(0.6) != "#c7130a" {
|
ExpectedColor string
|
||||||
t.Error("expected #c7130a from an uptime of 0.6, got", getBadgeColorFromUptime(0.6))
|
}{
|
||||||
}
|
{
|
||||||
if getBadgeColorFromUptime(0.25) != "#c7130a" {
|
ResponseTime: 10,
|
||||||
t.Error("expected #c7130a from an uptime of 0.25, got", getBadgeColorFromUptime(0.25))
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
}
|
},
|
||||||
if getBadgeColorFromUptime(0) != "#c7130a" {
|
{
|
||||||
t.Error("expected #c7130a from an uptime of 0, got", getBadgeColorFromUptime(0))
|
ResponseTime: 50,
|
||||||
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 75,
|
||||||
|
ExpectedColor: badgeColorHexGreat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 150,
|
||||||
|
ExpectedColor: badgeColorHexGreat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 201,
|
||||||
|
ExpectedColor: badgeColorHexGood,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 300,
|
||||||
|
ExpectedColor: badgeColorHexGood,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 301,
|
||||||
|
ExpectedColor: badgeColorHexPassable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 450,
|
||||||
|
ExpectedColor: badgeColorHexPassable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 700,
|
||||||
|
ExpectedColor: badgeColorHexBad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ResponseTime: 1500,
|
||||||
|
ExpectedColor: badgeColorHexVeryBad,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run("response-time-"+strconv.Itoa(scenario.ResponseTime), func(t *testing.T) {
|
||||||
|
if getBadgeColorFromResponseTime(scenario.ResponseTime) != scenario.ExpectedColor {
|
||||||
|
t.Errorf("expected %s from %d, got %v", scenario.ExpectedColor, scenario.ResponseTime, getBadgeColorFromResponseTime(scenario.ResponseTime))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
110
controller/chart.go
Normal file
110
controller/chart.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/storage"
|
||||||
|
"github.com/TwinProduction/gatus/storage/store/common"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const timeFormat = "3:04PM"
|
||||||
|
|
||||||
|
var (
|
||||||
|
gridStyle = chart.Style{
|
||||||
|
StrokeColor: drawing.Color{R: 119, G: 119, B: 119, A: 40},
|
||||||
|
StrokeWidth: 1.0,
|
||||||
|
}
|
||||||
|
axisStyle = chart.Style{
|
||||||
|
FontColor: drawing.Color{R: 119, G: 119, B: 119, A: 255},
|
||||||
|
}
|
||||||
|
transparentStyle = chart.Style{
|
||||||
|
FillColor: drawing.Color{R: 255, G: 255, B: 255, A: 0},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func responseTimeChartHandler(writer http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
duration := vars["duration"]
|
||||||
|
var from time.Time
|
||||||
|
switch duration {
|
||||||
|
case "7d":
|
||||||
|
from = time.Now().Add(-24 * 7 * time.Hour)
|
||||||
|
case "24h":
|
||||||
|
from = time.Now().Add(-24 * time.Hour)
|
||||||
|
default:
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = writer.Write([]byte("Durations supported: 7d, 24h"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hourlyAverageResponseTime, err := storage.Get().GetHourlyAverageResponseTimeByKey(vars["key"], from, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
if err == common.ErrServiceNotFound {
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
} else if err == common.ErrInvalidTimeRange {
|
||||||
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
|
} else {
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
_, _ = writer.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
series := chart.TimeSeries{
|
||||||
|
Name: "Average response time per hour",
|
||||||
|
Style: chart.Style{
|
||||||
|
StrokeWidth: 1.5,
|
||||||
|
DotWidth: 2.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
keys := make([]int, 0, len(hourlyAverageResponseTime))
|
||||||
|
for hourlyTimestamp := range hourlyAverageResponseTime {
|
||||||
|
keys = append(keys, int(hourlyTimestamp))
|
||||||
|
}
|
||||||
|
sort.Ints(keys)
|
||||||
|
var maxAverageResponseTime float64
|
||||||
|
for _, key := range keys {
|
||||||
|
averageResponseTime := float64(hourlyAverageResponseTime[int64(key)])
|
||||||
|
if maxAverageResponseTime < averageResponseTime {
|
||||||
|
maxAverageResponseTime = averageResponseTime
|
||||||
|
}
|
||||||
|
series.XValues = append(series.XValues, time.Unix(int64(key), 0))
|
||||||
|
series.YValues = append(series.YValues, averageResponseTime)
|
||||||
|
}
|
||||||
|
graph := chart.Chart{
|
||||||
|
Canvas: transparentStyle,
|
||||||
|
Background: transparentStyle,
|
||||||
|
Width: 1280,
|
||||||
|
Height: 300,
|
||||||
|
XAxis: chart.XAxis{
|
||||||
|
ValueFormatter: chart.TimeValueFormatterWithFormat(timeFormat),
|
||||||
|
GridMajorStyle: gridStyle,
|
||||||
|
GridMinorStyle: gridStyle,
|
||||||
|
Style: axisStyle,
|
||||||
|
NameStyle: axisStyle,
|
||||||
|
},
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
Name: "Average response time",
|
||||||
|
GridMajorStyle: gridStyle,
|
||||||
|
GridMinorStyle: gridStyle,
|
||||||
|
Style: axisStyle,
|
||||||
|
NameStyle: axisStyle,
|
||||||
|
Range: &chart.ContinuousRange{
|
||||||
|
Min: 0,
|
||||||
|
Max: math.Ceil(maxAverageResponseTime * 1.25),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Series: []chart.Series{series},
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
if err := graph.Render(chart.SVG, writer); err != nil {
|
||||||
|
writer.Header().Set("Content-Type", "text/plain")
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = writer.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
@ -75,9 +75,18 @@ func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Rou
|
|||||||
}
|
}
|
||||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET")
|
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET")
|
||||||
router.HandleFunc("/api/v1/statuses", secureIfNecessary(securityConfig, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content
|
// Deprecated endpoints
|
||||||
|
router.HandleFunc("/api/v1/statuses", secureIfNecessary(securityConfig, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already
|
||||||
router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||||
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET")
|
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", uptimeBadgeHandler).Methods("GET")
|
||||||
|
// New endpoints
|
||||||
|
router.HandleFunc("/api/v1/services/statuses", secureIfNecessary(securityConfig, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already
|
||||||
|
router.HandleFunc("/api/v1/services/{key}/statuses", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||||
|
// TODO: router.HandleFunc("/api/v1/services/{key}/uptimes", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceUptimesHandler))).Methods("GET")
|
||||||
|
// TODO: router.HandleFunc("/api/v1/services/{key}/events", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceEventsHandler))).Methods("GET")
|
||||||
|
router.HandleFunc("/api/v1/services/{key}/uptimes/{duration}/badge.svg", uptimeBadgeHandler).Methods("GET")
|
||||||
|
router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/badge.svg", responseTimeBadgeHandler).Methods("GET")
|
||||||
|
router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/chart.svg", responseTimeChartHandler).Methods("GET")
|
||||||
// SPA
|
// SPA
|
||||||
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
||||||
// Everything else falls back on static content
|
// Everything else falls back on static content
|
||||||
|
@ -123,60 +123,167 @@ func TestCreateRouter(t *testing.T) {
|
|||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badges-1h",
|
Name: "old-badge-1h",
|
||||||
Path: "/api/v1/badges/uptime/1h/core_frontend.svg",
|
Path: "/api/v1/badges/uptime/1h/core_frontend.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badges-24h",
|
Name: "old-badge-24h",
|
||||||
Path: "/api/v1/badges/uptime/24h/core_backend.svg",
|
Path: "/api/v1/badges/uptime/24h/core_backend.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badges-7d",
|
Name: "old-badge-7d",
|
||||||
Path: "/api/v1/badges/uptime/7d/core_frontend.svg",
|
Path: "/api/v1/badges/uptime/7d/core_frontend.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badges-with-invalid-duration",
|
Name: "old-badge-with-invalid-duration",
|
||||||
Path: "/api/v1/badges/uptime/3d/core_backend.svg",
|
Path: "/api/v1/badges/uptime/3d/core_backend.svg",
|
||||||
ExpectedCode: http.StatusBadRequest,
|
ExpectedCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badges-for-invalid-key",
|
Name: "old-badge-for-invalid-key",
|
||||||
Path: "/api/v1/badges/uptime/7d/invalid_key.svg",
|
Path: "/api/v1/badges/uptime/7d/invalid_key.svg",
|
||||||
ExpectedCode: http.StatusNotFound,
|
ExpectedCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "service-statuses",
|
Name: "badge-uptime-1h",
|
||||||
|
Path: "/api/v1/services/core_frontend/uptimes/1h/badge.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-uptime-24h",
|
||||||
|
Path: "/api/v1/services/core_backend/uptimes/24h/badge.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-uptime-7d",
|
||||||
|
Path: "/api/v1/services/core_frontend/uptimes/7d/badge.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-uptime-with-invalid-duration",
|
||||||
|
Path: "/api/v1/services/core_backend/uptimes/3d/badge.svg",
|
||||||
|
ExpectedCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-uptime-for-invalid-key",
|
||||||
|
Path: "/api/v1/services/invalid_key/uptimes/7d/badge.svg",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-response-time-1h",
|
||||||
|
Path: "/api/v1/services/core_frontend/response-times/1h/badge.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-response-time-24h",
|
||||||
|
Path: "/api/v1/services/core_backend/response-times/24h/badge.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-response-time-7d",
|
||||||
|
Path: "/api/v1/services/core_frontend/response-times/7d/badge.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-response-time-with-invalid-duration",
|
||||||
|
Path: "/api/v1/services/core_backend/response-times/3d/badge.svg",
|
||||||
|
ExpectedCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-response-time-for-invalid-key",
|
||||||
|
Path: "/api/v1/services/invalid_key/response-times/7d/badge.svg",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chart-response-time-24h",
|
||||||
|
Path: "/api/v1/services/core_backend/response-times/24h/chart.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chart-response-time-7d",
|
||||||
|
Path: "/api/v1/services/core_frontend/response-times/7d/chart.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chart-response-time-with-invalid-duration",
|
||||||
|
Path: "/api/v1/services/core_backend/response-times/3d/chart.svg",
|
||||||
|
ExpectedCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chart-response-time-for-invalid-key",
|
||||||
|
Path: "/api/v1/services/invalid_key/response-times/7d/chart.svg",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "old-service-statuses",
|
||||||
Path: "/api/v1/statuses",
|
Path: "/api/v1/statuses",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "service-statuses-gzip",
|
Name: "old-service-statuses-gzip",
|
||||||
Path: "/api/v1/statuses",
|
Path: "/api/v1/statuses",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
Gzip: true,
|
Gzip: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "old-service-statuses-pagination",
|
||||||
|
Path: "/api/v1/statuses?page=1&pageSize=20",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "old-service-status",
|
||||||
|
Path: "/api/v1/statuses/core_frontend",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "old-service-status-gzip",
|
||||||
|
Path: "/api/v1/statuses/core_frontend",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
Gzip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "old-service-status-for-invalid-key",
|
||||||
|
Path: "/api/v1/statuses/invalid_key",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service-statuses",
|
||||||
|
Path: "/api/v1/services/statuses",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "service-statuses-gzip",
|
||||||
|
Path: "/api/v1/services/statuses",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
Gzip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "service-statuses-pagination",
|
Name: "service-statuses-pagination",
|
||||||
Path: "/api/v1/statuses?page=1&pageSize=20",
|
Path: "/api/v1/services/statuses?page=1&pageSize=20",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "service-status",
|
Name: "service-status",
|
||||||
Path: "/api/v1/statuses/core_frontend",
|
Path: "/api/v1/services/core_frontend/statuses",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "service-status-gzip",
|
Name: "service-status-gzip",
|
||||||
Path: "/api/v1/statuses/core_frontend",
|
Path: "/api/v1/services/core_frontend/statuses",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
Gzip: true,
|
Gzip: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "service-status-pagination",
|
||||||
|
Path: "/api/v1/services/core_frontend/statuses?page=1&pageSize=20",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "service-status-for-invalid-key",
|
Name: "service-status-for-invalid-key",
|
||||||
Path: "/api/v1/statuses/invalid_key",
|
Path: "/api/v1/services/invalid_key/statuses",
|
||||||
ExpectedCode: http.StatusNotFound,
|
ExpectedCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
20
vendor/github.com/golang/freetype/AUTHORS
generated
vendored
Normal file
20
vendor/github.com/golang/freetype/AUTHORS
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# This is the official list of Freetype-Go authors for copyright purposes.
|
||||||
|
# This file is distinct from the CONTRIBUTORS files.
|
||||||
|
# See the latter for an explanation.
|
||||||
|
#
|
||||||
|
# Freetype-Go is derived from Freetype, which is written in C. The latter
|
||||||
|
# is copyright 1996-2010 David Turner, Robert Wilhelm, and Werner Lemberg.
|
||||||
|
|
||||||
|
# Names should be added to this file as
|
||||||
|
# Name or Organization <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Google Inc.
|
||||||
|
Jeff R. Allen <jra@nella.org>
|
||||||
|
Maksim Kochkin <maxxarts@gmail.com>
|
||||||
|
Michael Fogleman <fogleman@gmail.com>
|
||||||
|
Rémy Oudompheng <oudomphe@phare.normalesup.org>
|
||||||
|
Roger Peppe <rogpeppe@gmail.com>
|
||||||
|
Steven Edwards <steven@stephenwithav.com>
|
38
vendor/github.com/golang/freetype/CONTRIBUTORS
generated
vendored
Normal file
38
vendor/github.com/golang/freetype/CONTRIBUTORS
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# This is the official list of people who can contribute
|
||||||
|
# (and typically have contributed) code to the Freetype-Go repository.
|
||||||
|
# The AUTHORS file lists the copyright holders; this file
|
||||||
|
# lists people. For example, Google employees are listed here
|
||||||
|
# but not in AUTHORS, because Google holds the copyright.
|
||||||
|
#
|
||||||
|
# The submission process automatically checks to make sure
|
||||||
|
# that people submitting code are listed in this file (by email address).
|
||||||
|
#
|
||||||
|
# Names should be added to this file only after verifying that
|
||||||
|
# the individual or the individual's organization has agreed to
|
||||||
|
# the appropriate Contributor License Agreement, found here:
|
||||||
|
#
|
||||||
|
# http://code.google.com/legal/individual-cla-v1.0.html
|
||||||
|
# http://code.google.com/legal/corporate-cla-v1.0.html
|
||||||
|
#
|
||||||
|
# The agreement for individuals can be filled out on the web.
|
||||||
|
#
|
||||||
|
# When adding J Random Contributor's name to this file,
|
||||||
|
# either J's name or J's organization's name should be
|
||||||
|
# added to the AUTHORS file, depending on whether the
|
||||||
|
# individual or corporate CLA was used.
|
||||||
|
|
||||||
|
# Names should be added to this file like so:
|
||||||
|
# Name <email address>
|
||||||
|
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
Andrew Gerrand <adg@golang.org>
|
||||||
|
Jeff R. Allen <jra@nella.org> <jeff.allen@gmail.com>
|
||||||
|
Maksim Kochkin <maxxarts@gmail.com>
|
||||||
|
Michael Fogleman <fogleman@gmail.com>
|
||||||
|
Nigel Tao <nigeltao@golang.org>
|
||||||
|
Rémy Oudompheng <oudomphe@phare.normalesup.org> <remyoudompheng@gmail.com>
|
||||||
|
Rob Pike <r@golang.org>
|
||||||
|
Roger Peppe <rogpeppe@gmail.com>
|
||||||
|
Russ Cox <rsc@golang.org>
|
||||||
|
Steven Edwards <steven@stephenwithav.com>
|
12
vendor/github.com/golang/freetype/LICENSE
generated
vendored
Normal file
12
vendor/github.com/golang/freetype/LICENSE
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Use of the Freetype-Go software is subject to your choice of exactly one of
|
||||||
|
the following two licenses:
|
||||||
|
* The FreeType License, which is similar to the original BSD license with
|
||||||
|
an advertising clause, or
|
||||||
|
* The GNU General Public License (GPL), version 2 or later.
|
||||||
|
|
||||||
|
The text of these licenses are available in the licenses/ftl.txt and the
|
||||||
|
licenses/gpl.txt files respectively. They are also available at
|
||||||
|
http://freetype.sourceforge.net/license.html
|
||||||
|
|
||||||
|
The Luxi fonts in the testdata directory are licensed separately. See the
|
||||||
|
testdata/COPYING file for details.
|
245
vendor/github.com/golang/freetype/raster/geom.go
generated
vendored
Normal file
245
vendor/github.com/golang/freetype/raster/geom.go
generated
vendored
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package raster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxAbs returns the maximum of abs(a) and abs(b).
|
||||||
|
func maxAbs(a, b fixed.Int26_6) fixed.Int26_6 {
|
||||||
|
if a < 0 {
|
||||||
|
a = -a
|
||||||
|
}
|
||||||
|
if b < 0 {
|
||||||
|
b = -b
|
||||||
|
}
|
||||||
|
if a < b {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// pNeg returns the vector -p, or equivalently p rotated by 180 degrees.
|
||||||
|
func pNeg(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{-p.X, -p.Y}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pDot returns the dot product p·q.
|
||||||
|
func pDot(p fixed.Point26_6, q fixed.Point26_6) fixed.Int52_12 {
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx, qy := int64(q.X), int64(q.Y)
|
||||||
|
return fixed.Int52_12(px*qx + py*qy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pLen returns the length of the vector p.
|
||||||
|
func pLen(p fixed.Point26_6) fixed.Int26_6 {
|
||||||
|
// TODO(nigeltao): use fixed point math.
|
||||||
|
x := float64(p.X)
|
||||||
|
y := float64(p.Y)
|
||||||
|
return fixed.Int26_6(math.Sqrt(x*x + y*y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// pNorm returns the vector p normalized to the given length, or zero if p is
|
||||||
|
// degenerate.
|
||||||
|
func pNorm(p fixed.Point26_6, length fixed.Int26_6) fixed.Point26_6 {
|
||||||
|
d := pLen(p)
|
||||||
|
if d == 0 {
|
||||||
|
return fixed.Point26_6{}
|
||||||
|
}
|
||||||
|
s, t := int64(length), int64(d)
|
||||||
|
x := int64(p.X) * s / t
|
||||||
|
y := int64(p.Y) * s / t
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(x), fixed.Int26_6(y)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot45CW returns the vector p rotated clockwise by 45 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot45CW is {1/√2, 1/√2}.
|
||||||
|
func pRot45CW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (+px - py) * 181 / 256
|
||||||
|
qy := (+px + py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot90CW returns the vector p rotated clockwise by 90 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot90CW is {0, 1}.
|
||||||
|
func pRot90CW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{-p.Y, p.X}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot135CW returns the vector p rotated clockwise by 135 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot135CW is {-1/√2, 1/√2}.
|
||||||
|
func pRot135CW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (-px - py) * 181 / 256
|
||||||
|
qy := (+px - py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot45CCW returns the vector p rotated counter-clockwise by 45 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot45CCW is {1/√2, -1/√2}.
|
||||||
|
func pRot45CCW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (+px + py) * 181 / 256
|
||||||
|
qy := (-px + py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot90CCW returns the vector p rotated counter-clockwise by 90 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot90CCW is {0, -1}.
|
||||||
|
func pRot90CCW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{p.Y, -p.X}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pRot135CCW returns the vector p rotated counter-clockwise by 135 degrees.
|
||||||
|
//
|
||||||
|
// Note that the Y-axis grows downwards, so {1, 0}.Rot135CCW is {-1/√2, -1/√2}.
|
||||||
|
func pRot135CCW(p fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
// 181/256 is approximately 1/√2, or sin(π/4).
|
||||||
|
px, py := int64(p.X), int64(p.Y)
|
||||||
|
qx := (-px + py) * 181 / 256
|
||||||
|
qy := (-px - py) * 181 / 256
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(qx), fixed.Int26_6(qy)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Adder accumulates points on a curve.
|
||||||
|
type Adder interface {
|
||||||
|
// Start starts a new curve at the given point.
|
||||||
|
Start(a fixed.Point26_6)
|
||||||
|
// Add1 adds a linear segment to the current curve.
|
||||||
|
Add1(b fixed.Point26_6)
|
||||||
|
// Add2 adds a quadratic segment to the current curve.
|
||||||
|
Add2(b, c fixed.Point26_6)
|
||||||
|
// Add3 adds a cubic segment to the current curve.
|
||||||
|
Add3(b, c, d fixed.Point26_6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Path is a sequence of curves, and a curve is a start point followed by a
|
||||||
|
// sequence of linear, quadratic or cubic segments.
|
||||||
|
type Path []fixed.Int26_6
|
||||||
|
|
||||||
|
// String returns a human-readable representation of a Path.
|
||||||
|
func (p Path) String() string {
|
||||||
|
s := ""
|
||||||
|
for i := 0; i < len(p); {
|
||||||
|
if i != 0 {
|
||||||
|
s += " "
|
||||||
|
}
|
||||||
|
switch p[i] {
|
||||||
|
case 0:
|
||||||
|
s += "S0" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+3]))
|
||||||
|
i += 4
|
||||||
|
case 1:
|
||||||
|
s += "A1" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+3]))
|
||||||
|
i += 4
|
||||||
|
case 2:
|
||||||
|
s += "A2" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+5]))
|
||||||
|
i += 6
|
||||||
|
case 3:
|
||||||
|
s += "A3" + fmt.Sprint([]fixed.Int26_6(p[i+1:i+7]))
|
||||||
|
i += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cancels any previous calls to p.Start or p.AddXxx.
|
||||||
|
func (p *Path) Clear() {
|
||||||
|
*p = (*p)[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts a new curve at the given point.
|
||||||
|
func (p *Path) Start(a fixed.Point26_6) {
|
||||||
|
*p = append(*p, 0, a.X, a.Y, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add1 adds a linear segment to the current curve.
|
||||||
|
func (p *Path) Add1(b fixed.Point26_6) {
|
||||||
|
*p = append(*p, 1, b.X, b.Y, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add2 adds a quadratic segment to the current curve.
|
||||||
|
func (p *Path) Add2(b, c fixed.Point26_6) {
|
||||||
|
*p = append(*p, 2, b.X, b.Y, c.X, c.Y, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add3 adds a cubic segment to the current curve.
|
||||||
|
func (p *Path) Add3(b, c, d fixed.Point26_6) {
|
||||||
|
*p = append(*p, 3, b.X, b.Y, c.X, c.Y, d.X, d.Y, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPath adds the Path q to p.
|
||||||
|
func (p *Path) AddPath(q Path) {
|
||||||
|
*p = append(*p, q...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStroke adds a stroked Path.
|
||||||
|
func (p *Path) AddStroke(q Path, width fixed.Int26_6, cr Capper, jr Joiner) {
|
||||||
|
Stroke(p, q, width, cr, jr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstPoint returns the first point in a non-empty Path.
|
||||||
|
func (p Path) firstPoint() fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{p[1], p[2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastPoint returns the last point in a non-empty Path.
|
||||||
|
func (p Path) lastPoint() fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{p[len(p)-3], p[len(p)-2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addPathReversed adds q reversed to p.
|
||||||
|
// For example, if q consists of a linear segment from A to B followed by a
|
||||||
|
// quadratic segment from B to C to D, then the values of q looks like:
|
||||||
|
// index: 01234567890123
|
||||||
|
// value: 0AA01BB12CCDD2
|
||||||
|
// So, when adding q backwards to p, we want to Add2(C, B) followed by Add1(A).
|
||||||
|
func addPathReversed(p Adder, q Path) {
|
||||||
|
if len(q) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i := len(q) - 1
|
||||||
|
for {
|
||||||
|
switch q[i] {
|
||||||
|
case 0:
|
||||||
|
return
|
||||||
|
case 1:
|
||||||
|
i -= 4
|
||||||
|
p.Add1(
|
||||||
|
fixed.Point26_6{q[i-2], q[i-1]},
|
||||||
|
)
|
||||||
|
case 2:
|
||||||
|
i -= 6
|
||||||
|
p.Add2(
|
||||||
|
fixed.Point26_6{q[i+2], q[i+3]},
|
||||||
|
fixed.Point26_6{q[i-2], q[i-1]},
|
||||||
|
)
|
||||||
|
case 3:
|
||||||
|
i -= 8
|
||||||
|
p.Add3(
|
||||||
|
fixed.Point26_6{q[i+4], q[i+5]},
|
||||||
|
fixed.Point26_6{q[i+2], q[i+3]},
|
||||||
|
fixed.Point26_6{q[i-2], q[i-1]},
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
287
vendor/github.com/golang/freetype/raster/paint.go
generated
vendored
Normal file
287
vendor/github.com/golang/freetype/raster/paint.go
generated
vendored
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package raster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Span is a horizontal segment of pixels with constant alpha. X0 is an
|
||||||
|
// inclusive bound and X1 is exclusive, the same as for slices. A fully opaque
|
||||||
|
// Span has Alpha == 0xffff.
|
||||||
|
type Span struct {
|
||||||
|
Y, X0, X1 int
|
||||||
|
Alpha uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Painter knows how to paint a batch of Spans. Rasterization may involve
|
||||||
|
// Painting multiple batches, and done will be true for the final batch. The
|
||||||
|
// Spans' Y values are monotonically increasing during a rasterization. Paint
|
||||||
|
// may use all of ss as scratch space during the call.
|
||||||
|
type Painter interface {
|
||||||
|
Paint(ss []Span, done bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PainterFunc type adapts an ordinary function to the Painter interface.
|
||||||
|
type PainterFunc func(ss []Span, done bool)
|
||||||
|
|
||||||
|
// Paint just delegates the call to f.
|
||||||
|
func (f PainterFunc) Paint(ss []Span, done bool) { f(ss, done) }
|
||||||
|
|
||||||
|
// An AlphaOverPainter is a Painter that paints Spans onto a *image.Alpha using
|
||||||
|
// the Over Porter-Duff composition operator.
|
||||||
|
type AlphaOverPainter struct {
|
||||||
|
Image *image.Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint satisfies the Painter interface.
|
||||||
|
func (r AlphaOverPainter) Paint(ss []Span, done bool) {
|
||||||
|
b := r.Image.Bounds()
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride - r.Image.Rect.Min.X
|
||||||
|
p := r.Image.Pix[base+s.X0 : base+s.X1]
|
||||||
|
a := int(s.Alpha >> 8)
|
||||||
|
for i, c := range p {
|
||||||
|
v := int(c)
|
||||||
|
p[i] = uint8((v*255 + (255-v)*a) / 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAlphaOverPainter creates a new AlphaOverPainter for the given image.
|
||||||
|
func NewAlphaOverPainter(m *image.Alpha) AlphaOverPainter {
|
||||||
|
return AlphaOverPainter{m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An AlphaSrcPainter is a Painter that paints Spans onto a *image.Alpha using
|
||||||
|
// the Src Porter-Duff composition operator.
|
||||||
|
type AlphaSrcPainter struct {
|
||||||
|
Image *image.Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint satisfies the Painter interface.
|
||||||
|
func (r AlphaSrcPainter) Paint(ss []Span, done bool) {
|
||||||
|
b := r.Image.Bounds()
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride - r.Image.Rect.Min.X
|
||||||
|
p := r.Image.Pix[base+s.X0 : base+s.X1]
|
||||||
|
color := uint8(s.Alpha >> 8)
|
||||||
|
for i := range p {
|
||||||
|
p[i] = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAlphaSrcPainter creates a new AlphaSrcPainter for the given image.
|
||||||
|
func NewAlphaSrcPainter(m *image.Alpha) AlphaSrcPainter {
|
||||||
|
return AlphaSrcPainter{m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An RGBAPainter is a Painter that paints Spans onto a *image.RGBA.
|
||||||
|
type RGBAPainter struct {
|
||||||
|
// Image is the image to compose onto.
|
||||||
|
Image *image.RGBA
|
||||||
|
// Op is the Porter-Duff composition operator.
|
||||||
|
Op draw.Op
|
||||||
|
// cr, cg, cb and ca are the 16-bit color to paint the spans.
|
||||||
|
cr, cg, cb, ca uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint satisfies the Painter interface.
|
||||||
|
func (r *RGBAPainter) Paint(ss []Span, done bool) {
|
||||||
|
b := r.Image.Bounds()
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// This code mimics drawGlyphOver in $GOROOT/src/image/draw/draw.go.
|
||||||
|
ma := s.Alpha
|
||||||
|
const m = 1<<16 - 1
|
||||||
|
i0 := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride + (s.X0-r.Image.Rect.Min.X)*4
|
||||||
|
i1 := i0 + (s.X1-s.X0)*4
|
||||||
|
if r.Op == draw.Over {
|
||||||
|
for i := i0; i < i1; i += 4 {
|
||||||
|
dr := uint32(r.Image.Pix[i+0])
|
||||||
|
dg := uint32(r.Image.Pix[i+1])
|
||||||
|
db := uint32(r.Image.Pix[i+2])
|
||||||
|
da := uint32(r.Image.Pix[i+3])
|
||||||
|
a := (m - (r.ca * ma / m)) * 0x101
|
||||||
|
r.Image.Pix[i+0] = uint8((dr*a + r.cr*ma) / m >> 8)
|
||||||
|
r.Image.Pix[i+1] = uint8((dg*a + r.cg*ma) / m >> 8)
|
||||||
|
r.Image.Pix[i+2] = uint8((db*a + r.cb*ma) / m >> 8)
|
||||||
|
r.Image.Pix[i+3] = uint8((da*a + r.ca*ma) / m >> 8)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := i0; i < i1; i += 4 {
|
||||||
|
r.Image.Pix[i+0] = uint8(r.cr * ma / m >> 8)
|
||||||
|
r.Image.Pix[i+1] = uint8(r.cg * ma / m >> 8)
|
||||||
|
r.Image.Pix[i+2] = uint8(r.cb * ma / m >> 8)
|
||||||
|
r.Image.Pix[i+3] = uint8(r.ca * ma / m >> 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColor sets the color to paint the spans.
|
||||||
|
func (r *RGBAPainter) SetColor(c color.Color) {
|
||||||
|
r.cr, r.cg, r.cb, r.ca = c.RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRGBAPainter creates a new RGBAPainter for the given image.
|
||||||
|
func NewRGBAPainter(m *image.RGBA) *RGBAPainter {
|
||||||
|
return &RGBAPainter{Image: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A MonochromePainter wraps another Painter, quantizing each Span's alpha to
|
||||||
|
// be either fully opaque or fully transparent.
|
||||||
|
type MonochromePainter struct {
|
||||||
|
Painter Painter
|
||||||
|
y, x0, x1 int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint delegates to the wrapped Painter after quantizing each Span's alpha
|
||||||
|
// value and merging adjacent fully opaque Spans.
|
||||||
|
func (m *MonochromePainter) Paint(ss []Span, done bool) {
|
||||||
|
// We compact the ss slice, discarding any Spans whose alpha quantizes to zero.
|
||||||
|
j := 0
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Alpha >= 0x8000 {
|
||||||
|
if m.y == s.Y && m.x1 == s.X0 {
|
||||||
|
m.x1 = s.X1
|
||||||
|
} else {
|
||||||
|
ss[j] = Span{m.y, m.x0, m.x1, 1<<16 - 1}
|
||||||
|
j++
|
||||||
|
m.y, m.x0, m.x1 = s.Y, s.X0, s.X1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
// Flush the accumulated Span.
|
||||||
|
finalSpan := Span{m.y, m.x0, m.x1, 1<<16 - 1}
|
||||||
|
if j < len(ss) {
|
||||||
|
ss[j] = finalSpan
|
||||||
|
j++
|
||||||
|
m.Painter.Paint(ss[:j], true)
|
||||||
|
} else if j == len(ss) {
|
||||||
|
m.Painter.Paint(ss, false)
|
||||||
|
if cap(ss) > 0 {
|
||||||
|
ss = ss[:1]
|
||||||
|
} else {
|
||||||
|
ss = make([]Span, 1)
|
||||||
|
}
|
||||||
|
ss[0] = finalSpan
|
||||||
|
m.Painter.Paint(ss, true)
|
||||||
|
} else {
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
// Reset the accumulator, so that this Painter can be re-used.
|
||||||
|
m.y, m.x0, m.x1 = 0, 0, 0
|
||||||
|
} else {
|
||||||
|
m.Painter.Paint(ss[:j], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMonochromePainter creates a new MonochromePainter that wraps the given
|
||||||
|
// Painter.
|
||||||
|
func NewMonochromePainter(p Painter) *MonochromePainter {
|
||||||
|
return &MonochromePainter{Painter: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A GammaCorrectionPainter wraps another Painter, performing gamma-correction
|
||||||
|
// on each Span's alpha value.
|
||||||
|
type GammaCorrectionPainter struct {
|
||||||
|
// Painter is the wrapped Painter.
|
||||||
|
Painter Painter
|
||||||
|
// a is the precomputed alpha values for linear interpolation, with fully
|
||||||
|
// opaque == 0xffff.
|
||||||
|
a [256]uint16
|
||||||
|
// gammaIsOne is whether gamma correction is a no-op.
|
||||||
|
gammaIsOne bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint delegates to the wrapped Painter after performing gamma-correction on
|
||||||
|
// each Span.
|
||||||
|
func (g *GammaCorrectionPainter) Paint(ss []Span, done bool) {
|
||||||
|
if !g.gammaIsOne {
|
||||||
|
const n = 0x101
|
||||||
|
for i, s := range ss {
|
||||||
|
if s.Alpha == 0 || s.Alpha == 0xffff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, q := s.Alpha/n, s.Alpha%n
|
||||||
|
// The resultant alpha is a linear interpolation of g.a[p] and g.a[p+1].
|
||||||
|
a := uint32(g.a[p])*(n-q) + uint32(g.a[p+1])*q
|
||||||
|
ss[i].Alpha = (a + n/2) / n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.Painter.Paint(ss, done)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGamma sets the gamma value.
|
||||||
|
func (g *GammaCorrectionPainter) SetGamma(gamma float64) {
|
||||||
|
g.gammaIsOne = gamma == 1
|
||||||
|
if g.gammaIsOne {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
a := float64(i) / 0xff
|
||||||
|
a = math.Pow(a, gamma)
|
||||||
|
g.a[i] = uint16(0xffff * a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGammaCorrectionPainter creates a new GammaCorrectionPainter that wraps
|
||||||
|
// the given Painter.
|
||||||
|
func NewGammaCorrectionPainter(p Painter, gamma float64) *GammaCorrectionPainter {
|
||||||
|
g := &GammaCorrectionPainter{Painter: p}
|
||||||
|
g.SetGamma(gamma)
|
||||||
|
return g
|
||||||
|
}
|
601
vendor/github.com/golang/freetype/raster/raster.go
generated
vendored
Normal file
601
vendor/github.com/golang/freetype/raster/raster.go
generated
vendored
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package raster provides an anti-aliasing 2-D rasterizer.
|
||||||
|
//
|
||||||
|
// It is part of the larger Freetype suite of font-related packages, but the
|
||||||
|
// raster package is not specific to font rasterization, and can be used
|
||||||
|
// standalone without any other Freetype package.
|
||||||
|
//
|
||||||
|
// Rasterization is done by the same area/coverage accumulation algorithm as
|
||||||
|
// the Freetype "smooth" module, and the Anti-Grain Geometry library. A
|
||||||
|
// description of the area/coverage algorithm is at
|
||||||
|
// http://projects.tuxee.net/cl-vectors/section-the-cl-aa-algorithm
|
||||||
|
package raster // import "github.com/golang/freetype/raster"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A cell is part of a linked list (for a given yi co-ordinate) of accumulated
|
||||||
|
// area/coverage for the pixel at (xi, yi).
|
||||||
|
type cell struct {
|
||||||
|
xi int
|
||||||
|
area, cover int
|
||||||
|
next int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rasterizer struct {
|
||||||
|
// If false, the default behavior is to use the even-odd winding fill
|
||||||
|
// rule during Rasterize.
|
||||||
|
UseNonZeroWinding bool
|
||||||
|
// An offset (in pixels) to the painted spans.
|
||||||
|
Dx, Dy int
|
||||||
|
|
||||||
|
// The width of the Rasterizer. The height is implicit in len(cellIndex).
|
||||||
|
width int
|
||||||
|
// splitScaleN is the scaling factor used to determine how many times
|
||||||
|
// to decompose a quadratic or cubic segment into a linear approximation.
|
||||||
|
splitScale2, splitScale3 int
|
||||||
|
|
||||||
|
// The current pen position.
|
||||||
|
a fixed.Point26_6
|
||||||
|
// The current cell and its area/coverage being accumulated.
|
||||||
|
xi, yi int
|
||||||
|
area, cover int
|
||||||
|
|
||||||
|
// Saved cells.
|
||||||
|
cell []cell
|
||||||
|
// Linked list of cells, one per row.
|
||||||
|
cellIndex []int
|
||||||
|
// Buffers.
|
||||||
|
cellBuf [256]cell
|
||||||
|
cellIndexBuf [64]int
|
||||||
|
spanBuf [64]Span
|
||||||
|
}
|
||||||
|
|
||||||
|
// findCell returns the index in r.cell for the cell corresponding to
|
||||||
|
// (r.xi, r.yi). The cell is created if necessary.
|
||||||
|
func (r *Rasterizer) findCell() int {
|
||||||
|
if r.yi < 0 || r.yi >= len(r.cellIndex) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
xi := r.xi
|
||||||
|
if xi < 0 {
|
||||||
|
xi = -1
|
||||||
|
} else if xi > r.width {
|
||||||
|
xi = r.width
|
||||||
|
}
|
||||||
|
i, prev := r.cellIndex[r.yi], -1
|
||||||
|
for i != -1 && r.cell[i].xi <= xi {
|
||||||
|
if r.cell[i].xi == xi {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
i, prev = r.cell[i].next, i
|
||||||
|
}
|
||||||
|
c := len(r.cell)
|
||||||
|
if c == cap(r.cell) {
|
||||||
|
buf := make([]cell, c, 4*c)
|
||||||
|
copy(buf, r.cell)
|
||||||
|
r.cell = buf[0 : c+1]
|
||||||
|
} else {
|
||||||
|
r.cell = r.cell[0 : c+1]
|
||||||
|
}
|
||||||
|
r.cell[c] = cell{xi, 0, 0, i}
|
||||||
|
if prev == -1 {
|
||||||
|
r.cellIndex[r.yi] = c
|
||||||
|
} else {
|
||||||
|
r.cell[prev].next = c
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveCell saves any accumulated r.area/r.cover for (r.xi, r.yi).
|
||||||
|
func (r *Rasterizer) saveCell() {
|
||||||
|
if r.area != 0 || r.cover != 0 {
|
||||||
|
i := r.findCell()
|
||||||
|
if i != -1 {
|
||||||
|
r.cell[i].area += r.area
|
||||||
|
r.cell[i].cover += r.cover
|
||||||
|
}
|
||||||
|
r.area = 0
|
||||||
|
r.cover = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCell sets the (xi, yi) cell that r is accumulating area/coverage for.
|
||||||
|
func (r *Rasterizer) setCell(xi, yi int) {
|
||||||
|
if r.xi != xi || r.yi != yi {
|
||||||
|
r.saveCell()
|
||||||
|
r.xi, r.yi = xi, yi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan accumulates area/coverage for the yi'th scanline, going from
|
||||||
|
// x0 to x1 in the horizontal direction (in 26.6 fixed point co-ordinates)
|
||||||
|
// and from y0f to y1f fractional vertical units within that scanline.
|
||||||
|
func (r *Rasterizer) scan(yi int, x0, y0f, x1, y1f fixed.Int26_6) {
|
||||||
|
// Break the 26.6 fixed point X co-ordinates into integral and fractional parts.
|
||||||
|
x0i := int(x0) / 64
|
||||||
|
x0f := x0 - fixed.Int26_6(64*x0i)
|
||||||
|
x1i := int(x1) / 64
|
||||||
|
x1f := x1 - fixed.Int26_6(64*x1i)
|
||||||
|
|
||||||
|
// A perfectly horizontal scan.
|
||||||
|
if y0f == y1f {
|
||||||
|
r.setCell(x1i, yi)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dx, dy := x1-x0, y1f-y0f
|
||||||
|
// A single cell scan.
|
||||||
|
if x0i == x1i {
|
||||||
|
r.area += int((x0f + x1f) * dy)
|
||||||
|
r.cover += int(dy)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// There are at least two cells. Apart from the first and last cells,
|
||||||
|
// all intermediate cells go through the full width of the cell,
|
||||||
|
// or 64 units in 26.6 fixed point format.
|
||||||
|
var (
|
||||||
|
p, q, edge0, edge1 fixed.Int26_6
|
||||||
|
xiDelta int
|
||||||
|
)
|
||||||
|
if dx > 0 {
|
||||||
|
p, q = (64-x0f)*dy, dx
|
||||||
|
edge0, edge1, xiDelta = 0, 64, 1
|
||||||
|
} else {
|
||||||
|
p, q = x0f*dy, -dx
|
||||||
|
edge0, edge1, xiDelta = 64, 0, -1
|
||||||
|
}
|
||||||
|
yDelta, yRem := p/q, p%q
|
||||||
|
if yRem < 0 {
|
||||||
|
yDelta -= 1
|
||||||
|
yRem += q
|
||||||
|
}
|
||||||
|
// Do the first cell.
|
||||||
|
xi, y := x0i, y0f
|
||||||
|
r.area += int((x0f + edge1) * yDelta)
|
||||||
|
r.cover += int(yDelta)
|
||||||
|
xi, y = xi+xiDelta, y+yDelta
|
||||||
|
r.setCell(xi, yi)
|
||||||
|
if xi != x1i {
|
||||||
|
// Do all the intermediate cells.
|
||||||
|
p = 64 * (y1f - y + yDelta)
|
||||||
|
fullDelta, fullRem := p/q, p%q
|
||||||
|
if fullRem < 0 {
|
||||||
|
fullDelta -= 1
|
||||||
|
fullRem += q
|
||||||
|
}
|
||||||
|
yRem -= q
|
||||||
|
for xi != x1i {
|
||||||
|
yDelta = fullDelta
|
||||||
|
yRem += fullRem
|
||||||
|
if yRem >= 0 {
|
||||||
|
yDelta += 1
|
||||||
|
yRem -= q
|
||||||
|
}
|
||||||
|
r.area += int(64 * yDelta)
|
||||||
|
r.cover += int(yDelta)
|
||||||
|
xi, y = xi+xiDelta, y+yDelta
|
||||||
|
r.setCell(xi, yi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do the last cell.
|
||||||
|
yDelta = y1f - y
|
||||||
|
r.area += int((edge0 + x1f) * yDelta)
|
||||||
|
r.cover += int(yDelta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts a new curve at the given point.
|
||||||
|
func (r *Rasterizer) Start(a fixed.Point26_6) {
|
||||||
|
r.setCell(int(a.X/64), int(a.Y/64))
|
||||||
|
r.a = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add1 adds a linear segment to the current curve.
|
||||||
|
func (r *Rasterizer) Add1(b fixed.Point26_6) {
|
||||||
|
x0, y0 := r.a.X, r.a.Y
|
||||||
|
x1, y1 := b.X, b.Y
|
||||||
|
dx, dy := x1-x0, y1-y0
|
||||||
|
// Break the 26.6 fixed point Y co-ordinates into integral and fractional
|
||||||
|
// parts.
|
||||||
|
y0i := int(y0) / 64
|
||||||
|
y0f := y0 - fixed.Int26_6(64*y0i)
|
||||||
|
y1i := int(y1) / 64
|
||||||
|
y1f := y1 - fixed.Int26_6(64*y1i)
|
||||||
|
|
||||||
|
if y0i == y1i {
|
||||||
|
// There is only one scanline.
|
||||||
|
r.scan(y0i, x0, y0f, x1, y1f)
|
||||||
|
|
||||||
|
} else if dx == 0 {
|
||||||
|
// This is a vertical line segment. We avoid calling r.scan and instead
|
||||||
|
// manipulate r.area and r.cover directly.
|
||||||
|
var (
|
||||||
|
edge0, edge1 fixed.Int26_6
|
||||||
|
yiDelta int
|
||||||
|
)
|
||||||
|
if dy > 0 {
|
||||||
|
edge0, edge1, yiDelta = 0, 64, 1
|
||||||
|
} else {
|
||||||
|
edge0, edge1, yiDelta = 64, 0, -1
|
||||||
|
}
|
||||||
|
x0i, yi := int(x0)/64, y0i
|
||||||
|
x0fTimes2 := (int(x0) - (64 * x0i)) * 2
|
||||||
|
// Do the first pixel.
|
||||||
|
dcover := int(edge1 - y0f)
|
||||||
|
darea := int(x0fTimes2 * dcover)
|
||||||
|
r.area += darea
|
||||||
|
r.cover += dcover
|
||||||
|
yi += yiDelta
|
||||||
|
r.setCell(x0i, yi)
|
||||||
|
// Do all the intermediate pixels.
|
||||||
|
dcover = int(edge1 - edge0)
|
||||||
|
darea = int(x0fTimes2 * dcover)
|
||||||
|
for yi != y1i {
|
||||||
|
r.area += darea
|
||||||
|
r.cover += dcover
|
||||||
|
yi += yiDelta
|
||||||
|
r.setCell(x0i, yi)
|
||||||
|
}
|
||||||
|
// Do the last pixel.
|
||||||
|
dcover = int(y1f - edge0)
|
||||||
|
darea = int(x0fTimes2 * dcover)
|
||||||
|
r.area += darea
|
||||||
|
r.cover += dcover
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// There are at least two scanlines. Apart from the first and last
|
||||||
|
// scanlines, all intermediate scanlines go through the full height of
|
||||||
|
// the row, or 64 units in 26.6 fixed point format.
|
||||||
|
var (
|
||||||
|
p, q, edge0, edge1 fixed.Int26_6
|
||||||
|
yiDelta int
|
||||||
|
)
|
||||||
|
if dy > 0 {
|
||||||
|
p, q = (64-y0f)*dx, dy
|
||||||
|
edge0, edge1, yiDelta = 0, 64, 1
|
||||||
|
} else {
|
||||||
|
p, q = y0f*dx, -dy
|
||||||
|
edge0, edge1, yiDelta = 64, 0, -1
|
||||||
|
}
|
||||||
|
xDelta, xRem := p/q, p%q
|
||||||
|
if xRem < 0 {
|
||||||
|
xDelta -= 1
|
||||||
|
xRem += q
|
||||||
|
}
|
||||||
|
// Do the first scanline.
|
||||||
|
x, yi := x0, y0i
|
||||||
|
r.scan(yi, x, y0f, x+xDelta, edge1)
|
||||||
|
x, yi = x+xDelta, yi+yiDelta
|
||||||
|
r.setCell(int(x)/64, yi)
|
||||||
|
if yi != y1i {
|
||||||
|
// Do all the intermediate scanlines.
|
||||||
|
p = 64 * dx
|
||||||
|
fullDelta, fullRem := p/q, p%q
|
||||||
|
if fullRem < 0 {
|
||||||
|
fullDelta -= 1
|
||||||
|
fullRem += q
|
||||||
|
}
|
||||||
|
xRem -= q
|
||||||
|
for yi != y1i {
|
||||||
|
xDelta = fullDelta
|
||||||
|
xRem += fullRem
|
||||||
|
if xRem >= 0 {
|
||||||
|
xDelta += 1
|
||||||
|
xRem -= q
|
||||||
|
}
|
||||||
|
r.scan(yi, x, edge0, x+xDelta, edge1)
|
||||||
|
x, yi = x+xDelta, yi+yiDelta
|
||||||
|
r.setCell(int(x)/64, yi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do the last scanline.
|
||||||
|
r.scan(yi, x, edge0, x1, y1f)
|
||||||
|
}
|
||||||
|
// The next lineTo starts from b.
|
||||||
|
r.a = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add2 adds a quadratic segment to the current curve.
|
||||||
|
func (r *Rasterizer) Add2(b, c fixed.Point26_6) {
|
||||||
|
// Calculate nSplit (the number of recursive decompositions) based on how
|
||||||
|
// 'curvy' it is. Specifically, how much the middle point b deviates from
|
||||||
|
// (a+c)/2.
|
||||||
|
dev := maxAbs(r.a.X-2*b.X+c.X, r.a.Y-2*b.Y+c.Y) / fixed.Int26_6(r.splitScale2)
|
||||||
|
nsplit := 0
|
||||||
|
for dev > 0 {
|
||||||
|
dev /= 4
|
||||||
|
nsplit++
|
||||||
|
}
|
||||||
|
// dev is 32-bit, and nsplit++ every time we shift off 2 bits, so maxNsplit
|
||||||
|
// is 16.
|
||||||
|
const maxNsplit = 16
|
||||||
|
if nsplit > maxNsplit {
|
||||||
|
panic("freetype/raster: Add2 nsplit too large: " + strconv.Itoa(nsplit))
|
||||||
|
}
|
||||||
|
// Recursively decompose the curve nSplit levels deep.
|
||||||
|
var (
|
||||||
|
pStack [2*maxNsplit + 3]fixed.Point26_6
|
||||||
|
sStack [maxNsplit + 1]int
|
||||||
|
i int
|
||||||
|
)
|
||||||
|
sStack[0] = nsplit
|
||||||
|
pStack[0] = c
|
||||||
|
pStack[1] = b
|
||||||
|
pStack[2] = r.a
|
||||||
|
for i >= 0 {
|
||||||
|
s := sStack[i]
|
||||||
|
p := pStack[2*i:]
|
||||||
|
if s > 0 {
|
||||||
|
// Split the quadratic curve p[:3] into an equivalent set of two
|
||||||
|
// shorter curves: p[:3] and p[2:5]. The new p[4] is the old p[2],
|
||||||
|
// and p[0] is unchanged.
|
||||||
|
mx := p[1].X
|
||||||
|
p[4].X = p[2].X
|
||||||
|
p[3].X = (p[4].X + mx) / 2
|
||||||
|
p[1].X = (p[0].X + mx) / 2
|
||||||
|
p[2].X = (p[1].X + p[3].X) / 2
|
||||||
|
my := p[1].Y
|
||||||
|
p[4].Y = p[2].Y
|
||||||
|
p[3].Y = (p[4].Y + my) / 2
|
||||||
|
p[1].Y = (p[0].Y + my) / 2
|
||||||
|
p[2].Y = (p[1].Y + p[3].Y) / 2
|
||||||
|
// The two shorter curves have one less split to do.
|
||||||
|
sStack[i] = s - 1
|
||||||
|
sStack[i+1] = s - 1
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
// Replace the level-0 quadratic with a two-linear-piece
|
||||||
|
// approximation.
|
||||||
|
midx := (p[0].X + 2*p[1].X + p[2].X) / 4
|
||||||
|
midy := (p[0].Y + 2*p[1].Y + p[2].Y) / 4
|
||||||
|
r.Add1(fixed.Point26_6{midx, midy})
|
||||||
|
r.Add1(p[0])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add3 adds a cubic segment to the current curve.
|
||||||
|
func (r *Rasterizer) Add3(b, c, d fixed.Point26_6) {
|
||||||
|
// Calculate nSplit (the number of recursive decompositions) based on how
|
||||||
|
// 'curvy' it is.
|
||||||
|
dev2 := maxAbs(r.a.X-3*(b.X+c.X)+d.X, r.a.Y-3*(b.Y+c.Y)+d.Y) / fixed.Int26_6(r.splitScale2)
|
||||||
|
dev3 := maxAbs(r.a.X-2*b.X+d.X, r.a.Y-2*b.Y+d.Y) / fixed.Int26_6(r.splitScale3)
|
||||||
|
nsplit := 0
|
||||||
|
for dev2 > 0 || dev3 > 0 {
|
||||||
|
dev2 /= 8
|
||||||
|
dev3 /= 4
|
||||||
|
nsplit++
|
||||||
|
}
|
||||||
|
// devN is 32-bit, and nsplit++ every time we shift off 2 bits, so
|
||||||
|
// maxNsplit is 16.
|
||||||
|
const maxNsplit = 16
|
||||||
|
if nsplit > maxNsplit {
|
||||||
|
panic("freetype/raster: Add3 nsplit too large: " + strconv.Itoa(nsplit))
|
||||||
|
}
|
||||||
|
// Recursively decompose the curve nSplit levels deep.
|
||||||
|
var (
|
||||||
|
pStack [3*maxNsplit + 4]fixed.Point26_6
|
||||||
|
sStack [maxNsplit + 1]int
|
||||||
|
i int
|
||||||
|
)
|
||||||
|
sStack[0] = nsplit
|
||||||
|
pStack[0] = d
|
||||||
|
pStack[1] = c
|
||||||
|
pStack[2] = b
|
||||||
|
pStack[3] = r.a
|
||||||
|
for i >= 0 {
|
||||||
|
s := sStack[i]
|
||||||
|
p := pStack[3*i:]
|
||||||
|
if s > 0 {
|
||||||
|
// Split the cubic curve p[:4] into an equivalent set of two
|
||||||
|
// shorter curves: p[:4] and p[3:7]. The new p[6] is the old p[3],
|
||||||
|
// and p[0] is unchanged.
|
||||||
|
m01x := (p[0].X + p[1].X) / 2
|
||||||
|
m12x := (p[1].X + p[2].X) / 2
|
||||||
|
m23x := (p[2].X + p[3].X) / 2
|
||||||
|
p[6].X = p[3].X
|
||||||
|
p[5].X = m23x
|
||||||
|
p[1].X = m01x
|
||||||
|
p[2].X = (m01x + m12x) / 2
|
||||||
|
p[4].X = (m12x + m23x) / 2
|
||||||
|
p[3].X = (p[2].X + p[4].X) / 2
|
||||||
|
m01y := (p[0].Y + p[1].Y) / 2
|
||||||
|
m12y := (p[1].Y + p[2].Y) / 2
|
||||||
|
m23y := (p[2].Y + p[3].Y) / 2
|
||||||
|
p[6].Y = p[3].Y
|
||||||
|
p[5].Y = m23y
|
||||||
|
p[1].Y = m01y
|
||||||
|
p[2].Y = (m01y + m12y) / 2
|
||||||
|
p[4].Y = (m12y + m23y) / 2
|
||||||
|
p[3].Y = (p[2].Y + p[4].Y) / 2
|
||||||
|
// The two shorter curves have one less split to do.
|
||||||
|
sStack[i] = s - 1
|
||||||
|
sStack[i+1] = s - 1
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
// Replace the level-0 cubic with a two-linear-piece approximation.
|
||||||
|
midx := (p[0].X + 3*(p[1].X+p[2].X) + p[3].X) / 8
|
||||||
|
midy := (p[0].Y + 3*(p[1].Y+p[2].Y) + p[3].Y) / 8
|
||||||
|
r.Add1(fixed.Point26_6{midx, midy})
|
||||||
|
r.Add1(p[0])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPath adds the given Path.
|
||||||
|
func (r *Rasterizer) AddPath(p Path) {
|
||||||
|
for i := 0; i < len(p); {
|
||||||
|
switch p[i] {
|
||||||
|
case 0:
|
||||||
|
r.Start(
|
||||||
|
fixed.Point26_6{p[i+1], p[i+2]},
|
||||||
|
)
|
||||||
|
i += 4
|
||||||
|
case 1:
|
||||||
|
r.Add1(
|
||||||
|
fixed.Point26_6{p[i+1], p[i+2]},
|
||||||
|
)
|
||||||
|
i += 4
|
||||||
|
case 2:
|
||||||
|
r.Add2(
|
||||||
|
fixed.Point26_6{p[i+1], p[i+2]},
|
||||||
|
fixed.Point26_6{p[i+3], p[i+4]},
|
||||||
|
)
|
||||||
|
i += 6
|
||||||
|
case 3:
|
||||||
|
r.Add3(
|
||||||
|
fixed.Point26_6{p[i+1], p[i+2]},
|
||||||
|
fixed.Point26_6{p[i+3], p[i+4]},
|
||||||
|
fixed.Point26_6{p[i+5], p[i+6]},
|
||||||
|
)
|
||||||
|
i += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStroke adds a stroked Path.
|
||||||
|
func (r *Rasterizer) AddStroke(q Path, width fixed.Int26_6, cr Capper, jr Joiner) {
|
||||||
|
Stroke(r, q, width, cr, jr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// areaToAlpha converts an area value to a uint32 alpha value. A completely
|
||||||
|
// filled pixel corresponds to an area of 64*64*2, and an alpha of 0xffff. The
|
||||||
|
// conversion of area values greater than this depends on the winding rule:
|
||||||
|
// even-odd or non-zero.
|
||||||
|
func (r *Rasterizer) areaToAlpha(area int) uint32 {
|
||||||
|
// The C Freetype implementation (version 2.3.12) does "alpha := area>>1"
|
||||||
|
// without the +1. Round-to-nearest gives a more symmetric result than
|
||||||
|
// round-down. The C implementation also returns 8-bit alpha, not 16-bit
|
||||||
|
// alpha.
|
||||||
|
a := (area + 1) >> 1
|
||||||
|
if a < 0 {
|
||||||
|
a = -a
|
||||||
|
}
|
||||||
|
alpha := uint32(a)
|
||||||
|
if r.UseNonZeroWinding {
|
||||||
|
if alpha > 0x0fff {
|
||||||
|
alpha = 0x0fff
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alpha &= 0x1fff
|
||||||
|
if alpha > 0x1000 {
|
||||||
|
alpha = 0x2000 - alpha
|
||||||
|
} else if alpha == 0x1000 {
|
||||||
|
alpha = 0x0fff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// alpha is now in the range [0x0000, 0x0fff]. Convert that 12-bit alpha to
|
||||||
|
// 16-bit alpha.
|
||||||
|
return alpha<<4 | alpha>>8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rasterize converts r's accumulated curves into Spans for p. The Spans passed
|
||||||
|
// to p are non-overlapping, and sorted by Y and then X. They all have non-zero
|
||||||
|
// width (and 0 <= X0 < X1 <= r.width) and non-zero A, except for the final
|
||||||
|
// Span, which has Y, X0, X1 and A all equal to zero.
|
||||||
|
func (r *Rasterizer) Rasterize(p Painter) {
|
||||||
|
r.saveCell()
|
||||||
|
s := 0
|
||||||
|
for yi := 0; yi < len(r.cellIndex); yi++ {
|
||||||
|
xi, cover := 0, 0
|
||||||
|
for c := r.cellIndex[yi]; c != -1; c = r.cell[c].next {
|
||||||
|
if cover != 0 && r.cell[c].xi > xi {
|
||||||
|
alpha := r.areaToAlpha(cover * 64 * 2)
|
||||||
|
if alpha != 0 {
|
||||||
|
xi0, xi1 := xi, r.cell[c].xi
|
||||||
|
if xi0 < 0 {
|
||||||
|
xi0 = 0
|
||||||
|
}
|
||||||
|
if xi1 >= r.width {
|
||||||
|
xi1 = r.width
|
||||||
|
}
|
||||||
|
if xi0 < xi1 {
|
||||||
|
r.spanBuf[s] = Span{yi + r.Dy, xi0 + r.Dx, xi1 + r.Dx, alpha}
|
||||||
|
s++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cover += r.cell[c].cover
|
||||||
|
alpha := r.areaToAlpha(cover*64*2 - r.cell[c].area)
|
||||||
|
xi = r.cell[c].xi + 1
|
||||||
|
if alpha != 0 {
|
||||||
|
xi0, xi1 := r.cell[c].xi, xi
|
||||||
|
if xi0 < 0 {
|
||||||
|
xi0 = 0
|
||||||
|
}
|
||||||
|
if xi1 >= r.width {
|
||||||
|
xi1 = r.width
|
||||||
|
}
|
||||||
|
if xi0 < xi1 {
|
||||||
|
r.spanBuf[s] = Span{yi + r.Dy, xi0 + r.Dx, xi1 + r.Dx, alpha}
|
||||||
|
s++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s > len(r.spanBuf)-2 {
|
||||||
|
p.Paint(r.spanBuf[:s], false)
|
||||||
|
s = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Paint(r.spanBuf[:s], true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cancels any previous calls to r.Start or r.AddXxx.
|
||||||
|
func (r *Rasterizer) Clear() {
|
||||||
|
r.a = fixed.Point26_6{}
|
||||||
|
r.xi = 0
|
||||||
|
r.yi = 0
|
||||||
|
r.area = 0
|
||||||
|
r.cover = 0
|
||||||
|
r.cell = r.cell[:0]
|
||||||
|
for i := 0; i < len(r.cellIndex); i++ {
|
||||||
|
r.cellIndex[i] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBounds sets the maximum width and height of the rasterized image and
|
||||||
|
// calls Clear. The width and height are in pixels, not fixed.Int26_6 units.
|
||||||
|
func (r *Rasterizer) SetBounds(width, height int) {
|
||||||
|
if width < 0 {
|
||||||
|
width = 0
|
||||||
|
}
|
||||||
|
if height < 0 {
|
||||||
|
height = 0
|
||||||
|
}
|
||||||
|
// Use the same ssN heuristic as the C Freetype (version 2.4.0)
|
||||||
|
// implementation.
|
||||||
|
ss2, ss3 := 32, 16
|
||||||
|
if width > 24 || height > 24 {
|
||||||
|
ss2, ss3 = 2*ss2, 2*ss3
|
||||||
|
if width > 120 || height > 120 {
|
||||||
|
ss2, ss3 = 2*ss2, 2*ss3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.width = width
|
||||||
|
r.splitScale2 = ss2
|
||||||
|
r.splitScale3 = ss3
|
||||||
|
r.cell = r.cellBuf[:0]
|
||||||
|
if height > len(r.cellIndexBuf) {
|
||||||
|
r.cellIndex = make([]int, height)
|
||||||
|
} else {
|
||||||
|
r.cellIndex = r.cellIndexBuf[:height]
|
||||||
|
}
|
||||||
|
r.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRasterizer creates a new Rasterizer with the given bounds.
|
||||||
|
func NewRasterizer(width, height int) *Rasterizer {
|
||||||
|
r := new(Rasterizer)
|
||||||
|
r.SetBounds(width, height)
|
||||||
|
return r
|
||||||
|
}
|
483
vendor/github.com/golang/freetype/raster/stroke.go
generated
vendored
Normal file
483
vendor/github.com/golang/freetype/raster/stroke.go
generated
vendored
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package raster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Two points are considered practically equal if the square of the distance
|
||||||
|
// between them is less than one quarter (i.e. 1024 / 4096).
|
||||||
|
const epsilon = fixed.Int52_12(1024)
|
||||||
|
|
||||||
|
// A Capper signifies how to begin or end a stroked path.
|
||||||
|
type Capper interface {
|
||||||
|
// Cap adds a cap to p given a pivot point and the normal vector of a
|
||||||
|
// terminal segment. The normal's length is half of the stroke width.
|
||||||
|
Cap(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CapperFunc type adapts an ordinary function to be a Capper.
|
||||||
|
type CapperFunc func(Adder, fixed.Int26_6, fixed.Point26_6, fixed.Point26_6)
|
||||||
|
|
||||||
|
func (f CapperFunc) Cap(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
f(p, halfWidth, pivot, n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Joiner signifies how to join interior nodes of a stroked path.
|
||||||
|
type Joiner interface {
|
||||||
|
// Join adds a join to the two sides of a stroked path given a pivot
|
||||||
|
// point and the normal vectors of the trailing and leading segments.
|
||||||
|
// Both normals have length equal to half of the stroke width.
|
||||||
|
Join(lhs, rhs Adder, halfWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The JoinerFunc type adapts an ordinary function to be a Joiner.
|
||||||
|
type JoinerFunc func(lhs, rhs Adder, halfWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6)
|
||||||
|
|
||||||
|
func (f JoinerFunc) Join(lhs, rhs Adder, halfWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
f(lhs, rhs, halfWidth, pivot, n0, n1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundCapper adds round caps to a stroked path.
|
||||||
|
var RoundCapper Capper = CapperFunc(roundCapper)
|
||||||
|
|
||||||
|
func roundCapper(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
// The cubic Bézier approximation to a circle involves the magic number
|
||||||
|
// (√2 - 1) * 4/3, which is approximately 35/64.
|
||||||
|
const k = 35
|
||||||
|
e := pRot90CCW(n1)
|
||||||
|
side := pivot.Add(e)
|
||||||
|
start, end := pivot.Sub(n1), pivot.Add(n1)
|
||||||
|
d, e := n1.Mul(k), e.Mul(k)
|
||||||
|
p.Add3(start.Add(e), side.Sub(d), side)
|
||||||
|
p.Add3(side.Add(d), end.Add(e), end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ButtCapper adds butt caps to a stroked path.
|
||||||
|
var ButtCapper Capper = CapperFunc(buttCapper)
|
||||||
|
|
||||||
|
func buttCapper(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
p.Add1(pivot.Add(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SquareCapper adds square caps to a stroked path.
|
||||||
|
var SquareCapper Capper = CapperFunc(squareCapper)
|
||||||
|
|
||||||
|
func squareCapper(p Adder, halfWidth fixed.Int26_6, pivot, n1 fixed.Point26_6) {
|
||||||
|
e := pRot90CCW(n1)
|
||||||
|
side := pivot.Add(e)
|
||||||
|
p.Add1(side.Sub(n1))
|
||||||
|
p.Add1(side.Add(n1))
|
||||||
|
p.Add1(pivot.Add(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundJoiner adds round joins to a stroked path.
|
||||||
|
var RoundJoiner Joiner = JoinerFunc(roundJoiner)
|
||||||
|
|
||||||
|
func roundJoiner(lhs, rhs Adder, haflWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
dot := pDot(pRot90CW(n0), n1)
|
||||||
|
if dot >= 0 {
|
||||||
|
addArc(lhs, pivot, n0, n1)
|
||||||
|
rhs.Add1(pivot.Sub(n1))
|
||||||
|
} else {
|
||||||
|
lhs.Add1(pivot.Add(n1))
|
||||||
|
addArc(rhs, pivot, pNeg(n0), pNeg(n1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BevelJoiner adds bevel joins to a stroked path.
|
||||||
|
var BevelJoiner Joiner = JoinerFunc(bevelJoiner)
|
||||||
|
|
||||||
|
func bevelJoiner(lhs, rhs Adder, haflWidth fixed.Int26_6, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
lhs.Add1(pivot.Add(n1))
|
||||||
|
rhs.Add1(pivot.Sub(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addArc adds a circular arc from pivot+n0 to pivot+n1 to p. The shorter of
|
||||||
|
// the two possible arcs is taken, i.e. the one spanning <= 180 degrees. The
|
||||||
|
// two vectors n0 and n1 must be of equal length.
|
||||||
|
func addArc(p Adder, pivot, n0, n1 fixed.Point26_6) {
|
||||||
|
// r2 is the square of the length of n0.
|
||||||
|
r2 := pDot(n0, n0)
|
||||||
|
if r2 < epsilon {
|
||||||
|
// The arc radius is so small that we collapse to a straight line.
|
||||||
|
p.Add1(pivot.Add(n1))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We approximate the arc by 0, 1, 2 or 3 45-degree quadratic segments plus
|
||||||
|
// a final quadratic segment from s to n1. Each 45-degree segment has
|
||||||
|
// control points {1, 0}, {1, tan(π/8)} and {1/√2, 1/√2} suitably scaled,
|
||||||
|
// rotated and translated. tan(π/8) is approximately 27/64.
|
||||||
|
const tpo8 = 27
|
||||||
|
var s fixed.Point26_6
|
||||||
|
// We determine which octant the angle between n0 and n1 is in via three
|
||||||
|
// dot products. m0, m1 and m2 are n0 rotated clockwise by 45, 90 and 135
|
||||||
|
// degrees.
|
||||||
|
m0 := pRot45CW(n0)
|
||||||
|
m1 := pRot90CW(n0)
|
||||||
|
m2 := pRot90CW(m0)
|
||||||
|
if pDot(m1, n1) >= 0 {
|
||||||
|
if pDot(n0, n1) >= 0 {
|
||||||
|
if pDot(m2, n1) <= 0 {
|
||||||
|
// n1 is between 0 and 45 degrees clockwise of n0.
|
||||||
|
s = n0
|
||||||
|
} else {
|
||||||
|
// n1 is between 45 and 90 degrees clockwise of n0.
|
||||||
|
p.Add2(pivot.Add(n0).Add(m1.Mul(tpo8)), pivot.Add(m0))
|
||||||
|
s = m0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pm1, n0t := pivot.Add(m1), n0.Mul(tpo8)
|
||||||
|
p.Add2(pivot.Add(n0).Add(m1.Mul(tpo8)), pivot.Add(m0))
|
||||||
|
p.Add2(pm1.Add(n0t), pm1)
|
||||||
|
if pDot(m0, n1) >= 0 {
|
||||||
|
// n1 is between 90 and 135 degrees clockwise of n0.
|
||||||
|
s = m1
|
||||||
|
} else {
|
||||||
|
// n1 is between 135 and 180 degrees clockwise of n0.
|
||||||
|
p.Add2(pm1.Sub(n0t), pivot.Add(m2))
|
||||||
|
s = m2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pDot(n0, n1) >= 0 {
|
||||||
|
if pDot(m0, n1) >= 0 {
|
||||||
|
// n1 is between 0 and 45 degrees counter-clockwise of n0.
|
||||||
|
s = n0
|
||||||
|
} else {
|
||||||
|
// n1 is between 45 and 90 degrees counter-clockwise of n0.
|
||||||
|
p.Add2(pivot.Add(n0).Sub(m1.Mul(tpo8)), pivot.Sub(m2))
|
||||||
|
s = pNeg(m2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pm1, n0t := pivot.Sub(m1), n0.Mul(tpo8)
|
||||||
|
p.Add2(pivot.Add(n0).Sub(m1.Mul(tpo8)), pivot.Sub(m2))
|
||||||
|
p.Add2(pm1.Add(n0t), pm1)
|
||||||
|
if pDot(m2, n1) <= 0 {
|
||||||
|
// n1 is between 90 and 135 degrees counter-clockwise of n0.
|
||||||
|
s = pNeg(m1)
|
||||||
|
} else {
|
||||||
|
// n1 is between 135 and 180 degrees counter-clockwise of n0.
|
||||||
|
p.Add2(pm1.Sub(n0t), pivot.Sub(m0))
|
||||||
|
s = pNeg(m0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The final quadratic segment has two endpoints s and n1 and the middle
|
||||||
|
// control point is a multiple of s.Add(n1), i.e. it is on the angle
|
||||||
|
// bisector of those two points. The multiple ranges between 128/256 and
|
||||||
|
// 150/256 as the angle between s and n1 ranges between 0 and 45 degrees.
|
||||||
|
//
|
||||||
|
// When the angle is 0 degrees (i.e. s and n1 are coincident) then
|
||||||
|
// s.Add(n1) is twice s and so the middle control point of the degenerate
|
||||||
|
// quadratic segment should be half s.Add(n1), and half = 128/256.
|
||||||
|
//
|
||||||
|
// When the angle is 45 degrees then 150/256 is the ratio of the lengths of
|
||||||
|
// the two vectors {1, tan(π/8)} and {1 + 1/√2, 1/√2}.
|
||||||
|
//
|
||||||
|
// d is the normalized dot product between s and n1. Since the angle ranges
|
||||||
|
// between 0 and 45 degrees then d ranges between 256/256 and 181/256.
|
||||||
|
d := 256 * pDot(s, n1) / r2
|
||||||
|
multiple := fixed.Int26_6(150-(150-128)*(d-181)/(256-181)) >> 2
|
||||||
|
p.Add2(pivot.Add(s.Add(n1).Mul(multiple)), pivot.Add(n1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// midpoint returns the midpoint of two Points.
|
||||||
|
func midpoint(a, b fixed.Point26_6) fixed.Point26_6 {
|
||||||
|
return fixed.Point26_6{(a.X + b.X) / 2, (a.Y + b.Y) / 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
// angleGreaterThan45 returns whether the angle between two vectors is more
|
||||||
|
// than 45 degrees.
|
||||||
|
func angleGreaterThan45(v0, v1 fixed.Point26_6) bool {
|
||||||
|
v := pRot45CCW(v0)
|
||||||
|
return pDot(v, v1) < 0 || pDot(pRot90CW(v), v1) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// interpolate returns the point (1-t)*a + t*b.
|
||||||
|
func interpolate(a, b fixed.Point26_6, t fixed.Int52_12) fixed.Point26_6 {
|
||||||
|
s := 1<<12 - t
|
||||||
|
x := s*fixed.Int52_12(a.X) + t*fixed.Int52_12(b.X)
|
||||||
|
y := s*fixed.Int52_12(a.Y) + t*fixed.Int52_12(b.Y)
|
||||||
|
return fixed.Point26_6{fixed.Int26_6(x >> 12), fixed.Int26_6(y >> 12)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// curviest2 returns the value of t for which the quadratic parametric curve
|
||||||
|
// (1-t)²*a + 2*t*(1-t).b + t²*c has maximum curvature.
|
||||||
|
//
|
||||||
|
// The curvature of the parametric curve f(t) = (x(t), y(t)) is
|
||||||
|
// |x′y″-y′x″| / (x′²+y′²)^(3/2).
|
||||||
|
//
|
||||||
|
// Let d = b-a and e = c-2*b+a, so that f′(t) = 2*d+2*e*t and f″(t) = 2*e.
|
||||||
|
// The curvature's numerator is (2*dx+2*ex*t)*(2*ey)-(2*dy+2*ey*t)*(2*ex),
|
||||||
|
// which simplifies to 4*dx*ey-4*dy*ex, which is constant with respect to t.
|
||||||
|
//
|
||||||
|
// Thus, curvature is extreme where the denominator is extreme, i.e. where
|
||||||
|
// (x′²+y′²) is extreme. The first order condition is that
|
||||||
|
// 2*x′*x″+2*y′*y″ = 0, or (dx+ex*t)*ex + (dy+ey*t)*ey = 0.
|
||||||
|
// Solving for t gives t = -(dx*ex+dy*ey) / (ex*ex+ey*ey).
|
||||||
|
func curviest2(a, b, c fixed.Point26_6) fixed.Int52_12 {
|
||||||
|
dx := int64(b.X - a.X)
|
||||||
|
dy := int64(b.Y - a.Y)
|
||||||
|
ex := int64(c.X - 2*b.X + a.X)
|
||||||
|
ey := int64(c.Y - 2*b.Y + a.Y)
|
||||||
|
if ex == 0 && ey == 0 {
|
||||||
|
return 2048
|
||||||
|
}
|
||||||
|
return fixed.Int52_12(-4096 * (dx*ex + dy*ey) / (ex*ex + ey*ey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stroker holds state for stroking a path.
|
||||||
|
type stroker struct {
|
||||||
|
// p is the destination that records the stroked path.
|
||||||
|
p Adder
|
||||||
|
// u is the half-width of the stroke.
|
||||||
|
u fixed.Int26_6
|
||||||
|
// cr and jr specify how to end and connect path segments.
|
||||||
|
cr Capper
|
||||||
|
jr Joiner
|
||||||
|
// r is the reverse path. Stroking a path involves constructing two
|
||||||
|
// parallel paths 2*u apart. The first path is added immediately to p,
|
||||||
|
// the second path is accumulated in r and eventually added in reverse.
|
||||||
|
r Path
|
||||||
|
// a is the most recent segment point. anorm is the segment normal of
|
||||||
|
// length u at that point.
|
||||||
|
a, anorm fixed.Point26_6
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNonCurvy2 adds a quadratic segment to the stroker, where the segment
|
||||||
|
// defined by (k.a, b, c) achieves maximum curvature at either k.a or c.
|
||||||
|
func (k *stroker) addNonCurvy2(b, c fixed.Point26_6) {
|
||||||
|
// We repeatedly divide the segment at its middle until it is straight
|
||||||
|
// enough to approximate the stroke by just translating the control points.
|
||||||
|
// ds and ps are stacks of depths and points. t is the top of the stack.
|
||||||
|
const maxDepth = 5
|
||||||
|
var (
|
||||||
|
ds [maxDepth + 1]int
|
||||||
|
ps [2*maxDepth + 3]fixed.Point26_6
|
||||||
|
t int
|
||||||
|
)
|
||||||
|
// Initially the ps stack has one quadratic segment of depth zero.
|
||||||
|
ds[0] = 0
|
||||||
|
ps[2] = k.a
|
||||||
|
ps[1] = b
|
||||||
|
ps[0] = c
|
||||||
|
anorm := k.anorm
|
||||||
|
var cnorm fixed.Point26_6
|
||||||
|
|
||||||
|
for {
|
||||||
|
depth := ds[t]
|
||||||
|
a := ps[2*t+2]
|
||||||
|
b := ps[2*t+1]
|
||||||
|
c := ps[2*t+0]
|
||||||
|
ab := b.Sub(a)
|
||||||
|
bc := c.Sub(b)
|
||||||
|
abIsSmall := pDot(ab, ab) < fixed.Int52_12(1<<12)
|
||||||
|
bcIsSmall := pDot(bc, bc) < fixed.Int52_12(1<<12)
|
||||||
|
if abIsSmall && bcIsSmall {
|
||||||
|
// Approximate the segment by a circular arc.
|
||||||
|
cnorm = pRot90CCW(pNorm(bc, k.u))
|
||||||
|
mac := midpoint(a, c)
|
||||||
|
addArc(k.p, mac, anorm, cnorm)
|
||||||
|
addArc(&k.r, mac, pNeg(anorm), pNeg(cnorm))
|
||||||
|
} else if depth < maxDepth && angleGreaterThan45(ab, bc) {
|
||||||
|
// Divide the segment in two and push both halves on the stack.
|
||||||
|
mab := midpoint(a, b)
|
||||||
|
mbc := midpoint(b, c)
|
||||||
|
t++
|
||||||
|
ds[t+0] = depth + 1
|
||||||
|
ds[t-1] = depth + 1
|
||||||
|
ps[2*t+2] = a
|
||||||
|
ps[2*t+1] = mab
|
||||||
|
ps[2*t+0] = midpoint(mab, mbc)
|
||||||
|
ps[2*t-1] = mbc
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// Translate the control points.
|
||||||
|
bnorm := pRot90CCW(pNorm(c.Sub(a), k.u))
|
||||||
|
cnorm = pRot90CCW(pNorm(bc, k.u))
|
||||||
|
k.p.Add2(b.Add(bnorm), c.Add(cnorm))
|
||||||
|
k.r.Add2(b.Sub(bnorm), c.Sub(cnorm))
|
||||||
|
}
|
||||||
|
if t == 0 {
|
||||||
|
k.a, k.anorm = c, cnorm
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t--
|
||||||
|
anorm = cnorm
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add1 adds a linear segment to the stroker.
|
||||||
|
func (k *stroker) Add1(b fixed.Point26_6) {
|
||||||
|
bnorm := pRot90CCW(pNorm(b.Sub(k.a), k.u))
|
||||||
|
if len(k.r) == 0 {
|
||||||
|
k.p.Start(k.a.Add(bnorm))
|
||||||
|
k.r.Start(k.a.Sub(bnorm))
|
||||||
|
} else {
|
||||||
|
k.jr.Join(k.p, &k.r, k.u, k.a, k.anorm, bnorm)
|
||||||
|
}
|
||||||
|
k.p.Add1(b.Add(bnorm))
|
||||||
|
k.r.Add1(b.Sub(bnorm))
|
||||||
|
k.a, k.anorm = b, bnorm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add2 adds a quadratic segment to the stroker.
|
||||||
|
func (k *stroker) Add2(b, c fixed.Point26_6) {
|
||||||
|
ab := b.Sub(k.a)
|
||||||
|
bc := c.Sub(b)
|
||||||
|
abnorm := pRot90CCW(pNorm(ab, k.u))
|
||||||
|
if len(k.r) == 0 {
|
||||||
|
k.p.Start(k.a.Add(abnorm))
|
||||||
|
k.r.Start(k.a.Sub(abnorm))
|
||||||
|
} else {
|
||||||
|
k.jr.Join(k.p, &k.r, k.u, k.a, k.anorm, abnorm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approximate nearly-degenerate quadratics by linear segments.
|
||||||
|
abIsSmall := pDot(ab, ab) < epsilon
|
||||||
|
bcIsSmall := pDot(bc, bc) < epsilon
|
||||||
|
if abIsSmall || bcIsSmall {
|
||||||
|
acnorm := pRot90CCW(pNorm(c.Sub(k.a), k.u))
|
||||||
|
k.p.Add1(c.Add(acnorm))
|
||||||
|
k.r.Add1(c.Sub(acnorm))
|
||||||
|
k.a, k.anorm = c, acnorm
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The quadratic segment (k.a, b, c) has a point of maximum curvature.
|
||||||
|
// If this occurs at an end point, we process the segment as a whole.
|
||||||
|
t := curviest2(k.a, b, c)
|
||||||
|
if t <= 0 || 4096 <= t {
|
||||||
|
k.addNonCurvy2(b, c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we perform a de Casteljau decomposition at the point of
|
||||||
|
// maximum curvature and process the two straighter parts.
|
||||||
|
mab := interpolate(k.a, b, t)
|
||||||
|
mbc := interpolate(b, c, t)
|
||||||
|
mabc := interpolate(mab, mbc, t)
|
||||||
|
|
||||||
|
// If the vectors ab and bc are close to being in opposite directions,
|
||||||
|
// then the decomposition can become unstable, so we approximate the
|
||||||
|
// quadratic segment by two linear segments joined by an arc.
|
||||||
|
bcnorm := pRot90CCW(pNorm(bc, k.u))
|
||||||
|
if pDot(abnorm, bcnorm) < -fixed.Int52_12(k.u)*fixed.Int52_12(k.u)*2047/2048 {
|
||||||
|
pArc := pDot(abnorm, bc) < 0
|
||||||
|
|
||||||
|
k.p.Add1(mabc.Add(abnorm))
|
||||||
|
if pArc {
|
||||||
|
z := pRot90CW(abnorm)
|
||||||
|
addArc(k.p, mabc, abnorm, z)
|
||||||
|
addArc(k.p, mabc, z, bcnorm)
|
||||||
|
}
|
||||||
|
k.p.Add1(mabc.Add(bcnorm))
|
||||||
|
k.p.Add1(c.Add(bcnorm))
|
||||||
|
|
||||||
|
k.r.Add1(mabc.Sub(abnorm))
|
||||||
|
if !pArc {
|
||||||
|
z := pRot90CW(abnorm)
|
||||||
|
addArc(&k.r, mabc, pNeg(abnorm), z)
|
||||||
|
addArc(&k.r, mabc, z, pNeg(bcnorm))
|
||||||
|
}
|
||||||
|
k.r.Add1(mabc.Sub(bcnorm))
|
||||||
|
k.r.Add1(c.Sub(bcnorm))
|
||||||
|
|
||||||
|
k.a, k.anorm = c, bcnorm
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the decomposed parts.
|
||||||
|
k.addNonCurvy2(mab, mabc)
|
||||||
|
k.addNonCurvy2(mbc, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add3 adds a cubic segment to the stroker.
|
||||||
|
func (k *stroker) Add3(b, c, d fixed.Point26_6) {
|
||||||
|
panic("freetype/raster: stroke unimplemented for cubic segments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stroke adds the stroked Path q to p, where q consists of exactly one curve.
|
||||||
|
func (k *stroker) stroke(q Path) {
|
||||||
|
// Stroking is implemented by deriving two paths each k.u apart from q.
|
||||||
|
// The left-hand-side path is added immediately to k.p; the right-hand-side
|
||||||
|
// path is accumulated in k.r. Once we've finished adding the LHS to k.p,
|
||||||
|
// we add the RHS in reverse order.
|
||||||
|
k.r = make(Path, 0, len(q))
|
||||||
|
k.a = fixed.Point26_6{q[1], q[2]}
|
||||||
|
for i := 4; i < len(q); {
|
||||||
|
switch q[i] {
|
||||||
|
case 1:
|
||||||
|
k.Add1(
|
||||||
|
fixed.Point26_6{q[i+1], q[i+2]},
|
||||||
|
)
|
||||||
|
i += 4
|
||||||
|
case 2:
|
||||||
|
k.Add2(
|
||||||
|
fixed.Point26_6{q[i+1], q[i+2]},
|
||||||
|
fixed.Point26_6{q[i+3], q[i+4]},
|
||||||
|
)
|
||||||
|
i += 6
|
||||||
|
case 3:
|
||||||
|
k.Add3(
|
||||||
|
fixed.Point26_6{q[i+1], q[i+2]},
|
||||||
|
fixed.Point26_6{q[i+3], q[i+4]},
|
||||||
|
fixed.Point26_6{q[i+5], q[i+6]},
|
||||||
|
)
|
||||||
|
i += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(k.r) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO(nigeltao): if q is a closed curve then we should join the first and
|
||||||
|
// last segments instead of capping them.
|
||||||
|
k.cr.Cap(k.p, k.u, q.lastPoint(), pNeg(k.anorm))
|
||||||
|
addPathReversed(k.p, k.r)
|
||||||
|
pivot := q.firstPoint()
|
||||||
|
k.cr.Cap(k.p, k.u, pivot, pivot.Sub(fixed.Point26_6{k.r[1], k.r[2]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke adds q stroked with the given width to p. The result is typically
|
||||||
|
// self-intersecting and should be rasterized with UseNonZeroWinding.
|
||||||
|
// cr and jr may be nil, which defaults to a RoundCapper or RoundJoiner.
|
||||||
|
func Stroke(p Adder, q Path, width fixed.Int26_6, cr Capper, jr Joiner) {
|
||||||
|
if len(q) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cr == nil {
|
||||||
|
cr = RoundCapper
|
||||||
|
}
|
||||||
|
if jr == nil {
|
||||||
|
jr = RoundJoiner
|
||||||
|
}
|
||||||
|
if q[0] != 0 {
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
s := stroker{p: p, u: width / 2, cr: cr, jr: jr}
|
||||||
|
i := 0
|
||||||
|
for j := 4; j < len(q); {
|
||||||
|
switch q[j] {
|
||||||
|
case 0:
|
||||||
|
s.stroke(q[i:j])
|
||||||
|
i, j = j, j+4
|
||||||
|
case 1:
|
||||||
|
j += 4
|
||||||
|
case 2:
|
||||||
|
j += 6
|
||||||
|
case 3:
|
||||||
|
j += 8
|
||||||
|
default:
|
||||||
|
panic("freetype/raster: bad path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.stroke(q[i:])
|
||||||
|
}
|
507
vendor/github.com/golang/freetype/truetype/face.go
generated
vendored
Normal file
507
vendor/github.com/golang/freetype/truetype/face.go
generated
vendored
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
// Copyright 2015 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package truetype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func powerOf2(i int) bool {
|
||||||
|
return i != 0 && (i&(i-1)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options are optional arguments to NewFace.
|
||||||
|
type Options struct {
|
||||||
|
// Size is the font size in points, as in "a 10 point font size".
|
||||||
|
//
|
||||||
|
// A zero value means to use a 12 point font size.
|
||||||
|
Size float64
|
||||||
|
|
||||||
|
// DPI is the dots-per-inch resolution.
|
||||||
|
//
|
||||||
|
// A zero value means to use 72 DPI.
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
// Hinting is how to quantize the glyph nodes.
|
||||||
|
//
|
||||||
|
// A zero value means to use no hinting.
|
||||||
|
Hinting font.Hinting
|
||||||
|
|
||||||
|
// GlyphCacheEntries is the number of entries in the glyph mask image
|
||||||
|
// cache.
|
||||||
|
//
|
||||||
|
// If non-zero, it must be a power of 2.
|
||||||
|
//
|
||||||
|
// A zero value means to use 512 entries.
|
||||||
|
GlyphCacheEntries int
|
||||||
|
|
||||||
|
// SubPixelsX is the number of sub-pixel locations a glyph's dot is
|
||||||
|
// quantized to, in the horizontal direction. For example, a value of 8
|
||||||
|
// means that the dot is quantized to 1/8th of a pixel. This quantization
|
||||||
|
// only affects the glyph mask image, not its bounding box or advance
|
||||||
|
// width. A higher value gives a more faithful glyph image, but reduces the
|
||||||
|
// effectiveness of the glyph cache.
|
||||||
|
//
|
||||||
|
// If non-zero, it must be a power of 2, and be between 1 and 64 inclusive.
|
||||||
|
//
|
||||||
|
// A zero value means to use 4 sub-pixel locations.
|
||||||
|
SubPixelsX int
|
||||||
|
|
||||||
|
// SubPixelsY is the number of sub-pixel locations a glyph's dot is
|
||||||
|
// quantized to, in the vertical direction. For example, a value of 8
|
||||||
|
// means that the dot is quantized to 1/8th of a pixel. This quantization
|
||||||
|
// only affects the glyph mask image, not its bounding box or advance
|
||||||
|
// width. A higher value gives a more faithful glyph image, but reduces the
|
||||||
|
// effectiveness of the glyph cache.
|
||||||
|
//
|
||||||
|
// If non-zero, it must be a power of 2, and be between 1 and 64 inclusive.
|
||||||
|
//
|
||||||
|
// A zero value means to use 1 sub-pixel location.
|
||||||
|
SubPixelsY int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) size() float64 {
|
||||||
|
if o != nil && o.Size > 0 {
|
||||||
|
return o.Size
|
||||||
|
}
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) dpi() float64 {
|
||||||
|
if o != nil && o.DPI > 0 {
|
||||||
|
return o.DPI
|
||||||
|
}
|
||||||
|
return 72
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) hinting() font.Hinting {
|
||||||
|
if o != nil {
|
||||||
|
switch o.Hinting {
|
||||||
|
case font.HintingVertical, font.HintingFull:
|
||||||
|
// TODO: support vertical hinting.
|
||||||
|
return font.HintingFull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return font.HintingNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) glyphCacheEntries() int {
|
||||||
|
if o != nil && powerOf2(o.GlyphCacheEntries) {
|
||||||
|
return o.GlyphCacheEntries
|
||||||
|
}
|
||||||
|
// 512 is 128 * 4 * 1, which lets us cache 128 glyphs at 4 * 1 subpixel
|
||||||
|
// locations in the X and Y direction.
|
||||||
|
return 512
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) subPixelsX() (value uint32, halfQuantum, mask fixed.Int26_6) {
|
||||||
|
if o != nil {
|
||||||
|
switch o.SubPixelsX {
|
||||||
|
case 1, 2, 4, 8, 16, 32, 64:
|
||||||
|
return subPixels(o.SubPixelsX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This default value of 4 isn't based on anything scientific, merely as
|
||||||
|
// small a number as possible that looks almost as good as no quantization,
|
||||||
|
// or returning subPixels(64).
|
||||||
|
return subPixels(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) subPixelsY() (value uint32, halfQuantum, mask fixed.Int26_6) {
|
||||||
|
if o != nil {
|
||||||
|
switch o.SubPixelsX {
|
||||||
|
case 1, 2, 4, 8, 16, 32, 64:
|
||||||
|
return subPixels(o.SubPixelsX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This default value of 1 isn't based on anything scientific, merely that
|
||||||
|
// vertical sub-pixel glyph rendering is pretty rare. Baseline locations
|
||||||
|
// can usually afford to snap to the pixel grid, so the vertical direction
|
||||||
|
// doesn't have the deal with the horizontal's fractional advance widths.
|
||||||
|
return subPixels(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subPixels returns q and the bias and mask that leads to q quantized
|
||||||
|
// sub-pixel locations per full pixel.
|
||||||
|
//
|
||||||
|
// For example, q == 4 leads to a bias of 8 and a mask of 0xfffffff0, or -16,
|
||||||
|
// because we want to round fractions of fixed.Int26_6 as:
|
||||||
|
// - 0 to 7 rounds to 0.
|
||||||
|
// - 8 to 23 rounds to 16.
|
||||||
|
// - 24 to 39 rounds to 32.
|
||||||
|
// - 40 to 55 rounds to 48.
|
||||||
|
// - 56 to 63 rounds to 64.
|
||||||
|
// which means to add 8 and then bitwise-and with -16, in two's complement
|
||||||
|
// representation.
|
||||||
|
//
|
||||||
|
// When q == 1, we want bias == 32 and mask == -64.
|
||||||
|
// When q == 2, we want bias == 16 and mask == -32.
|
||||||
|
// When q == 4, we want bias == 8 and mask == -16.
|
||||||
|
// ...
|
||||||
|
// When q == 64, we want bias == 0 and mask == -1. (The no-op case).
|
||||||
|
// The pattern is clear.
|
||||||
|
func subPixels(q int) (value uint32, bias, mask fixed.Int26_6) {
|
||||||
|
return uint32(q), 32 / fixed.Int26_6(q), -64 / fixed.Int26_6(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// glyphCacheEntry caches the arguments and return values of rasterize.
|
||||||
|
type glyphCacheEntry struct {
|
||||||
|
key glyphCacheKey
|
||||||
|
val glyphCacheVal
|
||||||
|
}
|
||||||
|
|
||||||
|
type glyphCacheKey struct {
|
||||||
|
index Index
|
||||||
|
fx, fy uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type glyphCacheVal struct {
|
||||||
|
advanceWidth fixed.Int26_6
|
||||||
|
offset image.Point
|
||||||
|
gw int
|
||||||
|
gh int
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexCacheEntry struct {
|
||||||
|
rune rune
|
||||||
|
index Index
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFace returns a new font.Face for the given Font.
|
||||||
|
func NewFace(f *Font, opts *Options) font.Face {
|
||||||
|
a := &face{
|
||||||
|
f: f,
|
||||||
|
hinting: opts.hinting(),
|
||||||
|
scale: fixed.Int26_6(0.5 + (opts.size() * opts.dpi() * 64 / 72)),
|
||||||
|
glyphCache: make([]glyphCacheEntry, opts.glyphCacheEntries()),
|
||||||
|
}
|
||||||
|
a.subPixelX, a.subPixelBiasX, a.subPixelMaskX = opts.subPixelsX()
|
||||||
|
a.subPixelY, a.subPixelBiasY, a.subPixelMaskY = opts.subPixelsY()
|
||||||
|
|
||||||
|
// Fill the cache with invalid entries. Valid glyph cache entries have fx
|
||||||
|
// and fy in the range [0, 64). Valid index cache entries have rune >= 0.
|
||||||
|
for i := range a.glyphCache {
|
||||||
|
a.glyphCache[i].key.fy = 0xff
|
||||||
|
}
|
||||||
|
for i := range a.indexCache {
|
||||||
|
a.indexCache[i].rune = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the rasterizer's bounds to be big enough to handle the largest glyph.
|
||||||
|
b := f.Bounds(a.scale)
|
||||||
|
xmin := +int(b.Min.X) >> 6
|
||||||
|
ymin := -int(b.Max.Y) >> 6
|
||||||
|
xmax := +int(b.Max.X+63) >> 6
|
||||||
|
ymax := -int(b.Min.Y-63) >> 6
|
||||||
|
a.maxw = xmax - xmin
|
||||||
|
a.maxh = ymax - ymin
|
||||||
|
a.masks = image.NewAlpha(image.Rect(0, 0, a.maxw, a.maxh*len(a.glyphCache)))
|
||||||
|
a.r.SetBounds(a.maxw, a.maxh)
|
||||||
|
a.p = facePainter{a}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
type face struct {
|
||||||
|
f *Font
|
||||||
|
hinting font.Hinting
|
||||||
|
scale fixed.Int26_6
|
||||||
|
subPixelX uint32
|
||||||
|
subPixelBiasX fixed.Int26_6
|
||||||
|
subPixelMaskX fixed.Int26_6
|
||||||
|
subPixelY uint32
|
||||||
|
subPixelBiasY fixed.Int26_6
|
||||||
|
subPixelMaskY fixed.Int26_6
|
||||||
|
masks *image.Alpha
|
||||||
|
glyphCache []glyphCacheEntry
|
||||||
|
r raster.Rasterizer
|
||||||
|
p raster.Painter
|
||||||
|
paintOffset int
|
||||||
|
maxw int
|
||||||
|
maxh int
|
||||||
|
glyphBuf GlyphBuf
|
||||||
|
indexCache [indexCacheLen]indexCacheEntry
|
||||||
|
|
||||||
|
// TODO: clip rectangle?
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexCacheLen = 256
|
||||||
|
|
||||||
|
func (a *face) index(r rune) Index {
|
||||||
|
const mask = indexCacheLen - 1
|
||||||
|
c := &a.indexCache[r&mask]
|
||||||
|
if c.rune == r {
|
||||||
|
return c.index
|
||||||
|
}
|
||||||
|
i := a.f.Index(r)
|
||||||
|
c.rune = r
|
||||||
|
c.index = i
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close satisfies the font.Face interface.
|
||||||
|
func (a *face) Close() error { return nil }
|
||||||
|
|
||||||
|
// Metrics satisfies the font.Face interface.
|
||||||
|
func (a *face) Metrics() font.Metrics {
|
||||||
|
scale := float64(a.scale)
|
||||||
|
fupe := float64(a.f.FUnitsPerEm())
|
||||||
|
return font.Metrics{
|
||||||
|
Height: a.scale,
|
||||||
|
Ascent: fixed.Int26_6(math.Ceil(scale * float64(+a.f.ascent) / fupe)),
|
||||||
|
Descent: fixed.Int26_6(math.Ceil(scale * float64(-a.f.descent) / fupe)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kern satisfies the font.Face interface.
|
||||||
|
func (a *face) Kern(r0, r1 rune) fixed.Int26_6 {
|
||||||
|
i0 := a.index(r0)
|
||||||
|
i1 := a.index(r1)
|
||||||
|
kern := a.f.Kern(a.scale, i0, i1)
|
||||||
|
if a.hinting != font.HintingNone {
|
||||||
|
kern = (kern + 32) &^ 63
|
||||||
|
}
|
||||||
|
return kern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glyph satisfies the font.Face interface.
|
||||||
|
func (a *face) Glyph(dot fixed.Point26_6, r rune) (
|
||||||
|
dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
|
||||||
|
|
||||||
|
// Quantize to the sub-pixel granularity.
|
||||||
|
dotX := (dot.X + a.subPixelBiasX) & a.subPixelMaskX
|
||||||
|
dotY := (dot.Y + a.subPixelBiasY) & a.subPixelMaskY
|
||||||
|
|
||||||
|
// Split the coordinates into their integer and fractional parts.
|
||||||
|
ix, fx := int(dotX>>6), dotX&0x3f
|
||||||
|
iy, fy := int(dotY>>6), dotY&0x3f
|
||||||
|
|
||||||
|
index := a.index(r)
|
||||||
|
cIndex := uint32(index)
|
||||||
|
cIndex = cIndex*a.subPixelX - uint32(fx/a.subPixelMaskX)
|
||||||
|
cIndex = cIndex*a.subPixelY - uint32(fy/a.subPixelMaskY)
|
||||||
|
cIndex &= uint32(len(a.glyphCache) - 1)
|
||||||
|
a.paintOffset = a.maxh * int(cIndex)
|
||||||
|
k := glyphCacheKey{
|
||||||
|
index: index,
|
||||||
|
fx: uint8(fx),
|
||||||
|
fy: uint8(fy),
|
||||||
|
}
|
||||||
|
var v glyphCacheVal
|
||||||
|
if a.glyphCache[cIndex].key != k {
|
||||||
|
var ok bool
|
||||||
|
v, ok = a.rasterize(index, fx, fy)
|
||||||
|
if !ok {
|
||||||
|
return image.Rectangle{}, nil, image.Point{}, 0, false
|
||||||
|
}
|
||||||
|
a.glyphCache[cIndex] = glyphCacheEntry{k, v}
|
||||||
|
} else {
|
||||||
|
v = a.glyphCache[cIndex].val
|
||||||
|
}
|
||||||
|
|
||||||
|
dr.Min = image.Point{
|
||||||
|
X: ix + v.offset.X,
|
||||||
|
Y: iy + v.offset.Y,
|
||||||
|
}
|
||||||
|
dr.Max = image.Point{
|
||||||
|
X: dr.Min.X + v.gw,
|
||||||
|
Y: dr.Min.Y + v.gh,
|
||||||
|
}
|
||||||
|
return dr, a.masks, image.Point{Y: a.paintOffset}, v.advanceWidth, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
|
||||||
|
if err := a.glyphBuf.Load(a.f, a.scale, a.index(r), a.hinting); err != nil {
|
||||||
|
return fixed.Rectangle26_6{}, 0, false
|
||||||
|
}
|
||||||
|
xmin := +a.glyphBuf.Bounds.Min.X
|
||||||
|
ymin := -a.glyphBuf.Bounds.Max.Y
|
||||||
|
xmax := +a.glyphBuf.Bounds.Max.X
|
||||||
|
ymax := -a.glyphBuf.Bounds.Min.Y
|
||||||
|
if xmin > xmax || ymin > ymax {
|
||||||
|
return fixed.Rectangle26_6{}, 0, false
|
||||||
|
}
|
||||||
|
return fixed.Rectangle26_6{
|
||||||
|
Min: fixed.Point26_6{
|
||||||
|
X: xmin,
|
||||||
|
Y: ymin,
|
||||||
|
},
|
||||||
|
Max: fixed.Point26_6{
|
||||||
|
X: xmax,
|
||||||
|
Y: ymax,
|
||||||
|
},
|
||||||
|
}, a.glyphBuf.AdvanceWidth, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *face) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
|
||||||
|
if err := a.glyphBuf.Load(a.f, a.scale, a.index(r), a.hinting); err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return a.glyphBuf.AdvanceWidth, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rasterize returns the advance width, integer-pixel offset to render at, and
|
||||||
|
// the width and height of the given glyph at the given sub-pixel offsets.
|
||||||
|
//
|
||||||
|
// The 26.6 fixed point arguments fx and fy must be in the range [0, 1).
|
||||||
|
func (a *face) rasterize(index Index, fx, fy fixed.Int26_6) (v glyphCacheVal, ok bool) {
|
||||||
|
if err := a.glyphBuf.Load(a.f, a.scale, index, a.hinting); err != nil {
|
||||||
|
return glyphCacheVal{}, false
|
||||||
|
}
|
||||||
|
// Calculate the integer-pixel bounds for the glyph.
|
||||||
|
xmin := int(fx+a.glyphBuf.Bounds.Min.X) >> 6
|
||||||
|
ymin := int(fy-a.glyphBuf.Bounds.Max.Y) >> 6
|
||||||
|
xmax := int(fx+a.glyphBuf.Bounds.Max.X+0x3f) >> 6
|
||||||
|
ymax := int(fy-a.glyphBuf.Bounds.Min.Y+0x3f) >> 6
|
||||||
|
if xmin > xmax || ymin > ymax {
|
||||||
|
return glyphCacheVal{}, false
|
||||||
|
}
|
||||||
|
// A TrueType's glyph's nodes can have negative co-ordinates, but the
|
||||||
|
// rasterizer clips anything left of x=0 or above y=0. xmin and ymin are
|
||||||
|
// the pixel offsets, based on the font's FUnit metrics, that let a
|
||||||
|
// negative co-ordinate in TrueType space be non-negative in rasterizer
|
||||||
|
// space. xmin and ymin are typically <= 0.
|
||||||
|
fx -= fixed.Int26_6(xmin << 6)
|
||||||
|
fy -= fixed.Int26_6(ymin << 6)
|
||||||
|
// Rasterize the glyph's vectors.
|
||||||
|
a.r.Clear()
|
||||||
|
pixOffset := a.paintOffset * a.maxw
|
||||||
|
clear(a.masks.Pix[pixOffset : pixOffset+a.maxw*a.maxh])
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range a.glyphBuf.Ends {
|
||||||
|
a.drawContour(a.glyphBuf.Points[e0:e1], fx, fy)
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
a.r.Rasterize(a.p)
|
||||||
|
return glyphCacheVal{
|
||||||
|
a.glyphBuf.AdvanceWidth,
|
||||||
|
image.Point{xmin, ymin},
|
||||||
|
xmax - xmin,
|
||||||
|
ymax - ymin,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear(pix []byte) {
|
||||||
|
for i := range pix {
|
||||||
|
pix[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawContour draws the given closed contour with the given offset.
|
||||||
|
func (a *face) drawContour(ps []Point, dx, dy fixed.Int26_6) {
|
||||||
|
if len(ps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The low bit of each point's Flags value is whether the point is on the
|
||||||
|
// curve. Truetype fonts only have quadratic Bézier curves, not cubics.
|
||||||
|
// Thus, two consecutive off-curve points imply an on-curve point in the
|
||||||
|
// middle of those two.
|
||||||
|
//
|
||||||
|
// See http://chanae.walon.org/pub/ttf/ttf_glyphs.htm for more details.
|
||||||
|
|
||||||
|
// ps[0] is a truetype.Point measured in FUnits and positive Y going
|
||||||
|
// upwards. start is the same thing measured in fixed point units and
|
||||||
|
// positive Y going downwards, and offset by (dx, dy).
|
||||||
|
start := fixed.Point26_6{
|
||||||
|
X: dx + ps[0].X,
|
||||||
|
Y: dy - ps[0].Y,
|
||||||
|
}
|
||||||
|
var others []Point
|
||||||
|
if ps[0].Flags&0x01 != 0 {
|
||||||
|
others = ps[1:]
|
||||||
|
} else {
|
||||||
|
last := fixed.Point26_6{
|
||||||
|
X: dx + ps[len(ps)-1].X,
|
||||||
|
Y: dy - ps[len(ps)-1].Y,
|
||||||
|
}
|
||||||
|
if ps[len(ps)-1].Flags&0x01 != 0 {
|
||||||
|
start = last
|
||||||
|
others = ps[:len(ps)-1]
|
||||||
|
} else {
|
||||||
|
start = fixed.Point26_6{
|
||||||
|
X: (start.X + last.X) / 2,
|
||||||
|
Y: (start.Y + last.Y) / 2,
|
||||||
|
}
|
||||||
|
others = ps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.r.Start(start)
|
||||||
|
q0, on0 := start, true
|
||||||
|
for _, p := range others {
|
||||||
|
q := fixed.Point26_6{
|
||||||
|
X: dx + p.X,
|
||||||
|
Y: dy - p.Y,
|
||||||
|
}
|
||||||
|
on := p.Flags&0x01 != 0
|
||||||
|
if on {
|
||||||
|
if on0 {
|
||||||
|
a.r.Add1(q)
|
||||||
|
} else {
|
||||||
|
a.r.Add2(q0, q)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if on0 {
|
||||||
|
// No-op.
|
||||||
|
} else {
|
||||||
|
mid := fixed.Point26_6{
|
||||||
|
X: (q0.X + q.X) / 2,
|
||||||
|
Y: (q0.Y + q.Y) / 2,
|
||||||
|
}
|
||||||
|
a.r.Add2(q0, mid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q0, on0 = q, on
|
||||||
|
}
|
||||||
|
// Close the curve.
|
||||||
|
if on0 {
|
||||||
|
a.r.Add1(start)
|
||||||
|
} else {
|
||||||
|
a.r.Add2(q0, start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// facePainter is like a raster.AlphaSrcPainter, with an additional Y offset
|
||||||
|
// (face.paintOffset) to the painted spans.
|
||||||
|
type facePainter struct {
|
||||||
|
a *face
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p facePainter) Paint(ss []raster.Span, done bool) {
|
||||||
|
m := p.a.masks
|
||||||
|
b := m.Bounds()
|
||||||
|
b.Min.Y = p.a.paintOffset
|
||||||
|
b.Max.Y = p.a.paintOffset + p.a.maxh
|
||||||
|
for _, s := range ss {
|
||||||
|
s.Y += p.a.paintOffset
|
||||||
|
if s.Y < b.Min.Y {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Y >= b.Max.Y {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.X0 < b.Min.X {
|
||||||
|
s.X0 = b.Min.X
|
||||||
|
}
|
||||||
|
if s.X1 > b.Max.X {
|
||||||
|
s.X1 = b.Max.X
|
||||||
|
}
|
||||||
|
if s.X0 >= s.X1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
base := (s.Y-m.Rect.Min.Y)*m.Stride - m.Rect.Min.X
|
||||||
|
p := m.Pix[base+s.X0 : base+s.X1]
|
||||||
|
color := uint8(s.Alpha >> 8)
|
||||||
|
for i := range p {
|
||||||
|
p[i] = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
522
vendor/github.com/golang/freetype/truetype/glyph.go
generated
vendored
Normal file
522
vendor/github.com/golang/freetype/truetype/glyph.go
generated
vendored
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package truetype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: implement VerticalHinting.
|
||||||
|
|
||||||
|
// A Point is a co-ordinate pair plus whether it is 'on' a contour or an 'off'
|
||||||
|
// control point.
|
||||||
|
type Point struct {
|
||||||
|
X, Y fixed.Int26_6
|
||||||
|
// The Flags' LSB means whether or not this Point is 'on' the contour.
|
||||||
|
// Other bits are reserved for internal use.
|
||||||
|
Flags uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// A GlyphBuf holds a glyph's contours. A GlyphBuf can be re-used to load a
|
||||||
|
// series of glyphs from a Font.
|
||||||
|
type GlyphBuf struct {
|
||||||
|
// AdvanceWidth is the glyph's advance width.
|
||||||
|
AdvanceWidth fixed.Int26_6
|
||||||
|
// Bounds is the glyph's bounding box.
|
||||||
|
Bounds fixed.Rectangle26_6
|
||||||
|
// Points contains all Points from all contours of the glyph. If hinting
|
||||||
|
// was used to load a glyph then Unhinted contains those Points before they
|
||||||
|
// were hinted, and InFontUnits contains those Points before they were
|
||||||
|
// hinted and scaled.
|
||||||
|
Points, Unhinted, InFontUnits []Point
|
||||||
|
// Ends is the point indexes of the end point of each contour. The length
|
||||||
|
// of Ends is the number of contours in the glyph. The i'th contour
|
||||||
|
// consists of points Points[Ends[i-1]:Ends[i]], where Ends[-1] is
|
||||||
|
// interpreted to mean zero.
|
||||||
|
Ends []int
|
||||||
|
|
||||||
|
font *Font
|
||||||
|
scale fixed.Int26_6
|
||||||
|
hinting font.Hinting
|
||||||
|
hinter hinter
|
||||||
|
// phantomPoints are the co-ordinates of the synthetic phantom points
|
||||||
|
// used for hinting and bounding box calculations.
|
||||||
|
phantomPoints [4]Point
|
||||||
|
// pp1x is the X co-ordinate of the first phantom point. The '1' is
|
||||||
|
// using 1-based indexing; pp1x is almost always phantomPoints[0].X.
|
||||||
|
// TODO: eliminate this and consistently use phantomPoints[0].X.
|
||||||
|
pp1x fixed.Int26_6
|
||||||
|
// metricsSet is whether the glyph's metrics have been set yet. For a
|
||||||
|
// compound glyph, a sub-glyph may override the outer glyph's metrics.
|
||||||
|
metricsSet bool
|
||||||
|
// tmp is a scratch buffer.
|
||||||
|
tmp []Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags for decoding a glyph's contours. These flags are documented at
|
||||||
|
// http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html.
|
||||||
|
const (
|
||||||
|
flagOnCurve = 1 << iota
|
||||||
|
flagXShortVector
|
||||||
|
flagYShortVector
|
||||||
|
flagRepeat
|
||||||
|
flagPositiveXShortVector
|
||||||
|
flagPositiveYShortVector
|
||||||
|
|
||||||
|
// The remaining flags are for internal use.
|
||||||
|
flagTouchedX
|
||||||
|
flagTouchedY
|
||||||
|
)
|
||||||
|
|
||||||
|
// The same flag bits (0x10 and 0x20) are overloaded to have two meanings,
|
||||||
|
// dependent on the value of the flag{X,Y}ShortVector bits.
|
||||||
|
const (
|
||||||
|
flagThisXIsSame = flagPositiveXShortVector
|
||||||
|
flagThisYIsSame = flagPositiveYShortVector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load loads a glyph's contours from a Font, overwriting any previously loaded
|
||||||
|
// contours for this GlyphBuf. scale is the number of 26.6 fixed point units in
|
||||||
|
// 1 em, i is the glyph index, and h is the hinting policy.
|
||||||
|
func (g *GlyphBuf) Load(f *Font, scale fixed.Int26_6, i Index, h font.Hinting) error {
|
||||||
|
g.Points = g.Points[:0]
|
||||||
|
g.Unhinted = g.Unhinted[:0]
|
||||||
|
g.InFontUnits = g.InFontUnits[:0]
|
||||||
|
g.Ends = g.Ends[:0]
|
||||||
|
g.font = f
|
||||||
|
g.hinting = h
|
||||||
|
g.scale = scale
|
||||||
|
g.pp1x = 0
|
||||||
|
g.phantomPoints = [4]Point{}
|
||||||
|
g.metricsSet = false
|
||||||
|
|
||||||
|
if h != font.HintingNone {
|
||||||
|
if err := g.hinter.init(f, scale); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := g.load(0, i, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: this selection of either g.pp1x or g.phantomPoints[0].X isn't ideal,
|
||||||
|
// and should be cleaned up once we have all the testScaling tests passing,
|
||||||
|
// plus additional tests for Freetype-Go's bounding boxes matching C Freetype's.
|
||||||
|
pp1x := g.pp1x
|
||||||
|
if h != font.HintingNone {
|
||||||
|
pp1x = g.phantomPoints[0].X
|
||||||
|
}
|
||||||
|
if pp1x != 0 {
|
||||||
|
for i := range g.Points {
|
||||||
|
g.Points[i].X -= pp1x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceWidth := g.phantomPoints[1].X - g.phantomPoints[0].X
|
||||||
|
if h != font.HintingNone {
|
||||||
|
if len(f.hdmx) >= 8 {
|
||||||
|
if n := u32(f.hdmx, 4); n > 3+uint32(i) {
|
||||||
|
for hdmx := f.hdmx[8:]; uint32(len(hdmx)) >= n; hdmx = hdmx[n:] {
|
||||||
|
if fixed.Int26_6(hdmx[0]) == scale>>6 {
|
||||||
|
advanceWidth = fixed.Int26_6(hdmx[2+i]) << 6
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
advanceWidth = (advanceWidth + 32) &^ 63
|
||||||
|
}
|
||||||
|
g.AdvanceWidth = advanceWidth
|
||||||
|
|
||||||
|
// Set g.Bounds to the 'control box', which is the bounding box of the
|
||||||
|
// Bézier curves' control points. This is easier to calculate, no smaller
|
||||||
|
// than and often equal to the tightest possible bounding box of the curves
|
||||||
|
// themselves. This approach is what C Freetype does. We can't just scale
|
||||||
|
// the nominal bounding box in the glyf data as the hinting process and
|
||||||
|
// phantom point adjustment may move points outside of that box.
|
||||||
|
if len(g.Points) == 0 {
|
||||||
|
g.Bounds = fixed.Rectangle26_6{}
|
||||||
|
} else {
|
||||||
|
p := g.Points[0]
|
||||||
|
g.Bounds.Min.X = p.X
|
||||||
|
g.Bounds.Max.X = p.X
|
||||||
|
g.Bounds.Min.Y = p.Y
|
||||||
|
g.Bounds.Max.Y = p.Y
|
||||||
|
for _, p := range g.Points[1:] {
|
||||||
|
if g.Bounds.Min.X > p.X {
|
||||||
|
g.Bounds.Min.X = p.X
|
||||||
|
} else if g.Bounds.Max.X < p.X {
|
||||||
|
g.Bounds.Max.X = p.X
|
||||||
|
}
|
||||||
|
if g.Bounds.Min.Y > p.Y {
|
||||||
|
g.Bounds.Min.Y = p.Y
|
||||||
|
} else if g.Bounds.Max.Y < p.Y {
|
||||||
|
g.Bounds.Max.Y = p.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Snap the box to the grid, if hinting is on.
|
||||||
|
if h != font.HintingNone {
|
||||||
|
g.Bounds.Min.X &^= 63
|
||||||
|
g.Bounds.Min.Y &^= 63
|
||||||
|
g.Bounds.Max.X += 63
|
||||||
|
g.Bounds.Max.X &^= 63
|
||||||
|
g.Bounds.Max.Y += 63
|
||||||
|
g.Bounds.Max.Y &^= 63
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlyphBuf) load(recursion uint32, i Index, useMyMetrics bool) (err error) {
|
||||||
|
// The recursion limit here is arbitrary, but defends against malformed glyphs.
|
||||||
|
if recursion >= 32 {
|
||||||
|
return UnsupportedError("excessive compound glyph recursion")
|
||||||
|
}
|
||||||
|
// Find the relevant slice of g.font.glyf.
|
||||||
|
var g0, g1 uint32
|
||||||
|
if g.font.locaOffsetFormat == locaOffsetFormatShort {
|
||||||
|
g0 = 2 * uint32(u16(g.font.loca, 2*int(i)))
|
||||||
|
g1 = 2 * uint32(u16(g.font.loca, 2*int(i)+2))
|
||||||
|
} else {
|
||||||
|
g0 = u32(g.font.loca, 4*int(i))
|
||||||
|
g1 = u32(g.font.loca, 4*int(i)+4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the contour count and nominal bounding box, from the first
|
||||||
|
// 10 bytes of the glyf data. boundsYMin and boundsXMax, at offsets 4
|
||||||
|
// and 6, are unused.
|
||||||
|
glyf, ne, boundsXMin, boundsYMax := []byte(nil), 0, fixed.Int26_6(0), fixed.Int26_6(0)
|
||||||
|
if g0+10 <= g1 {
|
||||||
|
glyf = g.font.glyf[g0:g1]
|
||||||
|
ne = int(int16(u16(glyf, 0)))
|
||||||
|
boundsXMin = fixed.Int26_6(int16(u16(glyf, 2)))
|
||||||
|
boundsYMax = fixed.Int26_6(int16(u16(glyf, 8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the phantom points.
|
||||||
|
uhm, pp1x := g.font.unscaledHMetric(i), fixed.Int26_6(0)
|
||||||
|
uvm := g.font.unscaledVMetric(i, boundsYMax)
|
||||||
|
g.phantomPoints = [4]Point{
|
||||||
|
{X: boundsXMin - uhm.LeftSideBearing},
|
||||||
|
{X: boundsXMin - uhm.LeftSideBearing + uhm.AdvanceWidth},
|
||||||
|
{X: uhm.AdvanceWidth / 2, Y: boundsYMax + uvm.TopSideBearing},
|
||||||
|
{X: uhm.AdvanceWidth / 2, Y: boundsYMax + uvm.TopSideBearing - uvm.AdvanceHeight},
|
||||||
|
}
|
||||||
|
if len(glyf) == 0 {
|
||||||
|
g.addPhantomsAndScale(len(g.Points), len(g.Points), true, true)
|
||||||
|
copy(g.phantomPoints[:], g.Points[len(g.Points)-4:])
|
||||||
|
g.Points = g.Points[:len(g.Points)-4]
|
||||||
|
// TODO: also trim g.InFontUnits and g.Unhinted?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and hint the contours.
|
||||||
|
if ne < 0 {
|
||||||
|
if ne != -1 {
|
||||||
|
// http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html says that
|
||||||
|
// "the values -2, -3, and so forth, are reserved for future use."
|
||||||
|
return UnsupportedError("negative number of contours")
|
||||||
|
}
|
||||||
|
pp1x = g.font.scale(g.scale * (boundsXMin - uhm.LeftSideBearing))
|
||||||
|
if err := g.loadCompound(recursion, uhm, i, glyf, useMyMetrics); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
np0, ne0 := len(g.Points), len(g.Ends)
|
||||||
|
program := g.loadSimple(glyf, ne)
|
||||||
|
g.addPhantomsAndScale(np0, np0, true, true)
|
||||||
|
pp1x = g.Points[len(g.Points)-4].X
|
||||||
|
if g.hinting != font.HintingNone {
|
||||||
|
if len(program) != 0 {
|
||||||
|
err := g.hinter.run(
|
||||||
|
program,
|
||||||
|
g.Points[np0:],
|
||||||
|
g.Unhinted[np0:],
|
||||||
|
g.InFontUnits[np0:],
|
||||||
|
g.Ends[ne0:],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Drop the four phantom points.
|
||||||
|
g.InFontUnits = g.InFontUnits[:len(g.InFontUnits)-4]
|
||||||
|
g.Unhinted = g.Unhinted[:len(g.Unhinted)-4]
|
||||||
|
}
|
||||||
|
if useMyMetrics {
|
||||||
|
copy(g.phantomPoints[:], g.Points[len(g.Points)-4:])
|
||||||
|
}
|
||||||
|
g.Points = g.Points[:len(g.Points)-4]
|
||||||
|
if np0 != 0 {
|
||||||
|
// The hinting program expects the []Ends values to be indexed
|
||||||
|
// relative to the inner glyph, not the outer glyph, so we delay
|
||||||
|
// adding np0 until after the hinting program (if any) has run.
|
||||||
|
for i := ne0; i < len(g.Ends); i++ {
|
||||||
|
g.Ends[i] += np0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if useMyMetrics && !g.metricsSet {
|
||||||
|
g.metricsSet = true
|
||||||
|
g.pp1x = pp1x
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOffset is the initial offset for loadSimple and loadCompound. The first
|
||||||
|
// 10 bytes are the number of contours and the bounding box.
|
||||||
|
const loadOffset = 10
|
||||||
|
|
||||||
|
func (g *GlyphBuf) loadSimple(glyf []byte, ne int) (program []byte) {
|
||||||
|
offset := loadOffset
|
||||||
|
for i := 0; i < ne; i++ {
|
||||||
|
g.Ends = append(g.Ends, 1+int(u16(glyf, offset)))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note the TrueType hinting instructions.
|
||||||
|
instrLen := int(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
program = glyf[offset : offset+instrLen]
|
||||||
|
offset += instrLen
|
||||||
|
|
||||||
|
if ne == 0 {
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
np0 := len(g.Points)
|
||||||
|
np1 := np0 + int(g.Ends[len(g.Ends)-1])
|
||||||
|
|
||||||
|
// Decode the flags.
|
||||||
|
for i := np0; i < np1; {
|
||||||
|
c := uint32(glyf[offset])
|
||||||
|
offset++
|
||||||
|
g.Points = append(g.Points, Point{Flags: c})
|
||||||
|
i++
|
||||||
|
if c&flagRepeat != 0 {
|
||||||
|
count := glyf[offset]
|
||||||
|
offset++
|
||||||
|
for ; count > 0; count-- {
|
||||||
|
g.Points = append(g.Points, Point{Flags: c})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the co-ordinates.
|
||||||
|
var x int16
|
||||||
|
for i := np0; i < np1; i++ {
|
||||||
|
f := g.Points[i].Flags
|
||||||
|
if f&flagXShortVector != 0 {
|
||||||
|
dx := int16(glyf[offset])
|
||||||
|
offset++
|
||||||
|
if f&flagPositiveXShortVector == 0 {
|
||||||
|
x -= dx
|
||||||
|
} else {
|
||||||
|
x += dx
|
||||||
|
}
|
||||||
|
} else if f&flagThisXIsSame == 0 {
|
||||||
|
x += int16(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
g.Points[i].X = fixed.Int26_6(x)
|
||||||
|
}
|
||||||
|
var y int16
|
||||||
|
for i := np0; i < np1; i++ {
|
||||||
|
f := g.Points[i].Flags
|
||||||
|
if f&flagYShortVector != 0 {
|
||||||
|
dy := int16(glyf[offset])
|
||||||
|
offset++
|
||||||
|
if f&flagPositiveYShortVector == 0 {
|
||||||
|
y -= dy
|
||||||
|
} else {
|
||||||
|
y += dy
|
||||||
|
}
|
||||||
|
} else if f&flagThisYIsSame == 0 {
|
||||||
|
y += int16(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
g.Points[i].Y = fixed.Int26_6(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlyphBuf) loadCompound(recursion uint32, uhm HMetric, i Index,
|
||||||
|
glyf []byte, useMyMetrics bool) error {
|
||||||
|
|
||||||
|
// Flags for decoding a compound glyph. These flags are documented at
|
||||||
|
// http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html.
|
||||||
|
const (
|
||||||
|
flagArg1And2AreWords = 1 << iota
|
||||||
|
flagArgsAreXYValues
|
||||||
|
flagRoundXYToGrid
|
||||||
|
flagWeHaveAScale
|
||||||
|
flagUnused
|
||||||
|
flagMoreComponents
|
||||||
|
flagWeHaveAnXAndYScale
|
||||||
|
flagWeHaveATwoByTwo
|
||||||
|
flagWeHaveInstructions
|
||||||
|
flagUseMyMetrics
|
||||||
|
flagOverlapCompound
|
||||||
|
)
|
||||||
|
np0, ne0 := len(g.Points), len(g.Ends)
|
||||||
|
offset := loadOffset
|
||||||
|
for {
|
||||||
|
flags := u16(glyf, offset)
|
||||||
|
component := Index(u16(glyf, offset+2))
|
||||||
|
dx, dy, transform, hasTransform := fixed.Int26_6(0), fixed.Int26_6(0), [4]int16{}, false
|
||||||
|
if flags&flagArg1And2AreWords != 0 {
|
||||||
|
dx = fixed.Int26_6(int16(u16(glyf, offset+4)))
|
||||||
|
dy = fixed.Int26_6(int16(u16(glyf, offset+6)))
|
||||||
|
offset += 8
|
||||||
|
} else {
|
||||||
|
dx = fixed.Int26_6(int16(int8(glyf[offset+4])))
|
||||||
|
dy = fixed.Int26_6(int16(int8(glyf[offset+5])))
|
||||||
|
offset += 6
|
||||||
|
}
|
||||||
|
if flags&flagArgsAreXYValues == 0 {
|
||||||
|
return UnsupportedError("compound glyph transform vector")
|
||||||
|
}
|
||||||
|
if flags&(flagWeHaveAScale|flagWeHaveAnXAndYScale|flagWeHaveATwoByTwo) != 0 {
|
||||||
|
hasTransform = true
|
||||||
|
switch {
|
||||||
|
case flags&flagWeHaveAScale != 0:
|
||||||
|
transform[0] = int16(u16(glyf, offset+0))
|
||||||
|
transform[3] = transform[0]
|
||||||
|
offset += 2
|
||||||
|
case flags&flagWeHaveAnXAndYScale != 0:
|
||||||
|
transform[0] = int16(u16(glyf, offset+0))
|
||||||
|
transform[3] = int16(u16(glyf, offset+2))
|
||||||
|
offset += 4
|
||||||
|
case flags&flagWeHaveATwoByTwo != 0:
|
||||||
|
transform[0] = int16(u16(glyf, offset+0))
|
||||||
|
transform[1] = int16(u16(glyf, offset+2))
|
||||||
|
transform[2] = int16(u16(glyf, offset+4))
|
||||||
|
transform[3] = int16(u16(glyf, offset+6))
|
||||||
|
offset += 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savedPP := g.phantomPoints
|
||||||
|
np0 := len(g.Points)
|
||||||
|
componentUMM := useMyMetrics && (flags&flagUseMyMetrics != 0)
|
||||||
|
if err := g.load(recursion+1, component, componentUMM); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags&flagUseMyMetrics == 0 {
|
||||||
|
g.phantomPoints = savedPP
|
||||||
|
}
|
||||||
|
if hasTransform {
|
||||||
|
for j := np0; j < len(g.Points); j++ {
|
||||||
|
p := &g.Points[j]
|
||||||
|
newX := 0 +
|
||||||
|
fixed.Int26_6((int64(p.X)*int64(transform[0])+1<<13)>>14) +
|
||||||
|
fixed.Int26_6((int64(p.Y)*int64(transform[2])+1<<13)>>14)
|
||||||
|
newY := 0 +
|
||||||
|
fixed.Int26_6((int64(p.X)*int64(transform[1])+1<<13)>>14) +
|
||||||
|
fixed.Int26_6((int64(p.Y)*int64(transform[3])+1<<13)>>14)
|
||||||
|
p.X, p.Y = newX, newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dx = g.font.scale(g.scale * dx)
|
||||||
|
dy = g.font.scale(g.scale * dy)
|
||||||
|
if flags&flagRoundXYToGrid != 0 {
|
||||||
|
dx = (dx + 32) &^ 63
|
||||||
|
dy = (dy + 32) &^ 63
|
||||||
|
}
|
||||||
|
for j := np0; j < len(g.Points); j++ {
|
||||||
|
p := &g.Points[j]
|
||||||
|
p.X += dx
|
||||||
|
p.Y += dy
|
||||||
|
}
|
||||||
|
// TODO: also adjust g.InFontUnits and g.Unhinted?
|
||||||
|
if flags&flagMoreComponents == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instrLen := 0
|
||||||
|
if g.hinting != font.HintingNone && offset+2 <= len(glyf) {
|
||||||
|
instrLen = int(u16(glyf, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
g.addPhantomsAndScale(np0, len(g.Points), false, instrLen > 0)
|
||||||
|
points, ends := g.Points[np0:], g.Ends[ne0:]
|
||||||
|
g.Points = g.Points[:len(g.Points)-4]
|
||||||
|
for j := range points {
|
||||||
|
points[j].Flags &^= flagTouchedX | flagTouchedY
|
||||||
|
}
|
||||||
|
|
||||||
|
if instrLen == 0 {
|
||||||
|
if !g.metricsSet {
|
||||||
|
copy(g.phantomPoints[:], points[len(points)-4:])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hint the compound glyph.
|
||||||
|
program := glyf[offset : offset+instrLen]
|
||||||
|
// Temporarily adjust the ends to be relative to this compound glyph.
|
||||||
|
if np0 != 0 {
|
||||||
|
for i := range ends {
|
||||||
|
ends[i] -= np0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hinting instructions of a composite glyph completely refer to the
|
||||||
|
// (already) hinted subglyphs.
|
||||||
|
g.tmp = append(g.tmp[:0], points...)
|
||||||
|
if err := g.hinter.run(program, points, g.tmp, g.tmp, ends); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if np0 != 0 {
|
||||||
|
for i := range ends {
|
||||||
|
ends[i] += np0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !g.metricsSet {
|
||||||
|
copy(g.phantomPoints[:], points[len(points)-4:])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlyphBuf) addPhantomsAndScale(np0, np1 int, simple, adjust bool) {
|
||||||
|
// Add the four phantom points.
|
||||||
|
g.Points = append(g.Points, g.phantomPoints[:]...)
|
||||||
|
// Scale the points.
|
||||||
|
if simple && g.hinting != font.HintingNone {
|
||||||
|
g.InFontUnits = append(g.InFontUnits, g.Points[np1:]...)
|
||||||
|
}
|
||||||
|
for i := np1; i < len(g.Points); i++ {
|
||||||
|
p := &g.Points[i]
|
||||||
|
p.X = g.font.scale(g.scale * p.X)
|
||||||
|
p.Y = g.font.scale(g.scale * p.Y)
|
||||||
|
}
|
||||||
|
if g.hinting == font.HintingNone {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Round the 1st phantom point to the grid, shifting all other points equally.
|
||||||
|
// Note that "all other points" starts from np0, not np1.
|
||||||
|
// TODO: delete this adjustment and the np0/np1 distinction, when
|
||||||
|
// we update the compatibility tests to C Freetype 2.5.3.
|
||||||
|
// See http://git.savannah.gnu.org/cgit/freetype/freetype2.git/commit/?id=05c786d990390a7ca18e62962641dac740bacb06
|
||||||
|
if adjust {
|
||||||
|
pp1x := g.Points[len(g.Points)-4].X
|
||||||
|
if dx := ((pp1x + 32) &^ 63) - pp1x; dx != 0 {
|
||||||
|
for i := np0; i < len(g.Points); i++ {
|
||||||
|
g.Points[i].X += dx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if simple {
|
||||||
|
g.Unhinted = append(g.Unhinted, g.Points[np1:]...)
|
||||||
|
}
|
||||||
|
// Round the 2nd and 4th phantom point to the grid.
|
||||||
|
p := &g.Points[len(g.Points)-3]
|
||||||
|
p.X = (p.X + 32) &^ 63
|
||||||
|
p = &g.Points[len(g.Points)-1]
|
||||||
|
p.Y = (p.Y + 32) &^ 63
|
||||||
|
}
|
1770
vendor/github.com/golang/freetype/truetype/hint.go
generated
vendored
Normal file
1770
vendor/github.com/golang/freetype/truetype/hint.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
289
vendor/github.com/golang/freetype/truetype/opcodes.go
generated
vendored
Normal file
289
vendor/github.com/golang/freetype/truetype/opcodes.go
generated
vendored
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
// Copyright 2012 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package truetype
|
||||||
|
|
||||||
|
// The Truetype opcodes are summarized at
|
||||||
|
// https://developer.apple.com/fonts/TTRefMan/RM07/appendixA.html
|
||||||
|
|
||||||
|
const (
|
||||||
|
opSVTCA0 = 0x00 // Set freedom and projection Vectors To Coordinate Axis
|
||||||
|
opSVTCA1 = 0x01 // .
|
||||||
|
opSPVTCA0 = 0x02 // Set Projection Vector To Coordinate Axis
|
||||||
|
opSPVTCA1 = 0x03 // .
|
||||||
|
opSFVTCA0 = 0x04 // Set Freedom Vector to Coordinate Axis
|
||||||
|
opSFVTCA1 = 0x05 // .
|
||||||
|
opSPVTL0 = 0x06 // Set Projection Vector To Line
|
||||||
|
opSPVTL1 = 0x07 // .
|
||||||
|
opSFVTL0 = 0x08 // Set Freedom Vector To Line
|
||||||
|
opSFVTL1 = 0x09 // .
|
||||||
|
opSPVFS = 0x0a // Set Projection Vector From Stack
|
||||||
|
opSFVFS = 0x0b // Set Freedom Vector From Stack
|
||||||
|
opGPV = 0x0c // Get Projection Vector
|
||||||
|
opGFV = 0x0d // Get Freedom Vector
|
||||||
|
opSFVTPV = 0x0e // Set Freedom Vector To Projection Vector
|
||||||
|
opISECT = 0x0f // moves point p to the InterSECTion of two lines
|
||||||
|
opSRP0 = 0x10 // Set Reference Point 0
|
||||||
|
opSRP1 = 0x11 // Set Reference Point 1
|
||||||
|
opSRP2 = 0x12 // Set Reference Point 2
|
||||||
|
opSZP0 = 0x13 // Set Zone Pointer 0
|
||||||
|
opSZP1 = 0x14 // Set Zone Pointer 1
|
||||||
|
opSZP2 = 0x15 // Set Zone Pointer 2
|
||||||
|
opSZPS = 0x16 // Set Zone PointerS
|
||||||
|
opSLOOP = 0x17 // Set LOOP variable
|
||||||
|
opRTG = 0x18 // Round To Grid
|
||||||
|
opRTHG = 0x19 // Round To Half Grid
|
||||||
|
opSMD = 0x1a // Set Minimum Distance
|
||||||
|
opELSE = 0x1b // ELSE clause
|
||||||
|
opJMPR = 0x1c // JuMP Relative
|
||||||
|
opSCVTCI = 0x1d // Set Control Value Table Cut-In
|
||||||
|
opSSWCI = 0x1e // Set Single Width Cut-In
|
||||||
|
opSSW = 0x1f // Set Single Width
|
||||||
|
opDUP = 0x20 // DUPlicate top stack element
|
||||||
|
opPOP = 0x21 // POP top stack element
|
||||||
|
opCLEAR = 0x22 // CLEAR the stack
|
||||||
|
opSWAP = 0x23 // SWAP the top two elements on the stack
|
||||||
|
opDEPTH = 0x24 // DEPTH of the stack
|
||||||
|
opCINDEX = 0x25 // Copy the INDEXed element to the top of the stack
|
||||||
|
opMINDEX = 0x26 // Move the INDEXed element to the top of the stack
|
||||||
|
opALIGNPTS = 0x27 // ALIGN PoinTS
|
||||||
|
op_0x28 = 0x28 // deprecated
|
||||||
|
opUTP = 0x29 // UnTouch Point
|
||||||
|
opLOOPCALL = 0x2a // LOOP and CALL function
|
||||||
|
opCALL = 0x2b // CALL function
|
||||||
|
opFDEF = 0x2c // Function DEFinition
|
||||||
|
opENDF = 0x2d // END Function definition
|
||||||
|
opMDAP0 = 0x2e // Move Direct Absolute Point
|
||||||
|
opMDAP1 = 0x2f // .
|
||||||
|
opIUP0 = 0x30 // Interpolate Untouched Points through the outline
|
||||||
|
opIUP1 = 0x31 // .
|
||||||
|
opSHP0 = 0x32 // SHift Point using reference point
|
||||||
|
opSHP1 = 0x33 // .
|
||||||
|
opSHC0 = 0x34 // SHift Contour using reference point
|
||||||
|
opSHC1 = 0x35 // .
|
||||||
|
opSHZ0 = 0x36 // SHift Zone using reference point
|
||||||
|
opSHZ1 = 0x37 // .
|
||||||
|
opSHPIX = 0x38 // SHift point by a PIXel amount
|
||||||
|
opIP = 0x39 // Interpolate Point
|
||||||
|
opMSIRP0 = 0x3a // Move Stack Indirect Relative Point
|
||||||
|
opMSIRP1 = 0x3b // .
|
||||||
|
opALIGNRP = 0x3c // ALIGN to Reference Point
|
||||||
|
opRTDG = 0x3d // Round To Double Grid
|
||||||
|
opMIAP0 = 0x3e // Move Indirect Absolute Point
|
||||||
|
opMIAP1 = 0x3f // .
|
||||||
|
opNPUSHB = 0x40 // PUSH N Bytes
|
||||||
|
opNPUSHW = 0x41 // PUSH N Words
|
||||||
|
opWS = 0x42 // Write Store
|
||||||
|
opRS = 0x43 // Read Store
|
||||||
|
opWCVTP = 0x44 // Write Control Value Table in Pixel units
|
||||||
|
opRCVT = 0x45 // Read Control Value Table entry
|
||||||
|
opGC0 = 0x46 // Get Coordinate projected onto the projection vector
|
||||||
|
opGC1 = 0x47 // .
|
||||||
|
opSCFS = 0x48 // Sets Coordinate From the Stack using projection vector and freedom vector
|
||||||
|
opMD0 = 0x49 // Measure Distance
|
||||||
|
opMD1 = 0x4a // .
|
||||||
|
opMPPEM = 0x4b // Measure Pixels Per EM
|
||||||
|
opMPS = 0x4c // Measure Point Size
|
||||||
|
opFLIPON = 0x4d // set the auto FLIP Boolean to ON
|
||||||
|
opFLIPOFF = 0x4e // set the auto FLIP Boolean to OFF
|
||||||
|
opDEBUG = 0x4f // DEBUG call
|
||||||
|
opLT = 0x50 // Less Than
|
||||||
|
opLTEQ = 0x51 // Less Than or EQual
|
||||||
|
opGT = 0x52 // Greater Than
|
||||||
|
opGTEQ = 0x53 // Greater Than or EQual
|
||||||
|
opEQ = 0x54 // EQual
|
||||||
|
opNEQ = 0x55 // Not EQual
|
||||||
|
opODD = 0x56 // ODD
|
||||||
|
opEVEN = 0x57 // EVEN
|
||||||
|
opIF = 0x58 // IF test
|
||||||
|
opEIF = 0x59 // End IF
|
||||||
|
opAND = 0x5a // logical AND
|
||||||
|
opOR = 0x5b // logical OR
|
||||||
|
opNOT = 0x5c // logical NOT
|
||||||
|
opDELTAP1 = 0x5d // DELTA exception P1
|
||||||
|
opSDB = 0x5e // Set Delta Base in the graphics state
|
||||||
|
opSDS = 0x5f // Set Delta Shift in the graphics state
|
||||||
|
opADD = 0x60 // ADD
|
||||||
|
opSUB = 0x61 // SUBtract
|
||||||
|
opDIV = 0x62 // DIVide
|
||||||
|
opMUL = 0x63 // MULtiply
|
||||||
|
opABS = 0x64 // ABSolute value
|
||||||
|
opNEG = 0x65 // NEGate
|
||||||
|
opFLOOR = 0x66 // FLOOR
|
||||||
|
opCEILING = 0x67 // CEILING
|
||||||
|
opROUND00 = 0x68 // ROUND value
|
||||||
|
opROUND01 = 0x69 // .
|
||||||
|
opROUND10 = 0x6a // .
|
||||||
|
opROUND11 = 0x6b // .
|
||||||
|
opNROUND00 = 0x6c // No ROUNDing of value
|
||||||
|
opNROUND01 = 0x6d // .
|
||||||
|
opNROUND10 = 0x6e // .
|
||||||
|
opNROUND11 = 0x6f // .
|
||||||
|
opWCVTF = 0x70 // Write Control Value Table in Funits
|
||||||
|
opDELTAP2 = 0x71 // DELTA exception P2
|
||||||
|
opDELTAP3 = 0x72 // DELTA exception P3
|
||||||
|
opDELTAC1 = 0x73 // DELTA exception C1
|
||||||
|
opDELTAC2 = 0x74 // DELTA exception C2
|
||||||
|
opDELTAC3 = 0x75 // DELTA exception C3
|
||||||
|
opSROUND = 0x76 // Super ROUND
|
||||||
|
opS45ROUND = 0x77 // Super ROUND 45 degrees
|
||||||
|
opJROT = 0x78 // Jump Relative On True
|
||||||
|
opJROF = 0x79 // Jump Relative On False
|
||||||
|
opROFF = 0x7a // Round OFF
|
||||||
|
op_0x7b = 0x7b // deprecated
|
||||||
|
opRUTG = 0x7c // Round Up To Grid
|
||||||
|
opRDTG = 0x7d // Round Down To Grid
|
||||||
|
opSANGW = 0x7e // Set ANGle Weight
|
||||||
|
opAA = 0x7f // Adjust Angle
|
||||||
|
opFLIPPT = 0x80 // FLIP PoinT
|
||||||
|
opFLIPRGON = 0x81 // FLIP RanGe ON
|
||||||
|
opFLIPRGOFF = 0x82 // FLIP RanGe OFF
|
||||||
|
op_0x83 = 0x83 // deprecated
|
||||||
|
op_0x84 = 0x84 // deprecated
|
||||||
|
opSCANCTRL = 0x85 // SCAN conversion ConTRoL
|
||||||
|
opSDPVTL0 = 0x86 // Set Dual Projection Vector To Line
|
||||||
|
opSDPVTL1 = 0x87 // .
|
||||||
|
opGETINFO = 0x88 // GET INFOrmation
|
||||||
|
opIDEF = 0x89 // Instruction DEFinition
|
||||||
|
opROLL = 0x8a // ROLL the top three stack elements
|
||||||
|
opMAX = 0x8b // MAXimum of top two stack elements
|
||||||
|
opMIN = 0x8c // MINimum of top two stack elements
|
||||||
|
opSCANTYPE = 0x8d // SCANTYPE
|
||||||
|
opINSTCTRL = 0x8e // INSTRuction execution ConTRoL
|
||||||
|
op_0x8f = 0x8f
|
||||||
|
op_0x90 = 0x90
|
||||||
|
op_0x91 = 0x91
|
||||||
|
op_0x92 = 0x92
|
||||||
|
op_0x93 = 0x93
|
||||||
|
op_0x94 = 0x94
|
||||||
|
op_0x95 = 0x95
|
||||||
|
op_0x96 = 0x96
|
||||||
|
op_0x97 = 0x97
|
||||||
|
op_0x98 = 0x98
|
||||||
|
op_0x99 = 0x99
|
||||||
|
op_0x9a = 0x9a
|
||||||
|
op_0x9b = 0x9b
|
||||||
|
op_0x9c = 0x9c
|
||||||
|
op_0x9d = 0x9d
|
||||||
|
op_0x9e = 0x9e
|
||||||
|
op_0x9f = 0x9f
|
||||||
|
op_0xa0 = 0xa0
|
||||||
|
op_0xa1 = 0xa1
|
||||||
|
op_0xa2 = 0xa2
|
||||||
|
op_0xa3 = 0xa3
|
||||||
|
op_0xa4 = 0xa4
|
||||||
|
op_0xa5 = 0xa5
|
||||||
|
op_0xa6 = 0xa6
|
||||||
|
op_0xa7 = 0xa7
|
||||||
|
op_0xa8 = 0xa8
|
||||||
|
op_0xa9 = 0xa9
|
||||||
|
op_0xaa = 0xaa
|
||||||
|
op_0xab = 0xab
|
||||||
|
op_0xac = 0xac
|
||||||
|
op_0xad = 0xad
|
||||||
|
op_0xae = 0xae
|
||||||
|
op_0xaf = 0xaf
|
||||||
|
opPUSHB000 = 0xb0 // PUSH Bytes
|
||||||
|
opPUSHB001 = 0xb1 // .
|
||||||
|
opPUSHB010 = 0xb2 // .
|
||||||
|
opPUSHB011 = 0xb3 // .
|
||||||
|
opPUSHB100 = 0xb4 // .
|
||||||
|
opPUSHB101 = 0xb5 // .
|
||||||
|
opPUSHB110 = 0xb6 // .
|
||||||
|
opPUSHB111 = 0xb7 // .
|
||||||
|
opPUSHW000 = 0xb8 // PUSH Words
|
||||||
|
opPUSHW001 = 0xb9 // .
|
||||||
|
opPUSHW010 = 0xba // .
|
||||||
|
opPUSHW011 = 0xbb // .
|
||||||
|
opPUSHW100 = 0xbc // .
|
||||||
|
opPUSHW101 = 0xbd // .
|
||||||
|
opPUSHW110 = 0xbe // .
|
||||||
|
opPUSHW111 = 0xbf // .
|
||||||
|
opMDRP00000 = 0xc0 // Move Direct Relative Point
|
||||||
|
opMDRP00001 = 0xc1 // .
|
||||||
|
opMDRP00010 = 0xc2 // .
|
||||||
|
opMDRP00011 = 0xc3 // .
|
||||||
|
opMDRP00100 = 0xc4 // .
|
||||||
|
opMDRP00101 = 0xc5 // .
|
||||||
|
opMDRP00110 = 0xc6 // .
|
||||||
|
opMDRP00111 = 0xc7 // .
|
||||||
|
opMDRP01000 = 0xc8 // .
|
||||||
|
opMDRP01001 = 0xc9 // .
|
||||||
|
opMDRP01010 = 0xca // .
|
||||||
|
opMDRP01011 = 0xcb // .
|
||||||
|
opMDRP01100 = 0xcc // .
|
||||||
|
opMDRP01101 = 0xcd // .
|
||||||
|
opMDRP01110 = 0xce // .
|
||||||
|
opMDRP01111 = 0xcf // .
|
||||||
|
opMDRP10000 = 0xd0 // .
|
||||||
|
opMDRP10001 = 0xd1 // .
|
||||||
|
opMDRP10010 = 0xd2 // .
|
||||||
|
opMDRP10011 = 0xd3 // .
|
||||||
|
opMDRP10100 = 0xd4 // .
|
||||||
|
opMDRP10101 = 0xd5 // .
|
||||||
|
opMDRP10110 = 0xd6 // .
|
||||||
|
opMDRP10111 = 0xd7 // .
|
||||||
|
opMDRP11000 = 0xd8 // .
|
||||||
|
opMDRP11001 = 0xd9 // .
|
||||||
|
opMDRP11010 = 0xda // .
|
||||||
|
opMDRP11011 = 0xdb // .
|
||||||
|
opMDRP11100 = 0xdc // .
|
||||||
|
opMDRP11101 = 0xdd // .
|
||||||
|
opMDRP11110 = 0xde // .
|
||||||
|
opMDRP11111 = 0xdf // .
|
||||||
|
opMIRP00000 = 0xe0 // Move Indirect Relative Point
|
||||||
|
opMIRP00001 = 0xe1 // .
|
||||||
|
opMIRP00010 = 0xe2 // .
|
||||||
|
opMIRP00011 = 0xe3 // .
|
||||||
|
opMIRP00100 = 0xe4 // .
|
||||||
|
opMIRP00101 = 0xe5 // .
|
||||||
|
opMIRP00110 = 0xe6 // .
|
||||||
|
opMIRP00111 = 0xe7 // .
|
||||||
|
opMIRP01000 = 0xe8 // .
|
||||||
|
opMIRP01001 = 0xe9 // .
|
||||||
|
opMIRP01010 = 0xea // .
|
||||||
|
opMIRP01011 = 0xeb // .
|
||||||
|
opMIRP01100 = 0xec // .
|
||||||
|
opMIRP01101 = 0xed // .
|
||||||
|
opMIRP01110 = 0xee // .
|
||||||
|
opMIRP01111 = 0xef // .
|
||||||
|
opMIRP10000 = 0xf0 // .
|
||||||
|
opMIRP10001 = 0xf1 // .
|
||||||
|
opMIRP10010 = 0xf2 // .
|
||||||
|
opMIRP10011 = 0xf3 // .
|
||||||
|
opMIRP10100 = 0xf4 // .
|
||||||
|
opMIRP10101 = 0xf5 // .
|
||||||
|
opMIRP10110 = 0xf6 // .
|
||||||
|
opMIRP10111 = 0xf7 // .
|
||||||
|
opMIRP11000 = 0xf8 // .
|
||||||
|
opMIRP11001 = 0xf9 // .
|
||||||
|
opMIRP11010 = 0xfa // .
|
||||||
|
opMIRP11011 = 0xfb // .
|
||||||
|
opMIRP11100 = 0xfc // .
|
||||||
|
opMIRP11101 = 0xfd // .
|
||||||
|
opMIRP11110 = 0xfe // .
|
||||||
|
opMIRP11111 = 0xff // .
|
||||||
|
)
|
||||||
|
|
||||||
|
// popCount is the number of stack elements that each opcode pops.
|
||||||
|
var popCount = [256]uint8{
|
||||||
|
// 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f
|
||||||
|
0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 5, // 0x00 - 0x0f
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, // 0x10 - 0x1f
|
||||||
|
1, 1, 0, 2, 0, 1, 1, 2, 0, 1, 2, 1, 1, 0, 1, 1, // 0x20 - 0x2f
|
||||||
|
0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 2, 2, 0, 0, 2, 2, // 0x30 - 0x3f
|
||||||
|
0, 0, 2, 1, 2, 1, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0, // 0x40 - 0x4f
|
||||||
|
2, 2, 2, 2, 2, 2, 1, 1, 1, 0, 2, 2, 1, 1, 1, 1, // 0x50 - 0x5f
|
||||||
|
2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6f
|
||||||
|
2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 0, 0, 0, 0, 1, 1, // 0x70 - 0x7f
|
||||||
|
0, 2, 2, 0, 0, 1, 2, 2, 1, 1, 3, 2, 2, 1, 2, 0, // 0x80 - 0x8f
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90 - 0x9f
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xa0 - 0xaf
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0 - 0xbf
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xc0 - 0xcf
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xd0 - 0xdf
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 - 0xef
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 - 0xff
|
||||||
|
}
|
653
vendor/github.com/golang/freetype/truetype/truetype.go
generated
vendored
Normal file
653
vendor/github.com/golang/freetype/truetype/truetype.go
generated
vendored
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
// Copyright 2010 The Freetype-Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by your choice of either the
|
||||||
|
// FreeType License or the GNU General Public License version 2 (or
|
||||||
|
// any later version), both of which can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package truetype provides a parser for the TTF and TTC file formats.
|
||||||
|
// Those formats are documented at http://developer.apple.com/fonts/TTRefMan/
|
||||||
|
// and http://www.microsoft.com/typography/otspec/
|
||||||
|
//
|
||||||
|
// Some of a font's methods provide lengths or co-ordinates, e.g. bounds, font
|
||||||
|
// metrics and control points. All these methods take a scale parameter, which
|
||||||
|
// is the number of pixels in 1 em, expressed as a 26.6 fixed point value. For
|
||||||
|
// example, if 1 em is 10 pixels then scale is fixed.I(10), which is equal to
|
||||||
|
// fixed.Int26_6(10 << 6).
|
||||||
|
//
|
||||||
|
// To measure a TrueType font in ideal FUnit space, use scale equal to
|
||||||
|
// font.FUnitsPerEm().
|
||||||
|
package truetype // import "github.com/golang/freetype/truetype"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An Index is a Font's index of a rune.
|
||||||
|
type Index uint16
|
||||||
|
|
||||||
|
// A NameID identifies a name table entry.
|
||||||
|
//
|
||||||
|
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
|
||||||
|
type NameID uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
NameIDCopyright NameID = 0
|
||||||
|
NameIDFontFamily = 1
|
||||||
|
NameIDFontSubfamily = 2
|
||||||
|
NameIDUniqueSubfamilyID = 3
|
||||||
|
NameIDFontFullName = 4
|
||||||
|
NameIDNameTableVersion = 5
|
||||||
|
NameIDPostscriptName = 6
|
||||||
|
NameIDTrademarkNotice = 7
|
||||||
|
NameIDManufacturerName = 8
|
||||||
|
NameIDDesignerName = 9
|
||||||
|
NameIDFontDescription = 10
|
||||||
|
NameIDFontVendorURL = 11
|
||||||
|
NameIDFontDesignerURL = 12
|
||||||
|
NameIDFontLicense = 13
|
||||||
|
NameIDFontLicenseURL = 14
|
||||||
|
NameIDPreferredFamily = 16
|
||||||
|
NameIDPreferredSubfamily = 17
|
||||||
|
NameIDCompatibleName = 18
|
||||||
|
NameIDSampleText = 19
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A 32-bit encoding consists of a most-significant 16-bit Platform ID and a
|
||||||
|
// least-significant 16-bit Platform Specific ID. The magic numbers are
|
||||||
|
// specified at https://www.microsoft.com/typography/otspec/name.htm
|
||||||
|
unicodeEncodingBMPOnly = 0x00000003 // PID = 0 (Unicode), PSID = 3 (Unicode 2.0 BMP Only)
|
||||||
|
unicodeEncodingFull = 0x00000004 // PID = 0 (Unicode), PSID = 4 (Unicode 2.0 Full Repertoire)
|
||||||
|
microsoftSymbolEncoding = 0x00030000 // PID = 3 (Microsoft), PSID = 0 (Symbol)
|
||||||
|
microsoftUCS2Encoding = 0x00030001 // PID = 3 (Microsoft), PSID = 1 (UCS-2)
|
||||||
|
microsoftUCS4Encoding = 0x0003000a // PID = 3 (Microsoft), PSID = 10 (UCS-4)
|
||||||
|
)
|
||||||
|
|
||||||
|
// An HMetric holds the horizontal metrics of a single glyph.
|
||||||
|
type HMetric struct {
|
||||||
|
AdvanceWidth, LeftSideBearing fixed.Int26_6
|
||||||
|
}
|
||||||
|
|
||||||
|
// A VMetric holds the vertical metrics of a single glyph.
|
||||||
|
type VMetric struct {
|
||||||
|
AdvanceHeight, TopSideBearing fixed.Int26_6
|
||||||
|
}
|
||||||
|
|
||||||
|
// A FormatError reports that the input is not a valid TrueType font.
|
||||||
|
type FormatError string
|
||||||
|
|
||||||
|
func (e FormatError) Error() string {
|
||||||
|
return "freetype: invalid TrueType format: " + string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An UnsupportedError reports that the input uses a valid but unimplemented
|
||||||
|
// TrueType feature.
|
||||||
|
type UnsupportedError string
|
||||||
|
|
||||||
|
func (e UnsupportedError) Error() string {
|
||||||
|
return "freetype: unsupported TrueType feature: " + string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// u32 returns the big-endian uint32 at b[i:].
|
||||||
|
func u32(b []byte, i int) uint32 {
|
||||||
|
return uint32(b[i])<<24 | uint32(b[i+1])<<16 | uint32(b[i+2])<<8 | uint32(b[i+3])
|
||||||
|
}
|
||||||
|
|
||||||
|
// u16 returns the big-endian uint16 at b[i:].
|
||||||
|
func u16(b []byte, i int) uint16 {
|
||||||
|
return uint16(b[i])<<8 | uint16(b[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// readTable returns a slice of the TTF data given by a table's directory entry.
|
||||||
|
func readTable(ttf []byte, offsetLength []byte) ([]byte, error) {
|
||||||
|
offset := int(u32(offsetLength, 0))
|
||||||
|
if offset < 0 {
|
||||||
|
return nil, FormatError(fmt.Sprintf("offset too large: %d", uint32(offset)))
|
||||||
|
}
|
||||||
|
length := int(u32(offsetLength, 4))
|
||||||
|
if length < 0 {
|
||||||
|
return nil, FormatError(fmt.Sprintf("length too large: %d", uint32(length)))
|
||||||
|
}
|
||||||
|
end := offset + length
|
||||||
|
if end < 0 || end > len(ttf) {
|
||||||
|
return nil, FormatError(fmt.Sprintf("offset + length too large: %d", uint32(offset)+uint32(length)))
|
||||||
|
}
|
||||||
|
return ttf[offset:end], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSubtables returns the offset and platformID of the best subtable in
|
||||||
|
// table, where best favors a Unicode cmap encoding, and failing that, a
|
||||||
|
// Microsoft cmap encoding. offset is the offset of the first subtable in
|
||||||
|
// table, and size is the size of each subtable.
|
||||||
|
//
|
||||||
|
// If pred is non-nil, then only subtables that satisfy that predicate will be
|
||||||
|
// considered.
|
||||||
|
func parseSubtables(table []byte, name string, offset, size int, pred func([]byte) bool) (
|
||||||
|
bestOffset int, bestPID uint32, retErr error) {
|
||||||
|
|
||||||
|
if len(table) < 4 {
|
||||||
|
return 0, 0, FormatError(name + " too short")
|
||||||
|
}
|
||||||
|
nSubtables := int(u16(table, 2))
|
||||||
|
if len(table) < size*nSubtables+offset {
|
||||||
|
return 0, 0, FormatError(name + " too short")
|
||||||
|
}
|
||||||
|
ok := false
|
||||||
|
for i := 0; i < nSubtables; i, offset = i+1, offset+size {
|
||||||
|
if pred != nil && !pred(table[offset:]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We read the 16-bit Platform ID and 16-bit Platform Specific ID as a single uint32.
|
||||||
|
// All values are big-endian.
|
||||||
|
pidPsid := u32(table, offset)
|
||||||
|
// We prefer the Unicode cmap encoding. Failing to find that, we fall
|
||||||
|
// back onto the Microsoft cmap encoding.
|
||||||
|
if pidPsid == unicodeEncodingBMPOnly || pidPsid == unicodeEncodingFull {
|
||||||
|
bestOffset, bestPID, ok = offset, pidPsid>>16, true
|
||||||
|
break
|
||||||
|
|
||||||
|
} else if pidPsid == microsoftSymbolEncoding ||
|
||||||
|
pidPsid == microsoftUCS2Encoding ||
|
||||||
|
pidPsid == microsoftUCS4Encoding {
|
||||||
|
|
||||||
|
bestOffset, bestPID, ok = offset, pidPsid>>16, true
|
||||||
|
// We don't break out of the for loop, so that Unicode can override Microsoft.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return 0, 0, UnsupportedError(name + " encoding")
|
||||||
|
}
|
||||||
|
return bestOffset, bestPID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
locaOffsetFormatUnknown int = iota
|
||||||
|
locaOffsetFormatShort
|
||||||
|
locaOffsetFormatLong
|
||||||
|
)
|
||||||
|
|
||||||
|
// A cm holds a parsed cmap entry.
|
||||||
|
type cm struct {
|
||||||
|
start, end, delta, offset uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Font represents a Truetype font.
|
||||||
|
type Font struct {
|
||||||
|
// Tables sliced from the TTF data. The different tables are documented
|
||||||
|
// at http://developer.apple.com/fonts/TTRefMan/RM06/Chap6.html
|
||||||
|
cmap, cvt, fpgm, glyf, hdmx, head, hhea, hmtx, kern, loca, maxp, name, os2, prep, vmtx []byte
|
||||||
|
|
||||||
|
cmapIndexes []byte
|
||||||
|
|
||||||
|
// Cached values derived from the raw ttf data.
|
||||||
|
cm []cm
|
||||||
|
locaOffsetFormat int
|
||||||
|
nGlyph, nHMetric, nKern int
|
||||||
|
fUnitsPerEm int32
|
||||||
|
ascent int32 // In FUnits.
|
||||||
|
descent int32 // In FUnits; typically negative.
|
||||||
|
bounds fixed.Rectangle26_6 // In FUnits.
|
||||||
|
// Values from the maxp section.
|
||||||
|
maxTwilightPoints, maxStorage, maxFunctionDefs, maxStackElements uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseCmap() error {
|
||||||
|
const (
|
||||||
|
cmapFormat4 = 4
|
||||||
|
cmapFormat12 = 12
|
||||||
|
languageIndependent = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
offset, _, err := parseSubtables(f.cmap, "cmap", 4, 8, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
offset = int(u32(f.cmap, offset+4))
|
||||||
|
if offset <= 0 || offset > len(f.cmap) {
|
||||||
|
return FormatError("bad cmap offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmapFormat := u16(f.cmap, offset)
|
||||||
|
switch cmapFormat {
|
||||||
|
case cmapFormat4:
|
||||||
|
language := u16(f.cmap, offset+4)
|
||||||
|
if language != languageIndependent {
|
||||||
|
return UnsupportedError(fmt.Sprintf("language: %d", language))
|
||||||
|
}
|
||||||
|
segCountX2 := int(u16(f.cmap, offset+6))
|
||||||
|
if segCountX2%2 == 1 {
|
||||||
|
return FormatError(fmt.Sprintf("bad segCountX2: %d", segCountX2))
|
||||||
|
}
|
||||||
|
segCount := segCountX2 / 2
|
||||||
|
offset += 14
|
||||||
|
f.cm = make([]cm, segCount)
|
||||||
|
for i := 0; i < segCount; i++ {
|
||||||
|
f.cm[i].end = uint32(u16(f.cmap, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
for i := 0; i < segCount; i++ {
|
||||||
|
f.cm[i].start = uint32(u16(f.cmap, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
for i := 0; i < segCount; i++ {
|
||||||
|
f.cm[i].delta = uint32(u16(f.cmap, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
for i := 0; i < segCount; i++ {
|
||||||
|
f.cm[i].offset = uint32(u16(f.cmap, offset))
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
f.cmapIndexes = f.cmap[offset:]
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case cmapFormat12:
|
||||||
|
if u16(f.cmap, offset+2) != 0 {
|
||||||
|
return FormatError(fmt.Sprintf("cmap format: % x", f.cmap[offset:offset+4]))
|
||||||
|
}
|
||||||
|
length := u32(f.cmap, offset+4)
|
||||||
|
language := u32(f.cmap, offset+8)
|
||||||
|
if language != languageIndependent {
|
||||||
|
return UnsupportedError(fmt.Sprintf("language: %d", language))
|
||||||
|
}
|
||||||
|
nGroups := u32(f.cmap, offset+12)
|
||||||
|
if length != 12*nGroups+16 {
|
||||||
|
return FormatError("inconsistent cmap length")
|
||||||
|
}
|
||||||
|
offset += 16
|
||||||
|
f.cm = make([]cm, nGroups)
|
||||||
|
for i := uint32(0); i < nGroups; i++ {
|
||||||
|
f.cm[i].start = u32(f.cmap, offset+0)
|
||||||
|
f.cm[i].end = u32(f.cmap, offset+4)
|
||||||
|
f.cm[i].delta = u32(f.cmap, offset+8) - f.cm[i].start
|
||||||
|
offset += 12
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UnsupportedError(fmt.Sprintf("cmap format: %d", cmapFormat))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseHead() error {
|
||||||
|
if len(f.head) != 54 {
|
||||||
|
return FormatError(fmt.Sprintf("bad head length: %d", len(f.head)))
|
||||||
|
}
|
||||||
|
f.fUnitsPerEm = int32(u16(f.head, 18))
|
||||||
|
f.bounds.Min.X = fixed.Int26_6(int16(u16(f.head, 36)))
|
||||||
|
f.bounds.Min.Y = fixed.Int26_6(int16(u16(f.head, 38)))
|
||||||
|
f.bounds.Max.X = fixed.Int26_6(int16(u16(f.head, 40)))
|
||||||
|
f.bounds.Max.Y = fixed.Int26_6(int16(u16(f.head, 42)))
|
||||||
|
switch i := u16(f.head, 50); i {
|
||||||
|
case 0:
|
||||||
|
f.locaOffsetFormat = locaOffsetFormatShort
|
||||||
|
case 1:
|
||||||
|
f.locaOffsetFormat = locaOffsetFormatLong
|
||||||
|
default:
|
||||||
|
return FormatError(fmt.Sprintf("bad indexToLocFormat: %d", i))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseHhea() error {
|
||||||
|
if len(f.hhea) != 36 {
|
||||||
|
return FormatError(fmt.Sprintf("bad hhea length: %d", len(f.hhea)))
|
||||||
|
}
|
||||||
|
f.ascent = int32(int16(u16(f.hhea, 4)))
|
||||||
|
f.descent = int32(int16(u16(f.hhea, 6)))
|
||||||
|
f.nHMetric = int(u16(f.hhea, 34))
|
||||||
|
if 4*f.nHMetric+2*(f.nGlyph-f.nHMetric) != len(f.hmtx) {
|
||||||
|
return FormatError(fmt.Sprintf("bad hmtx length: %d", len(f.hmtx)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseKern() error {
|
||||||
|
// Apple's TrueType documentation (http://developer.apple.com/fonts/TTRefMan/RM06/Chap6kern.html) says:
|
||||||
|
// "Previous versions of the 'kern' table defined both the version and nTables fields in the header
|
||||||
|
// as UInt16 values and not UInt32 values. Use of the older format on the Mac OS is discouraged
|
||||||
|
// (although AAT can sense an old kerning table and still make correct use of it). Microsoft
|
||||||
|
// Windows still uses the older format for the 'kern' table and will not recognize the newer one.
|
||||||
|
// Fonts targeted for the Mac OS only should use the new format; fonts targeted for both the Mac OS
|
||||||
|
// and Windows should use the old format."
|
||||||
|
// Since we expect that almost all fonts aim to be Windows-compatible, we only parse the "older" format,
|
||||||
|
// just like the C Freetype implementation.
|
||||||
|
if len(f.kern) == 0 {
|
||||||
|
if f.nKern != 0 {
|
||||||
|
return FormatError("bad kern table length")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(f.kern) < 18 {
|
||||||
|
return FormatError("kern data too short")
|
||||||
|
}
|
||||||
|
version, offset := u16(f.kern, 0), 2
|
||||||
|
if version != 0 {
|
||||||
|
return UnsupportedError(fmt.Sprintf("kern version: %d", version))
|
||||||
|
}
|
||||||
|
|
||||||
|
n, offset := u16(f.kern, offset), offset+2
|
||||||
|
if n == 0 {
|
||||||
|
return UnsupportedError("kern nTables: 0")
|
||||||
|
}
|
||||||
|
// TODO: support multiple subtables. In practice, almost all .ttf files
|
||||||
|
// have only one subtable, if they have a kern table at all. But it's not
|
||||||
|
// impossible. Xolonium Regular (https://fontlibrary.org/en/font/xolonium)
|
||||||
|
// has 3 subtables. Those subtables appear to be disjoint, rather than
|
||||||
|
// being the same kerning pairs encoded in three different ways.
|
||||||
|
//
|
||||||
|
// For now, we'll use only the first subtable.
|
||||||
|
|
||||||
|
offset += 2 // Skip the version.
|
||||||
|
length, offset := int(u16(f.kern, offset)), offset+2
|
||||||
|
coverage, offset := u16(f.kern, offset), offset+2
|
||||||
|
if coverage != 0x0001 {
|
||||||
|
// We only support horizontal kerning.
|
||||||
|
return UnsupportedError(fmt.Sprintf("kern coverage: 0x%04x", coverage))
|
||||||
|
}
|
||||||
|
f.nKern, offset = int(u16(f.kern, offset)), offset+2
|
||||||
|
if 6*f.nKern != length-14 {
|
||||||
|
return FormatError("bad kern table length")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Font) parseMaxp() error {
|
||||||
|
if len(f.maxp) != 32 {
|
||||||
|
return FormatError(fmt.Sprintf("bad maxp length: %d", len(f.maxp)))
|
||||||
|
}
|
||||||
|
f.nGlyph = int(u16(f.maxp, 4))
|
||||||
|
f.maxTwilightPoints = u16(f.maxp, 16)
|
||||||
|
f.maxStorage = u16(f.maxp, 18)
|
||||||
|
f.maxFunctionDefs = u16(f.maxp, 20)
|
||||||
|
f.maxStackElements = u16(f.maxp, 24)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scale returns x divided by f.fUnitsPerEm, rounded to the nearest integer.
|
||||||
|
func (f *Font) scale(x fixed.Int26_6) fixed.Int26_6 {
|
||||||
|
if x >= 0 {
|
||||||
|
x += fixed.Int26_6(f.fUnitsPerEm) / 2
|
||||||
|
} else {
|
||||||
|
x -= fixed.Int26_6(f.fUnitsPerEm) / 2
|
||||||
|
}
|
||||||
|
return x / fixed.Int26_6(f.fUnitsPerEm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds returns the union of a Font's glyphs' bounds.
|
||||||
|
func (f *Font) Bounds(scale fixed.Int26_6) fixed.Rectangle26_6 {
|
||||||
|
b := f.bounds
|
||||||
|
b.Min.X = f.scale(scale * b.Min.X)
|
||||||
|
b.Min.Y = f.scale(scale * b.Min.Y)
|
||||||
|
b.Max.X = f.scale(scale * b.Max.X)
|
||||||
|
b.Max.Y = f.scale(scale * b.Max.Y)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUnitsPerEm returns the number of FUnits in a Font's em-square's side.
|
||||||
|
func (f *Font) FUnitsPerEm() int32 {
|
||||||
|
return f.fUnitsPerEm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index returns a Font's index for the given rune.
|
||||||
|
func (f *Font) Index(x rune) Index {
|
||||||
|
c := uint32(x)
|
||||||
|
for i, j := 0, len(f.cm); i < j; {
|
||||||
|
h := i + (j-i)/2
|
||||||
|
cm := &f.cm[h]
|
||||||
|
if c < cm.start {
|
||||||
|
j = h
|
||||||
|
} else if cm.end < c {
|
||||||
|
i = h + 1
|
||||||
|
} else if cm.offset == 0 {
|
||||||
|
return Index(c + cm.delta)
|
||||||
|
} else {
|
||||||
|
offset := int(cm.offset) + 2*(h-len(f.cm)+int(c-cm.start))
|
||||||
|
return Index(u16(f.cmapIndexes, offset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the Font's name value for the given NameID. It returns "" if
|
||||||
|
// there was an error, or if that name was not found.
|
||||||
|
func (f *Font) Name(id NameID) string {
|
||||||
|
x, platformID, err := parseSubtables(f.name, "name", 6, 12, func(b []byte) bool {
|
||||||
|
return NameID(u16(b, 6)) == id
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
offset, length := u16(f.name, 4)+u16(f.name, x+10), u16(f.name, x+8)
|
||||||
|
// Return the ASCII value of the encoded string.
|
||||||
|
// The string is encoded as UTF-16 on non-Apple platformIDs; Apple is platformID 1.
|
||||||
|
src := f.name[offset : offset+length]
|
||||||
|
var dst []byte
|
||||||
|
if platformID != 1 { // UTF-16.
|
||||||
|
if len(src)&1 != 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
dst = make([]byte, len(src)/2)
|
||||||
|
for i := range dst {
|
||||||
|
dst[i] = printable(u16(src, 2*i))
|
||||||
|
}
|
||||||
|
} else { // ASCII.
|
||||||
|
dst = make([]byte, len(src))
|
||||||
|
for i, c := range src {
|
||||||
|
dst[i] = printable(uint16(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printable(r uint16) byte {
|
||||||
|
if 0x20 <= r && r < 0x7f {
|
||||||
|
return byte(r)
|
||||||
|
}
|
||||||
|
return '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
// unscaledHMetric returns the unscaled horizontal metrics for the glyph with
|
||||||
|
// the given index.
|
||||||
|
func (f *Font) unscaledHMetric(i Index) (h HMetric) {
|
||||||
|
j := int(i)
|
||||||
|
if j < 0 || f.nGlyph <= j {
|
||||||
|
return HMetric{}
|
||||||
|
}
|
||||||
|
if j >= f.nHMetric {
|
||||||
|
p := 4 * (f.nHMetric - 1)
|
||||||
|
return HMetric{
|
||||||
|
AdvanceWidth: fixed.Int26_6(u16(f.hmtx, p)),
|
||||||
|
LeftSideBearing: fixed.Int26_6(int16(u16(f.hmtx, p+2*(j-f.nHMetric)+4))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HMetric{
|
||||||
|
AdvanceWidth: fixed.Int26_6(u16(f.hmtx, 4*j)),
|
||||||
|
LeftSideBearing: fixed.Int26_6(int16(u16(f.hmtx, 4*j+2))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMetric returns the horizontal metrics for the glyph with the given index.
|
||||||
|
func (f *Font) HMetric(scale fixed.Int26_6, i Index) HMetric {
|
||||||
|
h := f.unscaledHMetric(i)
|
||||||
|
h.AdvanceWidth = f.scale(scale * h.AdvanceWidth)
|
||||||
|
h.LeftSideBearing = f.scale(scale * h.LeftSideBearing)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// unscaledVMetric returns the unscaled vertical metrics for the glyph with
|
||||||
|
// the given index. yMax is the top of the glyph's bounding box.
|
||||||
|
func (f *Font) unscaledVMetric(i Index, yMax fixed.Int26_6) (v VMetric) {
|
||||||
|
j := int(i)
|
||||||
|
if j < 0 || f.nGlyph <= j {
|
||||||
|
return VMetric{}
|
||||||
|
}
|
||||||
|
if 4*j+4 <= len(f.vmtx) {
|
||||||
|
return VMetric{
|
||||||
|
AdvanceHeight: fixed.Int26_6(u16(f.vmtx, 4*j)),
|
||||||
|
TopSideBearing: fixed.Int26_6(int16(u16(f.vmtx, 4*j+2))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The OS/2 table has grown over time.
|
||||||
|
// https://developer.apple.com/fonts/TTRefMan/RM06/Chap6OS2.html
|
||||||
|
// says that it was originally 68 bytes. Optional fields, including
|
||||||
|
// the ascender and descender, are described at
|
||||||
|
// http://www.microsoft.com/typography/otspec/os2.htm
|
||||||
|
if len(f.os2) >= 72 {
|
||||||
|
sTypoAscender := fixed.Int26_6(int16(u16(f.os2, 68)))
|
||||||
|
sTypoDescender := fixed.Int26_6(int16(u16(f.os2, 70)))
|
||||||
|
return VMetric{
|
||||||
|
AdvanceHeight: sTypoAscender - sTypoDescender,
|
||||||
|
TopSideBearing: sTypoAscender - yMax,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return VMetric{
|
||||||
|
AdvanceHeight: fixed.Int26_6(f.fUnitsPerEm),
|
||||||
|
TopSideBearing: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VMetric returns the vertical metrics for the glyph with the given index.
|
||||||
|
func (f *Font) VMetric(scale fixed.Int26_6, i Index) VMetric {
|
||||||
|
// TODO: should 0 be bounds.YMax?
|
||||||
|
v := f.unscaledVMetric(i, 0)
|
||||||
|
v.AdvanceHeight = f.scale(scale * v.AdvanceHeight)
|
||||||
|
v.TopSideBearing = f.scale(scale * v.TopSideBearing)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kern returns the horizontal adjustment for the given glyph pair. A positive
|
||||||
|
// kern means to move the glyphs further apart.
|
||||||
|
func (f *Font) Kern(scale fixed.Int26_6, i0, i1 Index) fixed.Int26_6 {
|
||||||
|
if f.nKern == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
g := uint32(i0)<<16 | uint32(i1)
|
||||||
|
lo, hi := 0, f.nKern
|
||||||
|
for lo < hi {
|
||||||
|
i := (lo + hi) / 2
|
||||||
|
ig := u32(f.kern, 18+6*i)
|
||||||
|
if ig < g {
|
||||||
|
lo = i + 1
|
||||||
|
} else if ig > g {
|
||||||
|
hi = i
|
||||||
|
} else {
|
||||||
|
return f.scale(scale * fixed.Int26_6(int16(u16(f.kern, 22+6*i))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse returns a new Font for the given TTF or TTC data.
|
||||||
|
//
|
||||||
|
// For TrueType Collections, the first font in the collection is parsed.
|
||||||
|
func Parse(ttf []byte) (font *Font, err error) {
|
||||||
|
return parse(ttf, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(ttf []byte, offset int) (font *Font, err error) {
|
||||||
|
if len(ttf)-offset < 12 {
|
||||||
|
err = FormatError("TTF data is too short")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originalOffset := offset
|
||||||
|
magic, offset := u32(ttf, offset), offset+4
|
||||||
|
switch magic {
|
||||||
|
case 0x00010000:
|
||||||
|
// No-op.
|
||||||
|
case 0x74746366: // "ttcf" as a big-endian uint32.
|
||||||
|
if originalOffset != 0 {
|
||||||
|
err = FormatError("recursive TTC")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ttcVersion, offset := u32(ttf, offset), offset+4
|
||||||
|
if ttcVersion != 0x00010000 && ttcVersion != 0x00020000 {
|
||||||
|
err = FormatError("bad TTC version")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
numFonts, offset := int(u32(ttf, offset)), offset+4
|
||||||
|
if numFonts <= 0 {
|
||||||
|
err = FormatError("bad number of TTC fonts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ttf[offset:])/4 < numFonts {
|
||||||
|
err = FormatError("TTC offset table is too short")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: provide an API to select which font in a TrueType collection to return,
|
||||||
|
// not just the first one. This may require an API to parse a TTC's name tables,
|
||||||
|
// so users of this package can select the font in a TTC by name.
|
||||||
|
offset = int(u32(ttf, offset))
|
||||||
|
if offset <= 0 || offset > len(ttf) {
|
||||||
|
err = FormatError("bad TTC offset")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return parse(ttf, offset)
|
||||||
|
default:
|
||||||
|
err = FormatError("bad TTF version")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, offset := int(u16(ttf, offset)), offset+2
|
||||||
|
offset += 6 // Skip the searchRange, entrySelector and rangeShift.
|
||||||
|
if len(ttf) < 16*n+offset {
|
||||||
|
err = FormatError("TTF data is too short")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f := new(Font)
|
||||||
|
// Assign the table slices.
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
x := 16*i + offset
|
||||||
|
switch string(ttf[x : x+4]) {
|
||||||
|
case "cmap":
|
||||||
|
f.cmap, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "cvt ":
|
||||||
|
f.cvt, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "fpgm":
|
||||||
|
f.fpgm, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "glyf":
|
||||||
|
f.glyf, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "hdmx":
|
||||||
|
f.hdmx, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "head":
|
||||||
|
f.head, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "hhea":
|
||||||
|
f.hhea, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "hmtx":
|
||||||
|
f.hmtx, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "kern":
|
||||||
|
f.kern, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "loca":
|
||||||
|
f.loca, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "maxp":
|
||||||
|
f.maxp, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "name":
|
||||||
|
f.name, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "OS/2":
|
||||||
|
f.os2, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "prep":
|
||||||
|
f.prep, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
case "vmtx":
|
||||||
|
f.vmtx, err = readTable(ttf, ttf[x+8:x+16])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Parse and sanity-check the TTF data.
|
||||||
|
if err = f.parseHead(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = f.parseMaxp(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = f.parseCmap(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = f.parseKern(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = f.parseHhea(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
font = f
|
||||||
|
return
|
||||||
|
}
|
19
vendor/github.com/wcharczuk/go-chart/v2/.gitignore
generated
vendored
Normal file
19
vendor/github.com/wcharczuk/go-chart/v2/.gitignore
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, build with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||||
|
.glide/
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
coverage.html
|
1
vendor/github.com/wcharczuk/go-chart/v2/COVERAGE
generated
vendored
Normal file
1
vendor/github.com/wcharczuk/go-chart/v2/COVERAGE
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
29.02
|
21
vendor/github.com/wcharczuk/go-chart/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/wcharczuk/go-chart/v2/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 William Charczuk.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
10
vendor/github.com/wcharczuk/go-chart/v2/Makefile
generated
vendored
Normal file
10
vendor/github.com/wcharczuk/go-chart/v2/Makefile
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
all: new-install test
|
||||||
|
|
||||||
|
new-install:
|
||||||
|
@go get -v -u ./...
|
||||||
|
|
||||||
|
generate:
|
||||||
|
@go generate ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test ./...
|
4
vendor/github.com/wcharczuk/go-chart/v2/PROFANITY_RULES.yml
generated
vendored
Normal file
4
vendor/github.com/wcharczuk/go-chart/v2/PROFANITY_RULES.yml
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
go-sdk:
|
||||||
|
excludeFiles: [ "*_test.go" ]
|
||||||
|
importsContain: [ github.com/blend/go-sdk/* ]
|
||||||
|
description: "please don't use go-sdk in this repo"
|
95
vendor/github.com/wcharczuk/go-chart/v2/README.md
generated
vendored
Normal file
95
vendor/github.com/wcharczuk/go-chart/v2/README.md
generated
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
go-chart
|
||||||
|
========
|
||||||
|
[](https://circleci.com/gh/wcharczuk/go-chart) [](https://goreportcard.com/report/github.com/wcharczuk/go-chart)
|
||||||
|
|
||||||
|
Package `chart` is a very simple golang native charting library that supports timeseries and continuous line charts.
|
||||||
|
|
||||||
|
Master should now be on the v3.x codebase, which overhauls the api significantly. Per usual, see `examples` for more information.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
To install `chart` run the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> go get -u github.com/wcharczuk/go-chart
|
||||||
|
```
|
||||||
|
|
||||||
|
Most of the components are interchangeable so feel free to crib whatever you want.
|
||||||
|
|
||||||
|
# Output Examples
|
||||||
|
|
||||||
|
Spark Lines:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Single axis:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Two axis:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Other Chart Types
|
||||||
|
|
||||||
|
Pie Chart:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The code for this chart can be found in `examples/pie_chart/main.go`.
|
||||||
|
|
||||||
|
Stacked Bar:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The code for this chart can be found in `examples/stacked_bar/main.go`.
|
||||||
|
|
||||||
|
# Code Examples
|
||||||
|
|
||||||
|
Actual chart configurations and examples can be found in the `./examples/` directory. They are simple CLI programs that write to `output.png` (they are also updated with `go generate`.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Everything starts with the `chart.Chart` object. The bare minimum to draw a chart would be the following:
|
||||||
|
|
||||||
|
```golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
...
|
||||||
|
"bytes"
|
||||||
|
...
|
||||||
|
"github.com/wcharczuk/go-chart" //exposes "chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
XValues: []float64{1.0, 2.0, 3.0, 4.0},
|
||||||
|
YValues: []float64{1.0, 2.0, 3.0, 4.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.NewBuffer([]byte{})
|
||||||
|
err := graph.Render(chart.PNG, buffer)
|
||||||
|
```
|
||||||
|
|
||||||
|
Explanation of the above: A `chart` can have many `Series`, a `Series` is a collection of things that need to be drawn according to the X range and the Y range(s).
|
||||||
|
|
||||||
|
Here, we have a single series with x range values as float64s, rendered to a PNG. Note; we can pass any type of `io.Writer` into `Render(...)`, meaning that we can render the chart to a file or a resonse or anything else that implements `io.Writer`.
|
||||||
|
|
||||||
|
# API Overview
|
||||||
|
|
||||||
|
Everything on the `chart.Chart` object has defaults that can be overriden. Whenever a developer sets a property on the chart object, it is to be assumed that value will be used instead of the default.
|
||||||
|
|
||||||
|
The best way to see the api in action is to look at the examples in the `./_examples/` directory.
|
||||||
|
|
||||||
|
# Design Philosophy
|
||||||
|
|
||||||
|
I wanted to make a charting library that used only native golang, that could be stood up on a server (i.e. it had built in fonts).
|
||||||
|
|
||||||
|
The goal with the API itself is to have the "zero value be useful", and to require the user to not code more than they absolutely needed.
|
||||||
|
|
||||||
|
# Contributions
|
||||||
|
|
||||||
|
Contributions are welcome though this library is in a holding pattern for the forseable future.
|
91
vendor/github.com/wcharczuk/go-chart/v2/annotation_series.go
generated
vendored
Normal file
91
vendor/github.com/wcharczuk/go-chart/v2/annotation_series.go
generated
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*AnnotationSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnnotationSeries is a series of labels on the chart.
|
||||||
|
type AnnotationSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
Annotations []Value2
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (as AnnotationSeries) GetName() string {
|
||||||
|
return as.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (as AnnotationSeries) GetStyle() Style {
|
||||||
|
return as.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (as AnnotationSeries) GetYAxis() YAxisType {
|
||||||
|
return as.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style {
|
||||||
|
return Style{
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
Font: defaults.Font,
|
||||||
|
FillColor: DefaultAnnotationFillColor,
|
||||||
|
FontSize: DefaultAnnotationFontSize,
|
||||||
|
StrokeColor: defaults.StrokeColor,
|
||||||
|
StrokeWidth: defaults.StrokeWidth,
|
||||||
|
Padding: DefaultAnnotationPadding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure returns a bounds box of the series.
|
||||||
|
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
|
||||||
|
box := Box{
|
||||||
|
Top: math.MaxInt32,
|
||||||
|
Left: math.MaxInt32,
|
||||||
|
Right: 0,
|
||||||
|
Bottom: 0,
|
||||||
|
}
|
||||||
|
if !as.Style.Hidden {
|
||||||
|
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||||
|
for _, a := range as.Annotations {
|
||||||
|
style := a.Style.InheritFrom(seriesStyle)
|
||||||
|
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||||
|
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||||
|
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
|
box.Top = MinInt(box.Top, ab.Top)
|
||||||
|
box.Left = MinInt(box.Left, ab.Left)
|
||||||
|
box.Right = MaxInt(box.Right, ab.Right)
|
||||||
|
box.Bottom = MaxInt(box.Bottom, ab.Bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render draws the series.
|
||||||
|
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
if !as.Style.Hidden {
|
||||||
|
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||||
|
for _, a := range as.Annotations {
|
||||||
|
style := a.Style.InheritFrom(seriesStyle)
|
||||||
|
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||||
|
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||||
|
Draw.Annotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (as AnnotationSeries) Validate() error {
|
||||||
|
if len(as.Annotations) == 0 {
|
||||||
|
return fmt.Errorf("annotation series requires annotations to be set and not empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
24
vendor/github.com/wcharczuk/go-chart/v2/array.go
generated
vendored
Normal file
24
vendor/github.com/wcharczuk/go-chart/v2/array.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Sequence = (*Array)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewArray returns a new array from a given set of values.
|
||||||
|
// Array implements Sequence, which allows it to be used with the sequence helpers.
|
||||||
|
func NewArray(values ...float64) Array {
|
||||||
|
return Array(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array is a wrapper for an array of floats that implements `ValuesProvider`.
|
||||||
|
type Array []float64
|
||||||
|
|
||||||
|
// Len returns the value provider length.
|
||||||
|
func (a Array) Len() int {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value at a given index.
|
||||||
|
func (a Array) GetValue(index int) float64 {
|
||||||
|
return a[index]
|
||||||
|
}
|
45
vendor/github.com/wcharczuk/go-chart/v2/axis.go
generated
vendored
Normal file
45
vendor/github.com/wcharczuk/go-chart/v2/axis.go
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// TickPosition is an enumeration of possible tick drawing positions.
|
||||||
|
type TickPosition int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TickPositionUnset means to use the default tick position.
|
||||||
|
TickPositionUnset TickPosition = 0
|
||||||
|
// TickPositionBetweenTicks draws the labels for a tick between the previous and current tick.
|
||||||
|
TickPositionBetweenTicks TickPosition = 1
|
||||||
|
// TickPositionUnderTick draws the tick below the tick.
|
||||||
|
TickPositionUnderTick TickPosition = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// YAxisType is a type of y-axis; it can either be primary or secondary.
|
||||||
|
type YAxisType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// YAxisPrimary is the primary axis.
|
||||||
|
YAxisPrimary YAxisType = 0
|
||||||
|
// YAxisSecondary is the secondary axis.
|
||||||
|
YAxisSecondary YAxisType = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Axis is a chart feature detailing what values happen where.
|
||||||
|
type Axis interface {
|
||||||
|
GetName() string
|
||||||
|
SetName(name string)
|
||||||
|
|
||||||
|
GetStyle() Style
|
||||||
|
SetStyle(style Style)
|
||||||
|
|
||||||
|
GetTicks() []Tick
|
||||||
|
GenerateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick
|
||||||
|
|
||||||
|
// GenerateGridLines returns the gridlines for the axis.
|
||||||
|
GetGridLines(ticks []Tick) []GridLine
|
||||||
|
|
||||||
|
// Measure should return an absolute box for the axis.
|
||||||
|
// This is used when auto-fitting the canvas to the background.
|
||||||
|
Measure(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) Box
|
||||||
|
|
||||||
|
// Render renders the axis.
|
||||||
|
Render(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick)
|
||||||
|
}
|
516
vendor/github.com/wcharczuk/go-chart/v2/bar_chart.go
generated
vendored
Normal file
516
vendor/github.com/wcharczuk/go-chart/v2/bar_chart.go
generated
vendored
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BarChart is a chart that draws bars on a range.
|
||||||
|
type BarChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
BarWidth int
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
|
||||||
|
XAxis Style
|
||||||
|
YAxis YAxis
|
||||||
|
|
||||||
|
BarSpacing int
|
||||||
|
|
||||||
|
UseBaseValue bool
|
||||||
|
BaseValue float64
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Bars []Value
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (bc BarChart) GetDPI() float64 {
|
||||||
|
if bc.DPI == 0 {
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return bc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (bc BarChart) GetFont() *truetype.Font {
|
||||||
|
if bc.Font == nil {
|
||||||
|
return bc.defaultFont
|
||||||
|
}
|
||||||
|
return bc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (bc BarChart) GetWidth() int {
|
||||||
|
if bc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return bc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (bc BarChart) GetHeight() int {
|
||||||
|
if bc.Height == 0 {
|
||||||
|
return DefaultChartHeight
|
||||||
|
}
|
||||||
|
return bc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBarSpacing returns the spacing between bars.
|
||||||
|
func (bc BarChart) GetBarSpacing() int {
|
||||||
|
if bc.BarSpacing == 0 {
|
||||||
|
return DefaultBarSpacing
|
||||||
|
}
|
||||||
|
return bc.BarSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBarWidth returns the default bar width.
|
||||||
|
func (bc BarChart) GetBarWidth() int {
|
||||||
|
if bc.BarWidth == 0 {
|
||||||
|
return DefaultBarWidth
|
||||||
|
}
|
||||||
|
return bc.BarWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(bc.Bars) == 0 {
|
||||||
|
return errors.New("please provide at least one bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(bc.GetWidth(), bc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(bc.GetDPI())
|
||||||
|
|
||||||
|
bc.drawBackground(r)
|
||||||
|
|
||||||
|
var canvasBox Box
|
||||||
|
var yt []Tick
|
||||||
|
var yr Range
|
||||||
|
var yf ValueFormatter
|
||||||
|
|
||||||
|
canvasBox = bc.getDefaultCanvasBox()
|
||||||
|
yr = bc.getRanges()
|
||||||
|
if yr.GetMax()-yr.GetMin() == 0 {
|
||||||
|
return fmt.Errorf("invalid data range; cannot be zero")
|
||||||
|
}
|
||||||
|
yr = bc.setRangeDomains(canvasBox, yr)
|
||||||
|
yf = bc.getValueFormatters()
|
||||||
|
|
||||||
|
if bc.hasAxes() {
|
||||||
|
yt = bc.getAxesTicks(r, yr, yf)
|
||||||
|
canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
|
||||||
|
yr = bc.setRangeDomains(canvasBox, yr)
|
||||||
|
}
|
||||||
|
bc.drawCanvas(r, canvasBox)
|
||||||
|
bc.drawBars(r, canvasBox, yr)
|
||||||
|
bc.drawXAxis(r, canvasBox)
|
||||||
|
bc.drawYAxis(r, canvasBox, yr, yt)
|
||||||
|
|
||||||
|
bc.drawTitle(r)
|
||||||
|
for _, a := range bc.Elements {
|
||||||
|
a(r, canvasBox, bc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, bc.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getRanges() Range {
|
||||||
|
var yrange Range
|
||||||
|
if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
|
||||||
|
yrange = bc.YAxis.Range
|
||||||
|
} else {
|
||||||
|
yrange = &ContinuousRange{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !yrange.IsZero() {
|
||||||
|
return yrange
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bc.YAxis.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range bc.YAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
yrange.SetMin(tickMin)
|
||||||
|
yrange.SetMax(tickMax)
|
||||||
|
return yrange
|
||||||
|
}
|
||||||
|
|
||||||
|
min, max := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, b := range bc.Bars {
|
||||||
|
min = math.Min(b.Value, min)
|
||||||
|
max = math.Max(b.Value, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
yrange.SetMin(min)
|
||||||
|
yrange.SetMax(max)
|
||||||
|
|
||||||
|
return yrange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: bc.GetWidth(),
|
||||||
|
Bottom: bc.GetHeight(),
|
||||||
|
}, bc.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
|
||||||
|
xoffset := canvasBox.Left
|
||||||
|
|
||||||
|
width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
|
||||||
|
bs2 := spacing >> 1
|
||||||
|
|
||||||
|
var barBox Box
|
||||||
|
var bxl, bxr, by int
|
||||||
|
for index, bar := range bc.Bars {
|
||||||
|
bxl = xoffset + bs2
|
||||||
|
bxr = bxl + width
|
||||||
|
|
||||||
|
by = canvasBox.Bottom - yr.Translate(bar.Value)
|
||||||
|
|
||||||
|
if bc.UseBaseValue {
|
||||||
|
barBox = Box{
|
||||||
|
Top: by,
|
||||||
|
Left: bxl,
|
||||||
|
Right: bxr,
|
||||||
|
Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
barBox = Box{
|
||||||
|
Top: by,
|
||||||
|
Left: bxl,
|
||||||
|
Right: bxr,
|
||||||
|
Bottom: canvasBox.Bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
|
||||||
|
|
||||||
|
xoffset += width + spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
|
||||||
|
if !bc.XAxis.Hidden {
|
||||||
|
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for index, bar := range bc.Bars {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + width + spacing,
|
||||||
|
Bottom: bc.GetHeight(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bar.Label) > 0 {
|
||||||
|
Draw.TextWithin(r, bar.Label, barLabelBox, axisStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
if index < len(bc.Bars)-1 {
|
||||||
|
r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
cursor += width + spacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) {
|
||||||
|
if !bc.YAxis.Style.Hidden {
|
||||||
|
axisStyle := bc.YAxis.Style.InheritFrom(bc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Right, canvasBox.Top)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
var ty int
|
||||||
|
var tb Box
|
||||||
|
for _, t := range ticks {
|
||||||
|
ty = canvasBox.Bottom - yr.Translate(t.Value)
|
||||||
|
|
||||||
|
axisStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
|
r.MoveTo(canvasBox.Right, ty)
|
||||||
|
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
tb = r.MeasureText(t.Label)
|
||||||
|
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) drawTitle(r Renderer) {
|
||||||
|
if len(bc.Title) > 0 && !bc.TitleStyle.Hidden {
|
||||||
|
r.SetFont(bc.TitleStyle.GetFont(bc.GetFont()))
|
||||||
|
r.SetFontColor(bc.TitleStyle.GetFontColor(bc.GetColorPalette().TextColor()))
|
||||||
|
titleFontSize := bc.TitleStyle.GetFontSize(bc.getTitleFontSize())
|
||||||
|
r.SetFontSize(titleFontSize)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(bc.Title)
|
||||||
|
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
textHeight := textBox.Height()
|
||||||
|
|
||||||
|
titleX := (bc.GetWidth() >> 1) - (textWidth >> 1)
|
||||||
|
titleY := bc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||||
|
|
||||||
|
r.Text(bc.Title, titleX, titleY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getCanvasStyle() Style {
|
||||||
|
return bc.Canvas.InheritFrom(bc.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: bc.GetColorPalette().CanvasColor(),
|
||||||
|
StrokeColor: bc.GetColorPalette().CanvasStrokeColor(),
|
||||||
|
StrokeWidth: DefaultCanvasStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) hasAxes() bool {
|
||||||
|
return !bc.YAxis.Style.Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
|
||||||
|
yr.SetDomain(canvasBox.Height())
|
||||||
|
return yr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getDefaultCanvasBox() Box {
|
||||||
|
return bc.box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getValueFormatters() ValueFormatter {
|
||||||
|
if bc.YAxis.ValueFormatter != nil {
|
||||||
|
return bc.YAxis.ValueFormatter
|
||||||
|
}
|
||||||
|
return FloatValueFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
|
||||||
|
if !bc.YAxis.Style.Hidden {
|
||||||
|
yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateEffectiveBarSpacing(canvasBox Box) int {
|
||||||
|
totalWithBaseSpacing := bc.calculateTotalBarWidth(bc.GetBarWidth(), bc.GetBarSpacing())
|
||||||
|
if totalWithBaseSpacing > canvasBox.Width() {
|
||||||
|
lessBarWidths := canvasBox.Width() - (len(bc.Bars) * bc.GetBarWidth())
|
||||||
|
if lessBarWidths > 0 {
|
||||||
|
return int(math.Ceil(float64(lessBarWidths) / float64(len(bc.Bars))))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return bc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateEffectiveBarWidth(canvasBox Box, spacing int) int {
|
||||||
|
totalWithBaseWidth := bc.calculateTotalBarWidth(bc.GetBarWidth(), spacing)
|
||||||
|
if totalWithBaseWidth > canvasBox.Width() {
|
||||||
|
totalLessBarSpacings := canvasBox.Width() - (len(bc.Bars) * spacing)
|
||||||
|
if totalLessBarSpacings > 0 {
|
||||||
|
return int(math.Ceil(float64(totalLessBarSpacings) / float64(len(bc.Bars))))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return bc.GetBarWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateTotalBarWidth(barWidth, spacing int) int {
|
||||||
|
return len(bc.Bars) * (barWidth + spacing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) calculateScaledTotalWidth(canvasBox Box) (width, spacing, total int) {
|
||||||
|
spacing = bc.calculateEffectiveBarSpacing(canvasBox)
|
||||||
|
width = bc.calculateEffectiveBarWidth(canvasBox, spacing)
|
||||||
|
total = bc.calculateTotalBarWidth(width, spacing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range, yticks []Tick) Box {
|
||||||
|
axesOuterBox := canvasBox.Clone()
|
||||||
|
|
||||||
|
_, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
|
||||||
|
|
||||||
|
if !bc.XAxis.Hidden {
|
||||||
|
xaxisHeight := DefaultVerticalTickHeight
|
||||||
|
|
||||||
|
axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for _, bar := range bc.Bars {
|
||||||
|
if len(bar.Label) > 0 {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + bc.GetBarWidth() + bc.GetBarSpacing(),
|
||||||
|
Bottom: bc.GetHeight(),
|
||||||
|
}
|
||||||
|
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
|
xaxisHeight = MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xbox := Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Left + totalWidth,
|
||||||
|
Bottom: bc.GetHeight() - xaxisHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
axesOuterBox = axesOuterBox.Grow(xbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bc.YAxis.Style.Hidden {
|
||||||
|
axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
|
||||||
|
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.OuterConstrain(bc.box(), axesOuterBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
// box returns the chart bounds as a box.
|
||||||
|
func (bc BarChart) box() Box {
|
||||||
|
dpr := bc.Background.Padding.GetRight(10)
|
||||||
|
dpb := bc.Background.Padding.GetBottom(50)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: bc.Background.Padding.GetTop(20),
|
||||||
|
Left: bc.Background.Padding.GetLeft(20),
|
||||||
|
Right: bc.GetWidth() - dpr,
|
||||||
|
Bottom: bc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getBackgroundStyle() Style {
|
||||||
|
return bc.Background.InheritFrom(bc.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: bc.GetColorPalette().BackgroundColor(),
|
||||||
|
StrokeColor: bc.GetColorPalette().BackgroundStrokeColor(),
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsBar(index int) Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: bc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
StrokeWidth: 3.0,
|
||||||
|
FillColor: bc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsTitle() Style {
|
||||||
|
return bc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: bc.GetColorPalette().TextColor(),
|
||||||
|
Font: bc.GetFont(),
|
||||||
|
FontSize: bc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(bc.GetWidth(), bc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24
|
||||||
|
} else if effectiveDimension >= 512 {
|
||||||
|
return 18
|
||||||
|
} else if effectiveDimension >= 256 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsAxes() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: bc.GetColorPalette().AxisStrokeColor(),
|
||||||
|
Font: bc.GetFont(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
FontColor: bc.GetColorPalette().TextColor(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BarChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: bc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (bc BarChart) GetColorPalette() ColorPalette {
|
||||||
|
if bc.ColorPalette != nil {
|
||||||
|
return bc.ColorPalette
|
||||||
|
}
|
||||||
|
return AlternateColorPalette
|
||||||
|
}
|
135
vendor/github.com/wcharczuk/go-chart/v2/bollinger_band_series.go
generated
vendored
Normal file
135
vendor/github.com/wcharczuk/go-chart/v2/bollinger_band_series.go
generated
vendored
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*BollingerBandsSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// BollingerBandsSeries draws bollinger bands for an inner series.
|
||||||
|
// Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev.
|
||||||
|
type BollingerBandsSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Period int
|
||||||
|
K float64
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
valueBuffer *ValueBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (bbs BollingerBandsSeries) GetName() string {
|
||||||
|
return bbs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (bbs BollingerBandsSeries) GetStyle() Style {
|
||||||
|
return bbs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (bbs BollingerBandsSeries) GetYAxis() YAxisType {
|
||||||
|
return bbs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriod returns the window size.
|
||||||
|
func (bbs BollingerBandsSeries) GetPeriod() int {
|
||||||
|
if bbs.Period == 0 {
|
||||||
|
return DefaultSimpleMovingAveragePeriod
|
||||||
|
}
|
||||||
|
return bbs.Period
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetK returns the K value, or the number of standard deviations above and below
|
||||||
|
// to band the simple moving average with.
|
||||||
|
// Typical K value is 2.0.
|
||||||
|
func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
|
||||||
|
if bbs.K == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 2.0
|
||||||
|
}
|
||||||
|
return bbs.K
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (bbs BollingerBandsSeries) Len() int {
|
||||||
|
return bbs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoundedValues gets the bounded value for the series.
|
||||||
|
func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
|
||||||
|
if bbs.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bbs.valueBuffer == nil || index == 0 {
|
||||||
|
bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod())
|
||||||
|
}
|
||||||
|
if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
|
||||||
|
bbs.valueBuffer.Dequeue()
|
||||||
|
}
|
||||||
|
px, py := bbs.InnerSeries.GetValues(index)
|
||||||
|
bbs.valueBuffer.Enqueue(py)
|
||||||
|
x = px
|
||||||
|
|
||||||
|
ay := Seq{bbs.valueBuffer}.Average()
|
||||||
|
std := Seq{bbs.valueBuffer}.StdDev()
|
||||||
|
|
||||||
|
y1 = ay + (bbs.GetK() * std)
|
||||||
|
y2 = ay - (bbs.GetK() * std)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoundedLastValues returns the last bounded value for the series.
|
||||||
|
func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
|
||||||
|
if bbs.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
period := bbs.GetPeriod()
|
||||||
|
seriesLength := bbs.InnerSeries.Len()
|
||||||
|
startAt := seriesLength - period
|
||||||
|
if startAt < 0 {
|
||||||
|
startAt = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
vb := NewValueBufferWithCapacity(period)
|
||||||
|
for index := startAt; index < seriesLength; index++ {
|
||||||
|
xn, yn := bbs.InnerSeries.GetValues(index)
|
||||||
|
vb.Enqueue(yn)
|
||||||
|
x = xn
|
||||||
|
}
|
||||||
|
|
||||||
|
ay := Seq{vb}.Average()
|
||||||
|
std := Seq{vb}.StdDev()
|
||||||
|
|
||||||
|
y1 = ay + (bbs.GetK() * std)
|
||||||
|
y2 = ay - (bbs.GetK() * std)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{
|
||||||
|
StrokeWidth: 1.0,
|
||||||
|
StrokeColor: DefaultAxisColor.WithAlpha(64),
|
||||||
|
FillColor: DefaultAxisColor.WithAlpha(32),
|
||||||
|
}))
|
||||||
|
|
||||||
|
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (bbs BollingerBandsSeries) Validate() error {
|
||||||
|
if bbs.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("bollinger bands series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
36
vendor/github.com/wcharczuk/go-chart/v2/bounded_last_values_annotation_series.go
generated
vendored
Normal file
36
vendor/github.com/wcharczuk/go-chart/v2/bounded_last_values_annotation_series.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// BoundedLastValuesAnnotationSeries returns a last value annotation series for a bounded values provider.
|
||||||
|
func BoundedLastValuesAnnotationSeries(innerSeries FullBoundedValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
|
||||||
|
lvx, lvy1, lvy2 := innerSeries.GetBoundedLastValues()
|
||||||
|
|
||||||
|
var vf ValueFormatter
|
||||||
|
if len(vfs) > 0 {
|
||||||
|
vf = vfs[0]
|
||||||
|
} else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
|
||||||
|
_, vf = typed.GetValueFormatters()
|
||||||
|
} else {
|
||||||
|
vf = FloatValueFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
label1 := vf(lvy1)
|
||||||
|
label2 := vf(lvy2)
|
||||||
|
|
||||||
|
var seriesName string
|
||||||
|
var seriesStyle Style
|
||||||
|
if typed, isTyped := innerSeries.(Series); isTyped {
|
||||||
|
seriesName = fmt.Sprintf("%s - Last Values", typed.GetName())
|
||||||
|
seriesStyle = typed.GetStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnnotationSeries{
|
||||||
|
Name: seriesName,
|
||||||
|
Style: seriesStyle,
|
||||||
|
Annotations: []Value2{
|
||||||
|
{XValue: lvx, YValue: lvy1, Label: label1},
|
||||||
|
{XValue: lvx, YValue: lvy2, Label: label2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
351
vendor/github.com/wcharczuk/go-chart/v2/box.go
generated
vendored
Normal file
351
vendor/github.com/wcharczuk/go-chart/v2/box.go
generated
vendored
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// BoxZero is a preset box that represents an intentional zero value.
|
||||||
|
BoxZero = Box{IsSet: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBox returns a new (set) box.
|
||||||
|
func NewBox(top, left, right, bottom int) Box {
|
||||||
|
return Box{
|
||||||
|
IsSet: true,
|
||||||
|
Top: top,
|
||||||
|
Left: left,
|
||||||
|
Right: right,
|
||||||
|
Bottom: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box represents the main 4 dimensions of a box.
|
||||||
|
type Box struct {
|
||||||
|
Top int
|
||||||
|
Left int
|
||||||
|
Right int
|
||||||
|
Bottom int
|
||||||
|
IsSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the box is set or not.
|
||||||
|
func (b Box) IsZero() bool {
|
||||||
|
if b.IsSet {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the box.
|
||||||
|
func (b Box) String() string {
|
||||||
|
return fmt.Sprintf("box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTop returns a coalesced value with a default.
|
||||||
|
func (b Box) GetTop(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Top == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Top
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeft returns a coalesced value with a default.
|
||||||
|
func (b Box) GetLeft(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Left == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Left
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRight returns a coalesced value with a default.
|
||||||
|
func (b Box) GetRight(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Right == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBottom returns a coalesced value with a default.
|
||||||
|
func (b Box) GetBottom(defaults ...int) int {
|
||||||
|
if !b.IsSet && b.Bottom == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.Bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the width
|
||||||
|
func (b Box) Width() int {
|
||||||
|
return AbsInt(b.Right - b.Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the height
|
||||||
|
func (b Box) Height() int {
|
||||||
|
return AbsInt(b.Bottom - b.Top)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center returns the center of the box
|
||||||
|
func (b Box) Center() (x, y int) {
|
||||||
|
w2, h2 := b.Width()>>1, b.Height()>>1
|
||||||
|
return b.Left + w2, b.Top + h2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspect returns the aspect ratio of the box.
|
||||||
|
func (b Box) Aspect() float64 {
|
||||||
|
return float64(b.Width()) / float64(b.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a new copy of the box.
|
||||||
|
func (b Box) Clone() Box {
|
||||||
|
return Box{
|
||||||
|
IsSet: b.IsSet,
|
||||||
|
Top: b.Top,
|
||||||
|
Left: b.Left,
|
||||||
|
Right: b.Right,
|
||||||
|
Bottom: b.Bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBiggerThan returns if a box is bigger than another box.
|
||||||
|
func (b Box) IsBiggerThan(other Box) bool {
|
||||||
|
return b.Top < other.Top ||
|
||||||
|
b.Bottom > other.Bottom ||
|
||||||
|
b.Left < other.Left ||
|
||||||
|
b.Right > other.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSmallerThan returns if a box is smaller than another box.
|
||||||
|
func (b Box) IsSmallerThan(other Box) bool {
|
||||||
|
return b.Top > other.Top &&
|
||||||
|
b.Bottom < other.Bottom &&
|
||||||
|
b.Left > other.Left &&
|
||||||
|
b.Right < other.Right
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if the box equals another box.
|
||||||
|
func (b Box) Equals(other Box) bool {
|
||||||
|
return b.Top == other.Top &&
|
||||||
|
b.Left == other.Left &&
|
||||||
|
b.Right == other.Right &&
|
||||||
|
b.Bottom == other.Bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow grows a box based on another box.
|
||||||
|
func (b Box) Grow(other Box) Box {
|
||||||
|
return Box{
|
||||||
|
Top: MinInt(b.Top, other.Top),
|
||||||
|
Left: MinInt(b.Left, other.Left),
|
||||||
|
Right: MaxInt(b.Right, other.Right),
|
||||||
|
Bottom: MaxInt(b.Bottom, other.Bottom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift pushes a box by x,y.
|
||||||
|
func (b Box) Shift(x, y int) Box {
|
||||||
|
return Box{
|
||||||
|
Top: b.Top + y,
|
||||||
|
Left: b.Left + x,
|
||||||
|
Right: b.Right + x,
|
||||||
|
Bottom: b.Bottom + y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners returns the box as a set of corners.
|
||||||
|
func (b Box) Corners() BoxCorners {
|
||||||
|
return BoxCorners{
|
||||||
|
TopLeft: Point{b.Left, b.Top},
|
||||||
|
TopRight: Point{b.Right, b.Top},
|
||||||
|
BottomRight: Point{b.Right, b.Bottom},
|
||||||
|
BottomLeft: Point{b.Left, b.Bottom},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fit is functionally the inverse of grow.
|
||||||
|
// Fit maintains the original aspect ratio of the `other` box,
|
||||||
|
// but constrains it to the bounds of the target box.
|
||||||
|
func (b Box) Fit(other Box) Box {
|
||||||
|
ba := b.Aspect()
|
||||||
|
oa := other.Aspect()
|
||||||
|
|
||||||
|
if oa == ba {
|
||||||
|
return b.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
bw, bh := float64(b.Width()), float64(b.Height())
|
||||||
|
bw2 := int(bw) >> 1
|
||||||
|
bh2 := int(bh) >> 1
|
||||||
|
if oa > ba { // ex. 16:9 vs. 4:3
|
||||||
|
var noh2 int
|
||||||
|
if oa > 1.0 {
|
||||||
|
noh2 = int(bw/oa) >> 1
|
||||||
|
} else {
|
||||||
|
noh2 = int(bh*oa) >> 1
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: (b.Top + bh2) - noh2,
|
||||||
|
Left: b.Left,
|
||||||
|
Right: b.Right,
|
||||||
|
Bottom: (b.Top + bh2) + noh2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var now2 int
|
||||||
|
if oa > 1.0 {
|
||||||
|
now2 = int(bh/oa) >> 1
|
||||||
|
} else {
|
||||||
|
now2 = int(bw*oa) >> 1
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: b.Top,
|
||||||
|
Left: (b.Left + bw2) - now2,
|
||||||
|
Right: (b.Left + bw2) + now2,
|
||||||
|
Bottom: b.Bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constrain is similar to `Fit` except that it will work
|
||||||
|
// more literally like the opposite of grow.
|
||||||
|
func (b Box) Constrain(other Box) Box {
|
||||||
|
newBox := b.Clone()
|
||||||
|
|
||||||
|
newBox.Top = MaxInt(newBox.Top, other.Top)
|
||||||
|
newBox.Left = MaxInt(newBox.Left, other.Left)
|
||||||
|
newBox.Right = MinInt(newBox.Right, other.Right)
|
||||||
|
newBox.Bottom = MinInt(newBox.Bottom, other.Bottom)
|
||||||
|
|
||||||
|
return newBox
|
||||||
|
}
|
||||||
|
|
||||||
|
// OuterConstrain is similar to `Constraint` with the difference
|
||||||
|
// that it applies corrections
|
||||||
|
func (b Box) OuterConstrain(bounds, other Box) Box {
|
||||||
|
newBox := b.Clone()
|
||||||
|
if other.Top < bounds.Top {
|
||||||
|
delta := bounds.Top - other.Top
|
||||||
|
newBox.Top = b.Top + delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Left < bounds.Left {
|
||||||
|
delta := bounds.Left - other.Left
|
||||||
|
newBox.Left = b.Left + delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Right > bounds.Right {
|
||||||
|
delta := other.Right - bounds.Right
|
||||||
|
newBox.Right = b.Right - delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Bottom > bounds.Bottom {
|
||||||
|
delta := other.Bottom - bounds.Bottom
|
||||||
|
newBox.Bottom = b.Bottom - delta
|
||||||
|
}
|
||||||
|
return newBox
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoxCorners is a box with independent corners.
|
||||||
|
type BoxCorners struct {
|
||||||
|
TopLeft, TopRight, BottomRight, BottomLeft Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box return the BoxCorners as a regular box.
|
||||||
|
func (bc BoxCorners) Box() Box {
|
||||||
|
return Box{
|
||||||
|
Top: MinInt(bc.TopLeft.Y, bc.TopRight.Y),
|
||||||
|
Left: MinInt(bc.TopLeft.X, bc.BottomLeft.X),
|
||||||
|
Right: MaxInt(bc.TopRight.X, bc.BottomRight.X),
|
||||||
|
Bottom: MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the width
|
||||||
|
func (bc BoxCorners) Width() int {
|
||||||
|
minLeft := MinInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
maxRight := MaxInt(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
return maxRight - minLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the height
|
||||||
|
func (bc BoxCorners) Height() int {
|
||||||
|
minTop := MinInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
maxBottom := MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||||
|
return maxBottom - minTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center returns the center of the box
|
||||||
|
func (bc BoxCorners) Center() (x, y int) {
|
||||||
|
|
||||||
|
left := MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
right := MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
x = ((right - left) >> 1) + left
|
||||||
|
|
||||||
|
top := MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
bottom := MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||||
|
y = ((bottom - top) >> 1) + top
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate rotates the box.
|
||||||
|
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
|
||||||
|
cx, cy := bc.Center()
|
||||||
|
|
||||||
|
thetaRadians := DegreesToRadians(thetaDegrees)
|
||||||
|
|
||||||
|
tlx, tly := RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
|
||||||
|
trx, try := RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
|
||||||
|
brx, bry := RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
|
||||||
|
blx, bly := RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
|
||||||
|
|
||||||
|
return BoxCorners{
|
||||||
|
TopLeft: Point{tlx, tly},
|
||||||
|
TopRight: Point{trx, try},
|
||||||
|
BottomRight: Point{brx, bry},
|
||||||
|
BottomLeft: Point{blx, bly},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if the box equals another box.
|
||||||
|
func (bc BoxCorners) Equals(other BoxCorners) bool {
|
||||||
|
return bc.TopLeft.Equals(other.TopLeft) &&
|
||||||
|
bc.TopRight.Equals(other.TopRight) &&
|
||||||
|
bc.BottomRight.Equals(other.BottomRight) &&
|
||||||
|
bc.BottomLeft.Equals(other.BottomLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BoxCorners) String() string {
|
||||||
|
return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point is an X,Y pair
|
||||||
|
type Point struct {
|
||||||
|
X, Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceTo calculates the distance to another point.
|
||||||
|
func (p Point) DistanceTo(other Point) float64 {
|
||||||
|
dx := math.Pow(float64(p.X-other.X), 2)
|
||||||
|
dy := math.Pow(float64(p.Y-other.Y), 2)
|
||||||
|
return math.Pow(dx+dy, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if a point equals another point.
|
||||||
|
func (p Point) Equals(other Point) bool {
|
||||||
|
return p.X == other.X && p.Y == other.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the point.
|
||||||
|
func (p Point) String() string {
|
||||||
|
return fmt.Sprintf("P{%d,%d}", p.X, p.Y)
|
||||||
|
}
|
577
vendor/github.com/wcharczuk/go-chart/v2/chart.go
generated
vendored
Normal file
577
vendor/github.com/wcharczuk/go-chart/v2/chart.go
generated
vendored
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chart is what we're drawing.
|
||||||
|
type Chart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
|
||||||
|
XAxis XAxis
|
||||||
|
YAxis YAxis
|
||||||
|
YAxisSecondary YAxis
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Series []Series
|
||||||
|
Elements []Renderable
|
||||||
|
|
||||||
|
Log Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (c Chart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if c.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return c.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (c Chart) GetFont() *truetype.Font {
|
||||||
|
if c.Font == nil {
|
||||||
|
return c.defaultFont
|
||||||
|
}
|
||||||
|
return c.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (c Chart) GetWidth() int {
|
||||||
|
if c.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return c.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (c Chart) GetHeight() int {
|
||||||
|
if c.Height == 0 {
|
||||||
|
return DefaultChartHeight
|
||||||
|
}
|
||||||
|
return c.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(c.Series) == 0 {
|
||||||
|
return errors.New("please provide at least one series")
|
||||||
|
}
|
||||||
|
if err := c.checkHasVisibleSeries(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.YAxisSecondary.AxisType = YAxisSecondary
|
||||||
|
|
||||||
|
r, err := rp(c.GetWidth(), c.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(c.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
c.drawBackground(r)
|
||||||
|
|
||||||
|
var xt, yt, yta []Tick
|
||||||
|
xr, yr, yra := c.getRanges()
|
||||||
|
canvasBox := c.getDefaultCanvasBox()
|
||||||
|
xf, yf, yfa := c.getValueFormatters()
|
||||||
|
|
||||||
|
Debugf(c.Log, "chart; canvas box: %v", canvasBox)
|
||||||
|
|
||||||
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
|
||||||
|
err = c.checkRanges(xr, yr, yra)
|
||||||
|
if err != nil {
|
||||||
|
r.Save(w)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasAxes() {
|
||||||
|
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||||
|
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||||
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
|
||||||
|
Debugf(c.Log, "chart; axes adjusted canvas box: %v", canvasBox)
|
||||||
|
|
||||||
|
// do a second pass in case things haven't settled yet.
|
||||||
|
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||||
|
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||||
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasAnnotationSeries() {
|
||||||
|
canvasBox = c.getAnnotationAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xf, yf, yfa)
|
||||||
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||||
|
|
||||||
|
Debugf(c.Log, "chart; annotation adjusted canvas box: %v", canvasBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.drawCanvas(r, canvasBox)
|
||||||
|
c.drawAxes(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||||
|
for index, series := range c.Series {
|
||||||
|
c.drawSeries(r, canvasBox, xr, yr, yra, series, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.drawTitle(r)
|
||||||
|
|
||||||
|
for _, a := range c.Elements {
|
||||||
|
a(r, canvasBox, c.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) checkHasVisibleSeries() error {
|
||||||
|
var style Style
|
||||||
|
for _, s := range c.Series {
|
||||||
|
style = s.GetStyle()
|
||||||
|
if !style.Hidden {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("chart render; must have (1) visible series")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) validateSeries() error {
|
||||||
|
var err error
|
||||||
|
for _, s := range c.Series {
|
||||||
|
err = s.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
|
var minx, maxx float64 = math.MaxFloat64, -math.MaxFloat64
|
||||||
|
var miny, maxy float64 = math.MaxFloat64, -math.MaxFloat64
|
||||||
|
var minya, maxya float64 = math.MaxFloat64, -math.MaxFloat64
|
||||||
|
|
||||||
|
seriesMappedToSecondaryAxis := false
|
||||||
|
|
||||||
|
// note: a possible future optimization is to not scan the series values if
|
||||||
|
// all axis are represented by either custom ticks or custom ranges.
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if !s.GetStyle().Hidden {
|
||||||
|
seriesAxis := s.GetYAxis()
|
||||||
|
if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
|
||||||
|
seriesLength := bvp.Len()
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy1, vy2 := bvp.GetBoundedValues(index)
|
||||||
|
|
||||||
|
minx = math.Min(minx, vx)
|
||||||
|
maxx = math.Max(maxx, vx)
|
||||||
|
|
||||||
|
if seriesAxis == YAxisPrimary {
|
||||||
|
miny = math.Min(miny, vy1)
|
||||||
|
miny = math.Min(miny, vy2)
|
||||||
|
maxy = math.Max(maxy, vy1)
|
||||||
|
maxy = math.Max(maxy, vy2)
|
||||||
|
} else if seriesAxis == YAxisSecondary {
|
||||||
|
minya = math.Min(minya, vy1)
|
||||||
|
minya = math.Min(minya, vy2)
|
||||||
|
maxya = math.Max(maxya, vy1)
|
||||||
|
maxya = math.Max(maxya, vy2)
|
||||||
|
seriesMappedToSecondaryAxis = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||||
|
seriesLength := vp.Len()
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy := vp.GetValues(index)
|
||||||
|
|
||||||
|
minx = math.Min(minx, vx)
|
||||||
|
maxx = math.Max(maxx, vx)
|
||||||
|
|
||||||
|
if seriesAxis == YAxisPrimary {
|
||||||
|
miny = math.Min(miny, vy)
|
||||||
|
maxy = math.Max(maxy, vy)
|
||||||
|
} else if seriesAxis == YAxisSecondary {
|
||||||
|
minya = math.Min(minya, vy)
|
||||||
|
maxya = math.Max(maxya, vy)
|
||||||
|
seriesMappedToSecondaryAxis = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.XAxis.Range == nil {
|
||||||
|
xrange = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
xrange = c.XAxis.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.YAxis.Range == nil {
|
||||||
|
yrange = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
yrange = c.YAxis.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.YAxisSecondary.Range == nil {
|
||||||
|
yrangeAlt = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
yrangeAlt = c.YAxisSecondary.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.XAxis.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range c.XAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
xrange.SetMin(tickMin)
|
||||||
|
xrange.SetMax(tickMax)
|
||||||
|
} else if xrange.IsZero() {
|
||||||
|
xrange.SetMin(minx)
|
||||||
|
xrange.SetMax(maxx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.YAxis.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range c.YAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
yrange.SetMin(tickMin)
|
||||||
|
yrange.SetMax(tickMax)
|
||||||
|
} else if yrange.IsZero() {
|
||||||
|
yrange.SetMin(miny)
|
||||||
|
yrange.SetMax(maxy)
|
||||||
|
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
delta := yrange.GetDelta()
|
||||||
|
roundTo := GetRoundToForDelta(delta)
|
||||||
|
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
|
||||||
|
|
||||||
|
yrange.SetMin(rmin)
|
||||||
|
yrange.SetMax(rmax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.YAxisSecondary.Ticks) > 0 {
|
||||||
|
tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
|
||||||
|
for _, t := range c.YAxis.Ticks {
|
||||||
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
|
}
|
||||||
|
yrangeAlt.SetMin(tickMin)
|
||||||
|
yrangeAlt.SetMax(tickMax)
|
||||||
|
} else if seriesMappedToSecondaryAxis && yrangeAlt.IsZero() {
|
||||||
|
yrangeAlt.SetMin(minya)
|
||||||
|
yrangeAlt.SetMax(maxya)
|
||||||
|
|
||||||
|
if !c.YAxisSecondary.Style.Hidden {
|
||||||
|
delta := yrangeAlt.GetDelta()
|
||||||
|
roundTo := GetRoundToForDelta(delta)
|
||||||
|
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||||
|
yrangeAlt.SetMin(rmin)
|
||||||
|
yrangeAlt.SetMax(rmax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) checkRanges(xr, yr, yra Range) error {
|
||||||
|
Debugf(c.Log, "checking xrange: %v", xr)
|
||||||
|
xDelta := xr.GetDelta()
|
||||||
|
if math.IsInf(xDelta, 0) {
|
||||||
|
return errors.New("infinite x-range delta")
|
||||||
|
}
|
||||||
|
if math.IsNaN(xDelta) {
|
||||||
|
return errors.New("nan x-range delta")
|
||||||
|
}
|
||||||
|
if xDelta == 0 {
|
||||||
|
return errors.New("zero x-range delta; there needs to be at least (2) values")
|
||||||
|
}
|
||||||
|
|
||||||
|
Debugf(c.Log, "checking yrange: %v", yr)
|
||||||
|
yDelta := yr.GetDelta()
|
||||||
|
if math.IsInf(yDelta, 0) {
|
||||||
|
return errors.New("infinite y-range delta")
|
||||||
|
}
|
||||||
|
if math.IsNaN(yDelta) {
|
||||||
|
return errors.New("nan y-range delta")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasSecondarySeries() {
|
||||||
|
Debugf(c.Log, "checking secondary yrange: %v", yra)
|
||||||
|
yraDelta := yra.GetDelta()
|
||||||
|
if math.IsInf(yraDelta, 0) {
|
||||||
|
return errors.New("infinite secondary y-range delta")
|
||||||
|
}
|
||||||
|
if math.IsNaN(yraDelta) {
|
||||||
|
return errors.New("nan secondary y-range delta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getDefaultCanvasBox() Box {
|
||||||
|
return c.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if vfp, isVfp := s.(ValueFormatterProvider); isVfp {
|
||||||
|
sx, sy := vfp.GetValueFormatters()
|
||||||
|
if s.GetYAxis() == YAxisPrimary {
|
||||||
|
x = sx
|
||||||
|
y = sy
|
||||||
|
} else if s.GetYAxis() == YAxisSecondary {
|
||||||
|
x = sx
|
||||||
|
ya = sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.XAxis.ValueFormatter != nil {
|
||||||
|
x = c.XAxis.GetValueFormatter()
|
||||||
|
}
|
||||||
|
if c.YAxis.ValueFormatter != nil {
|
||||||
|
y = c.YAxis.GetValueFormatter()
|
||||||
|
}
|
||||||
|
if c.YAxisSecondary.ValueFormatter != nil {
|
||||||
|
ya = c.YAxisSecondary.GetValueFormatter()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) hasAxes() bool {
|
||||||
|
return !c.XAxis.Style.Hidden || !c.YAxis.Style.Hidden || !c.YAxisSecondary.Style.Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) {
|
||||||
|
if !c.XAxis.Style.Hidden {
|
||||||
|
xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf)
|
||||||
|
}
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf)
|
||||||
|
}
|
||||||
|
if !c.YAxisSecondary.Style.Hidden {
|
||||||
|
yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
|
||||||
|
axesOuterBox := canvasBox.Clone()
|
||||||
|
if !c.XAxis.Style.Hidden {
|
||||||
|
axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
|
||||||
|
Debugf(c.Log, "chart; x-axis measured %v", axesBounds)
|
||||||
|
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||||
|
}
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
axesBounds := c.YAxis.Measure(r, canvasBox, yr, c.styleDefaultsAxes(), yticks)
|
||||||
|
Debugf(c.Log, "chart; y-axis measured %v", axesBounds)
|
||||||
|
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||||
|
}
|
||||||
|
if !c.YAxisSecondary.Style.Hidden && c.hasSecondarySeries() {
|
||||||
|
axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt)
|
||||||
|
Debugf(c.Log, "chart; y-axis secondary measured %v", axesBounds)
|
||||||
|
axesOuterBox = axesOuterBox.Grow(axesBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.OuterConstrain(c.Box(), axesOuterBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, Range) {
|
||||||
|
xr.SetDomain(canvasBox.Width())
|
||||||
|
yr.SetDomain(canvasBox.Height())
|
||||||
|
yra.SetDomain(canvasBox.Height())
|
||||||
|
return xr, yr, yra
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) hasAnnotationSeries() bool {
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
|
||||||
|
if !as.GetStyle().Hidden {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) hasSecondarySeries() bool {
|
||||||
|
for _, s := range c.Series {
|
||||||
|
if s.GetYAxis() == YAxisSecondary {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xf, yf, yfa ValueFormatter) Box {
|
||||||
|
annotationSeriesBox := canvasBox.Clone()
|
||||||
|
for seriesIndex, s := range c.Series {
|
||||||
|
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
|
||||||
|
if !as.GetStyle().Hidden {
|
||||||
|
style := c.styleDefaultsSeries(seriesIndex)
|
||||||
|
var annotationBounds Box
|
||||||
|
if as.YAxis == YAxisPrimary {
|
||||||
|
annotationBounds = as.Measure(r, canvasBox, xr, yr, style)
|
||||||
|
} else if as.YAxis == YAxisSecondary {
|
||||||
|
annotationBounds = as.Measure(r, canvasBox, xr, yra, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationSeriesBox = annotationSeriesBox.Grow(annotationBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.OuterConstrain(c.Box(), annotationSeriesBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getBackgroundStyle() Style {
|
||||||
|
return c.Background.InheritFrom(c.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: c.GetWidth(),
|
||||||
|
Bottom: c.GetHeight(),
|
||||||
|
}, c.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getCanvasStyle() Style {
|
||||||
|
return c.Canvas.InheritFrom(c.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, c.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
|
||||||
|
if !c.XAxis.Style.Hidden {
|
||||||
|
c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks)
|
||||||
|
}
|
||||||
|
if !c.YAxis.Style.Hidden {
|
||||||
|
c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks)
|
||||||
|
}
|
||||||
|
if !c.YAxisSecondary.Style.Hidden {
|
||||||
|
c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, c.styleDefaultsAxes(), yticksAlt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) {
|
||||||
|
if !s.GetStyle().Hidden {
|
||||||
|
if s.GetYAxis() == YAxisPrimary {
|
||||||
|
s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex))
|
||||||
|
} else if s.GetYAxis() == YAxisSecondary {
|
||||||
|
s.Render(r, canvasBox, xrange, yrangeAlt, c.styleDefaultsSeries(seriesIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawTitle(r Renderer) {
|
||||||
|
if len(c.Title) > 0 && !c.TitleStyle.Hidden {
|
||||||
|
r.SetFont(c.TitleStyle.GetFont(c.GetFont()))
|
||||||
|
r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
|
||||||
|
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
|
||||||
|
r.SetFontSize(titleFontSize)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(c.Title)
|
||||||
|
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
textHeight := textBox.Height()
|
||||||
|
|
||||||
|
titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
|
||||||
|
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||||
|
|
||||||
|
r.Text(c.Title, titleX, titleY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: c.GetColorPalette().BackgroundColor(),
|
||||||
|
StrokeColor: c.GetColorPalette().BackgroundStrokeColor(),
|
||||||
|
StrokeWidth: DefaultBackgroundStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: c.GetColorPalette().CanvasColor(),
|
||||||
|
StrokeColor: c.GetColorPalette().CanvasStrokeColor(),
|
||||||
|
StrokeWidth: DefaultCanvasStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
||||||
|
return Style{
|
||||||
|
DotColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
|
||||||
|
StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
|
||||||
|
StrokeWidth: DefaultSeriesLineWidth,
|
||||||
|
Font: c.GetFont(),
|
||||||
|
FontSize: DefaultFontSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsAxes() Style {
|
||||||
|
return Style{
|
||||||
|
Font: c.GetFont(),
|
||||||
|
FontColor: c.GetColorPalette().TextColor(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
StrokeColor: c.GetColorPalette().AxisStrokeColor(),
|
||||||
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: c.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (c Chart) GetColorPalette() ColorPalette {
|
||||||
|
if c.ColorPalette != nil {
|
||||||
|
return c.ColorPalette
|
||||||
|
}
|
||||||
|
return DefaultColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (c Chart) Box() Box {
|
||||||
|
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||||
|
dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||||
|
Left: c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
|
||||||
|
Right: c.GetWidth() - dpr,
|
||||||
|
Bottom: c.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
184
vendor/github.com/wcharczuk/go-chart/v2/colors.go
generated
vendored
Normal file
184
vendor/github.com/wcharczuk/go-chart/v2/colors.go
generated
vendored
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ColorWhite is white.
|
||||||
|
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
// ColorBlue is the basic theme blue color.
|
||||||
|
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
|
||||||
|
// ColorCyan is the basic theme cyan color.
|
||||||
|
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
|
||||||
|
// ColorGreen is the basic theme green color.
|
||||||
|
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
|
||||||
|
// ColorRed is the basic theme red color.
|
||||||
|
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
|
||||||
|
// ColorOrange is the basic theme orange color.
|
||||||
|
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
|
||||||
|
// ColorYellow is the basic theme yellow color.
|
||||||
|
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
|
||||||
|
// ColorBlack is the basic theme black color.
|
||||||
|
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
||||||
|
// ColorLightGray is the basic theme light gray color.
|
||||||
|
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
||||||
|
|
||||||
|
// ColorAlternateBlue is a alternate theme color.
|
||||||
|
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
|
||||||
|
// ColorAlternateGreen is a alternate theme color.
|
||||||
|
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
|
||||||
|
// ColorAlternateGray is a alternate theme color.
|
||||||
|
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
|
||||||
|
// ColorAlternateYellow is a alternate theme color.
|
||||||
|
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
|
||||||
|
// ColorAlternateLightGray is a alternate theme color.
|
||||||
|
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
|
||||||
|
|
||||||
|
// ColorTransparent is a transparent (alpha zero) color.
|
||||||
|
ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultBackgroundColor is the default chart background color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultBackgroundColor = ColorWhite
|
||||||
|
// DefaultBackgroundStrokeColor is the default chart border color.
|
||||||
|
// It is equivalent to color:white.
|
||||||
|
DefaultBackgroundStrokeColor = ColorWhite
|
||||||
|
// DefaultCanvasColor is the default chart canvas color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultCanvasColor = ColorWhite
|
||||||
|
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultCanvasStrokeColor = ColorWhite
|
||||||
|
// DefaultTextColor is the default chart text color.
|
||||||
|
// It is equivalent to #333333.
|
||||||
|
DefaultTextColor = ColorBlack
|
||||||
|
// DefaultAxisColor is the default chart axis line color.
|
||||||
|
// It is equivalent to #333333.
|
||||||
|
DefaultAxisColor = ColorBlack
|
||||||
|
// DefaultStrokeColor is the default chart border color.
|
||||||
|
// It is equivalent to #efefef.
|
||||||
|
DefaultStrokeColor = ColorLightGray
|
||||||
|
// DefaultFillColor is the default fill color.
|
||||||
|
// It is equivalent to #0074d9.
|
||||||
|
DefaultFillColor = ColorBlue
|
||||||
|
// DefaultAnnotationFillColor is the default annotation background color.
|
||||||
|
DefaultAnnotationFillColor = ColorWhite
|
||||||
|
// DefaultGridLineColor is the default grid line color.
|
||||||
|
DefaultGridLineColor = ColorLightGray
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultColors are a couple default series colors.
|
||||||
|
DefaultColors = []drawing.Color{
|
||||||
|
ColorBlue,
|
||||||
|
ColorGreen,
|
||||||
|
ColorRed,
|
||||||
|
ColorCyan,
|
||||||
|
ColorOrange,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAlternateColors are a couple alternate colors.
|
||||||
|
DefaultAlternateColors = []drawing.Color{
|
||||||
|
ColorAlternateBlue,
|
||||||
|
ColorAlternateGreen,
|
||||||
|
ColorAlternateGray,
|
||||||
|
ColorAlternateYellow,
|
||||||
|
ColorBlue,
|
||||||
|
ColorGreen,
|
||||||
|
ColorRed,
|
||||||
|
ColorCyan,
|
||||||
|
ColorOrange,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultColor returns a color from the default list by index.
|
||||||
|
// NOTE: the index will wrap around (using a modulo).
|
||||||
|
func GetDefaultColor(index int) drawing.Color {
|
||||||
|
finalIndex := index % len(DefaultColors)
|
||||||
|
return DefaultColors[finalIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlternateColor returns a color from the default list by index.
|
||||||
|
// NOTE: the index will wrap around (using a modulo).
|
||||||
|
func GetAlternateColor(index int) drawing.Color {
|
||||||
|
finalIndex := index % len(DefaultAlternateColors)
|
||||||
|
return DefaultAlternateColors[finalIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorPalette is a set of colors that.
|
||||||
|
type ColorPalette interface {
|
||||||
|
BackgroundColor() drawing.Color
|
||||||
|
BackgroundStrokeColor() drawing.Color
|
||||||
|
CanvasColor() drawing.Color
|
||||||
|
CanvasStrokeColor() drawing.Color
|
||||||
|
AxisStrokeColor() drawing.Color
|
||||||
|
TextColor() drawing.Color
|
||||||
|
GetSeriesColor(index int) drawing.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultColorPalette represents the default palatte.
|
||||||
|
var DefaultColorPalette defaultColorPalette
|
||||||
|
|
||||||
|
type defaultColorPalette struct{}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) BackgroundColor() drawing.Color {
|
||||||
|
return DefaultBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||||
|
return DefaultBackgroundStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) CanvasColor() drawing.Color {
|
||||||
|
return DefaultCanvasColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) CanvasStrokeColor() drawing.Color {
|
||||||
|
return DefaultCanvasStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) AxisStrokeColor() drawing.Color {
|
||||||
|
return DefaultAxisColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) TextColor() drawing.Color {
|
||||||
|
return DefaultTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp defaultColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||||
|
return GetDefaultColor(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlternateColorPalette represents the default palatte.
|
||||||
|
var AlternateColorPalette alternateColorPalette
|
||||||
|
|
||||||
|
type alternateColorPalette struct{}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) BackgroundColor() drawing.Color {
|
||||||
|
return DefaultBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||||
|
return DefaultBackgroundStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) CanvasColor() drawing.Color {
|
||||||
|
return DefaultCanvasColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) CanvasStrokeColor() drawing.Color {
|
||||||
|
return DefaultCanvasStrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) AxisStrokeColor() drawing.Color {
|
||||||
|
return DefaultAxisColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) TextColor() drawing.Color {
|
||||||
|
return DefaultTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap alternateColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||||
|
return GetAlternateColor(index)
|
||||||
|
}
|
44
vendor/github.com/wcharczuk/go-chart/v2/concat_series.go
generated
vendored
Normal file
44
vendor/github.com/wcharczuk/go-chart/v2/concat_series.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// ConcatSeries is a special type of series that concatenates its `InnerSeries`.
|
||||||
|
type ConcatSeries []Series
|
||||||
|
|
||||||
|
// Len returns the length of the concatenated set of series.
|
||||||
|
func (cs ConcatSeries) Len() int {
|
||||||
|
total := 0
|
||||||
|
for _, s := range cs {
|
||||||
|
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||||
|
total += typed.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value at the (meta) index (i.e 0 => totalLen-1)
|
||||||
|
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
|
||||||
|
cursor := 0
|
||||||
|
for _, s := range cs {
|
||||||
|
if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
|
||||||
|
len := typed.Len()
|
||||||
|
if index < cursor+len {
|
||||||
|
x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cursor += typed.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (cs ConcatSeries) Validate() error {
|
||||||
|
var err error
|
||||||
|
for _, s := range cs {
|
||||||
|
err = s.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
81
vendor/github.com/wcharczuk/go-chart/v2/continuous_range.go
generated
vendored
Normal file
81
vendor/github.com/wcharczuk/go-chart/v2/continuous_range.go
generated
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContinuousRange represents a boundary for a set of numbers.
|
||||||
|
type ContinuousRange struct {
|
||||||
|
Min float64
|
||||||
|
Max float64
|
||||||
|
Domain int
|
||||||
|
Descending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDescending returns if the range is descending.
|
||||||
|
func (r ContinuousRange) IsDescending() bool {
|
||||||
|
return r.Descending
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the ContinuousRange has been set or not.
|
||||||
|
func (r ContinuousRange) IsZero() bool {
|
||||||
|
return (r.Min == 0 || math.IsNaN(r.Min)) &&
|
||||||
|
(r.Max == 0 || math.IsNaN(r.Max)) &&
|
||||||
|
r.Domain == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMin gets the min value for the continuous range.
|
||||||
|
func (r ContinuousRange) GetMin() float64 {
|
||||||
|
return r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMin sets the min value for the continuous range.
|
||||||
|
func (r *ContinuousRange) SetMin(min float64) {
|
||||||
|
r.Min = min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMax returns the max value for the continuous range.
|
||||||
|
func (r ContinuousRange) GetMax() float64 {
|
||||||
|
return r.Max
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMax sets the max value for the continuous range.
|
||||||
|
func (r *ContinuousRange) SetMax(max float64) {
|
||||||
|
r.Max = max
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDelta returns the difference between the min and max value.
|
||||||
|
func (r ContinuousRange) GetDelta() float64 {
|
||||||
|
return r.Max - r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomain returns the range domain.
|
||||||
|
func (r ContinuousRange) GetDomain() int {
|
||||||
|
return r.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDomain sets the range domain.
|
||||||
|
func (r *ContinuousRange) SetDomain(domain int) {
|
||||||
|
r.Domain = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a simple string for the ContinuousRange.
|
||||||
|
func (r ContinuousRange) String() string {
|
||||||
|
if r.GetDelta() == 0 {
|
||||||
|
return "ContinuousRange [empty]"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate maps a given value into the ContinuousRange space.
|
||||||
|
func (r ContinuousRange) Translate(value float64) int {
|
||||||
|
normalized := value - r.Min
|
||||||
|
ratio := normalized / r.GetDelta()
|
||||||
|
|
||||||
|
if r.IsDescending() {
|
||||||
|
return r.Domain - int(math.Ceil(ratio*float64(r.Domain)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(math.Ceil(ratio * float64(r.Domain)))
|
||||||
|
}
|
96
vendor/github.com/wcharczuk/go-chart/v2/continuous_series.go
generated
vendored
Normal file
96
vendor/github.com/wcharczuk/go-chart/v2/continuous_series.go
generated
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*ContinuousSeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*ContinuousSeries)(nil)
|
||||||
|
_ LastValuesProvider = (*ContinuousSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContinuousSeries represents a line on a chart.
|
||||||
|
type ContinuousSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
XValueFormatter ValueFormatter
|
||||||
|
YValueFormatter ValueFormatter
|
||||||
|
|
||||||
|
XValues []float64
|
||||||
|
YValues []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (cs ContinuousSeries) GetName() string {
|
||||||
|
return cs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (cs ContinuousSeries) GetStyle() Style {
|
||||||
|
return cs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (cs ContinuousSeries) Len() int {
|
||||||
|
return len(cs.XValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets the x,y values at a given index.
|
||||||
|
func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
|
||||||
|
return cs.XValues[index], cs.YValues[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues gets the first x,y values.
|
||||||
|
func (cs ContinuousSeries) GetFirstValues() (float64, float64) {
|
||||||
|
return cs.XValues[0], cs.YValues[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues gets the last x,y values.
|
||||||
|
func (cs ContinuousSeries) GetLastValues() (float64, float64) {
|
||||||
|
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueFormatters returns value formatter defaults for the series.
|
||||||
|
func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) {
|
||||||
|
if cs.XValueFormatter != nil {
|
||||||
|
x = cs.XValueFormatter
|
||||||
|
} else {
|
||||||
|
x = FloatValueFormatter
|
||||||
|
}
|
||||||
|
if cs.YValueFormatter != nil {
|
||||||
|
y = cs.YValueFormatter
|
||||||
|
} else {
|
||||||
|
y = FloatValueFormatter
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (cs ContinuousSeries) GetYAxis() YAxisType {
|
||||||
|
return cs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := cs.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (cs ContinuousSeries) Validate() error {
|
||||||
|
if len(cs.XValues) == 0 {
|
||||||
|
return fmt.Errorf("continuous series; must have xvalues set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cs.YValues) == 0 {
|
||||||
|
return fmt.Errorf("continuous series; must have yvalues set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cs.XValues) != len(cs.YValues) {
|
||||||
|
return fmt.Errorf("continuous series; must have same length xvalues as yvalues")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
103
vendor/github.com/wcharczuk/go-chart/v2/defaults.go
generated
vendored
Normal file
103
vendor/github.com/wcharczuk/go-chart/v2/defaults.go
generated
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultChartHeight is the default chart height.
|
||||||
|
DefaultChartHeight = 400
|
||||||
|
// DefaultChartWidth is the default chart width.
|
||||||
|
DefaultChartWidth = 1024
|
||||||
|
// DefaultStrokeWidth is the default chart stroke width.
|
||||||
|
DefaultStrokeWidth = 0.0
|
||||||
|
// DefaultDotWidth is the default chart dot width.
|
||||||
|
DefaultDotWidth = 0.0
|
||||||
|
// DefaultSeriesLineWidth is the default line width.
|
||||||
|
DefaultSeriesLineWidth = 1.0
|
||||||
|
// DefaultAxisLineWidth is the line width of the axis lines.
|
||||||
|
DefaultAxisLineWidth = 1.0
|
||||||
|
//DefaultDPI is the default dots per inch for the chart.
|
||||||
|
DefaultDPI = 92.0
|
||||||
|
// DefaultMinimumFontSize is the default minimum font size.
|
||||||
|
DefaultMinimumFontSize = 8.0
|
||||||
|
// DefaultFontSize is the default font size.
|
||||||
|
DefaultFontSize = 10.0
|
||||||
|
// DefaultTitleFontSize is the default title font size.
|
||||||
|
DefaultTitleFontSize = 18.0
|
||||||
|
// DefaultAnnotationDeltaWidth is the width of the left triangle out of annotations.
|
||||||
|
DefaultAnnotationDeltaWidth = 10
|
||||||
|
// DefaultAnnotationFontSize is the font size of annotations.
|
||||||
|
DefaultAnnotationFontSize = 10.0
|
||||||
|
// DefaultAxisFontSize is the font size of the axis labels.
|
||||||
|
DefaultAxisFontSize = 10.0
|
||||||
|
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
||||||
|
DefaultTitleTop = 10
|
||||||
|
|
||||||
|
// DefaultBackgroundStrokeWidth is the default stroke on the chart background.
|
||||||
|
DefaultBackgroundStrokeWidth = 0.0
|
||||||
|
// DefaultCanvasStrokeWidth is the default stroke on the chart canvas.
|
||||||
|
DefaultCanvasStrokeWidth = 0.0
|
||||||
|
|
||||||
|
// DefaultLineSpacing is the default vertical distance between lines of text.
|
||||||
|
DefaultLineSpacing = 5
|
||||||
|
|
||||||
|
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
|
||||||
|
DefaultYAxisMargin = 10
|
||||||
|
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
|
||||||
|
DefaultXAxisMargin = 10
|
||||||
|
|
||||||
|
//DefaultVerticalTickHeight is half the margin.
|
||||||
|
DefaultVerticalTickHeight = DefaultXAxisMargin >> 1
|
||||||
|
//DefaultHorizontalTickWidth is half the margin.
|
||||||
|
DefaultHorizontalTickWidth = DefaultYAxisMargin >> 1
|
||||||
|
|
||||||
|
// DefaultTickCount is the default number of ticks to show
|
||||||
|
DefaultTickCount = 10
|
||||||
|
// DefaultTickCountSanityCheck is a hard limit on number of ticks to prevent infinite loops.
|
||||||
|
DefaultTickCountSanityCheck = 1 << 10 //1024
|
||||||
|
|
||||||
|
// DefaultMinimumTickHorizontalSpacing is the minimum distance between horizontal ticks.
|
||||||
|
DefaultMinimumTickHorizontalSpacing = 20
|
||||||
|
// DefaultMinimumTickVerticalSpacing is the minimum distance between vertical ticks.
|
||||||
|
DefaultMinimumTickVerticalSpacing = 20
|
||||||
|
|
||||||
|
// DefaultDateFormat is the default date format.
|
||||||
|
DefaultDateFormat = "2006-01-02"
|
||||||
|
// DefaultDateHourFormat is the date format for hour timestamp formats.
|
||||||
|
DefaultDateHourFormat = "01-02 3PM"
|
||||||
|
// DefaultDateMinuteFormat is the date format for minute range timestamp formats.
|
||||||
|
DefaultDateMinuteFormat = "01-02 3:04PM"
|
||||||
|
// DefaultFloatFormat is the default float format.
|
||||||
|
DefaultFloatFormat = "%.2f"
|
||||||
|
// DefaultPercentValueFormat is the default percent format.
|
||||||
|
DefaultPercentValueFormat = "%0.2f%%"
|
||||||
|
|
||||||
|
// DefaultBarSpacing is the default pixel spacing between bars.
|
||||||
|
DefaultBarSpacing = 100
|
||||||
|
// DefaultBarWidth is the default pixel width of bars in a bar chart.
|
||||||
|
DefaultBarWidth = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DashArrayDots is a dash array that represents '....' style stroke dashes.
|
||||||
|
DashArrayDots = []int{1, 1}
|
||||||
|
// DashArrayDashesSmall is a dash array that represents '- - -' style stroke dashes.
|
||||||
|
DashArrayDashesSmall = []int{3, 3}
|
||||||
|
// DashArrayDashesMedium is a dash array that represents '-- -- --' style stroke dashes.
|
||||||
|
DashArrayDashesMedium = []int{5, 5}
|
||||||
|
// DashArrayDashesLarge is a dash array that represents '----- ----- -----' style stroke dashes.
|
||||||
|
DashArrayDashesLarge = []int{10, 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultAnnotationPadding is the padding around an annotation.
|
||||||
|
DefaultAnnotationPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
|
||||||
|
|
||||||
|
// DefaultBackgroundPadding is the default canvas padding config.
|
||||||
|
DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContentTypePNG is the png mime type.
|
||||||
|
ContentTypePNG = "image/png"
|
||||||
|
|
||||||
|
// ContentTypeSVG is the svg mime type.
|
||||||
|
ContentTypeSVG = "image/svg+xml"
|
||||||
|
)
|
315
vendor/github.com/wcharczuk/go-chart/v2/donut_chart.go
generated
vendored
Normal file
315
vendor/github.com/wcharczuk/go-chart/v2/donut_chart.go
generated
vendored
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DonutChart is a chart that draws sections of a circle based on percentages with an hole.
|
||||||
|
type DonutChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
SliceStyle Style
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Values []Value
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (pc DonutChart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if pc.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return pc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (pc DonutChart) GetFont() *truetype.Font {
|
||||||
|
if pc.Font == nil {
|
||||||
|
return pc.defaultFont
|
||||||
|
}
|
||||||
|
return pc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (pc DonutChart) GetWidth() int {
|
||||||
|
if pc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return pc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (pc DonutChart) GetHeight() int {
|
||||||
|
if pc.Height == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return pc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (pc DonutChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(pc.Values) == 0 {
|
||||||
|
return errors.New("please provide at least one value")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(pc.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
canvasBox := pc.getDefaultCanvasBox()
|
||||||
|
canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
|
||||||
|
|
||||||
|
pc.drawBackground(r)
|
||||||
|
pc.drawCanvas(r, canvasBox)
|
||||||
|
|
||||||
|
finalValues, err := pc.finalizeValues(pc.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pc.drawSlices(r, canvasBox, finalValues)
|
||||||
|
pc.drawTitle(r)
|
||||||
|
for _, a := range pc.Elements {
|
||||||
|
a(r, canvasBox, pc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: pc.GetWidth(),
|
||||||
|
Bottom: pc.GetHeight(),
|
||||||
|
}, pc.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, pc.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) drawTitle(r Renderer) {
|
||||||
|
if len(pc.Title) > 0 && !pc.TitleStyle.Hidden {
|
||||||
|
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
||||||
|
cx, cy := canvasBox.Center()
|
||||||
|
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
|
radius := float64(diameter>>1) / 1.1
|
||||||
|
labelRadius := (radius * 2.83) / 3.0
|
||||||
|
|
||||||
|
// draw the donut slices
|
||||||
|
var rads, delta, delta2, total float64
|
||||||
|
var lx, ly int
|
||||||
|
|
||||||
|
if len(values) == 1 {
|
||||||
|
pc.styleDonutChartValue(0).WriteToRenderer(r)
|
||||||
|
r.MoveTo(cx, cy)
|
||||||
|
r.Circle(radius, cx, cy)
|
||||||
|
} else {
|
||||||
|
for index, v := range values {
|
||||||
|
v.Style.InheritFrom(pc.styleDonutChartValue(index)).WriteToRenderer(r)
|
||||||
|
r.MoveTo(cx, cy)
|
||||||
|
rads = PercentToRadians(total)
|
||||||
|
delta = PercentToRadians(v.Value)
|
||||||
|
|
||||||
|
r.ArcTo(cx, cy, (radius / 1.25), (radius / 1.25), rads, delta)
|
||||||
|
|
||||||
|
r.LineTo(cx, cy)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
total = total + v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//making the donut hole
|
||||||
|
v := Value{Value: 100, Label: "center"}
|
||||||
|
styletemp := pc.SliceStyle.InheritFrom(Style{
|
||||||
|
StrokeColor: ColorWhite, StrokeWidth: 4.0, FillColor: ColorWhite, FontColor: ColorWhite, //Font: pc.GetFont(),//FontSize: pc.getScaledFontSize(),
|
||||||
|
})
|
||||||
|
v.Style.InheritFrom(styletemp).WriteToRenderer(r)
|
||||||
|
r.MoveTo(cx, cy)
|
||||||
|
r.ArcTo(cx, cy, (radius / 3.5), (radius / 3.5), DegreesToRadians(0), DegreesToRadians(359))
|
||||||
|
r.LineTo(cx, cy)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
|
||||||
|
// draw the labels
|
||||||
|
total = 0
|
||||||
|
for index, v := range values {
|
||||||
|
v.Style.InheritFrom(pc.styleDonutChartValue(index)).WriteToRenderer(r)
|
||||||
|
if len(v.Label) > 0 {
|
||||||
|
delta2 = PercentToRadians(total + (v.Value / 2.0))
|
||||||
|
delta2 = RadianAdd(delta2, _pi2)
|
||||||
|
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
|
||||||
|
|
||||||
|
tb := r.MeasureText(v.Label)
|
||||||
|
lx = lx - (tb.Width() >> 1)
|
||||||
|
ly = ly + (tb.Height() >> 1)
|
||||||
|
|
||||||
|
r.Text(v.Label, lx, ly)
|
||||||
|
}
|
||||||
|
total = total + v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) finalizeValues(values []Value) ([]Value, error) {
|
||||||
|
finalValues := Values(values).Normalize()
|
||||||
|
if len(finalValues) == 0 {
|
||||||
|
return nil, fmt.Errorf("donut chart must contain at least (1) non-zero value")
|
||||||
|
}
|
||||||
|
return finalValues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) getDefaultCanvasBox() Box {
|
||||||
|
return pc.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
|
||||||
|
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
|
|
||||||
|
square := Box{
|
||||||
|
Right: circleDiameter,
|
||||||
|
Bottom: circleDiameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.Fit(square)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) getBackgroundStyle() Style {
|
||||||
|
return pc.Background.InheritFrom(pc.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) getCanvasStyle() Style {
|
||||||
|
return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: pc.GetColorPalette().CanvasColor(),
|
||||||
|
StrokeColor: pc.GetColorPalette().CanvasStrokeColor(),
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) styleDefaultsDonutChartValue() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: pc.GetColorPalette().TextColor(),
|
||||||
|
StrokeWidth: 4.0,
|
||||||
|
FillColor: pc.GetColorPalette().TextColor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) styleDonutChartValue(index int) Style {
|
||||||
|
return pc.SliceStyle.InheritFrom(Style{
|
||||||
|
StrokeColor: ColorWhite,
|
||||||
|
StrokeWidth: 4.0,
|
||||||
|
FillColor: pc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
FontSize: pc.getScaledFontSize(),
|
||||||
|
FontColor: pc.GetColorPalette().TextColor(),
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) getScaledFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48.0
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24.0
|
||||||
|
} else if effectiveDimension > 512 {
|
||||||
|
return 18.0
|
||||||
|
} else if effectiveDimension > 256 {
|
||||||
|
return 12.0
|
||||||
|
}
|
||||||
|
return 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: pc.GetColorPalette().BackgroundColor(),
|
||||||
|
StrokeColor: pc.GetColorPalette().BackgroundStrokeColor(),
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) styleDefaultsTitle() Style {
|
||||||
|
return pc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: pc.GetColorPalette().TextColor(),
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
FontSize: pc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc DonutChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24
|
||||||
|
} else if effectiveDimension >= 512 {
|
||||||
|
return 18
|
||||||
|
} else if effectiveDimension >= 256 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (pc DonutChart) GetColorPalette() ColorPalette {
|
||||||
|
if pc.ColorPalette != nil {
|
||||||
|
return pc.ColorPalette
|
||||||
|
}
|
||||||
|
return AlternateColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (pc DonutChart) Box() Box {
|
||||||
|
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||||
|
dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||||
|
Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
|
||||||
|
Right: pc.GetWidth() - dpr,
|
||||||
|
Bottom: pc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
325
vendor/github.com/wcharczuk/go-chart/v2/draw.go
generated
vendored
Normal file
325
vendor/github.com/wcharczuk/go-chart/v2/draw.go
generated
vendored
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Draw contains helpers for drawing common objects.
|
||||||
|
Draw = &draw{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type draw struct{}
|
||||||
|
|
||||||
|
// LineSeries draws a line series with a renderer.
|
||||||
|
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
|
||||||
|
if vs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
v0x, v0y := vs.GetValues(0)
|
||||||
|
x0 := cl + xrange.Translate(v0x)
|
||||||
|
y0 := cb - yrange.Translate(v0y)
|
||||||
|
|
||||||
|
yv0 := yrange.Translate(0)
|
||||||
|
|
||||||
|
var vx, vy float64
|
||||||
|
var x, y int
|
||||||
|
|
||||||
|
if style.ShouldDrawStroke() && style.ShouldDrawFill() {
|
||||||
|
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValues(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.LineTo(x, MinInt(cb, cb-yv0))
|
||||||
|
r.LineTo(x0, MinInt(cb, cb-yv0))
|
||||||
|
r.LineTo(x0, y0)
|
||||||
|
r.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.ShouldDrawStroke() {
|
||||||
|
style.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValues(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.ShouldDrawDot() {
|
||||||
|
defaultDotWidth := style.GetDotWidth()
|
||||||
|
|
||||||
|
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
for i := 0; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValues(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
|
||||||
|
dotWidth := defaultDotWidth
|
||||||
|
if style.DotWidthProvider != nil {
|
||||||
|
dotWidth = style.DotWidthProvider(xrange, yrange, i, vx, vy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.DotColorProvider != nil {
|
||||||
|
dotColor := style.DotColorProvider(xrange, yrange, i, vx, vy)
|
||||||
|
|
||||||
|
r.SetFillColor(dotColor)
|
||||||
|
r.SetStrokeColor(dotColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Circle(dotWidth, x, y)
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoundedSeries draws a series that implements BoundedValuesProvider.
|
||||||
|
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) {
|
||||||
|
drawOffsetIndex := 0
|
||||||
|
if len(drawOffsetIndexes) > 0 {
|
||||||
|
drawOffsetIndex = drawOffsetIndexes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
|
||||||
|
x0 := cl + xrange.Translate(v0x)
|
||||||
|
y0 := cb - yrange.Translate(v0y1)
|
||||||
|
|
||||||
|
var vx, vy1, vy2 float64
|
||||||
|
var x, y int
|
||||||
|
|
||||||
|
xvalues := make([]float64, bbs.Len())
|
||||||
|
xvalues[0] = v0x
|
||||||
|
y2values := make([]float64, bbs.Len())
|
||||||
|
y2values[0] = v0y2
|
||||||
|
|
||||||
|
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < bbs.Len(); i++ {
|
||||||
|
vx, vy1, vy2 = bbs.GetBoundedValues(i)
|
||||||
|
|
||||||
|
xvalues[i] = vx
|
||||||
|
y2values[i] = vy2
|
||||||
|
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy1)
|
||||||
|
if i > drawOffsetIndex {
|
||||||
|
r.LineTo(x, y)
|
||||||
|
} else {
|
||||||
|
r.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y = cb - yrange.Translate(vy2)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
for i := bbs.Len() - 1; i >= drawOffsetIndex; i-- {
|
||||||
|
vx, vy2 = xvalues[i], y2values[i]
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy2)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistogramSeries draws a value provider as boxes from 0.
|
||||||
|
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
|
||||||
|
if vs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//calculate bar width?
|
||||||
|
seriesLength := vs.Len()
|
||||||
|
barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength)))
|
||||||
|
if len(barWidths) > 0 {
|
||||||
|
barWidth = barWidths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
//foreach datapoint, draw a box.
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy := vs.GetValues(index)
|
||||||
|
y0 := yrange.Translate(0)
|
||||||
|
x := cl + xrange.Translate(vx)
|
||||||
|
y := yrange.Translate(vy)
|
||||||
|
|
||||||
|
d.Box(r, Box{
|
||||||
|
Top: cb - y0,
|
||||||
|
Left: x - (barWidth >> 1),
|
||||||
|
Right: x + (barWidth >> 1),
|
||||||
|
Bottom: cb - y,
|
||||||
|
}, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeasureAnnotation measures how big an annotation would be.
|
||||||
|
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
|
||||||
|
style.WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
textBox := r.MeasureText(label)
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
textHeight := textBox.Height()
|
||||||
|
halfTextHeight := textHeight >> 1
|
||||||
|
|
||||||
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||||
|
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||||
|
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||||
|
|
||||||
|
strokeWidth := style.GetStrokeWidth()
|
||||||
|
|
||||||
|
top := ly - (pt + halfTextHeight)
|
||||||
|
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
|
||||||
|
bottom := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: top,
|
||||||
|
Left: lx,
|
||||||
|
Right: right,
|
||||||
|
Bottom: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotation draws an anotation with a renderer.
|
||||||
|
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
textBox := r.MeasureText(label)
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
halfTextHeight := textBox.Height() >> 1
|
||||||
|
|
||||||
|
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||||
|
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||||
|
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||||
|
|
||||||
|
textX := lx + pl + DefaultAnnotationDeltaWidth
|
||||||
|
textY := ly + halfTextHeight
|
||||||
|
|
||||||
|
ltx := lx + DefaultAnnotationDeltaWidth
|
||||||
|
lty := ly - (pt + halfTextHeight)
|
||||||
|
|
||||||
|
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
||||||
|
rty := ly - (pt + halfTextHeight)
|
||||||
|
|
||||||
|
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
||||||
|
rby := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
lbx := lx + DefaultAnnotationDeltaWidth
|
||||||
|
lby := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(ltx, lty)
|
||||||
|
r.LineTo(rtx, rty)
|
||||||
|
r.LineTo(rbx, rby)
|
||||||
|
r.LineTo(lbx, lby)
|
||||||
|
r.LineTo(lx, ly)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
r.Text(label, textX, textY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box draws a box with a given style.
|
||||||
|
func (d draw) Box(r Renderer, b Box, s Style) {
|
||||||
|
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
r.MoveTo(b.Left, b.Top)
|
||||||
|
r.LineTo(b.Right, b.Top)
|
||||||
|
r.LineTo(b.Right, b.Bottom)
|
||||||
|
r.LineTo(b.Left, b.Bottom)
|
||||||
|
r.LineTo(b.Left, b.Top)
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
|
||||||
|
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) {
|
||||||
|
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
|
||||||
|
r.LineTo(bc.TopRight.X, bc.TopRight.Y)
|
||||||
|
r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
|
||||||
|
r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawText draws text with a given style.
|
||||||
|
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
r.Text(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
return r.MeasureText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextWithin draws the text within a given box.
|
||||||
|
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
lines := Text.WrapFit(r, text, box.Width(), style)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, style)
|
||||||
|
|
||||||
|
y := box.Top
|
||||||
|
|
||||||
|
switch style.GetTextVerticalAlign() {
|
||||||
|
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
|
||||||
|
y = y - linesBox.Height()
|
||||||
|
case TextVerticalAlignMiddle:
|
||||||
|
y = y + (box.Height() >> 1) - (linesBox.Height() >> 1)
|
||||||
|
case TextVerticalAlignMiddleBaseline:
|
||||||
|
y = y + (box.Height() >> 1) - linesBox.Height()
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx, ty int
|
||||||
|
for _, line := range lines {
|
||||||
|
lineBox := r.MeasureText(line)
|
||||||
|
switch style.GetTextHorizontalAlign() {
|
||||||
|
case TextHorizontalAlignCenter:
|
||||||
|
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
|
||||||
|
case TextHorizontalAlignRight:
|
||||||
|
tx = box.Right - lineBox.Width()
|
||||||
|
default:
|
||||||
|
tx = box.Left
|
||||||
|
}
|
||||||
|
if style.TextRotationDegrees == 0 {
|
||||||
|
ty = y + lineBox.Height()
|
||||||
|
} else {
|
||||||
|
ty = y
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Text(line, tx, ty)
|
||||||
|
y += lineBox.Height() + style.GetTextLineSpacing()
|
||||||
|
}
|
||||||
|
}
|
5
vendor/github.com/wcharczuk/go-chart/v2/drawing/README.md
generated
vendored
Normal file
5
vendor/github.com/wcharczuk/go-chart/v2/drawing/README.md
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
go-chart > drawing
|
||||||
|
==================
|
||||||
|
|
||||||
|
The bulk of the code in this package is based on [draw2d](https://github.com/llgcode/draw2d), but
|
||||||
|
with significant modifications to make the APIs more golang friendly and careful about units (points vs. pixels).
|
126
vendor/github.com/wcharczuk/go-chart/v2/drawing/color.go
generated
vendored
Normal file
126
vendor/github.com/wcharczuk/go-chart/v2/drawing/color.go
generated
vendored
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ColorTransparent is a fully transparent color.
|
||||||
|
ColorTransparent = Color{}
|
||||||
|
|
||||||
|
// ColorWhite is white.
|
||||||
|
ColorWhite = Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
|
||||||
|
// ColorBlack is black.
|
||||||
|
ColorBlack = Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
|
||||||
|
// ColorRed is red.
|
||||||
|
ColorRed = Color{R: 255, G: 0, B: 0, A: 255}
|
||||||
|
|
||||||
|
// ColorGreen is green.
|
||||||
|
ColorGreen = Color{R: 0, G: 255, B: 0, A: 255}
|
||||||
|
|
||||||
|
// ColorBlue is blue.
|
||||||
|
ColorBlue = Color{R: 0, G: 0, B: 255, A: 255}
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseHex(hex string) uint8 {
|
||||||
|
v, _ := strconv.ParseInt(hex, 16, 16)
|
||||||
|
return uint8(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorFromHex returns a color from a css hex code.
|
||||||
|
func ColorFromHex(hex string) Color {
|
||||||
|
var c Color
|
||||||
|
if len(hex) == 3 {
|
||||||
|
c.R = parseHex(string(hex[0])) * 0x11
|
||||||
|
c.G = parseHex(string(hex[1])) * 0x11
|
||||||
|
c.B = parseHex(string(hex[2])) * 0x11
|
||||||
|
} else {
|
||||||
|
c.R = parseHex(string(hex[0:2]))
|
||||||
|
c.G = parseHex(string(hex[2:4]))
|
||||||
|
c.B = parseHex(string(hex[4:6]))
|
||||||
|
}
|
||||||
|
c.A = 255
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
|
||||||
|
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
|
||||||
|
fa := float64(a) / 255.0
|
||||||
|
var c Color
|
||||||
|
c.R = uint8(float64(r) / fa)
|
||||||
|
c.G = uint8(float64(g) / fa)
|
||||||
|
c.B = uint8(float64(b) / fa)
|
||||||
|
c.A = uint8(a | (a >> 8))
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorChannelFromFloat returns a normalized byte from a given float value.
|
||||||
|
func ColorChannelFromFloat(v float64) uint8 {
|
||||||
|
return uint8(v * 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color is our internal color type because color.Color is bullshit.
|
||||||
|
type Color struct {
|
||||||
|
R, G, B, A uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the color as a pre-alpha mixed color set.
|
||||||
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
||||||
|
fa := float64(c.A) / 255.0
|
||||||
|
r = uint32(float64(uint32(c.R)) * fa)
|
||||||
|
r |= r << 8
|
||||||
|
g = uint32(float64(uint32(c.G)) * fa)
|
||||||
|
g |= g << 8
|
||||||
|
b = uint32(float64(uint32(c.B)) * fa)
|
||||||
|
b |= b << 8
|
||||||
|
a = uint32(c.A)
|
||||||
|
a |= a << 8
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the color has been set or not.
|
||||||
|
func (c Color) IsZero() bool {
|
||||||
|
return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTransparent returns if the colors alpha channel is zero.
|
||||||
|
func (c Color) IsTransparent() bool {
|
||||||
|
return c.A == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAlpha returns a copy of the color with a given alpha.
|
||||||
|
func (c Color) WithAlpha(a uint8) Color {
|
||||||
|
return Color{
|
||||||
|
R: c.R,
|
||||||
|
G: c.G,
|
||||||
|
B: c.B,
|
||||||
|
A: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns true if the color equals another.
|
||||||
|
func (c Color) Equals(other Color) bool {
|
||||||
|
return c.R == other.R &&
|
||||||
|
c.G == other.G &&
|
||||||
|
c.B == other.B &&
|
||||||
|
c.A == other.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// AverageWith averages two colors.
|
||||||
|
func (c Color) AverageWith(other Color) Color {
|
||||||
|
return Color{
|
||||||
|
R: (c.R + other.R) >> 1,
|
||||||
|
G: (c.G + other.G) >> 1,
|
||||||
|
B: (c.B + other.B) >> 1,
|
||||||
|
A: c.A,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a css string representation of the color.
|
||||||
|
func (c Color) String() string {
|
||||||
|
fa := float64(c.A) / float64(255)
|
||||||
|
return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa)
|
||||||
|
}
|
6
vendor/github.com/wcharczuk/go-chart/v2/drawing/constants.go
generated
vendored
Normal file
6
vendor/github.com/wcharczuk/go-chart/v2/drawing/constants.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultDPI is the default image DPI.
|
||||||
|
DefaultDPI = 96.0
|
||||||
|
)
|
185
vendor/github.com/wcharczuk/go-chart/v2/drawing/curve.go
generated
vendored
Normal file
185
vendor/github.com/wcharczuk/go-chart/v2/drawing/curve.go
generated
vendored
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
|
||||||
|
CurveRecursionLimit = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cubic
|
||||||
|
// x1, y1, cpx1, cpy1, cpx2, cpy2, x2, y2 float64
|
||||||
|
|
||||||
|
// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
|
||||||
|
// c1 and c2 parameters are the resulting curves
|
||||||
|
func SubdivideCubic(c, c1, c2 []float64) {
|
||||||
|
// First point of c is the first point of c1
|
||||||
|
c1[0], c1[1] = c[0], c[1]
|
||||||
|
// Last point of c is the last point of c2
|
||||||
|
c2[6], c2[7] = c[6], c[7]
|
||||||
|
|
||||||
|
// Subdivide segment using midpoints
|
||||||
|
c1[2] = (c[0] + c[2]) / 2
|
||||||
|
c1[3] = (c[1] + c[3]) / 2
|
||||||
|
|
||||||
|
midX := (c[2] + c[4]) / 2
|
||||||
|
midY := (c[3] + c[5]) / 2
|
||||||
|
|
||||||
|
c2[4] = (c[4] + c[6]) / 2
|
||||||
|
c2[5] = (c[5] + c[7]) / 2
|
||||||
|
|
||||||
|
c1[4] = (c1[2] + midX) / 2
|
||||||
|
c1[5] = (c1[3] + midY) / 2
|
||||||
|
|
||||||
|
c2[2] = (midX + c2[4]) / 2
|
||||||
|
c2[3] = (midY + c2[5]) / 2
|
||||||
|
|
||||||
|
c1[6] = (c1[4] + c2[2]) / 2
|
||||||
|
c1[7] = (c1[5] + c2[3]) / 2
|
||||||
|
|
||||||
|
// Last Point of c1 is equal to the first point of c2
|
||||||
|
c2[0], c2[1] = c1[6], c1[7]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceCubic generate lines subdividing the cubic curve using a Liner
|
||||||
|
// flattening_threshold helps determines the flattening expectation of the curve
|
||||||
|
func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
|
||||||
|
// Allocation curves
|
||||||
|
var curves [CurveRecursionLimit * 8]float64
|
||||||
|
copy(curves[0:8], cubic[0:8])
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
// current curve
|
||||||
|
var c []float64
|
||||||
|
|
||||||
|
var dx, dy, d2, d3 float64
|
||||||
|
|
||||||
|
for i >= 0 {
|
||||||
|
c = curves[i*8:]
|
||||||
|
dx = c[6] - c[0]
|
||||||
|
dy = c[7] - c[1]
|
||||||
|
|
||||||
|
d2 = math.Abs((c[2]-c[6])*dy - (c[3]-c[7])*dx)
|
||||||
|
d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
|
||||||
|
|
||||||
|
// if it's flat then trace a line
|
||||||
|
if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
|
||||||
|
t.LineTo(c[6], c[7])
|
||||||
|
i--
|
||||||
|
} else {
|
||||||
|
// second half of bezier go lower onto the stack
|
||||||
|
SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quad
|
||||||
|
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||||
|
|
||||||
|
// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
|
||||||
|
// c1 and c2 parameters are the resulting curves
|
||||||
|
func SubdivideQuad(c, c1, c2 []float64) {
|
||||||
|
// First point of c is the first point of c1
|
||||||
|
c1[0], c1[1] = c[0], c[1]
|
||||||
|
// Last point of c is the last point of c2
|
||||||
|
c2[4], c2[5] = c[4], c[5]
|
||||||
|
|
||||||
|
// Subdivide segment using midpoints
|
||||||
|
c1[2] = (c[0] + c[2]) / 2
|
||||||
|
c1[3] = (c[1] + c[3]) / 2
|
||||||
|
c2[2] = (c[2] + c[4]) / 2
|
||||||
|
c2[3] = (c[3] + c[5]) / 2
|
||||||
|
c1[4] = (c1[2] + c2[2]) / 2
|
||||||
|
c1[5] = (c1[3] + c2[3]) / 2
|
||||||
|
c2[0], c2[1] = c1[4], c1[5]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceWindowIndices(i int) (startAt, endAt int) {
|
||||||
|
startAt = i * 6
|
||||||
|
endAt = startAt + 6
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceCalcDeltas(c []float64) (dx, dy, d float64) {
|
||||||
|
dx = c[4] - c[0]
|
||||||
|
dy = c[5] - c[1]
|
||||||
|
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceIsFlat(dx, dy, d, threshold float64) bool {
|
||||||
|
return (d * d) < threshold*(dx*dx+dy*dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceGetWindow(curves []float64, i int) []float64 {
|
||||||
|
startAt, endAt := traceWindowIndices(i)
|
||||||
|
return curves[startAt:endAt]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceQuad generate lines subdividing the curve using a Liner
|
||||||
|
// flattening_threshold helps determines the flattening expectation of the curve
|
||||||
|
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
||||||
|
const curveLen = CurveRecursionLimit * 6
|
||||||
|
const curveEndIndex = curveLen - 1
|
||||||
|
const lastIteration = CurveRecursionLimit - 1
|
||||||
|
|
||||||
|
// Allocates curves stack
|
||||||
|
curves := make([]float64, curveLen)
|
||||||
|
|
||||||
|
// copy 6 elements from the quad path to the stack
|
||||||
|
copy(curves[0:6], quad[0:6])
|
||||||
|
|
||||||
|
var i int
|
||||||
|
var c []float64
|
||||||
|
var dx, dy, d float64
|
||||||
|
|
||||||
|
for i >= 0 {
|
||||||
|
c = traceGetWindow(curves, i)
|
||||||
|
dx, dy, d = traceCalcDeltas(c)
|
||||||
|
|
||||||
|
// bail early if the distance is 0
|
||||||
|
if d == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's flat then trace a line
|
||||||
|
if traceIsFlat(dx, dy, d, flatteningThreshold) || i == lastIteration {
|
||||||
|
t.LineTo(c[4], c[5])
|
||||||
|
i--
|
||||||
|
} else {
|
||||||
|
SubdivideQuad(c, traceGetWindow(curves, i+1), traceGetWindow(curves, i))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceArc trace an arc using a Liner
|
||||||
|
func TraceArc(t Liner, x, y, rx, ry, start, angle, scale float64) (lastX, lastY float64) {
|
||||||
|
end := start + angle
|
||||||
|
clockWise := true
|
||||||
|
if angle < 0 {
|
||||||
|
clockWise = false
|
||||||
|
}
|
||||||
|
ra := (math.Abs(rx) + math.Abs(ry)) / 2
|
||||||
|
da := math.Acos(ra/(ra+0.125/scale)) * 2
|
||||||
|
//normalize
|
||||||
|
if !clockWise {
|
||||||
|
da = -da
|
||||||
|
}
|
||||||
|
angle = start + da
|
||||||
|
var curX, curY float64
|
||||||
|
for {
|
||||||
|
if (angle < end-da/4) != clockWise {
|
||||||
|
curX = x + math.Cos(end)*rx
|
||||||
|
curY = y + math.Sin(end)*ry
|
||||||
|
return curX, curY
|
||||||
|
}
|
||||||
|
curX = x + math.Cos(angle)*rx
|
||||||
|
curY = y + math.Sin(angle)*ry
|
||||||
|
|
||||||
|
angle += da
|
||||||
|
t.LineTo(curX, curY)
|
||||||
|
}
|
||||||
|
}
|
89
vendor/github.com/wcharczuk/go-chart/v2/drawing/dasher.go
generated
vendored
Normal file
89
vendor/github.com/wcharczuk/go-chart/v2/drawing/dasher.go
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
// NewDashVertexConverter creates a new dash converter.
|
||||||
|
func NewDashVertexConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter {
|
||||||
|
var dasher DashVertexConverter
|
||||||
|
dasher.dash = dash
|
||||||
|
dasher.currentDash = 0
|
||||||
|
dasher.dashOffset = dashOffset
|
||||||
|
dasher.next = flattener
|
||||||
|
return &dasher
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashVertexConverter is a converter for dash vertexes.
|
||||||
|
type DashVertexConverter struct {
|
||||||
|
next Flattener
|
||||||
|
x, y, distance float64
|
||||||
|
dash []float64
|
||||||
|
currentDash int
|
||||||
|
dashOffset float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) LineTo(x, y float64) {
|
||||||
|
dasher.lineTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) MoveTo(x, y float64) {
|
||||||
|
dasher.next.MoveTo(x, y)
|
||||||
|
dasher.x, dasher.y = x, y
|
||||||
|
dasher.distance = dasher.dashOffset
|
||||||
|
dasher.currentDash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) LineJoin() {
|
||||||
|
dasher.next.LineJoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) Close() {
|
||||||
|
dasher.next.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the pathbuilder interface.
|
||||||
|
func (dasher *DashVertexConverter) End() {
|
||||||
|
dasher.next.End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dasher *DashVertexConverter) lineTo(x, y float64) {
|
||||||
|
rest := dasher.dash[dasher.currentDash] - dasher.distance
|
||||||
|
for rest < 0 {
|
||||||
|
dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
|
||||||
|
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
|
||||||
|
rest = dasher.dash[dasher.currentDash] - dasher.distance
|
||||||
|
}
|
||||||
|
d := distance(dasher.x, dasher.y, x, y)
|
||||||
|
for d >= rest {
|
||||||
|
k := rest / d
|
||||||
|
lx := dasher.x + k*(x-dasher.x)
|
||||||
|
ly := dasher.y + k*(y-dasher.y)
|
||||||
|
if dasher.currentDash%2 == 0 {
|
||||||
|
// line
|
||||||
|
dasher.next.LineTo(lx, ly)
|
||||||
|
} else {
|
||||||
|
// gap
|
||||||
|
dasher.next.End()
|
||||||
|
dasher.next.MoveTo(lx, ly)
|
||||||
|
}
|
||||||
|
d = d - rest
|
||||||
|
dasher.x, dasher.y = lx, ly
|
||||||
|
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
|
||||||
|
rest = dasher.dash[dasher.currentDash]
|
||||||
|
}
|
||||||
|
dasher.distance = d
|
||||||
|
if dasher.currentDash%2 == 0 {
|
||||||
|
// line
|
||||||
|
dasher.next.LineTo(x, y)
|
||||||
|
} else {
|
||||||
|
// gap
|
||||||
|
dasher.next.End()
|
||||||
|
dasher.next.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
if dasher.distance >= dasher.dash[dasher.currentDash] {
|
||||||
|
dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
|
||||||
|
dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
|
||||||
|
}
|
||||||
|
dasher.x, dasher.y = x, y
|
||||||
|
}
|
41
vendor/github.com/wcharczuk/go-chart/v2/drawing/demux_flattener.go
generated
vendored
Normal file
41
vendor/github.com/wcharczuk/go-chart/v2/drawing/demux_flattener.go
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
// DemuxFlattener is a flattener
|
||||||
|
type DemuxFlattener struct {
|
||||||
|
Flatteners []Flattener
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) MoveTo(x, y float64) {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) LineTo(x, y float64) {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) LineJoin() {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.LineJoin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) Close() {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (dc DemuxFlattener) End() {
|
||||||
|
for _, flattener := range dc.Flatteners {
|
||||||
|
flattener.End()
|
||||||
|
}
|
||||||
|
}
|
148
vendor/github.com/wcharczuk/go-chart/v2/drawing/drawing.go
generated
vendored
Normal file
148
vendor/github.com/wcharczuk/go-chart/v2/drawing/drawing.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FillRule defines the type for fill rules
|
||||||
|
type FillRule int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FillRuleEvenOdd determines the "insideness" of a point in the shape
|
||||||
|
// by drawing a ray from that point to infinity in any direction
|
||||||
|
// and counting the number of path segments from the given shape that the ray crosses.
|
||||||
|
// If this number is odd, the point is inside; if even, the point is outside.
|
||||||
|
FillRuleEvenOdd FillRule = iota
|
||||||
|
// FillRuleWinding determines the "insideness" of a point in the shape
|
||||||
|
// by drawing a ray from that point to infinity in any direction
|
||||||
|
// and then examining the places where a segment of the shape crosses the ray.
|
||||||
|
// Starting with a count of zero, add one each time a path segment crosses
|
||||||
|
// the ray from left to right and subtract one each time
|
||||||
|
// a path segment crosses the ray from right to left. After counting the crossings,
|
||||||
|
// if the result is zero then the point is outside the path. Otherwise, it is inside.
|
||||||
|
FillRuleWinding
|
||||||
|
)
|
||||||
|
|
||||||
|
// LineCap is the style of line extremities
|
||||||
|
type LineCap int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RoundCap defines a rounded shape at the end of the line
|
||||||
|
RoundCap LineCap = iota
|
||||||
|
// ButtCap defines a squared shape exactly at the end of the line
|
||||||
|
ButtCap
|
||||||
|
// SquareCap defines a squared shape at the end of the line
|
||||||
|
SquareCap
|
||||||
|
)
|
||||||
|
|
||||||
|
// LineJoin is the style of segments joint
|
||||||
|
type LineJoin int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BevelJoin represents cut segments joint
|
||||||
|
BevelJoin LineJoin = iota
|
||||||
|
// RoundJoin represents rounded segments joint
|
||||||
|
RoundJoin
|
||||||
|
// MiterJoin represents peaker segments joint
|
||||||
|
MiterJoin
|
||||||
|
)
|
||||||
|
|
||||||
|
// StrokeStyle keeps stroke style attributes
|
||||||
|
// that is used by the Stroke method of a Drawer
|
||||||
|
type StrokeStyle struct {
|
||||||
|
// Color defines the color of stroke
|
||||||
|
Color color.Color
|
||||||
|
// Line width
|
||||||
|
Width float64
|
||||||
|
// Line cap style rounded, butt or square
|
||||||
|
LineCap LineCap
|
||||||
|
// Line join style bevel, round or miter
|
||||||
|
LineJoin LineJoin
|
||||||
|
// offset of the first dash
|
||||||
|
DashOffset float64
|
||||||
|
// array represented dash length pair values are plain dash and impair are space between dash
|
||||||
|
// if empty display plain line
|
||||||
|
Dash []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// SolidFillStyle define style attributes for a solid fill style
|
||||||
|
type SolidFillStyle struct {
|
||||||
|
// Color defines the line color
|
||||||
|
Color color.Color
|
||||||
|
// FillRule defines the file rule to used
|
||||||
|
FillRule FillRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valign Vertical Alignment of the text
|
||||||
|
type Valign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ValignTop top align text
|
||||||
|
ValignTop Valign = iota
|
||||||
|
// ValignCenter centered text
|
||||||
|
ValignCenter
|
||||||
|
// ValignBottom bottom aligned text
|
||||||
|
ValignBottom
|
||||||
|
// ValignBaseline align text with the baseline of the font
|
||||||
|
ValignBaseline
|
||||||
|
)
|
||||||
|
|
||||||
|
// Halign Horizontal Alignment of the text
|
||||||
|
type Halign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HalignLeft Horizontally align to left
|
||||||
|
HalignLeft = iota
|
||||||
|
// HalignCenter Horizontally align to center
|
||||||
|
HalignCenter
|
||||||
|
// HalignRight Horizontally align to right
|
||||||
|
HalignRight
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextStyle describe text property
|
||||||
|
type TextStyle struct {
|
||||||
|
// Color defines the color of text
|
||||||
|
Color color.Color
|
||||||
|
// Size font size
|
||||||
|
Size float64
|
||||||
|
// The font to use
|
||||||
|
Font *truetype.Font
|
||||||
|
// Horizontal Alignment of the text
|
||||||
|
Halign Halign
|
||||||
|
// Vertical Alignment of the text
|
||||||
|
Valign Valign
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalingPolicy is a constant to define how to scale an image
|
||||||
|
type ScalingPolicy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ScalingNone no scaling applied
|
||||||
|
ScalingNone ScalingPolicy = iota
|
||||||
|
// ScalingStretch the image is stretched so that its width and height are exactly the given width and height
|
||||||
|
ScalingStretch
|
||||||
|
// ScalingWidth the image is scaled so that its width is exactly the given width
|
||||||
|
ScalingWidth
|
||||||
|
// ScalingHeight the image is scaled so that its height is exactly the given height
|
||||||
|
ScalingHeight
|
||||||
|
// ScalingFit the image is scaled to the largest scale that allow the image to fit within a rectangle width x height
|
||||||
|
ScalingFit
|
||||||
|
// ScalingSameArea the image is scaled so that its area is exactly the area of the given rectangle width x height
|
||||||
|
ScalingSameArea
|
||||||
|
// ScalingFill the image is scaled to the smallest scale that allow the image to fully cover a rectangle width x height
|
||||||
|
ScalingFill
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageScaling style attributes used to display the image
|
||||||
|
type ImageScaling struct {
|
||||||
|
// Horizontal Alignment of the image
|
||||||
|
Halign Halign
|
||||||
|
// Vertical Alignment of the image
|
||||||
|
Valign Valign
|
||||||
|
// Width Height used by scaling policy
|
||||||
|
Width, Height float64
|
||||||
|
// ScalingPolicy defines the scaling policy to applied to the image
|
||||||
|
ScalingPolicy ScalingPolicy
|
||||||
|
}
|
97
vendor/github.com/wcharczuk/go-chart/v2/drawing/flattener.go
generated
vendored
Normal file
97
vendor/github.com/wcharczuk/go-chart/v2/drawing/flattener.go
generated
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
// Liner receive segment definition
|
||||||
|
type Liner interface {
|
||||||
|
// LineTo Draw a line from the current position to the point (x, y)
|
||||||
|
LineTo(x, y float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flattener receive segment definition
|
||||||
|
type Flattener interface {
|
||||||
|
// MoveTo Start a New line from the point (x, y)
|
||||||
|
MoveTo(x, y float64)
|
||||||
|
// LineTo Draw a line from the current position to the point (x, y)
|
||||||
|
LineTo(x, y float64)
|
||||||
|
// LineJoin add the most recent starting point to close the path to create a polygon
|
||||||
|
LineJoin()
|
||||||
|
// Close add the most recent starting point to close the path to create a polygon
|
||||||
|
Close()
|
||||||
|
// End mark the current line as finished so we can draw caps
|
||||||
|
End()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten convert curves into straight segments keeping join segments info
|
||||||
|
func Flatten(path *Path, flattener Flattener, scale float64) {
|
||||||
|
// First Point
|
||||||
|
var startX, startY float64
|
||||||
|
// Current Point
|
||||||
|
var x, y float64
|
||||||
|
var i int
|
||||||
|
for _, cmp := range path.Components {
|
||||||
|
switch cmp {
|
||||||
|
case MoveToComponent:
|
||||||
|
x, y = path.Points[i], path.Points[i+1]
|
||||||
|
startX, startY = x, y
|
||||||
|
if i != 0 {
|
||||||
|
flattener.End()
|
||||||
|
}
|
||||||
|
flattener.MoveTo(x, y)
|
||||||
|
i += 2
|
||||||
|
case LineToComponent:
|
||||||
|
x, y = path.Points[i], path.Points[i+1]
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
flattener.LineJoin()
|
||||||
|
i += 2
|
||||||
|
case QuadCurveToComponent:
|
||||||
|
// we include the previous point for the start of the curve
|
||||||
|
TraceQuad(flattener, path.Points[i-2:], 0.5)
|
||||||
|
x, y = path.Points[i+2], path.Points[i+3]
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
i += 4
|
||||||
|
case CubicCurveToComponent:
|
||||||
|
TraceCubic(flattener, path.Points[i-2:], 0.5)
|
||||||
|
x, y = path.Points[i+4], path.Points[i+5]
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
i += 6
|
||||||
|
case ArcToComponent:
|
||||||
|
x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale)
|
||||||
|
flattener.LineTo(x, y)
|
||||||
|
i += 6
|
||||||
|
case CloseComponent:
|
||||||
|
flattener.LineTo(startX, startY)
|
||||||
|
flattener.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flattener.End()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SegmentedPath is a path of disparate point sectinos.
|
||||||
|
type SegmentedPath struct {
|
||||||
|
Points []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path interface.
|
||||||
|
func (p *SegmentedPath) MoveTo(x, y float64) {
|
||||||
|
p.Points = append(p.Points, x, y)
|
||||||
|
// TODO need to mark this point as moveto
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path interface.
|
||||||
|
func (p *SegmentedPath) LineTo(x, y float64) {
|
||||||
|
p.Points = append(p.Points, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path interface.
|
||||||
|
func (p *SegmentedPath) LineJoin() {
|
||||||
|
// TODO need to mark the current point as linejoin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path interface.
|
||||||
|
func (p *SegmentedPath) Close() {
|
||||||
|
// TODO Close
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path interface.
|
||||||
|
func (p *SegmentedPath) End() {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
30
vendor/github.com/wcharczuk/go-chart/v2/drawing/free_type_path.go
generated
vendored
Normal file
30
vendor/github.com/wcharczuk/go-chart/v2/drawing/free_type_path.go
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FtLineBuilder is a builder for freetype raster glyphs.
|
||||||
|
type FtLineBuilder struct {
|
||||||
|
Adder raster.Adder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) MoveTo(x, y float64) {
|
||||||
|
liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) LineTo(x, y float64) {
|
||||||
|
liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) LineJoin() {}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) Close() {}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (liner FtLineBuilder) End() {}
|
82
vendor/github.com/wcharczuk/go-chart/v2/drawing/graphic_context.go
generated
vendored
Normal file
82
vendor/github.com/wcharczuk/go-chart/v2/drawing/graphic_context.go
generated
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GraphicContext describes the interface for the various backends (images, pdf, opengl, ...)
|
||||||
|
type GraphicContext interface {
|
||||||
|
// PathBuilder describes the interface for path drawing
|
||||||
|
PathBuilder
|
||||||
|
// BeginPath creates a new path
|
||||||
|
BeginPath()
|
||||||
|
// GetMatrixTransform returns the current transformation matrix
|
||||||
|
GetMatrixTransform() Matrix
|
||||||
|
// SetMatrixTransform sets the current transformation matrix
|
||||||
|
SetMatrixTransform(tr Matrix)
|
||||||
|
// ComposeMatrixTransform composes the current transformation matrix with tr
|
||||||
|
ComposeMatrixTransform(tr Matrix)
|
||||||
|
// Rotate applies a rotation to the current transformation matrix. angle is in radian.
|
||||||
|
Rotate(angle float64)
|
||||||
|
// Translate applies a translation to the current transformation matrix.
|
||||||
|
Translate(tx, ty float64)
|
||||||
|
// Scale applies a scale to the current transformation matrix.
|
||||||
|
Scale(sx, sy float64)
|
||||||
|
// SetStrokeColor sets the current stroke color
|
||||||
|
SetStrokeColor(c color.Color)
|
||||||
|
// SetFillColor sets the current fill color
|
||||||
|
SetFillColor(c color.Color)
|
||||||
|
// SetFillRule sets the current fill rule
|
||||||
|
SetFillRule(f FillRule)
|
||||||
|
// SetLineWidth sets the current line width
|
||||||
|
SetLineWidth(lineWidth float64)
|
||||||
|
// SetLineCap sets the current line cap
|
||||||
|
SetLineCap(cap LineCap)
|
||||||
|
// SetLineJoin sets the current line join
|
||||||
|
SetLineJoin(join LineJoin)
|
||||||
|
// SetLineDash sets the current dash
|
||||||
|
SetLineDash(dash []float64, dashOffset float64)
|
||||||
|
// SetFontSize sets the current font size
|
||||||
|
SetFontSize(fontSize float64)
|
||||||
|
// GetFontSize gets the current font size
|
||||||
|
GetFontSize() float64
|
||||||
|
// SetFont sets the font for the context
|
||||||
|
SetFont(f *truetype.Font)
|
||||||
|
// GetFont returns the current font
|
||||||
|
GetFont() *truetype.Font
|
||||||
|
// DrawImage draws the raster image in the current canvas
|
||||||
|
DrawImage(image image.Image)
|
||||||
|
// Save the context and push it to the context stack
|
||||||
|
Save()
|
||||||
|
// Restore remove the current context and restore the last one
|
||||||
|
Restore()
|
||||||
|
// Clear fills the current canvas with a default transparent color
|
||||||
|
Clear()
|
||||||
|
// ClearRect fills the specified rectangle with a default transparent color
|
||||||
|
ClearRect(x1, y1, x2, y2 int)
|
||||||
|
// SetDPI sets the current DPI
|
||||||
|
SetDPI(dpi int)
|
||||||
|
// GetDPI gets the current DPI
|
||||||
|
GetDPI() int
|
||||||
|
// GetStringBounds gets pixel bounds(dimensions) of given string
|
||||||
|
GetStringBounds(s string) (left, top, right, bottom float64)
|
||||||
|
// CreateStringPath creates a path from the string s at x, y
|
||||||
|
CreateStringPath(text string, x, y float64) (cursor float64)
|
||||||
|
// FillString draws the text at point (0, 0)
|
||||||
|
FillString(text string) (cursor float64)
|
||||||
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
|
FillStringAt(text string, x, y float64) (cursor float64)
|
||||||
|
// StrokeString draws the contour of the text at point (0, 0)
|
||||||
|
StrokeString(text string) (cursor float64)
|
||||||
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
|
StrokeStringAt(text string, x, y float64) (cursor float64)
|
||||||
|
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||||
|
Stroke(paths ...*Path)
|
||||||
|
// Fill fills the paths with the color specified by SetFillColor
|
||||||
|
Fill(paths ...*Path)
|
||||||
|
// FillStroke first fills the paths and than strokes them
|
||||||
|
FillStroke(paths ...*Path)
|
||||||
|
}
|
13
vendor/github.com/wcharczuk/go-chart/v2/drawing/image_filter.go
generated
vendored
Normal file
13
vendor/github.com/wcharczuk/go-chart/v2/drawing/image_filter.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
// ImageFilter defines the type of filter to use
|
||||||
|
type ImageFilter int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LinearFilter defines a linear filter
|
||||||
|
LinearFilter ImageFilter = iota
|
||||||
|
// BilinearFilter defines a bilinear filter
|
||||||
|
BilinearFilter
|
||||||
|
// BicubicFilter defines a bicubic filter
|
||||||
|
BicubicFilter
|
||||||
|
)
|
48
vendor/github.com/wcharczuk/go-chart/v2/drawing/line.go
generated
vendored
Normal file
48
vendor/github.com/wcharczuk/go-chart/v2/drawing/line.go
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolylineBresenham draws a polyline to an image
|
||||||
|
func PolylineBresenham(img draw.Image, c color.Color, s ...float64) {
|
||||||
|
for i := 2; i < len(s); i += 2 {
|
||||||
|
Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham draws a line between (x0, y0) and (x1, y1)
|
||||||
|
func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) {
|
||||||
|
dx := abs(x1 - x0)
|
||||||
|
dy := abs(y1 - y0)
|
||||||
|
var sx, sy int
|
||||||
|
if x0 < x1 {
|
||||||
|
sx = 1
|
||||||
|
} else {
|
||||||
|
sx = -1
|
||||||
|
}
|
||||||
|
if y0 < y1 {
|
||||||
|
sy = 1
|
||||||
|
} else {
|
||||||
|
sy = -1
|
||||||
|
}
|
||||||
|
err := dx - dy
|
||||||
|
|
||||||
|
var e2 int
|
||||||
|
for {
|
||||||
|
img.Set(x0, y0, color)
|
||||||
|
if x0 == x1 && y0 == y1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e2 = 2 * err
|
||||||
|
if e2 > -dy {
|
||||||
|
err = err - dy
|
||||||
|
x0 = x0 + sx
|
||||||
|
}
|
||||||
|
if e2 < dx {
|
||||||
|
err = err + dx
|
||||||
|
y0 = y0 + sy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
220
vendor/github.com/wcharczuk/go-chart/v2/drawing/matrix.go
generated
vendored
Normal file
220
vendor/github.com/wcharczuk/go-chart/v2/drawing/matrix.go
generated
vendored
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Matrix represents an affine transformation
|
||||||
|
type Matrix [6]float64
|
||||||
|
|
||||||
|
const (
|
||||||
|
epsilon = 1e-6
|
||||||
|
)
|
||||||
|
|
||||||
|
// Determinant compute the determinant of the matrix
|
||||||
|
func (tr Matrix) Determinant() float64 {
|
||||||
|
return tr[0]*tr[3] - tr[1]*tr[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform applies the transformation matrix to points. It modify the points passed in parameter.
|
||||||
|
func (tr Matrix) Transform(points []float64) {
|
||||||
|
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
|
||||||
|
x := points[i]
|
||||||
|
y := points[j]
|
||||||
|
points[i] = x*tr[0] + y*tr[2] + tr[4]
|
||||||
|
points[j] = x*tr[1] + y*tr[3] + tr[5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformPoint applies the transformation matrix to point. It returns the point the transformed point.
|
||||||
|
func (tr Matrix) TransformPoint(x, y float64) (xres, yres float64) {
|
||||||
|
xres = x*tr[0] + y*tr[2] + tr[4]
|
||||||
|
yres = x*tr[1] + y*tr[3] + tr[5]
|
||||||
|
return xres, yres
|
||||||
|
}
|
||||||
|
|
||||||
|
func minMax(x, y float64) (min, max float64) {
|
||||||
|
if x > y {
|
||||||
|
return y, x
|
||||||
|
}
|
||||||
|
return x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformRectangle applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle
|
||||||
|
func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) {
|
||||||
|
points := []float64{x0, y0, x2, y0, x2, y2, x0, y2}
|
||||||
|
tr.Transform(points)
|
||||||
|
points[0], points[2] = minMax(points[0], points[2])
|
||||||
|
points[4], points[6] = minMax(points[4], points[6])
|
||||||
|
points[1], points[3] = minMax(points[1], points[3])
|
||||||
|
points[5], points[7] = minMax(points[5], points[7])
|
||||||
|
|
||||||
|
nx0 = math.Min(points[0], points[4])
|
||||||
|
ny0 = math.Min(points[1], points[5])
|
||||||
|
nx2 = math.Max(points[2], points[6])
|
||||||
|
ny2 = math.Max(points[3], points[7])
|
||||||
|
return nx0, ny0, nx2, ny2
|
||||||
|
}
|
||||||
|
|
||||||
|
// InverseTransform applies the transformation inverse matrix to the rectangle represented by the min and the max point of the rectangle
|
||||||
|
func (tr Matrix) InverseTransform(points []float64) {
|
||||||
|
d := tr.Determinant() // matrix determinant
|
||||||
|
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
|
||||||
|
x := points[i]
|
||||||
|
y := points[j]
|
||||||
|
points[i] = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
|
||||||
|
points[j] = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InverseTransformPoint applies the transformation inverse matrix to point. It returns the point the transformed point.
|
||||||
|
func (tr Matrix) InverseTransformPoint(x, y float64) (xres, yres float64) {
|
||||||
|
d := tr.Determinant() // matrix determinant
|
||||||
|
xres = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
|
||||||
|
yres = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
|
||||||
|
return xres, yres
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorTransform applies the transformation matrix to points without using the translation parameter of the affine matrix.
|
||||||
|
// It modify the points passed in parameter.
|
||||||
|
func (tr Matrix) VectorTransform(points []float64) {
|
||||||
|
for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
|
||||||
|
x := points[i]
|
||||||
|
y := points[j]
|
||||||
|
points[i] = x*tr[0] + y*tr[2]
|
||||||
|
points[j] = x*tr[1] + y*tr[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIdentityMatrix creates an identity transformation matrix.
|
||||||
|
func NewIdentityMatrix() Matrix {
|
||||||
|
return Matrix{1, 0, 0, 1, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTranslationMatrix creates a transformation matrix with a translation tx and ty translation parameter
|
||||||
|
func NewTranslationMatrix(tx, ty float64) Matrix {
|
||||||
|
return Matrix{1, 0, 0, 1, tx, ty}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScaleMatrix creates a transformation matrix with a sx, sy scale factor
|
||||||
|
func NewScaleMatrix(sx, sy float64) Matrix {
|
||||||
|
return Matrix{sx, 0, 0, sy, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRotationMatrix creates a rotation transformation matrix. angle is in radian
|
||||||
|
func NewRotationMatrix(angle float64) Matrix {
|
||||||
|
c := math.Cos(angle)
|
||||||
|
s := math.Sin(angle)
|
||||||
|
return Matrix{c, s, -s, c, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMatrixFromRects creates a transformation matrix, combining a scale and a translation, that transform rectangle1 into rectangle2.
|
||||||
|
func NewMatrixFromRects(rectangle1, rectangle2 [4]float64) Matrix {
|
||||||
|
xScale := (rectangle2[2] - rectangle2[0]) / (rectangle1[2] - rectangle1[0])
|
||||||
|
yScale := (rectangle2[3] - rectangle2[1]) / (rectangle1[3] - rectangle1[1])
|
||||||
|
xOffset := rectangle2[0] - (rectangle1[0] * xScale)
|
||||||
|
yOffset := rectangle2[1] - (rectangle1[1] * yScale)
|
||||||
|
return Matrix{xScale, 0, 0, yScale, xOffset, yOffset}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse computes the inverse matrix
|
||||||
|
func (tr *Matrix) Inverse() {
|
||||||
|
d := tr.Determinant() // matrix determinant
|
||||||
|
tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
|
||||||
|
tr[0] = tr3 / d
|
||||||
|
tr[1] = -tr1 / d
|
||||||
|
tr[2] = -tr2 / d
|
||||||
|
tr[3] = tr0 / d
|
||||||
|
tr[4] = (tr2*tr5 - tr3*tr4) / d
|
||||||
|
tr[5] = (tr1*tr4 - tr0*tr5) / d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies the matrix.
|
||||||
|
func (tr Matrix) Copy() Matrix {
|
||||||
|
var result Matrix
|
||||||
|
copy(result[:], tr[:])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose multiplies trToConcat x tr
|
||||||
|
func (tr *Matrix) Compose(trToCompose Matrix) {
|
||||||
|
tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
|
||||||
|
tr[0] = trToCompose[0]*tr0 + trToCompose[1]*tr2
|
||||||
|
tr[1] = trToCompose[1]*tr3 + trToCompose[0]*tr1
|
||||||
|
tr[2] = trToCompose[2]*tr0 + trToCompose[3]*tr2
|
||||||
|
tr[3] = trToCompose[3]*tr3 + trToCompose[2]*tr1
|
||||||
|
tr[4] = trToCompose[4]*tr0 + trToCompose[5]*tr2 + tr4
|
||||||
|
tr[5] = trToCompose[5]*tr3 + trToCompose[4]*tr1 + tr5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale adds a scale to the matrix
|
||||||
|
func (tr *Matrix) Scale(sx, sy float64) {
|
||||||
|
tr[0] = sx * tr[0]
|
||||||
|
tr[1] = sx * tr[1]
|
||||||
|
tr[2] = sy * tr[2]
|
||||||
|
tr[3] = sy * tr[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate adds a translation to the matrix
|
||||||
|
func (tr *Matrix) Translate(tx, ty float64) {
|
||||||
|
tr[4] = tx*tr[0] + ty*tr[2] + tr[4]
|
||||||
|
tr[5] = ty*tr[3] + tx*tr[1] + tr[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate adds a rotation to the matrix.
|
||||||
|
func (tr *Matrix) Rotate(radians float64) {
|
||||||
|
c := math.Cos(radians)
|
||||||
|
s := math.Sin(radians)
|
||||||
|
t0 := c*tr[0] + s*tr[2]
|
||||||
|
t1 := s*tr[3] + c*tr[1]
|
||||||
|
t2 := c*tr[2] - s*tr[0]
|
||||||
|
t3 := c*tr[3] - s*tr[1]
|
||||||
|
tr[0] = t0
|
||||||
|
tr[1] = t1
|
||||||
|
tr[2] = t2
|
||||||
|
tr[3] = t3
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTranslation gets the matrix traslation.
|
||||||
|
func (tr Matrix) GetTranslation() (x, y float64) {
|
||||||
|
return tr[4], tr[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScaling gets the matrix scaling.
|
||||||
|
func (tr Matrix) GetScaling() (x, y float64) {
|
||||||
|
return tr[0], tr[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScale computes a scale for the matrix
|
||||||
|
func (tr Matrix) GetScale() float64 {
|
||||||
|
x := 0.707106781*tr[0] + 0.707106781*tr[1]
|
||||||
|
y := 0.707106781*tr[2] + 0.707106781*tr[3]
|
||||||
|
return math.Sqrt(x*x + y*y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ******************** Testing ********************
|
||||||
|
|
||||||
|
// Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements.
|
||||||
|
func (tr Matrix) Equals(tr2 Matrix) bool {
|
||||||
|
for i := 0; i < 6; i = i + 1 {
|
||||||
|
if !fequals(tr[i], tr2[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIdentity tests if a transformation is the identity transformation. A tolerance is applied when comparing matrix elements.
|
||||||
|
func (tr Matrix) IsIdentity() bool {
|
||||||
|
return fequals(tr[4], 0) && fequals(tr[5], 0) && tr.IsTranslation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTranslation tests if a transformation is is a pure translation. A tolerance is applied when comparing matrix elements.
|
||||||
|
func (tr Matrix) IsTranslation() bool {
|
||||||
|
return fequals(tr[0], 1) && fequals(tr[1], 0) && fequals(tr[2], 0) && fequals(tr[3], 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fequals compares two floats. return true if the distance between the two floats is less than epsilon, false otherwise
|
||||||
|
func fequals(float1, float2 float64) bool {
|
||||||
|
return math.Abs(float1-float2) <= epsilon
|
||||||
|
}
|
31
vendor/github.com/wcharczuk/go-chart/v2/drawing/painter.go
generated
vendored
Normal file
31
vendor/github.com/wcharczuk/go-chart/v2/drawing/painter.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/image/math/f64"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter
|
||||||
|
type Painter interface {
|
||||||
|
raster.Painter
|
||||||
|
SetColor(color color.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawImage draws an image into dest using an affine transformation matrix, an op and a filter
|
||||||
|
func DrawImage(src image.Image, dest draw.Image, tr Matrix, op draw.Op, filter ImageFilter) {
|
||||||
|
var transformer draw.Transformer
|
||||||
|
switch filter {
|
||||||
|
case LinearFilter:
|
||||||
|
transformer = draw.NearestNeighbor
|
||||||
|
case BilinearFilter:
|
||||||
|
transformer = draw.BiLinear
|
||||||
|
case BicubicFilter:
|
||||||
|
transformer = draw.CatmullRom
|
||||||
|
}
|
||||||
|
transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil)
|
||||||
|
}
|
186
vendor/github.com/wcharczuk/go-chart/v2/drawing/path.go
generated
vendored
Normal file
186
vendor/github.com/wcharczuk/go-chart/v2/drawing/path.go
generated
vendored
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PathBuilder describes the interface for path drawing.
|
||||||
|
type PathBuilder interface {
|
||||||
|
// LastPoint returns the current point of the current sub path
|
||||||
|
LastPoint() (x, y float64)
|
||||||
|
// MoveTo creates a new subpath that start at the specified point
|
||||||
|
MoveTo(x, y float64)
|
||||||
|
// LineTo adds a line to the current subpath
|
||||||
|
LineTo(x, y float64)
|
||||||
|
// QuadCurveTo adds a quadratic Bézier curve to the current subpath
|
||||||
|
QuadCurveTo(cx, cy, x, y float64)
|
||||||
|
// CubicCurveTo adds a cubic Bézier curve to the current subpath
|
||||||
|
CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64)
|
||||||
|
// ArcTo adds an arc to the current subpath
|
||||||
|
ArcTo(cx, cy, rx, ry, startAngle, angle float64)
|
||||||
|
// Close creates a line from the current point to the last MoveTo
|
||||||
|
// point (if not the same) and mark the path as closed so the
|
||||||
|
// first and last lines join nicely.
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathComponent represents component of a path
|
||||||
|
type PathComponent int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MoveToComponent is a MoveTo component in a Path
|
||||||
|
MoveToComponent PathComponent = iota
|
||||||
|
// LineToComponent is a LineTo component in a Path
|
||||||
|
LineToComponent
|
||||||
|
// QuadCurveToComponent is a QuadCurveTo component in a Path
|
||||||
|
QuadCurveToComponent
|
||||||
|
// CubicCurveToComponent is a CubicCurveTo component in a Path
|
||||||
|
CubicCurveToComponent
|
||||||
|
// ArcToComponent is a ArcTo component in a Path
|
||||||
|
ArcToComponent
|
||||||
|
// CloseComponent is a ArcTo component in a Path
|
||||||
|
CloseComponent
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path stores points
|
||||||
|
type Path struct {
|
||||||
|
// Components is a slice of PathComponent in a Path and mark the role of each points in the Path
|
||||||
|
Components []PathComponent
|
||||||
|
// Points are combined with Components to have a specific role in the path
|
||||||
|
Points []float64
|
||||||
|
// Last Point of the Path
|
||||||
|
x, y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Path) appendToPath(cmd PathComponent, points ...float64) {
|
||||||
|
p.Components = append(p.Components, cmd)
|
||||||
|
p.Points = append(p.Points, points...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastPoint returns the current point of the current path
|
||||||
|
func (p *Path) LastPoint() (x, y float64) {
|
||||||
|
return p.x, p.y
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo starts a new path at (x, y) position
|
||||||
|
func (p *Path) MoveTo(x, y float64) {
|
||||||
|
p.appendToPath(MoveToComponent, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo adds a line to the current path
|
||||||
|
func (p *Path) LineTo(x, y float64) {
|
||||||
|
if len(p.Components) == 0 { //special case when no move has been done
|
||||||
|
p.MoveTo(0, 0)
|
||||||
|
}
|
||||||
|
p.appendToPath(LineToComponent, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo adds a quadratic bezier curve to the current path
|
||||||
|
func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
|
||||||
|
if len(p.Components) == 0 { //special case when no move has been done
|
||||||
|
p.MoveTo(0, 0)
|
||||||
|
}
|
||||||
|
p.appendToPath(QuadCurveToComponent, cx, cy, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubicCurveTo adds a cubic bezier curve to the current path
|
||||||
|
func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
|
if len(p.Components) == 0 { //special case when no move has been done
|
||||||
|
p.MoveTo(0, 0)
|
||||||
|
}
|
||||||
|
p.appendToPath(CubicCurveToComponent, cx1, cy1, cx2, cy2, x, y)
|
||||||
|
p.x = x
|
||||||
|
p.y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArcTo adds an arc to the path
|
||||||
|
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
|
||||||
|
endAngle := startAngle + delta
|
||||||
|
clockWise := true
|
||||||
|
if delta < 0 {
|
||||||
|
clockWise = false
|
||||||
|
}
|
||||||
|
// normalize
|
||||||
|
if clockWise {
|
||||||
|
for endAngle < startAngle {
|
||||||
|
endAngle += math.Pi * 2.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for startAngle < endAngle {
|
||||||
|
startAngle += math.Pi * 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startX := cx + math.Cos(startAngle)*rx
|
||||||
|
startY := cy + math.Sin(startAngle)*ry
|
||||||
|
if len(p.Components) > 0 {
|
||||||
|
p.LineTo(startX, startY)
|
||||||
|
} else {
|
||||||
|
p.MoveTo(startX, startY)
|
||||||
|
}
|
||||||
|
p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, delta)
|
||||||
|
p.x = cx + math.Cos(endAngle)*rx
|
||||||
|
p.y = cy + math.Sin(endAngle)*ry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the current path
|
||||||
|
func (p *Path) Close() {
|
||||||
|
p.appendToPath(CloseComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy make a clone of the current path and return it
|
||||||
|
func (p *Path) Copy() (dest *Path) {
|
||||||
|
dest = new(Path)
|
||||||
|
dest.Components = make([]PathComponent, len(p.Components))
|
||||||
|
copy(dest.Components, p.Components)
|
||||||
|
dest.Points = make([]float64, len(p.Points))
|
||||||
|
copy(dest.Points, p.Points)
|
||||||
|
dest.x, dest.y = p.x, p.y
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear reset the path
|
||||||
|
func (p *Path) Clear() {
|
||||||
|
p.Components = p.Components[0:0]
|
||||||
|
p.Points = p.Points[0:0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the path is empty
|
||||||
|
func (p *Path) IsEmpty() bool {
|
||||||
|
return len(p.Components) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a debug text view of the path
|
||||||
|
func (p *Path) String() string {
|
||||||
|
s := ""
|
||||||
|
j := 0
|
||||||
|
for _, cmd := range p.Components {
|
||||||
|
switch cmd {
|
||||||
|
case MoveToComponent:
|
||||||
|
s += fmt.Sprintf("MoveTo: %f, %f\n", p.Points[j], p.Points[j+1])
|
||||||
|
j = j + 2
|
||||||
|
case LineToComponent:
|
||||||
|
s += fmt.Sprintf("LineTo: %f, %f\n", p.Points[j], p.Points[j+1])
|
||||||
|
j = j + 2
|
||||||
|
case QuadCurveToComponent:
|
||||||
|
s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3])
|
||||||
|
j = j + 4
|
||||||
|
case CubicCurveToComponent:
|
||||||
|
s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
|
||||||
|
j = j + 6
|
||||||
|
case ArcToComponent:
|
||||||
|
s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
|
||||||
|
j = j + 6
|
||||||
|
case CloseComponent:
|
||||||
|
s += "Close\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
283
vendor/github.com/wcharczuk/go-chart/v2/drawing/raster_graphic_context.go
generated
vendored
Normal file
283
vendor/github.com/wcharczuk/go-chart/v2/drawing/raster_graphic_context.go
generated
vendored
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRasterGraphicContext creates a new Graphic context from an image.
|
||||||
|
func NewRasterGraphicContext(img draw.Image) (*RasterGraphicContext, error) {
|
||||||
|
var painter Painter
|
||||||
|
switch selectImage := img.(type) {
|
||||||
|
case *image.RGBA:
|
||||||
|
painter = raster.NewRGBAPainter(selectImage)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("NewRasterGraphicContext() :: invalid image type")
|
||||||
|
}
|
||||||
|
return NewRasterGraphicContextWithPainter(img, painter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRasterGraphicContextWithPainter creates a new Graphic context from an image and a Painter (see Freetype-go)
|
||||||
|
func NewRasterGraphicContextWithPainter(img draw.Image, painter Painter) *RasterGraphicContext {
|
||||||
|
width, height := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
return &RasterGraphicContext{
|
||||||
|
NewStackGraphicContext(),
|
||||||
|
img,
|
||||||
|
painter,
|
||||||
|
raster.NewRasterizer(width, height),
|
||||||
|
raster.NewRasterizer(width, height),
|
||||||
|
&truetype.GlyphBuf{},
|
||||||
|
DefaultDPI,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RasterGraphicContext is the implementation of GraphicContext for a raster image
|
||||||
|
type RasterGraphicContext struct {
|
||||||
|
*StackGraphicContext
|
||||||
|
img draw.Image
|
||||||
|
painter Painter
|
||||||
|
fillRasterizer *raster.Rasterizer
|
||||||
|
strokeRasterizer *raster.Rasterizer
|
||||||
|
glyphBuf *truetype.GlyphBuf
|
||||||
|
DPI float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDPI sets the screen resolution in dots per inch.
|
||||||
|
func (rgc *RasterGraphicContext) SetDPI(dpi float64) {
|
||||||
|
rgc.DPI = dpi
|
||||||
|
rgc.recalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the resolution of the Image GraphicContext
|
||||||
|
func (rgc *RasterGraphicContext) GetDPI() float64 {
|
||||||
|
return rgc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear fills the current canvas with a default transparent color
|
||||||
|
func (rgc *RasterGraphicContext) Clear() {
|
||||||
|
width, height := rgc.img.Bounds().Dx(), rgc.img.Bounds().Dy()
|
||||||
|
rgc.ClearRect(0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearRect fills the current canvas with a default transparent color at the specified rectangle
|
||||||
|
func (rgc *RasterGraphicContext) ClearRect(x1, y1, x2, y2 int) {
|
||||||
|
imageColor := image.NewUniform(rgc.current.FillColor)
|
||||||
|
draw.Draw(rgc.img, image.Rect(x1, y1, x2, y2), imageColor, image.ZP, draw.Over)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawImage draws the raster image in the current canvas
|
||||||
|
func (rgc *RasterGraphicContext) DrawImage(img image.Image) {
|
||||||
|
DrawImage(img, rgc.img, rgc.current.Tr, draw.Over, BilinearFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillString draws the text at point (0, 0)
|
||||||
|
func (rgc *RasterGraphicContext) FillString(text string) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.FillStringAt(text, 0, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStringAt draws the text at the specified point (x, y)
|
||||||
|
func (rgc *RasterGraphicContext) FillStringAt(text string, x, y float64) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.CreateStringPath(text, x, y)
|
||||||
|
rgc.Fill()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeString draws the contour of the text at point (0, 0)
|
||||||
|
func (rgc *RasterGraphicContext) StrokeString(text string) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.StrokeStringAt(text, 0, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeStringAt draws the contour of the text at point (x, y)
|
||||||
|
func (rgc *RasterGraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64, err error) {
|
||||||
|
cursor, err = rgc.CreateStringPath(text, x, y)
|
||||||
|
rgc.Stroke()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rgc *RasterGraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
|
||||||
|
if err := rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), glyph, font.HintingNone); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range rgc.glyphBuf.Ends {
|
||||||
|
DrawContour(rgc, rgc.glyphBuf.Points[e0:e1], dx, dy)
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateStringPath creates a path from the string s at x, y, and returns the string width.
|
||||||
|
// The text is placed so that the left edge of the em square of the first character of s
|
||||||
|
// and the baseline intersect at x, y. The majority of the affected pixels will be
|
||||||
|
// above and to the right of the point, but some may be below or to the left.
|
||||||
|
// For example, drawing a string that starts with a 'J' in an italic font may
|
||||||
|
// affect pixels below and left of the point.
|
||||||
|
func (rgc *RasterGraphicContext) CreateStringPath(s string, x, y float64) (cursor float64, err error) {
|
||||||
|
f := rgc.GetFont()
|
||||||
|
if f == nil {
|
||||||
|
err = errors.New("No font loaded, cannot continue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rgc.recalc()
|
||||||
|
|
||||||
|
startx := x
|
||||||
|
prev, hasPrev := truetype.Index(0), false
|
||||||
|
for _, rc := range s {
|
||||||
|
index := f.Index(rc)
|
||||||
|
if hasPrev {
|
||||||
|
x += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
|
||||||
|
}
|
||||||
|
err = rgc.drawGlyph(index, x, y)
|
||||||
|
if err != nil {
|
||||||
|
cursor = x - startx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
|
||||||
|
prev, hasPrev = index, true
|
||||||
|
}
|
||||||
|
cursor = x - startx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringBounds returns the approximate pixel bounds of a string.
|
||||||
|
func (rgc *RasterGraphicContext) GetStringBounds(s string) (left, top, right, bottom float64, err error) {
|
||||||
|
f := rgc.GetFont()
|
||||||
|
if f == nil {
|
||||||
|
err = errors.New("No font loaded, cannot continue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rgc.recalc()
|
||||||
|
|
||||||
|
left = math.MaxFloat64
|
||||||
|
top = math.MaxFloat64
|
||||||
|
|
||||||
|
cursor := 0.0
|
||||||
|
prev, hasPrev := truetype.Index(0), false
|
||||||
|
for _, rc := range s {
|
||||||
|
index := f.Index(rc)
|
||||||
|
if hasPrev {
|
||||||
|
cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), index, font.HintingNone); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e0 := 0
|
||||||
|
for _, e1 := range rgc.glyphBuf.Ends {
|
||||||
|
ps := rgc.glyphBuf.Points[e0:e1]
|
||||||
|
for _, p := range ps {
|
||||||
|
x, y := pointToF64Point(p)
|
||||||
|
top = math.Min(top, y)
|
||||||
|
bottom = math.Max(bottom, y)
|
||||||
|
left = math.Min(left, x+cursor)
|
||||||
|
right = math.Max(right, x+cursor)
|
||||||
|
}
|
||||||
|
e0 = e1
|
||||||
|
}
|
||||||
|
cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
|
||||||
|
prev, hasPrev = index, true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// recalc recalculates scale and bounds values from the font size, screen
|
||||||
|
// resolution and font metrics, and invalidates the glyph cache.
|
||||||
|
func (rgc *RasterGraphicContext) recalc() {
|
||||||
|
rgc.current.Scale = rgc.current.FontSizePoints * float64(rgc.DPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFont sets the font used to draw text.
|
||||||
|
func (rgc *RasterGraphicContext) SetFont(font *truetype.Font) {
|
||||||
|
rgc.current.Font = font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the font used to draw text.
|
||||||
|
func (rgc *RasterGraphicContext) GetFont() *truetype.Font {
|
||||||
|
return rgc.current.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize sets the font size in points (as in ``a 12 point font'').
|
||||||
|
func (rgc *RasterGraphicContext) SetFontSize(fontSizePoints float64) {
|
||||||
|
rgc.current.FontSizePoints = fontSizePoints
|
||||||
|
rgc.recalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rgc *RasterGraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
|
||||||
|
rgc.painter.SetColor(color)
|
||||||
|
rasterizer.Rasterize(rgc.painter)
|
||||||
|
rasterizer.Clear()
|
||||||
|
rgc.current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke strokes the paths with the color specified by SetStrokeColor
|
||||||
|
func (rgc *RasterGraphicContext) Stroke(paths ...*Path) {
|
||||||
|
paths = append(paths, rgc.current.Path)
|
||||||
|
rgc.strokeRasterizer.UseNonZeroWinding = true
|
||||||
|
|
||||||
|
stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
|
||||||
|
stroker.HalfLineWidth = rgc.current.LineWidth / 2
|
||||||
|
|
||||||
|
var liner Flattener
|
||||||
|
if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
|
||||||
|
liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
|
||||||
|
} else {
|
||||||
|
liner = stroker
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
Flatten(p, liner, rgc.current.Tr.GetScale())
|
||||||
|
}
|
||||||
|
|
||||||
|
rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill fills the paths with the color specified by SetFillColor
|
||||||
|
func (rgc *RasterGraphicContext) Fill(paths ...*Path) {
|
||||||
|
paths = append(paths, rgc.current.Path)
|
||||||
|
rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
|
||||||
|
|
||||||
|
flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
|
||||||
|
for _, p := range paths {
|
||||||
|
Flatten(p, flattener, rgc.current.Tr.GetScale())
|
||||||
|
}
|
||||||
|
|
||||||
|
rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStroke first fills the paths and than strokes them
|
||||||
|
func (rgc *RasterGraphicContext) FillStroke(paths ...*Path) {
|
||||||
|
paths = append(paths, rgc.current.Path)
|
||||||
|
rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
|
||||||
|
rgc.strokeRasterizer.UseNonZeroWinding = true
|
||||||
|
|
||||||
|
flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
|
||||||
|
|
||||||
|
stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
|
||||||
|
stroker.HalfLineWidth = rgc.current.LineWidth / 2
|
||||||
|
|
||||||
|
var liner Flattener
|
||||||
|
if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
|
||||||
|
liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
|
||||||
|
} else {
|
||||||
|
liner = stroker
|
||||||
|
}
|
||||||
|
|
||||||
|
demux := DemuxFlattener{Flatteners: []Flattener{flattener, liner}}
|
||||||
|
for _, p := range paths {
|
||||||
|
Flatten(p, demux, rgc.current.Tr.GetScale())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
|
||||||
|
// Stroke
|
||||||
|
rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
|
||||||
|
}
|
211
vendor/github.com/wcharczuk/go-chart/v2/drawing/stack_graphic_context.go
generated
vendored
Normal file
211
vendor/github.com/wcharczuk/go-chart/v2/drawing/stack_graphic_context.go
generated
vendored
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackGraphicContext is a context that does thngs.
|
||||||
|
type StackGraphicContext struct {
|
||||||
|
current *ContextStack
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextStack is a graphic context implementation.
|
||||||
|
type ContextStack struct {
|
||||||
|
Tr Matrix
|
||||||
|
Path *Path
|
||||||
|
LineWidth float64
|
||||||
|
Dash []float64
|
||||||
|
DashOffset float64
|
||||||
|
StrokeColor color.Color
|
||||||
|
FillColor color.Color
|
||||||
|
FillRule FillRule
|
||||||
|
Cap LineCap
|
||||||
|
Join LineJoin
|
||||||
|
|
||||||
|
FontSizePoints float64
|
||||||
|
Font *truetype.Font
|
||||||
|
|
||||||
|
Scale float64
|
||||||
|
|
||||||
|
Previous *ContextStack
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStackGraphicContext Create a new Graphic context from an image
|
||||||
|
func NewStackGraphicContext() *StackGraphicContext {
|
||||||
|
gc := &StackGraphicContext{}
|
||||||
|
gc.current = new(ContextStack)
|
||||||
|
gc.current.Tr = NewIdentityMatrix()
|
||||||
|
gc.current.Path = new(Path)
|
||||||
|
gc.current.LineWidth = 1.0
|
||||||
|
gc.current.StrokeColor = image.Black
|
||||||
|
gc.current.FillColor = image.White
|
||||||
|
gc.current.Cap = RoundCap
|
||||||
|
gc.current.FillRule = FillRuleEvenOdd
|
||||||
|
gc.current.Join = RoundJoin
|
||||||
|
gc.current.FontSizePoints = 10
|
||||||
|
return gc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMatrixTransform returns the matrix transform.
|
||||||
|
func (gc *StackGraphicContext) GetMatrixTransform() Matrix {
|
||||||
|
return gc.current.Tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMatrixTransform sets the matrix transform.
|
||||||
|
func (gc *StackGraphicContext) SetMatrixTransform(tr Matrix) {
|
||||||
|
gc.current.Tr = tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeMatrixTransform composes a transform into the current transform.
|
||||||
|
func (gc *StackGraphicContext) ComposeMatrixTransform(tr Matrix) {
|
||||||
|
gc.current.Tr.Compose(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate rotates the matrix transform by an angle in degrees.
|
||||||
|
func (gc *StackGraphicContext) Rotate(angle float64) {
|
||||||
|
gc.current.Tr.Rotate(angle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate translates a transform.
|
||||||
|
func (gc *StackGraphicContext) Translate(tx, ty float64) {
|
||||||
|
gc.current.Tr.Translate(tx, ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale scales a transform.
|
||||||
|
func (gc *StackGraphicContext) Scale(sx, sy float64) {
|
||||||
|
gc.current.Tr.Scale(sx, sy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStrokeColor sets the stroke color.
|
||||||
|
func (gc *StackGraphicContext) SetStrokeColor(c color.Color) {
|
||||||
|
gc.current.StrokeColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFillColor sets the fill color.
|
||||||
|
func (gc *StackGraphicContext) SetFillColor(c color.Color) {
|
||||||
|
gc.current.FillColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFillRule sets the fill rule.
|
||||||
|
func (gc *StackGraphicContext) SetFillRule(f FillRule) {
|
||||||
|
gc.current.FillRule = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineWidth sets the line width.
|
||||||
|
func (gc *StackGraphicContext) SetLineWidth(lineWidth float64) {
|
||||||
|
gc.current.LineWidth = lineWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineCap sets the line cap.
|
||||||
|
func (gc *StackGraphicContext) SetLineCap(cap LineCap) {
|
||||||
|
gc.current.Cap = cap
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineJoin sets the line join.
|
||||||
|
func (gc *StackGraphicContext) SetLineJoin(join LineJoin) {
|
||||||
|
gc.current.Join = join
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineDash sets the line dash.
|
||||||
|
func (gc *StackGraphicContext) SetLineDash(dash []float64, dashOffset float64) {
|
||||||
|
gc.current.Dash = dash
|
||||||
|
gc.current.DashOffset = dashOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize sets the font size.
|
||||||
|
func (gc *StackGraphicContext) SetFontSize(fontSizePoints float64) {
|
||||||
|
gc.current.FontSizePoints = fontSizePoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFontSize gets the font size.
|
||||||
|
func (gc *StackGraphicContext) GetFontSize() float64 {
|
||||||
|
return gc.current.FontSizePoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFont sets the current font.
|
||||||
|
func (gc *StackGraphicContext) SetFont(f *truetype.Font) {
|
||||||
|
gc.current.Font = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the font.
|
||||||
|
func (gc *StackGraphicContext) GetFont() *truetype.Font {
|
||||||
|
return gc.current.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginPath starts a new path.
|
||||||
|
func (gc *StackGraphicContext) BeginPath() {
|
||||||
|
gc.current.Path.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns if the path is empty.
|
||||||
|
func (gc *StackGraphicContext) IsEmpty() bool {
|
||||||
|
return gc.current.Path.IsEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastPoint returns the last point on the path.
|
||||||
|
func (gc *StackGraphicContext) LastPoint() (x float64, y float64) {
|
||||||
|
return gc.current.Path.LastPoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo moves the cursor for a path.
|
||||||
|
func (gc *StackGraphicContext) MoveTo(x, y float64) {
|
||||||
|
gc.current.Path.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo draws a line.
|
||||||
|
func (gc *StackGraphicContext) LineTo(x, y float64) {
|
||||||
|
gc.current.Path.LineTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo draws a quad curve.
|
||||||
|
func (gc *StackGraphicContext) QuadCurveTo(cx, cy, x, y float64) {
|
||||||
|
gc.current.Path.QuadCurveTo(cx, cy, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubicCurveTo draws a cubic curve.
|
||||||
|
func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
|
gc.current.Path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArcTo draws an arc.
|
||||||
|
func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
|
||||||
|
gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes a path.
|
||||||
|
func (gc *StackGraphicContext) Close() {
|
||||||
|
gc.current.Path.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save pushes a context onto the stack.
|
||||||
|
func (gc *StackGraphicContext) Save() {
|
||||||
|
context := new(ContextStack)
|
||||||
|
context.FontSizePoints = gc.current.FontSizePoints
|
||||||
|
context.Font = gc.current.Font
|
||||||
|
context.LineWidth = gc.current.LineWidth
|
||||||
|
context.StrokeColor = gc.current.StrokeColor
|
||||||
|
context.FillColor = gc.current.FillColor
|
||||||
|
context.FillRule = gc.current.FillRule
|
||||||
|
context.Dash = gc.current.Dash
|
||||||
|
context.DashOffset = gc.current.DashOffset
|
||||||
|
context.Cap = gc.current.Cap
|
||||||
|
context.Join = gc.current.Join
|
||||||
|
context.Path = gc.current.Path.Copy()
|
||||||
|
context.Font = gc.current.Font
|
||||||
|
context.Scale = gc.current.Scale
|
||||||
|
copy(context.Tr[:], gc.current.Tr[:])
|
||||||
|
context.Previous = gc.current
|
||||||
|
gc.current = context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the previous context.
|
||||||
|
func (gc *StackGraphicContext) Restore() {
|
||||||
|
if gc.current.Previous != nil {
|
||||||
|
oldContext := gc.current
|
||||||
|
gc.current = gc.current.Previous
|
||||||
|
oldContext.Previous = nil
|
||||||
|
}
|
||||||
|
}
|
85
vendor/github.com/wcharczuk/go-chart/v2/drawing/stroker.go
generated
vendored
Normal file
85
vendor/github.com/wcharczuk/go-chart/v2/drawing/stroker.go
generated
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright 2010 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 13/12/2010 by Laurent Le Goff
|
||||||
|
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// NewLineStroker creates a new line stroker.
|
||||||
|
func NewLineStroker(c LineCap, j LineJoin, flattener Flattener) *LineStroker {
|
||||||
|
l := new(LineStroker)
|
||||||
|
l.Flattener = flattener
|
||||||
|
l.HalfLineWidth = 0.5
|
||||||
|
l.Cap = c
|
||||||
|
l.Join = j
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineStroker draws the stroke portion of a line.
|
||||||
|
type LineStroker struct {
|
||||||
|
Flattener Flattener
|
||||||
|
HalfLineWidth float64
|
||||||
|
Cap LineCap
|
||||||
|
Join LineJoin
|
||||||
|
vertices []float64
|
||||||
|
rewind []float64
|
||||||
|
x, y, nx, ny float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (l *LineStroker) MoveTo(x, y float64) {
|
||||||
|
l.x, l.y = x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (l *LineStroker) LineTo(x, y float64) {
|
||||||
|
l.line(l.x, l.y, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (l *LineStroker) LineJoin() {}
|
||||||
|
|
||||||
|
func (l *LineStroker) line(x1, y1, x2, y2 float64) {
|
||||||
|
dx := (x2 - x1)
|
||||||
|
dy := (y2 - y1)
|
||||||
|
d := vectorDistance(dx, dy)
|
||||||
|
if d != 0 {
|
||||||
|
nx := dy * l.HalfLineWidth / d
|
||||||
|
ny := -(dx * l.HalfLineWidth / d)
|
||||||
|
l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny)
|
||||||
|
l.x, l.y, l.nx, l.ny = x2, y2, nx, ny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (l *LineStroker) Close() {
|
||||||
|
if len(l.vertices) > 1 {
|
||||||
|
l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (l *LineStroker) End() {
|
||||||
|
if len(l.vertices) > 1 {
|
||||||
|
l.Flattener.MoveTo(l.vertices[0], l.vertices[1])
|
||||||
|
for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 {
|
||||||
|
l.Flattener.LineTo(l.vertices[i], l.vertices[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 {
|
||||||
|
l.Flattener.LineTo(l.rewind[i], l.rewind[j])
|
||||||
|
}
|
||||||
|
if len(l.vertices) > 1 {
|
||||||
|
l.Flattener.LineTo(l.vertices[0], l.vertices[1])
|
||||||
|
}
|
||||||
|
l.Flattener.End()
|
||||||
|
// reinit vertices
|
||||||
|
l.vertices = l.vertices[0:0]
|
||||||
|
l.rewind = l.rewind[0:0]
|
||||||
|
l.x, l.y, l.nx, l.ny = 0, 0, 0, 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LineStroker) appendVertex(vertices ...float64) {
|
||||||
|
s := len(vertices) / 2
|
||||||
|
l.vertices = append(l.vertices, vertices[:s]...)
|
||||||
|
l.rewind = append(l.rewind, vertices[s:]...)
|
||||||
|
}
|
67
vendor/github.com/wcharczuk/go-chart/v2/drawing/text.go
generated
vendored
Normal file
67
vendor/github.com/wcharczuk/go-chart/v2/drawing/text.go
generated
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DrawContour draws the given closed contour at the given sub-pixel offset.
|
||||||
|
func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) {
|
||||||
|
if len(ps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startX, startY := pointToF64Point(ps[0])
|
||||||
|
path.MoveTo(startX+dx, startY+dy)
|
||||||
|
q0X, q0Y, on0 := startX, startY, true
|
||||||
|
for _, p := range ps[1:] {
|
||||||
|
qX, qY := pointToF64Point(p)
|
||||||
|
on := p.Flags&0x01 != 0
|
||||||
|
if on {
|
||||||
|
if on0 {
|
||||||
|
path.LineTo(qX+dx, qY+dy)
|
||||||
|
} else {
|
||||||
|
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
|
||||||
|
}
|
||||||
|
} else if !on0 {
|
||||||
|
midX := (q0X + qX) / 2
|
||||||
|
midY := (q0Y + qY) / 2
|
||||||
|
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
|
||||||
|
}
|
||||||
|
q0X, q0Y, on0 = qX, qY, on
|
||||||
|
}
|
||||||
|
// Close the curve.
|
||||||
|
if on0 {
|
||||||
|
path.LineTo(startX+dx, startY+dy)
|
||||||
|
} else {
|
||||||
|
path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontExtents contains font metric information.
|
||||||
|
type FontExtents struct {
|
||||||
|
// Ascent is the distance that the text
|
||||||
|
// extends above the baseline.
|
||||||
|
Ascent float64
|
||||||
|
|
||||||
|
// Descent is the distance that the text
|
||||||
|
// extends below the baseline. The descent
|
||||||
|
// is given as a negative value.
|
||||||
|
Descent float64
|
||||||
|
|
||||||
|
// Height is the distance from the lowest
|
||||||
|
// descending point to the highest ascending
|
||||||
|
// point.
|
||||||
|
Height float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extents returns the FontExtents for a font.
|
||||||
|
// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro
|
||||||
|
func Extents(font *truetype.Font, size float64) FontExtents {
|
||||||
|
bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm()))
|
||||||
|
scale := size / float64(font.FUnitsPerEm())
|
||||||
|
return FontExtents{
|
||||||
|
Ascent: float64(bounds.Max.Y) * scale,
|
||||||
|
Descent: float64(bounds.Min.Y) * scale,
|
||||||
|
Height: float64(bounds.Max.Y-bounds.Min.Y) * scale,
|
||||||
|
}
|
||||||
|
}
|
39
vendor/github.com/wcharczuk/go-chart/v2/drawing/transformer.go
generated
vendored
Normal file
39
vendor/github.com/wcharczuk/go-chart/v2/drawing/transformer.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2010 The draw2d Authors. All rights reserved.
|
||||||
|
// created: 13/12/2010 by Laurent Le Goff
|
||||||
|
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
// Transformer apply the Matrix transformation tr
|
||||||
|
type Transformer struct {
|
||||||
|
Tr Matrix
|
||||||
|
Flattener Flattener
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the path builder interface.
|
||||||
|
func (t Transformer) MoveTo(x, y float64) {
|
||||||
|
u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
|
||||||
|
v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
|
||||||
|
t.Flattener.MoveTo(u, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the path builder interface.
|
||||||
|
func (t Transformer) LineTo(x, y float64) {
|
||||||
|
u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
|
||||||
|
v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
|
||||||
|
t.Flattener.LineTo(u, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineJoin implements the path builder interface.
|
||||||
|
func (t Transformer) LineJoin() {
|
||||||
|
t.Flattener.LineJoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the path builder interface.
|
||||||
|
func (t Transformer) Close() {
|
||||||
|
t.Flattener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// End implements the path builder interface.
|
||||||
|
func (t Transformer) End() {
|
||||||
|
t.Flattener.End()
|
||||||
|
}
|
68
vendor/github.com/wcharczuk/go-chart/v2/drawing/util.go
generated
vendored
Normal file
68
vendor/github.com/wcharczuk/go-chart/v2/drawing/util.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/raster"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PixelsToPoints returns the points for a given number of pixels at a DPI.
|
||||||
|
func PixelsToPoints(dpi, pixels float64) (points float64) {
|
||||||
|
points = (pixels * 72.0) / dpi
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointsToPixels returns the pixels for a given number of points at a DPI.
|
||||||
|
func PointsToPixels(dpi, points float64) (pixels float64) {
|
||||||
|
pixels = (points * dpi) / 72.0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(i int) int {
|
||||||
|
if i < 0 {
|
||||||
|
return -i
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(x1, y1, x2, y2 float64) float64 {
|
||||||
|
return vectorDistance(x2-x1, y2-y1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorDistance(dx, dy float64) float64 {
|
||||||
|
return float64(math.Sqrt(dx*dx + dy*dy))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFtCap(c LineCap) raster.Capper {
|
||||||
|
switch c {
|
||||||
|
case RoundCap:
|
||||||
|
return raster.RoundCapper
|
||||||
|
case ButtCap:
|
||||||
|
return raster.ButtCapper
|
||||||
|
case SquareCap:
|
||||||
|
return raster.SquareCapper
|
||||||
|
}
|
||||||
|
return raster.RoundCapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFtJoin(j LineJoin) raster.Joiner {
|
||||||
|
switch j {
|
||||||
|
case RoundJoin:
|
||||||
|
return raster.RoundJoiner
|
||||||
|
case BevelJoin:
|
||||||
|
return raster.BevelJoiner
|
||||||
|
}
|
||||||
|
return raster.RoundJoiner
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointToF64Point(p truetype.Point) (x, y float64) {
|
||||||
|
return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fUnitsToFloat64(x fixed.Int26_6) float64 {
|
||||||
|
scaled := x << 2
|
||||||
|
return float64(scaled/256) + float64(scaled%256)/256.0
|
||||||
|
}
|
131
vendor/github.com/wcharczuk/go-chart/v2/ema_series.go
generated
vendored
Normal file
131
vendor/github.com/wcharczuk/go-chart/v2/ema_series.go
generated
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultEMAPeriod is the default EMA period used in the sigma calculation.
|
||||||
|
DefaultEMAPeriod = 12
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*EMASeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*EMASeries)(nil)
|
||||||
|
_ LastValuesProvider = (*EMASeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// EMASeries is a computed series.
|
||||||
|
type EMASeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Period int
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
cache []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (ema EMASeries) GetName() string {
|
||||||
|
return ema.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (ema EMASeries) GetStyle() Style {
|
||||||
|
return ema.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (ema EMASeries) GetYAxis() YAxisType {
|
||||||
|
return ema.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriod returns the window size.
|
||||||
|
func (ema EMASeries) GetPeriod() int {
|
||||||
|
if ema.Period == 0 {
|
||||||
|
return DefaultEMAPeriod
|
||||||
|
}
|
||||||
|
return ema.Period
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (ema EMASeries) Len() int {
|
||||||
|
return ema.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigma returns the smoothing factor for the serise.
|
||||||
|
func (ema EMASeries) GetSigma() float64 {
|
||||||
|
return 2.0 / (float64(ema.GetPeriod()) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (ema *EMASeries) GetValues(index int) (x, y float64) {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ema.cache) == 0 {
|
||||||
|
ema.ensureCachedValues()
|
||||||
|
}
|
||||||
|
vx, _ := ema.InnerSeries.GetValues(index)
|
||||||
|
x = vx
|
||||||
|
y = ema.cache[index]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues computes the first moving average value.
|
||||||
|
func (ema *EMASeries) GetFirstValues() (x, y float64) {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ema.cache) == 0 {
|
||||||
|
ema.ensureCachedValues()
|
||||||
|
}
|
||||||
|
x, _ = ema.InnerSeries.GetValues(0)
|
||||||
|
y = ema.cache[0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues computes the last moving average value but walking back window size samples,
|
||||||
|
// and recomputing the last moving average chunk.
|
||||||
|
func (ema *EMASeries) GetLastValues() (x, y float64) {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ema.cache) == 0 {
|
||||||
|
ema.ensureCachedValues()
|
||||||
|
}
|
||||||
|
lastIndex := ema.InnerSeries.Len() - 1
|
||||||
|
x, _ = ema.InnerSeries.GetValues(lastIndex)
|
||||||
|
y = ema.cache[lastIndex]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ema *EMASeries) ensureCachedValues() {
|
||||||
|
seriesLength := ema.InnerSeries.Len()
|
||||||
|
ema.cache = make([]float64, seriesLength)
|
||||||
|
sigma := ema.GetSigma()
|
||||||
|
for x := 0; x < seriesLength; x++ {
|
||||||
|
_, y := ema.InnerSeries.GetValues(x)
|
||||||
|
if x == 0 {
|
||||||
|
ema.cache[x] = y
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
previousEMA := ema.cache[x-1]
|
||||||
|
ema.cache[x] = ((y - previousEMA) * sigma) + previousEMA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := ema.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (ema *EMASeries) Validate() error {
|
||||||
|
if ema.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("ema series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
49
vendor/github.com/wcharczuk/go-chart/v2/fileutil.go
generated
vendored
Normal file
49
vendor/github.com/wcharczuk/go-chart/v2/fileutil.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadLines reads a file and calls the handler for each line.
|
||||||
|
func ReadLines(filePath string, handler func(string) error) error {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
err = handler(line)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadChunks reads a file in `chunkSize` pieces, dispatched to the handler.
|
||||||
|
func ReadChunks(filePath string, chunkSize int, handler func([]byte) error) error {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
chunk := make([]byte, chunkSize)
|
||||||
|
for {
|
||||||
|
readBytes, err := f.Read(chunk)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
readData := chunk[:readBytes]
|
||||||
|
err = handler(readData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
37
vendor/github.com/wcharczuk/go-chart/v2/first_value_annotation.go
generated
vendored
Normal file
37
vendor/github.com/wcharczuk/go-chart/v2/first_value_annotation.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// FirstValueAnnotation returns an annotation series of just the first value of a value provider as an annotation.
|
||||||
|
func FirstValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
|
||||||
|
var vf ValueFormatter
|
||||||
|
if len(vfs) > 0 {
|
||||||
|
vf = vfs[0]
|
||||||
|
} else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
|
||||||
|
_, vf = typed.GetValueFormatters()
|
||||||
|
} else {
|
||||||
|
vf = FloatValueFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstValue Value2
|
||||||
|
if typed, isTyped := innerSeries.(FirstValuesProvider); isTyped {
|
||||||
|
firstValue.XValue, firstValue.YValue = typed.GetFirstValues()
|
||||||
|
firstValue.Label = vf(firstValue.YValue)
|
||||||
|
} else {
|
||||||
|
firstValue.XValue, firstValue.YValue = innerSeries.GetValues(0)
|
||||||
|
firstValue.Label = vf(firstValue.YValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var seriesName string
|
||||||
|
var seriesStyle Style
|
||||||
|
if typed, isTyped := innerSeries.(Series); isTyped {
|
||||||
|
seriesName = fmt.Sprintf("%s - First Value", typed.GetName())
|
||||||
|
seriesStyle = typed.GetStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnnotationSeries{
|
||||||
|
Name: seriesName,
|
||||||
|
Style: seriesStyle,
|
||||||
|
Annotations: []Value2{firstValue},
|
||||||
|
}
|
||||||
|
}
|
29
vendor/github.com/wcharczuk/go-chart/v2/font.go
generated
vendored
Normal file
29
vendor/github.com/wcharczuk/go-chart/v2/font.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/roboto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_defaultFontLock sync.Mutex
|
||||||
|
_defaultFont *truetype.Font
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDefaultFont returns the default font (Roboto-Medium).
|
||||||
|
func GetDefaultFont() (*truetype.Font, error) {
|
||||||
|
if _defaultFont == nil {
|
||||||
|
_defaultFontLock.Lock()
|
||||||
|
defer _defaultFontLock.Unlock()
|
||||||
|
if _defaultFont == nil {
|
||||||
|
font, err := truetype.Parse(roboto.Roboto)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_defaultFont = font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _defaultFont, nil
|
||||||
|
}
|
8
vendor/github.com/wcharczuk/go-chart/v2/go.mod
generated
vendored
Normal file
8
vendor/github.com/wcharczuk/go-chart/v2/go.mod
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module github.com/wcharczuk/go-chart/v2
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||||
|
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
||||||
|
)
|
5
vendor/github.com/wcharczuk/go-chart/v2/go.sum
generated
vendored
Normal file
5
vendor/github.com/wcharczuk/go-chart/v2/go.sum
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
72
vendor/github.com/wcharczuk/go-chart/v2/grid_line.go
generated
vendored
Normal file
72
vendor/github.com/wcharczuk/go-chart/v2/grid_line.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// GridLineProvider is a type that provides grid lines.
|
||||||
|
type GridLineProvider interface {
|
||||||
|
GetGridLines(ticks []Tick, isVertical bool, majorStyle, minorStyle Style) []GridLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// GridLine is a line on a graph canvas.
|
||||||
|
type GridLine struct {
|
||||||
|
IsMinor bool
|
||||||
|
Style Style
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Major returns if the gridline is a `major` line.
|
||||||
|
func (gl GridLine) Major() bool {
|
||||||
|
return !gl.IsMinor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minor returns if the gridline is a `minor` line.
|
||||||
|
func (gl GridLine) Minor() bool {
|
||||||
|
return gl.IsMinor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the gridline
|
||||||
|
func (gl GridLine) Render(r Renderer, canvasBox Box, ra Range, isVertical bool, defaults Style) {
|
||||||
|
r.SetStrokeColor(gl.Style.GetStrokeColor(defaults.GetStrokeColor()))
|
||||||
|
r.SetStrokeWidth(gl.Style.GetStrokeWidth(defaults.GetStrokeWidth()))
|
||||||
|
r.SetStrokeDashArray(gl.Style.GetStrokeDashArray(defaults.GetStrokeDashArray()))
|
||||||
|
|
||||||
|
if isVertical {
|
||||||
|
lineLeft := canvasBox.Left + ra.Translate(gl.Value)
|
||||||
|
lineBottom := canvasBox.Bottom
|
||||||
|
lineTop := canvasBox.Top
|
||||||
|
|
||||||
|
r.MoveTo(lineLeft, lineBottom)
|
||||||
|
r.LineTo(lineLeft, lineTop)
|
||||||
|
r.Stroke()
|
||||||
|
} else {
|
||||||
|
lineLeft := canvasBox.Left
|
||||||
|
lineRight := canvasBox.Right
|
||||||
|
lineHeight := canvasBox.Bottom - ra.Translate(gl.Value)
|
||||||
|
|
||||||
|
r.MoveTo(lineLeft, lineHeight)
|
||||||
|
r.LineTo(lineRight, lineHeight)
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateGridLines generates grid lines.
|
||||||
|
func GenerateGridLines(ticks []Tick, majorStyle, minorStyle Style) []GridLine {
|
||||||
|
var gl []GridLine
|
||||||
|
isMinor := false
|
||||||
|
|
||||||
|
if len(ticks) < 3 {
|
||||||
|
return gl
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range ticks[1 : len(ticks)-1] {
|
||||||
|
s := majorStyle
|
||||||
|
if isMinor {
|
||||||
|
s = minorStyle
|
||||||
|
}
|
||||||
|
gl = append(gl, GridLine{
|
||||||
|
Style: s,
|
||||||
|
IsMinor: isMinor,
|
||||||
|
Value: t.Value,
|
||||||
|
})
|
||||||
|
isMinor = !isMinor
|
||||||
|
}
|
||||||
|
return gl
|
||||||
|
}
|
67
vendor/github.com/wcharczuk/go-chart/v2/histogram_series.go
generated
vendored
Normal file
67
vendor/github.com/wcharczuk/go-chart/v2/histogram_series.go
generated
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// HistogramSeries is a special type of series that draws as a histogram.
|
||||||
|
// Some peculiarities; it will always be lower bounded at 0 (at the very least).
|
||||||
|
// This may alter ranges a bit and generally you want to put a histogram series on it's own y-axis.
|
||||||
|
type HistogramSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName implements Series.GetName.
|
||||||
|
func (hs HistogramSeries) GetName() string {
|
||||||
|
return hs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle implements Series.GetStyle.
|
||||||
|
func (hs HistogramSeries) GetStyle() Style {
|
||||||
|
return hs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which yaxis the series is mapped to.
|
||||||
|
func (hs HistogramSeries) GetYAxis() YAxisType {
|
||||||
|
return hs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len implements BoundedValuesProvider.Len.
|
||||||
|
func (hs HistogramSeries) Len() int {
|
||||||
|
return hs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues implements ValuesProvider.GetValues.
|
||||||
|
func (hs HistogramSeries) GetValues(index int) (x, y float64) {
|
||||||
|
return hs.InnerSeries.GetValues(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoundedValues implements BoundedValuesProvider.GetBoundedValue
|
||||||
|
func (hs HistogramSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
|
||||||
|
vx, vy := hs.InnerSeries.GetValues(index)
|
||||||
|
|
||||||
|
x = vx
|
||||||
|
|
||||||
|
if vy > 0 {
|
||||||
|
y1 = vy
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
y2 = vy
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements Series.Render.
|
||||||
|
func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := hs.Style.InheritFrom(defaults)
|
||||||
|
Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (hs HistogramSeries) Validate() error {
|
||||||
|
if hs.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("histogram series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
42
vendor/github.com/wcharczuk/go-chart/v2/image_writer.go
generated
vendored
Normal file
42
vendor/github.com/wcharczuk/go-chart/v2/image_writer.go
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RGBACollector is a render target for a chart.
|
||||||
|
type RGBACollector interface {
|
||||||
|
SetRGBA(i *image.RGBA)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageWriter is a special type of io.Writer that produces a final image.
|
||||||
|
type ImageWriter struct {
|
||||||
|
rgba *image.RGBA
|
||||||
|
contents *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *ImageWriter) Write(buffer []byte) (int, error) {
|
||||||
|
if ir.contents == nil {
|
||||||
|
ir.contents = bytes.NewBuffer([]byte{})
|
||||||
|
}
|
||||||
|
return ir.contents.Write(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRGBA sets a raw version of the image.
|
||||||
|
func (ir *ImageWriter) SetRGBA(i *image.RGBA) {
|
||||||
|
ir.rgba = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image returns an *image.Image for the result.
|
||||||
|
func (ir *ImageWriter) Image() (image.Image, error) {
|
||||||
|
if ir.rgba != nil {
|
||||||
|
return ir.rgba, nil
|
||||||
|
}
|
||||||
|
if ir.contents != nil && ir.contents.Len() > 0 {
|
||||||
|
return png.Decode(ir.contents)
|
||||||
|
}
|
||||||
|
return nil, errors.New("no valid sources for image data, cannot continue")
|
||||||
|
}
|
33
vendor/github.com/wcharczuk/go-chart/v2/jet.go
generated
vendored
Normal file
33
vendor/github.com/wcharczuk/go-chart/v2/jet.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
|
||||||
|
// Jet is a color map provider based on matlab's jet color map.
|
||||||
|
func Jet(v, vmin, vmax float64) drawing.Color {
|
||||||
|
c := drawing.Color{R: 0xff, G: 0xff, B: 0xff, A: 0xff} // white
|
||||||
|
var dv float64
|
||||||
|
|
||||||
|
if v < vmin {
|
||||||
|
v = vmin
|
||||||
|
}
|
||||||
|
if v > vmax {
|
||||||
|
v = vmax
|
||||||
|
}
|
||||||
|
dv = vmax - vmin
|
||||||
|
|
||||||
|
if v < (vmin + 0.25*dv) {
|
||||||
|
c.R = 0
|
||||||
|
c.G = drawing.ColorChannelFromFloat(4 * (v - vmin) / dv)
|
||||||
|
} else if v < (vmin + 0.5*dv) {
|
||||||
|
c.R = 0
|
||||||
|
c.B = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.25*dv-v)/dv)
|
||||||
|
} else if v < (vmin + 0.75*dv) {
|
||||||
|
c.R = drawing.ColorChannelFromFloat(4 * (v - vmin - 0.5*dv) / dv)
|
||||||
|
c.B = 0
|
||||||
|
} else {
|
||||||
|
c.G = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.75*dv-v)/dv)
|
||||||
|
c.B = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
37
vendor/github.com/wcharczuk/go-chart/v2/last_value_annotation_series.go
generated
vendored
Normal file
37
vendor/github.com/wcharczuk/go-chart/v2/last_value_annotation_series.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// LastValueAnnotationSeries returns an annotation series of just the last value of a value provider.
|
||||||
|
func LastValueAnnotationSeries(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
|
||||||
|
var vf ValueFormatter
|
||||||
|
if len(vfs) > 0 {
|
||||||
|
vf = vfs[0]
|
||||||
|
} else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
|
||||||
|
_, vf = typed.GetValueFormatters()
|
||||||
|
} else {
|
||||||
|
vf = FloatValueFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastValue Value2
|
||||||
|
if typed, isTyped := innerSeries.(LastValuesProvider); isTyped {
|
||||||
|
lastValue.XValue, lastValue.YValue = typed.GetLastValues()
|
||||||
|
lastValue.Label = vf(lastValue.YValue)
|
||||||
|
} else {
|
||||||
|
lastValue.XValue, lastValue.YValue = innerSeries.GetValues(innerSeries.Len() - 1)
|
||||||
|
lastValue.Label = vf(lastValue.YValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var seriesName string
|
||||||
|
var seriesStyle Style
|
||||||
|
if typed, isTyped := innerSeries.(Series); isTyped {
|
||||||
|
seriesName = fmt.Sprintf("%s - Last Value", typed.GetName())
|
||||||
|
seriesStyle = typed.GetStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnnotationSeries{
|
||||||
|
Name: seriesName,
|
||||||
|
Style: seriesStyle,
|
||||||
|
Annotations: []Value2{lastValue},
|
||||||
|
}
|
||||||
|
}
|
331
vendor/github.com/wcharczuk/go-chart/v2/legend.go
generated
vendored
Normal file
331
vendor/github.com/wcharczuk/go-chart/v2/legend.go
generated
vendored
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Legend returns a legend renderable function.
|
||||||
|
func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
return func(r Renderer, cb Box, chartDefaults Style) {
|
||||||
|
legendDefaults := Style{
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
FontSize: 8.0,
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
var legendStyle Style
|
||||||
|
if len(userDefaults) > 0 {
|
||||||
|
legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
|
||||||
|
} else {
|
||||||
|
legendStyle = chartDefaults.InheritFrom(legendDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFAULTS
|
||||||
|
legendPadding := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
Right: 5,
|
||||||
|
Bottom: 5,
|
||||||
|
}
|
||||||
|
lineTextGap := 5
|
||||||
|
lineLengthMinimum := 25
|
||||||
|
|
||||||
|
var labels []string
|
||||||
|
var lines []Style
|
||||||
|
for index, s := range c.Series {
|
||||||
|
if !s.GetStyle().Hidden {
|
||||||
|
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
|
||||||
|
labels = append(labels, s.GetName())
|
||||||
|
lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legend := Box{
|
||||||
|
Top: cb.Top,
|
||||||
|
Left: cb.Left,
|
||||||
|
// bottom and right will be sized by the legend content + relevant padding.
|
||||||
|
}
|
||||||
|
|
||||||
|
legendContent := Box{
|
||||||
|
Top: legend.Top + legendPadding.Top,
|
||||||
|
Left: legend.Left + legendPadding.Left,
|
||||||
|
Right: legend.Left + legendPadding.Left,
|
||||||
|
Bottom: legend.Top + legendPadding.Top,
|
||||||
|
}
|
||||||
|
|
||||||
|
legendStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
// measure
|
||||||
|
labelCount := 0
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
if len(labels[x]) > 0 {
|
||||||
|
tb := r.MeasureText(labels[x])
|
||||||
|
if labelCount > 0 {
|
||||||
|
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||||
|
}
|
||||||
|
legendContent.Bottom += tb.Height()
|
||||||
|
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||||
|
legendContent.Right = MaxInt(legendContent.Right, right)
|
||||||
|
labelCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legend = legend.Grow(legendContent)
|
||||||
|
legend.Right = legendContent.Right + legendPadding.Right
|
||||||
|
legend.Bottom = legendContent.Bottom + legendPadding.Bottom
|
||||||
|
|
||||||
|
Draw.Box(r, legend, legendStyle)
|
||||||
|
|
||||||
|
legendStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
ycursor := legendContent.Top
|
||||||
|
tx := legendContent.Left
|
||||||
|
legendCount := 0
|
||||||
|
var label string
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
label = labels[x]
|
||||||
|
if len(label) > 0 {
|
||||||
|
if legendCount > 0 {
|
||||||
|
ycursor += DefaultMinimumTickVerticalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
tb := r.MeasureText(label)
|
||||||
|
|
||||||
|
ty := ycursor + tb.Height()
|
||||||
|
r.Text(label, tx, ty)
|
||||||
|
|
||||||
|
th2 := tb.Height() >> 1
|
||||||
|
|
||||||
|
lx := tx + tb.Width() + lineTextGap
|
||||||
|
ly := ty - th2
|
||||||
|
lx2 := legendContent.Right - legendPadding.Right
|
||||||
|
|
||||||
|
r.SetStrokeColor(lines[x].GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(lines[x].GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(lx2, ly)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
ycursor += tb.Height()
|
||||||
|
legendCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegendThin is a legend that doesn't obscure the chart area.
|
||||||
|
func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
return func(r Renderer, cb Box, chartDefaults Style) {
|
||||||
|
legendDefaults := Style{
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
FontSize: 8.0,
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
Padding: Box{
|
||||||
|
Top: 2,
|
||||||
|
Left: 7,
|
||||||
|
Right: 7,
|
||||||
|
Bottom: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var legendStyle Style
|
||||||
|
if len(userDefaults) > 0 {
|
||||||
|
legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
|
||||||
|
} else {
|
||||||
|
legendStyle = chartDefaults.InheritFrom(legendDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.SetFont(legendStyle.GetFont())
|
||||||
|
r.SetFontColor(legendStyle.GetFontColor())
|
||||||
|
r.SetFontSize(legendStyle.GetFontSize())
|
||||||
|
|
||||||
|
var labels []string
|
||||||
|
var lines []Style
|
||||||
|
for index, s := range c.Series {
|
||||||
|
if !s.GetStyle().Hidden {
|
||||||
|
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
|
||||||
|
labels = append(labels, s.GetName())
|
||||||
|
lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var textHeight int
|
||||||
|
var textWidth int
|
||||||
|
var textBox Box
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
if len(labels[x]) > 0 {
|
||||||
|
textBox = r.MeasureText(labels[x])
|
||||||
|
textHeight = MaxInt(textBox.Height(), textHeight)
|
||||||
|
textWidth = MaxInt(textBox.Width(), textWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legendBoxHeight := textHeight + legendStyle.Padding.Top + legendStyle.Padding.Bottom
|
||||||
|
chartPadding := cb.Top
|
||||||
|
legendYMargin := (chartPadding - legendBoxHeight) >> 1
|
||||||
|
|
||||||
|
legendBox := Box{
|
||||||
|
Left: cb.Left,
|
||||||
|
Right: cb.Right,
|
||||||
|
Top: legendYMargin,
|
||||||
|
Bottom: legendYMargin + legendBoxHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw.Box(r, legendBox, legendDefaults)
|
||||||
|
|
||||||
|
r.SetFont(legendStyle.GetFont())
|
||||||
|
r.SetFontColor(legendStyle.GetFontColor())
|
||||||
|
r.SetFontSize(legendStyle.GetFontSize())
|
||||||
|
|
||||||
|
lineTextGap := 5
|
||||||
|
lineLengthMinimum := 25
|
||||||
|
|
||||||
|
tx := legendBox.Left + legendStyle.Padding.Left
|
||||||
|
ty := legendYMargin + legendStyle.Padding.Top + textHeight
|
||||||
|
var label string
|
||||||
|
var lx, ly int
|
||||||
|
th2 := textHeight >> 1
|
||||||
|
for index := range labels {
|
||||||
|
label = labels[index]
|
||||||
|
if len(label) > 0 {
|
||||||
|
textBox = r.MeasureText(label)
|
||||||
|
r.Text(label, tx, ty)
|
||||||
|
|
||||||
|
lx = tx + textBox.Width() + lineTextGap
|
||||||
|
ly = ty - th2
|
||||||
|
|
||||||
|
r.SetStrokeColor(lines[index].GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(lines[index].GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(lines[index].GetStrokeDashArray())
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(lx+lineLengthMinimum, ly)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegendLeft is a legend that is designed for longer series lists.
|
||||||
|
func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
return func(r Renderer, cb Box, chartDefaults Style) {
|
||||||
|
legendDefaults := Style{
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
FontSize: 8.0,
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
var legendStyle Style
|
||||||
|
if len(userDefaults) > 0 {
|
||||||
|
legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
|
||||||
|
} else {
|
||||||
|
legendStyle = chartDefaults.InheritFrom(legendDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFAULTS
|
||||||
|
legendPadding := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
Right: 5,
|
||||||
|
Bottom: 5,
|
||||||
|
}
|
||||||
|
lineTextGap := 5
|
||||||
|
lineLengthMinimum := 25
|
||||||
|
|
||||||
|
var labels []string
|
||||||
|
var lines []Style
|
||||||
|
for index, s := range c.Series {
|
||||||
|
if !s.GetStyle().Hidden {
|
||||||
|
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
|
||||||
|
labels = append(labels, s.GetName())
|
||||||
|
lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legend := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
// bottom and right will be sized by the legend content + relevant padding.
|
||||||
|
}
|
||||||
|
|
||||||
|
legendContent := Box{
|
||||||
|
Top: legend.Top + legendPadding.Top,
|
||||||
|
Left: legend.Left + legendPadding.Left,
|
||||||
|
Right: legend.Left + legendPadding.Left,
|
||||||
|
Bottom: legend.Top + legendPadding.Top,
|
||||||
|
}
|
||||||
|
|
||||||
|
legendStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
// measure
|
||||||
|
labelCount := 0
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
if len(labels[x]) > 0 {
|
||||||
|
tb := r.MeasureText(labels[x])
|
||||||
|
if labelCount > 0 {
|
||||||
|
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||||
|
}
|
||||||
|
legendContent.Bottom += tb.Height()
|
||||||
|
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||||
|
legendContent.Right = MaxInt(legendContent.Right, right)
|
||||||
|
labelCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legend = legend.Grow(legendContent)
|
||||||
|
legend.Right = legendContent.Right + legendPadding.Right
|
||||||
|
legend.Bottom = legendContent.Bottom + legendPadding.Bottom
|
||||||
|
|
||||||
|
Draw.Box(r, legend, legendStyle)
|
||||||
|
|
||||||
|
legendStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
ycursor := legendContent.Top
|
||||||
|
tx := legendContent.Left
|
||||||
|
legendCount := 0
|
||||||
|
var label string
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
label = labels[x]
|
||||||
|
if len(label) > 0 {
|
||||||
|
if legendCount > 0 {
|
||||||
|
ycursor += DefaultMinimumTickVerticalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
tb := r.MeasureText(label)
|
||||||
|
|
||||||
|
ty := ycursor + tb.Height()
|
||||||
|
r.Text(label, tx, ty)
|
||||||
|
|
||||||
|
th2 := tb.Height() >> 1
|
||||||
|
|
||||||
|
lx := tx + tb.Width() + lineTextGap
|
||||||
|
ly := ty - th2
|
||||||
|
lx2 := legendContent.Right - legendPadding.Right
|
||||||
|
|
||||||
|
r.SetStrokeColor(lines[x].GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(lines[x].GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(lx2, ly)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
ycursor += tb.Height()
|
||||||
|
legendCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
vendor/github.com/wcharczuk/go-chart/v2/linear_coefficient_provider.go
generated
vendored
Normal file
42
vendor/github.com/wcharczuk/go-chart/v2/linear_coefficient_provider.go
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// LinearCoefficientProvider is a type that returns linear cofficients.
|
||||||
|
type LinearCoefficientProvider interface {
|
||||||
|
Coefficients() (m, b, stdev, avg float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinearCoefficients returns a fixed linear coefficient pair.
|
||||||
|
func LinearCoefficients(m, b float64) LinearCoefficientSet {
|
||||||
|
return LinearCoefficientSet{
|
||||||
|
M: m,
|
||||||
|
B: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizedLinearCoefficients returns a fixed linear coefficient pair.
|
||||||
|
func NormalizedLinearCoefficients(m, b, stdev, avg float64) LinearCoefficientSet {
|
||||||
|
return LinearCoefficientSet{
|
||||||
|
M: m,
|
||||||
|
B: b,
|
||||||
|
StdDev: stdev,
|
||||||
|
Avg: avg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinearCoefficientSet is the m and b values for the linear equation in the form:
|
||||||
|
// y = (m*x) + b
|
||||||
|
type LinearCoefficientSet struct {
|
||||||
|
M float64
|
||||||
|
B float64
|
||||||
|
StdDev float64
|
||||||
|
Avg float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coefficients returns the coefficients.
|
||||||
|
func (lcs LinearCoefficientSet) Coefficients() (m, b, stdev, avg float64) {
|
||||||
|
m = lcs.M
|
||||||
|
b = lcs.B
|
||||||
|
stdev = lcs.StdDev
|
||||||
|
avg = lcs.Avg
|
||||||
|
return
|
||||||
|
}
|
187
vendor/github.com/wcharczuk/go-chart/v2/linear_regression_series.go
generated
vendored
Normal file
187
vendor/github.com/wcharczuk/go-chart/v2/linear_regression_series.go
generated
vendored
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*LinearRegressionSeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*LinearRegressionSeries)(nil)
|
||||||
|
_ LastValuesProvider = (*LinearRegressionSeries)(nil)
|
||||||
|
_ LinearCoefficientProvider = (*LinearRegressionSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinearRegressionSeries is a series that plots the n-nearest neighbors
|
||||||
|
// linear regression for the values.
|
||||||
|
type LinearRegressionSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
m float64
|
||||||
|
b float64
|
||||||
|
avgx float64
|
||||||
|
stddevx float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coefficients returns the linear coefficients for the series.
|
||||||
|
func (lrs LinearRegressionSeries) Coefficients() (m, b, stdev, avg float64) {
|
||||||
|
if lrs.IsZero() {
|
||||||
|
lrs.computeCoefficients()
|
||||||
|
}
|
||||||
|
|
||||||
|
m = lrs.m
|
||||||
|
b = lrs.b
|
||||||
|
stdev = lrs.stddevx
|
||||||
|
avg = lrs.avgx
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (lrs LinearRegressionSeries) GetName() string {
|
||||||
|
return lrs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (lrs LinearRegressionSeries) GetStyle() Style {
|
||||||
|
return lrs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (lrs LinearRegressionSeries) GetYAxis() YAxisType {
|
||||||
|
return lrs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (lrs LinearRegressionSeries) Len() int {
|
||||||
|
return MinInt(lrs.GetLimit(), lrs.InnerSeries.Len()-lrs.GetOffset())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLimit returns the window size.
|
||||||
|
func (lrs LinearRegressionSeries) GetLimit() int {
|
||||||
|
if lrs.Limit == 0 {
|
||||||
|
return lrs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
return lrs.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEndIndex returns the effective limit end.
|
||||||
|
func (lrs LinearRegressionSeries) GetEndIndex() int {
|
||||||
|
windowEnd := lrs.GetOffset() + lrs.GetLimit()
|
||||||
|
innerSeriesLastIndex := lrs.InnerSeries.Len() - 1
|
||||||
|
return MinInt(windowEnd, innerSeriesLastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOffset returns the data offset.
|
||||||
|
func (lrs LinearRegressionSeries) GetOffset() int {
|
||||||
|
if lrs.Offset == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return lrs.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) {
|
||||||
|
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lrs.IsZero() {
|
||||||
|
lrs.computeCoefficients()
|
||||||
|
}
|
||||||
|
offset := lrs.GetOffset()
|
||||||
|
effectiveIndex := MinInt(index+offset, lrs.InnerSeries.Len())
|
||||||
|
x, y = lrs.InnerSeries.GetValues(effectiveIndex)
|
||||||
|
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues computes the first linear regression value.
|
||||||
|
func (lrs *LinearRegressionSeries) GetFirstValues() (x, y float64) {
|
||||||
|
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lrs.IsZero() {
|
||||||
|
lrs.computeCoefficients()
|
||||||
|
}
|
||||||
|
x, y = lrs.InnerSeries.GetValues(0)
|
||||||
|
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues computes the last linear regression value.
|
||||||
|
func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) {
|
||||||
|
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lrs.IsZero() {
|
||||||
|
lrs.computeCoefficients()
|
||||||
|
}
|
||||||
|
endIndex := lrs.GetEndIndex()
|
||||||
|
x, y = lrs.InnerSeries.GetValues(endIndex)
|
||||||
|
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := lrs.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (lrs *LinearRegressionSeries) Validate() error {
|
||||||
|
if lrs.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("linear regression series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if we've computed the coefficients or not.
|
||||||
|
func (lrs *LinearRegressionSeries) IsZero() bool {
|
||||||
|
return lrs.m == 0 && lrs.b == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// internal helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
func (lrs *LinearRegressionSeries) normalize(xvalue float64) float64 {
|
||||||
|
return (xvalue - lrs.avgx) / lrs.stddevx
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`.
|
||||||
|
func (lrs *LinearRegressionSeries) computeCoefficients() {
|
||||||
|
startIndex := lrs.GetOffset()
|
||||||
|
endIndex := lrs.GetEndIndex()
|
||||||
|
|
||||||
|
p := float64(endIndex - startIndex)
|
||||||
|
|
||||||
|
xvalues := NewValueBufferWithCapacity(lrs.Len())
|
||||||
|
for index := startIndex; index < endIndex; index++ {
|
||||||
|
x, _ := lrs.InnerSeries.GetValues(index)
|
||||||
|
xvalues.Enqueue(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
lrs.avgx = Seq{xvalues}.Average()
|
||||||
|
lrs.stddevx = Seq{xvalues}.StdDev()
|
||||||
|
|
||||||
|
var sumx, sumy, sumxx, sumxy float64
|
||||||
|
for index := startIndex; index < endIndex; index++ {
|
||||||
|
x, y := lrs.InnerSeries.GetValues(index)
|
||||||
|
|
||||||
|
x = lrs.normalize(x)
|
||||||
|
|
||||||
|
sumx += x
|
||||||
|
sumy += y
|
||||||
|
sumxx += x * x
|
||||||
|
sumxy += x * y
|
||||||
|
}
|
||||||
|
|
||||||
|
lrs.m = (p*sumxy - sumx*sumy) / (p*sumxx - sumx*sumx)
|
||||||
|
lrs.b = (sumy / p) - (lrs.m * sumx / p)
|
||||||
|
}
|
73
vendor/github.com/wcharczuk/go-chart/v2/linear_sequence.go
generated
vendored
Normal file
73
vendor/github.com/wcharczuk/go-chart/v2/linear_sequence.go
generated
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// LinearRange returns an array of values representing the range from start to end, incremented by 1.0.
|
||||||
|
func LinearRange(start, end float64) []float64 {
|
||||||
|
return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(1.0)}.Values()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinearRangeWithStep returns the array values of a linear seq with a given start, end and optional step.
|
||||||
|
func LinearRangeWithStep(start, end, step float64) []float64 {
|
||||||
|
return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(step)}.Values()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLinearSequence returns a new linear generator.
|
||||||
|
func NewLinearSequence() *LinearSeq {
|
||||||
|
return &LinearSeq{step: 1.0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinearSeq is a stepwise generator.
|
||||||
|
type LinearSeq struct {
|
||||||
|
start float64
|
||||||
|
end float64
|
||||||
|
step float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start returns the start value.
|
||||||
|
func (lg LinearSeq) Start() float64 {
|
||||||
|
return lg.start
|
||||||
|
}
|
||||||
|
|
||||||
|
// End returns the end value.
|
||||||
|
func (lg LinearSeq) End() float64 {
|
||||||
|
return lg.end
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step returns the step value.
|
||||||
|
func (lg LinearSeq) Step() float64 {
|
||||||
|
return lg.step
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the seq.
|
||||||
|
func (lg LinearSeq) Len() int {
|
||||||
|
if lg.start < lg.end {
|
||||||
|
return int((lg.end-lg.start)/lg.step) + 1
|
||||||
|
}
|
||||||
|
return int((lg.start-lg.end)/lg.step) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value at a given index.
|
||||||
|
func (lg LinearSeq) GetValue(index int) float64 {
|
||||||
|
fi := float64(index)
|
||||||
|
if lg.start < lg.end {
|
||||||
|
return lg.start + (fi * lg.step)
|
||||||
|
}
|
||||||
|
return lg.start - (fi * lg.step)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStart sets the start and returns the linear generator.
|
||||||
|
func (lg *LinearSeq) WithStart(start float64) *LinearSeq {
|
||||||
|
lg.start = start
|
||||||
|
return lg
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEnd sets the end and returns the linear generator.
|
||||||
|
func (lg *LinearSeq) WithEnd(end float64) *LinearSeq {
|
||||||
|
lg.end = end
|
||||||
|
return lg
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStep sets the step and returns the linear generator.
|
||||||
|
func (lg *LinearSeq) WithStep(step float64) *LinearSeq {
|
||||||
|
lg.step = step
|
||||||
|
return lg
|
||||||
|
}
|
119
vendor/github.com/wcharczuk/go-chart/v2/linear_series.go
generated
vendored
Normal file
119
vendor/github.com/wcharczuk/go-chart/v2/linear_series.go
generated
vendored
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*LinearSeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*LinearSeries)(nil)
|
||||||
|
_ LastValuesProvider = (*LinearSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinearSeries is a series that plots a line in a given domain.
|
||||||
|
type LinearSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
XValues []float64
|
||||||
|
InnerSeries LinearCoefficientProvider
|
||||||
|
|
||||||
|
m float64
|
||||||
|
b float64
|
||||||
|
stdev float64
|
||||||
|
avg float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (ls LinearSeries) GetName() string {
|
||||||
|
return ls.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (ls LinearSeries) GetStyle() Style {
|
||||||
|
return ls.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (ls LinearSeries) GetYAxis() YAxisType {
|
||||||
|
return ls.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (ls LinearSeries) Len() int {
|
||||||
|
return len(ls.XValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEndIndex returns the effective limit end.
|
||||||
|
func (ls LinearSeries) GetEndIndex() int {
|
||||||
|
return len(ls.XValues) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (ls *LinearSeries) GetValues(index int) (x, y float64) {
|
||||||
|
if ls.InnerSeries == nil || len(ls.XValues) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ls.IsZero() {
|
||||||
|
ls.computeCoefficients()
|
||||||
|
}
|
||||||
|
x = ls.XValues[index]
|
||||||
|
y = (ls.m * ls.normalize(x)) + ls.b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues computes the first linear regression value.
|
||||||
|
func (ls *LinearSeries) GetFirstValues() (x, y float64) {
|
||||||
|
if ls.InnerSeries == nil || len(ls.XValues) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ls.IsZero() {
|
||||||
|
ls.computeCoefficients()
|
||||||
|
}
|
||||||
|
x, y = ls.GetValues(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues computes the last linear regression value.
|
||||||
|
func (ls *LinearSeries) GetLastValues() (x, y float64) {
|
||||||
|
if ls.InnerSeries == nil || len(ls.XValues) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ls.IsZero() {
|
||||||
|
ls.computeCoefficients()
|
||||||
|
}
|
||||||
|
x, y = ls.GetValues(ls.GetEndIndex())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (ls *LinearSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, ls.Style.InheritFrom(defaults), ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (ls LinearSeries) Validate() error {
|
||||||
|
if ls.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("linear regression series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the linear series has computed coefficients or not.
|
||||||
|
func (ls LinearSeries) IsZero() bool {
|
||||||
|
return ls.m == 0 && ls.b == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`.
|
||||||
|
func (ls *LinearSeries) computeCoefficients() {
|
||||||
|
ls.m, ls.b, ls.stdev, ls.avg = ls.InnerSeries.Coefficients()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LinearSeries) normalize(xvalue float64) float64 {
|
||||||
|
if ls.avg > 0 && ls.stdev > 0 {
|
||||||
|
return (xvalue - ls.avg) / ls.stdev
|
||||||
|
}
|
||||||
|
return xvalue
|
||||||
|
}
|
148
vendor/github.com/wcharczuk/go-chart/v2/logger.go
generated
vendored
Normal file
148
vendor/github.com/wcharczuk/go-chart/v2/logger.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Logger = (*StdoutLogger)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLogger returns a new logger.
|
||||||
|
func NewLogger(options ...LoggerOption) Logger {
|
||||||
|
stl := &StdoutLogger{
|
||||||
|
TimeFormat: time.RFC3339Nano,
|
||||||
|
Stdout: os.Stdout,
|
||||||
|
Stderr: os.Stderr,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(stl)
|
||||||
|
}
|
||||||
|
return stl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger is a type that implements the logging interface.
|
||||||
|
type Logger interface {
|
||||||
|
Info(...interface{})
|
||||||
|
Infof(string, ...interface{})
|
||||||
|
Debug(...interface{})
|
||||||
|
Debugf(string, ...interface{})
|
||||||
|
Err(error)
|
||||||
|
FatalErr(error)
|
||||||
|
Error(...interface{})
|
||||||
|
Errorf(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs an info message if the logger is set.
|
||||||
|
func Info(log Logger, arguments ...interface{}) {
|
||||||
|
if log == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info(arguments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs an info message if the logger is set.
|
||||||
|
func Infof(log Logger, format string, arguments ...interface{}) {
|
||||||
|
if log == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof(format, arguments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs an debug message if the logger is set.
|
||||||
|
func Debug(log Logger, arguments ...interface{}) {
|
||||||
|
if log == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug(arguments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs an debug message if the logger is set.
|
||||||
|
func Debugf(log Logger, format string, arguments ...interface{}) {
|
||||||
|
if log == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf(format, arguments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerOption mutates a stdout logger.
|
||||||
|
type LoggerOption = func(*StdoutLogger)
|
||||||
|
|
||||||
|
//OptLoggerStdout sets the Stdout writer.
|
||||||
|
func OptLoggerStdout(wr io.Writer) LoggerOption {
|
||||||
|
return func(stl *StdoutLogger) {
|
||||||
|
stl.Stdout = wr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptLoggerStderr sets the Stdout writer.
|
||||||
|
func OptLoggerStderr(wr io.Writer) LoggerOption {
|
||||||
|
return func(stl *StdoutLogger) {
|
||||||
|
stl.Stderr = wr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutLogger is a basic logger.
|
||||||
|
type StdoutLogger struct {
|
||||||
|
TimeFormat string
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info writes an info message.
|
||||||
|
func (l *StdoutLogger) Info(arguments ...interface{}) {
|
||||||
|
l.Println(append([]interface{}{"[INFO]"}, arguments...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof writes an info message.
|
||||||
|
func (l *StdoutLogger) Infof(format string, arguments ...interface{}) {
|
||||||
|
l.Println(append([]interface{}{"[INFO]"}, fmt.Sprintf(format, arguments...))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug writes an debug message.
|
||||||
|
func (l *StdoutLogger) Debug(arguments ...interface{}) {
|
||||||
|
l.Println(append([]interface{}{"[DEBUG]"}, arguments...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf writes an debug message.
|
||||||
|
func (l *StdoutLogger) Debugf(format string, arguments ...interface{}) {
|
||||||
|
l.Println(append([]interface{}{"[DEBUG]"}, fmt.Sprintf(format, arguments...))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error writes an error message.
|
||||||
|
func (l *StdoutLogger) Error(arguments ...interface{}) {
|
||||||
|
l.Println(append([]interface{}{"[ERROR]"}, arguments...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf writes an error message.
|
||||||
|
func (l *StdoutLogger) Errorf(format string, arguments ...interface{}) {
|
||||||
|
l.Println(append([]interface{}{"[ERROR]"}, fmt.Sprintf(format, arguments...))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err writes an error message.
|
||||||
|
func (l *StdoutLogger) Err(err error) {
|
||||||
|
if err != nil {
|
||||||
|
l.Println(append([]interface{}{"[ERROR]"}, err.Error())...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FatalErr writes an error message and exits.
|
||||||
|
func (l *StdoutLogger) FatalErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
l.Println(append([]interface{}{"[FATAL]"}, err.Error())...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println prints a new message.
|
||||||
|
func (l *StdoutLogger) Println(arguments ...interface{}) {
|
||||||
|
fmt.Fprintln(l.Stdout, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorln prints a new message.
|
||||||
|
func (l *StdoutLogger) Errorln(arguments ...interface{}) {
|
||||||
|
fmt.Fprintln(l.Stderr, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...)
|
||||||
|
}
|
338
vendor/github.com/wcharczuk/go-chart/v2/macd_series.go
generated
vendored
Normal file
338
vendor/github.com/wcharczuk/go-chart/v2/macd_series.go
generated
vendored
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultMACDPeriodPrimary is the long window.
|
||||||
|
DefaultMACDPeriodPrimary = 26
|
||||||
|
// DefaultMACDPeriodSecondary is the short window.
|
||||||
|
DefaultMACDPeriodSecondary = 12
|
||||||
|
// DefaultMACDSignalPeriod is the signal period to compute for the MACD.
|
||||||
|
DefaultMACDSignalPeriod = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
// MACDSeries computes the difference between the MACD line and the MACD Signal line.
|
||||||
|
// It is used in technical analysis and gives a lagging indicator of momentum.
|
||||||
|
type MACDSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
PrimaryPeriod int
|
||||||
|
SecondaryPeriod int
|
||||||
|
SignalPeriod int
|
||||||
|
|
||||||
|
signal *MACDSignalSeries
|
||||||
|
macdl *MACDLineSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (macd MACDSeries) Validate() error {
|
||||||
|
var err error
|
||||||
|
if macd.signal != nil {
|
||||||
|
err = macd.signal.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if macd.macdl != nil {
|
||||||
|
err = macd.macdl.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriods returns the primary and secondary periods.
|
||||||
|
func (macd MACDSeries) GetPeriods() (w1, w2, sig int) {
|
||||||
|
if macd.PrimaryPeriod == 0 {
|
||||||
|
w1 = DefaultMACDPeriodPrimary
|
||||||
|
} else {
|
||||||
|
w1 = macd.PrimaryPeriod
|
||||||
|
}
|
||||||
|
if macd.SecondaryPeriod == 0 {
|
||||||
|
w2 = DefaultMACDPeriodSecondary
|
||||||
|
} else {
|
||||||
|
w2 = macd.SecondaryPeriod
|
||||||
|
}
|
||||||
|
if macd.SignalPeriod == 0 {
|
||||||
|
sig = DefaultMACDSignalPeriod
|
||||||
|
} else {
|
||||||
|
sig = macd.SignalPeriod
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (macd MACDSeries) GetName() string {
|
||||||
|
return macd.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (macd MACDSeries) GetStyle() Style {
|
||||||
|
return macd.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (macd MACDSeries) GetYAxis() YAxisType {
|
||||||
|
return macd.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (macd MACDSeries) Len() int {
|
||||||
|
if macd.InnerSeries == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return macd.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index. For MACD it is the signal value.
|
||||||
|
func (macd *MACDSeries) GetValues(index int) (x float64, y float64) {
|
||||||
|
if macd.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if macd.signal == nil || macd.macdl == nil {
|
||||||
|
macd.ensureChildSeries()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, lv := macd.macdl.GetValues(index)
|
||||||
|
_, sv := macd.signal.GetValues(index)
|
||||||
|
|
||||||
|
x, _ = macd.InnerSeries.GetValues(index)
|
||||||
|
y = lv - sv
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (macd *MACDSeries) ensureChildSeries() {
|
||||||
|
w1, w2, sig := macd.GetPeriods()
|
||||||
|
|
||||||
|
macd.signal = &MACDSignalSeries{
|
||||||
|
InnerSeries: macd.InnerSeries,
|
||||||
|
PrimaryPeriod: w1,
|
||||||
|
SecondaryPeriod: w2,
|
||||||
|
SignalPeriod: sig,
|
||||||
|
}
|
||||||
|
|
||||||
|
macd.macdl = &MACDLineSeries{
|
||||||
|
InnerSeries: macd.InnerSeries,
|
||||||
|
PrimaryPeriod: w1,
|
||||||
|
SecondaryPeriod: w2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MACDSignalSeries computes the EMA of the MACDLineSeries.
|
||||||
|
type MACDSignalSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
PrimaryPeriod int
|
||||||
|
SecondaryPeriod int
|
||||||
|
SignalPeriod int
|
||||||
|
|
||||||
|
signal *EMASeries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (macds MACDSignalSeries) Validate() error {
|
||||||
|
if macds.signal != nil {
|
||||||
|
return macds.signal.Validate()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriods returns the primary and secondary periods.
|
||||||
|
func (macds MACDSignalSeries) GetPeriods() (w1, w2, sig int) {
|
||||||
|
if macds.PrimaryPeriod == 0 {
|
||||||
|
w1 = DefaultMACDPeriodPrimary
|
||||||
|
} else {
|
||||||
|
w1 = macds.PrimaryPeriod
|
||||||
|
}
|
||||||
|
if macds.SecondaryPeriod == 0 {
|
||||||
|
w2 = DefaultMACDPeriodSecondary
|
||||||
|
} else {
|
||||||
|
w2 = macds.SecondaryPeriod
|
||||||
|
}
|
||||||
|
if macds.SignalPeriod == 0 {
|
||||||
|
sig = DefaultMACDSignalPeriod
|
||||||
|
} else {
|
||||||
|
sig = macds.SignalPeriod
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (macds MACDSignalSeries) GetName() string {
|
||||||
|
return macds.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (macds MACDSignalSeries) GetStyle() Style {
|
||||||
|
return macds.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (macds MACDSignalSeries) GetYAxis() YAxisType {
|
||||||
|
return macds.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (macds *MACDSignalSeries) Len() int {
|
||||||
|
if macds.InnerSeries == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return macds.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index. For MACD it is the signal value.
|
||||||
|
func (macds *MACDSignalSeries) GetValues(index int) (x float64, y float64) {
|
||||||
|
if macds.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if macds.signal == nil {
|
||||||
|
macds.ensureSignal()
|
||||||
|
}
|
||||||
|
x, _ = macds.InnerSeries.GetValues(index)
|
||||||
|
_, y = macds.signal.GetValues(index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (macds *MACDSignalSeries) ensureSignal() {
|
||||||
|
w1, w2, sig := macds.GetPeriods()
|
||||||
|
|
||||||
|
macds.signal = &EMASeries{
|
||||||
|
InnerSeries: &MACDLineSeries{
|
||||||
|
InnerSeries: macds.InnerSeries,
|
||||||
|
PrimaryPeriod: w1,
|
||||||
|
SecondaryPeriod: w2,
|
||||||
|
},
|
||||||
|
Period: sig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := macds.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MACDLineSeries is a series that computes the inner ema1-ema2 value as a series.
|
||||||
|
type MACDLineSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
PrimaryPeriod int
|
||||||
|
SecondaryPeriod int
|
||||||
|
|
||||||
|
ema1 *EMASeries
|
||||||
|
ema2 *EMASeries
|
||||||
|
|
||||||
|
Sigma float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (macdl MACDLineSeries) Validate() error {
|
||||||
|
var err error
|
||||||
|
if macdl.ema1 != nil {
|
||||||
|
err = macdl.ema1.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if macdl.ema2 != nil {
|
||||||
|
err = macdl.ema2.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if macdl.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("MACDLineSeries: must provide an inner series")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (macdl MACDLineSeries) GetName() string {
|
||||||
|
return macdl.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (macdl MACDLineSeries) GetStyle() Style {
|
||||||
|
return macdl.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (macdl MACDLineSeries) GetYAxis() YAxisType {
|
||||||
|
return macdl.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriods returns the primary and secondary periods.
|
||||||
|
func (macdl MACDLineSeries) GetPeriods() (w1, w2 int) {
|
||||||
|
if macdl.PrimaryPeriod == 0 {
|
||||||
|
w1 = DefaultMACDPeriodPrimary
|
||||||
|
} else {
|
||||||
|
w1 = macdl.PrimaryPeriod
|
||||||
|
}
|
||||||
|
if macdl.SecondaryPeriod == 0 {
|
||||||
|
w2 = DefaultMACDPeriodSecondary
|
||||||
|
} else {
|
||||||
|
w2 = macdl.SecondaryPeriod
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (macdl *MACDLineSeries) Len() int {
|
||||||
|
if macdl.InnerSeries == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return macdl.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index. For MACD it is the signal value.
|
||||||
|
func (macdl *MACDLineSeries) GetValues(index int) (x float64, y float64) {
|
||||||
|
if macdl.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if macdl.ema1 == nil && macdl.ema2 == nil {
|
||||||
|
macdl.ensureEMASeries()
|
||||||
|
}
|
||||||
|
|
||||||
|
x, _ = macdl.InnerSeries.GetValues(index)
|
||||||
|
|
||||||
|
_, emav1 := macdl.ema1.GetValues(index)
|
||||||
|
_, emav2 := macdl.ema2.GetValues(index)
|
||||||
|
|
||||||
|
y = emav2 - emav1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (macdl *MACDLineSeries) ensureEMASeries() {
|
||||||
|
w1, w2 := macdl.GetPeriods()
|
||||||
|
|
||||||
|
macdl.ema1 = &EMASeries{
|
||||||
|
InnerSeries: macdl.InnerSeries,
|
||||||
|
Period: w1,
|
||||||
|
}
|
||||||
|
macdl.ema2 = &EMASeries{
|
||||||
|
InnerSeries: macdl.InnerSeries,
|
||||||
|
Period: w2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := macdl.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl)
|
||||||
|
}
|
252
vendor/github.com/wcharczuk/go-chart/v2/mathutil.go
generated
vendored
Normal file
252
vendor/github.com/wcharczuk/go-chart/v2/mathutil.go
generated
vendored
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
const (
|
||||||
|
_pi = math.Pi
|
||||||
|
_2pi = 2 * math.Pi
|
||||||
|
_3pi4 = (3 * math.Pi) / 4.0
|
||||||
|
_4pi3 = (4 * math.Pi) / 3.0
|
||||||
|
_3pi2 = (3 * math.Pi) / 2.0
|
||||||
|
_5pi4 = (5 * math.Pi) / 4.0
|
||||||
|
_7pi4 = (7 * math.Pi) / 4.0
|
||||||
|
_pi2 = math.Pi / 2.0
|
||||||
|
_pi4 = math.Pi / 4.0
|
||||||
|
_d2r = (math.Pi / 180.0)
|
||||||
|
_r2d = (180.0 / math.Pi)
|
||||||
|
)
|
||||||
|
|
||||||
|
// MinMax returns the minimum and maximum of a given set of values.
|
||||||
|
func MinMax(values ...float64) (min, max float64) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
max = values[0]
|
||||||
|
min = values[0]
|
||||||
|
var value float64
|
||||||
|
for index := 1; index < len(values); index++ {
|
||||||
|
value = values[index]
|
||||||
|
if value < min {
|
||||||
|
min = value
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinInt returns the minimum int.
|
||||||
|
func MinInt(values ...int) (min int) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
min = values[0]
|
||||||
|
var value int
|
||||||
|
for index := 1; index < len(values); index++ {
|
||||||
|
value = values[index]
|
||||||
|
if value < min {
|
||||||
|
min = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxInt returns the maximum int.
|
||||||
|
func MaxInt(values ...int) (max int) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
max = values[0]
|
||||||
|
var value int
|
||||||
|
for index := 1; index < len(values); index++ {
|
||||||
|
value = values[index]
|
||||||
|
if value > max {
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbsInt returns the absolute value of an int.
|
||||||
|
func AbsInt(value int) int {
|
||||||
|
if value < 0 {
|
||||||
|
return -value
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegreesToRadians returns degrees as radians.
|
||||||
|
func DegreesToRadians(degrees float64) float64 {
|
||||||
|
return degrees * _d2r
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadiansToDegrees translates a radian value to a degree value.
|
||||||
|
func RadiansToDegrees(value float64) float64 {
|
||||||
|
return math.Mod(value, _2pi) * _r2d
|
||||||
|
}
|
||||||
|
|
||||||
|
// PercentToRadians converts a normalized value (0,1) to radians.
|
||||||
|
func PercentToRadians(pct float64) float64 {
|
||||||
|
return DegreesToRadians(360.0 * pct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadianAdd adds a delta to a base in radians.
|
||||||
|
func RadianAdd(base, delta float64) float64 {
|
||||||
|
value := base + delta
|
||||||
|
if value > _2pi {
|
||||||
|
return math.Mod(value, _2pi)
|
||||||
|
} else if value < 0 {
|
||||||
|
return math.Mod(_2pi+value, _2pi)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegreesAdd adds a delta to a base in radians.
|
||||||
|
func DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
|
||||||
|
value := baseDegrees + deltaDegrees
|
||||||
|
if value > _2pi {
|
||||||
|
return math.Mod(value, 360.0)
|
||||||
|
} else if value < 0 {
|
||||||
|
return math.Mod(360.0+value, 360.0)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegreesToCompass returns the degree value in compass / clock orientation.
|
||||||
|
func DegreesToCompass(deg float64) float64 {
|
||||||
|
return DegreesAdd(deg, -90.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CirclePoint returns the absolute position of a circle diameter point given
|
||||||
|
// by the radius and the theta.
|
||||||
|
func CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) {
|
||||||
|
x = cx + int(radius*math.Sin(thetaRadians))
|
||||||
|
y = cy - int(radius*math.Cos(thetaRadians))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateCoordinate rotates a coordinate around a given center by a theta in radians.
|
||||||
|
func RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx, ry int) {
|
||||||
|
tempX, tempY := float64(x-cx), float64(y-cy)
|
||||||
|
rotatedX := tempX*math.Cos(thetaRadians) - tempY*math.Sin(thetaRadians)
|
||||||
|
rotatedY := tempX*math.Sin(thetaRadians) + tempY*math.Cos(thetaRadians)
|
||||||
|
rx = int(rotatedX) + cx
|
||||||
|
ry = int(rotatedY) + cy
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundUp rounds up to a given roundTo value.
|
||||||
|
func RoundUp(value, roundTo float64) float64 {
|
||||||
|
if roundTo < 0.000000000000001 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
d1 := math.Ceil(value / roundTo)
|
||||||
|
return d1 * roundTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundDown rounds down to a given roundTo value.
|
||||||
|
func RoundDown(value, roundTo float64) float64 {
|
||||||
|
if roundTo < 0.000000000000001 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
d1 := math.Floor(value / roundTo)
|
||||||
|
return d1 * roundTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize returns a set of numbers on the interval [0,1] for a given set of inputs.
|
||||||
|
// An example: 4,3,2,1 => 0.4, 0.3, 0.2, 0.1
|
||||||
|
// Caveat; the total may be < 1.0; there are going to be issues with irrational numbers etc.
|
||||||
|
func Normalize(values ...float64) []float64 {
|
||||||
|
var total float64
|
||||||
|
for _, v := range values {
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
output := make([]float64, len(values))
|
||||||
|
for x, v := range values {
|
||||||
|
output[x] = RoundDown(v/total, 0.0001)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mean returns the mean of a set of values
|
||||||
|
func Mean(values ...float64) float64 {
|
||||||
|
return Sum(values...) / float64(len(values))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeanInt returns the mean of a set of integer values.
|
||||||
|
func MeanInt(values ...int) int {
|
||||||
|
return SumInt(values...) / len(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum sums a set of values.
|
||||||
|
func Sum(values ...float64) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, v := range values {
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumInt sums a set of values.
|
||||||
|
func SumInt(values ...int) int {
|
||||||
|
var total int
|
||||||
|
for _, v := range values {
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// PercentDifference computes the percentage difference between two values.
|
||||||
|
// The formula is (v2-v1)/v1.
|
||||||
|
func PercentDifference(v1, v2 float64) float64 {
|
||||||
|
if v1 == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (v2 - v1) / v1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoundToForDelta returns a `roundTo` value for a given delta.
|
||||||
|
func GetRoundToForDelta(delta float64) float64 {
|
||||||
|
startingDeltaBound := math.Pow(10.0, 10.0)
|
||||||
|
for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 {
|
||||||
|
if delta > cursor {
|
||||||
|
return cursor / 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundPlaces rounds an input to a given places.
|
||||||
|
func RoundPlaces(input float64, places int) (rounded float64) {
|
||||||
|
if math.IsNaN(input) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
sign := 1.0
|
||||||
|
if input < 0 {
|
||||||
|
sign = -1
|
||||||
|
input *= -1
|
||||||
|
}
|
||||||
|
|
||||||
|
precision := math.Pow(10, float64(places))
|
||||||
|
digit := input * precision
|
||||||
|
_, decimal := math.Modf(digit)
|
||||||
|
|
||||||
|
if decimal >= 0.5 {
|
||||||
|
rounded = math.Ceil(digit)
|
||||||
|
} else {
|
||||||
|
rounded = math.Floor(digit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rounded / precision * sign
|
||||||
|
}
|
||||||
|
|
||||||
|
func f64i(value float64) int {
|
||||||
|
r := RoundPlaces(value, 0)
|
||||||
|
return int(r)
|
||||||
|
}
|
592
vendor/github.com/wcharczuk/go-chart/v2/matrix/matrix.go
generated
vendored
Normal file
592
vendor/github.com/wcharczuk/go-chart/v2/matrix/matrix.go
generated
vendored
Normal file
@ -0,0 +1,592 @@
|
|||||||
|
package matrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultEpsilon represents the minimum precision for matrix math operations.
|
||||||
|
DefaultEpsilon = 0.000001
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDimensionMismatch is a typical error.
|
||||||
|
ErrDimensionMismatch = errors.New("dimension mismatch")
|
||||||
|
|
||||||
|
// ErrSingularValue is a typical error.
|
||||||
|
ErrSingularValue = errors.New("singular value")
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new matrix.
|
||||||
|
func New(rows, cols int, values ...float64) *Matrix {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return &Matrix{
|
||||||
|
stride: cols,
|
||||||
|
epsilon: DefaultEpsilon,
|
||||||
|
elements: make([]float64, rows*cols),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elems := make([]float64, rows*cols)
|
||||||
|
copy(elems, values)
|
||||||
|
return &Matrix{
|
||||||
|
stride: cols,
|
||||||
|
epsilon: DefaultEpsilon,
|
||||||
|
elements: elems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity returns the identity matrix of a given order.
|
||||||
|
func Identity(order int) *Matrix {
|
||||||
|
m := New(order, order)
|
||||||
|
for i := 0; i < order; i++ {
|
||||||
|
m.Set(i, i, 1)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero returns a matrix of a given size zeroed.
|
||||||
|
func Zero(rows, cols int) *Matrix {
|
||||||
|
return New(rows, cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ones returns an matrix of ones.
|
||||||
|
func Ones(rows, cols int) *Matrix {
|
||||||
|
ones := make([]float64, rows*cols)
|
||||||
|
for i := 0; i < (rows * cols); i++ {
|
||||||
|
ones[i] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Matrix{
|
||||||
|
stride: cols,
|
||||||
|
epsilon: DefaultEpsilon,
|
||||||
|
elements: ones,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eye returns the eye matrix.
|
||||||
|
func Eye(n int) *Matrix {
|
||||||
|
m := Zero(n, n)
|
||||||
|
for i := 0; i < len(m.elements); i += n + 1 {
|
||||||
|
m.elements[i] = 1
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromArrays creates a matrix from a jagged array set.
|
||||||
|
func NewFromArrays(a [][]float64) *Matrix {
|
||||||
|
rows := len(a)
|
||||||
|
if rows == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cols := len(a[0])
|
||||||
|
m := New(rows, cols)
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
m.Set(row, col, a[row][col])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matrix represents a 2d dense array of floats.
|
||||||
|
type Matrix struct {
|
||||||
|
epsilon float64
|
||||||
|
elements []float64
|
||||||
|
stride int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the matrix.
|
||||||
|
func (m *Matrix) String() string {
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
rows, cols := m.Size()
|
||||||
|
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
buffer.WriteString(f64s(m.Get(row, col)))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteRune('\n')
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Epsilon returns the maximum precision for math operations.
|
||||||
|
func (m *Matrix) Epsilon() float64 {
|
||||||
|
return m.epsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEpsilon sets the epsilon on the matrix and returns a reference to the matrix.
|
||||||
|
func (m *Matrix) WithEpsilon(epsilon float64) *Matrix {
|
||||||
|
m.epsilon = epsilon
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each applies the action to each element of the matrix in
|
||||||
|
// rows => cols order.
|
||||||
|
func (m *Matrix) Each(action func(row, col int, value float64)) {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
action(row, col, m.Get(row, col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round rounds all the values in a matrix to it epsilon,
|
||||||
|
// returning a reference to the original
|
||||||
|
func (m *Matrix) Round() *Matrix {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
m.Set(row, col, roundToEpsilon(m.Get(row, col), m.epsilon))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrays returns the matrix as a two dimensional jagged array.
|
||||||
|
func (m *Matrix) Arrays() [][]float64 {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
a := make([][]float64, rows)
|
||||||
|
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
a[row] = make([]float64, cols)
|
||||||
|
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
a[row][col] = m.Get(row, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the dimensions of the matrix.
|
||||||
|
func (m *Matrix) Size() (rows, cols int) {
|
||||||
|
rows = len(m.elements) / m.stride
|
||||||
|
cols = m.stride
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSquare returns if the row count is equal to the column count.
|
||||||
|
func (m *Matrix) IsSquare() bool {
|
||||||
|
return m.stride == (len(m.elements) / m.stride)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSymmetric returns if the matrix is symmetric about its diagonal.
|
||||||
|
func (m *Matrix) IsSymmetric() bool {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
|
||||||
|
if rows != cols {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < rows; i++ {
|
||||||
|
for j := 0; j < i; j++ {
|
||||||
|
if m.Get(i, j) != m.Get(j, i) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the element at the given row, col.
|
||||||
|
func (m *Matrix) Get(row, col int) float64 {
|
||||||
|
index := (m.stride * row) + col
|
||||||
|
return m.elements[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a value.
|
||||||
|
func (m *Matrix) Set(row, col int, val float64) {
|
||||||
|
index := (m.stride * row) + col
|
||||||
|
m.elements[index] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Col returns a column of the matrix as a vector.
|
||||||
|
func (m *Matrix) Col(col int) Vector {
|
||||||
|
rows, _ := m.Size()
|
||||||
|
values := make([]float64, rows)
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
values[row] = m.Get(row, col)
|
||||||
|
}
|
||||||
|
return Vector(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row returns a row of the matrix as a vector.
|
||||||
|
func (m *Matrix) Row(row int) Vector {
|
||||||
|
_, cols := m.Size()
|
||||||
|
values := make([]float64, cols)
|
||||||
|
for col := 0; col < cols; col++ {
|
||||||
|
values[col] = m.Get(row, col)
|
||||||
|
}
|
||||||
|
return Vector(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubMatrix returns a sub matrix from a given outer matrix.
|
||||||
|
func (m *Matrix) SubMatrix(i, j, rows, cols int) *Matrix {
|
||||||
|
return &Matrix{
|
||||||
|
elements: m.elements[i*m.stride+j : i*m.stride+j+(rows-1)*m.stride+cols],
|
||||||
|
stride: m.stride,
|
||||||
|
epsilon: m.epsilon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScaleRow applies a scale to an entire row.
|
||||||
|
func (m *Matrix) ScaleRow(row int, scale float64) {
|
||||||
|
startIndex := row * m.stride
|
||||||
|
for i := startIndex; i < m.stride; i++ {
|
||||||
|
m.elements[i] = m.elements[i] * scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) scaleAddRow(rd int, rs int, f float64) {
|
||||||
|
indexd := rd * m.stride
|
||||||
|
indexs := rs * m.stride
|
||||||
|
for col := 0; col < m.stride; col++ {
|
||||||
|
m.elements[indexd] += f * m.elements[indexs]
|
||||||
|
indexd++
|
||||||
|
indexs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwapRows swaps a row in the matrix in place.
|
||||||
|
func (m *Matrix) SwapRows(i, j int) {
|
||||||
|
var vi, vj float64
|
||||||
|
for col := 0; col < m.stride; col++ {
|
||||||
|
vi = m.Get(i, col)
|
||||||
|
vj = m.Get(j, col)
|
||||||
|
m.Set(i, col, vj)
|
||||||
|
m.Set(j, col, vi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Augment concatenates two matrices about the horizontal.
|
||||||
|
func (m *Matrix) Augment(m2 *Matrix) (*Matrix, error) {
|
||||||
|
mr, mc := m.Size()
|
||||||
|
m2r, m2c := m2.Size()
|
||||||
|
if mr != m2r {
|
||||||
|
return nil, ErrDimensionMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
m3 := Zero(mr, mc+m2c)
|
||||||
|
for row := 0; row < mr; row++ {
|
||||||
|
for col := 0; col < mc; col++ {
|
||||||
|
m3.Set(row, col, m.Get(row, col))
|
||||||
|
}
|
||||||
|
for col := 0; col < m2c; col++ {
|
||||||
|
m3.Set(row, mc+col, m2.Get(row, col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m3, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a duplicate of a given matrix.
|
||||||
|
func (m *Matrix) Copy() *Matrix {
|
||||||
|
m2 := &Matrix{stride: m.stride, epsilon: m.epsilon, elements: make([]float64, len(m.elements))}
|
||||||
|
copy(m2.elements, m.elements)
|
||||||
|
return m2
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiagonalVector returns a vector from the diagonal of a matrix.
|
||||||
|
func (m *Matrix) DiagonalVector() Vector {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
rank := minInt(rows, cols)
|
||||||
|
values := make([]float64, rank)
|
||||||
|
|
||||||
|
for index := 0; index < rank; index++ {
|
||||||
|
values[index] = m.Get(index, index)
|
||||||
|
}
|
||||||
|
return Vector(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagonal returns a matrix from the diagonal of a matrix.
|
||||||
|
func (m *Matrix) Diagonal() *Matrix {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
rank := minInt(rows, cols)
|
||||||
|
m2 := New(rank, rank)
|
||||||
|
|
||||||
|
for index := 0; index < rank; index++ {
|
||||||
|
m2.Set(index, index, m.Get(index, index))
|
||||||
|
}
|
||||||
|
return m2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if a matrix equals another matrix.
|
||||||
|
func (m *Matrix) Equals(other *Matrix) bool {
|
||||||
|
if other == nil && m != nil {
|
||||||
|
return false
|
||||||
|
} else if other == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.stride != other.stride {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
msize := len(m.elements)
|
||||||
|
m2size := len(other.elements)
|
||||||
|
|
||||||
|
if msize != m2size {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < msize; i++ {
|
||||||
|
if m.elements[i] != other.elements[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// L returns the matrix with zeros below the diagonal.
|
||||||
|
func (m *Matrix) L() *Matrix {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
m2 := New(rows, cols)
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := row; col < cols; col++ {
|
||||||
|
m2.Set(row, col, m.Get(row, col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m2
|
||||||
|
}
|
||||||
|
|
||||||
|
// U returns the matrix with zeros above the diagonal.
|
||||||
|
// Does not include the diagonal.
|
||||||
|
func (m *Matrix) U() *Matrix {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
m2 := New(rows, cols)
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
for col := 0; col < row && col < cols; col++ {
|
||||||
|
m2.Set(row, col, m.Get(row, col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m2
|
||||||
|
}
|
||||||
|
|
||||||
|
// math operations
|
||||||
|
|
||||||
|
// Multiply multiplies two matrices.
|
||||||
|
func (m *Matrix) Multiply(m2 *Matrix) (m3 *Matrix, err error) {
|
||||||
|
if m.stride*m2.stride != len(m2.elements) {
|
||||||
|
return nil, ErrDimensionMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
m3 = &Matrix{epsilon: m.epsilon, stride: m2.stride, elements: make([]float64, (len(m.elements)/m.stride)*m2.stride)}
|
||||||
|
for m1c0, m3x := 0, 0; m1c0 < len(m.elements); m1c0 += m.stride {
|
||||||
|
for m2r0 := 0; m2r0 < m2.stride; m2r0++ {
|
||||||
|
for m1x, m2x := m1c0, m2r0; m2x < len(m2.elements); m2x += m2.stride {
|
||||||
|
m3.elements[m3x] += m.elements[m1x] * m2.elements[m2x]
|
||||||
|
m1x++
|
||||||
|
}
|
||||||
|
m3x++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pivotize does something i'm not sure what.
|
||||||
|
func (m *Matrix) Pivotize() *Matrix {
|
||||||
|
pv := make([]int, m.stride)
|
||||||
|
|
||||||
|
for i := range pv {
|
||||||
|
pv[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, dx := 0, 0; j < m.stride; j++ {
|
||||||
|
row := j
|
||||||
|
max := m.elements[dx]
|
||||||
|
for i, ixcj := j, dx; i < m.stride; i++ {
|
||||||
|
if m.elements[ixcj] > max {
|
||||||
|
max = m.elements[ixcj]
|
||||||
|
row = i
|
||||||
|
}
|
||||||
|
ixcj += m.stride
|
||||||
|
}
|
||||||
|
if j != row {
|
||||||
|
pv[row], pv[j] = pv[j], pv[row]
|
||||||
|
}
|
||||||
|
dx += m.stride + 1
|
||||||
|
}
|
||||||
|
p := Zero(m.stride, m.stride)
|
||||||
|
for r, c := range pv {
|
||||||
|
p.elements[r*m.stride+c] = 1
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times returns the product of a matrix and another.
|
||||||
|
func (m *Matrix) Times(m2 *Matrix) (*Matrix, error) {
|
||||||
|
mr, mc := m.Size()
|
||||||
|
m2r, m2c := m2.Size()
|
||||||
|
|
||||||
|
if mc != m2r {
|
||||||
|
return nil, fmt.Errorf("cannot multiply (%dx%d) and (%dx%d)", mr, mc, m2r, m2c)
|
||||||
|
//return nil, ErrDimensionMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Zero(mr, m2c)
|
||||||
|
|
||||||
|
for i := 0; i < mr; i++ {
|
||||||
|
sums := c.elements[i*c.stride : (i+1)*c.stride]
|
||||||
|
for k, a := range m.elements[i*m.stride : i*m.stride+m.stride] {
|
||||||
|
for j, b := range m2.elements[k*m2.stride : k*m2.stride+m2.stride] {
|
||||||
|
sums[j] += a * b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompositions
|
||||||
|
|
||||||
|
// LU performs the LU decomposition.
|
||||||
|
func (m *Matrix) LU() (l, u, p *Matrix) {
|
||||||
|
l = Zero(m.stride, m.stride)
|
||||||
|
u = Zero(m.stride, m.stride)
|
||||||
|
p = m.Pivotize()
|
||||||
|
m, _ = p.Multiply(m)
|
||||||
|
for j, jxc0 := 0, 0; j < m.stride; j++ {
|
||||||
|
l.elements[jxc0+j] = 1
|
||||||
|
for i, ixc0 := 0, 0; ixc0 <= jxc0; i++ {
|
||||||
|
sum := 0.
|
||||||
|
for k, kxcj := 0, j; k < i; k++ {
|
||||||
|
sum += u.elements[kxcj] * l.elements[ixc0+k]
|
||||||
|
kxcj += m.stride
|
||||||
|
}
|
||||||
|
u.elements[ixc0+j] = m.elements[ixc0+j] - sum
|
||||||
|
ixc0 += m.stride
|
||||||
|
}
|
||||||
|
for ixc0 := jxc0; ixc0 < len(m.elements); ixc0 += m.stride {
|
||||||
|
sum := 0.
|
||||||
|
for k, kxcj := 0, j; k < j; k++ {
|
||||||
|
sum += u.elements[kxcj] * l.elements[ixc0+k]
|
||||||
|
kxcj += m.stride
|
||||||
|
}
|
||||||
|
l.elements[ixc0+j] = (m.elements[ixc0+j] - sum) / u.elements[jxc0+j]
|
||||||
|
}
|
||||||
|
jxc0 += m.stride
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR performs the qr decomposition.
|
||||||
|
func (m *Matrix) QR() (q, r *Matrix) {
|
||||||
|
defer func() {
|
||||||
|
q = q.Round()
|
||||||
|
r = r.Round()
|
||||||
|
}()
|
||||||
|
|
||||||
|
rows, cols := m.Size()
|
||||||
|
qr := m.Copy()
|
||||||
|
q = New(rows, cols)
|
||||||
|
r = New(rows, cols)
|
||||||
|
|
||||||
|
var i, j, k int
|
||||||
|
var norm, s float64
|
||||||
|
|
||||||
|
for k = 0; k < cols; k++ {
|
||||||
|
norm = 0
|
||||||
|
for i = k; i < rows; i++ {
|
||||||
|
norm = math.Hypot(norm, qr.Get(i, k))
|
||||||
|
}
|
||||||
|
|
||||||
|
if norm != 0 {
|
||||||
|
if qr.Get(k, k) < 0 {
|
||||||
|
norm = -norm
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = k; i < rows; i++ {
|
||||||
|
qr.Set(i, k, qr.Get(i, k)/norm)
|
||||||
|
}
|
||||||
|
qr.Set(k, k, qr.Get(k, k)+1.0)
|
||||||
|
|
||||||
|
for j = k + 1; j < cols; j++ {
|
||||||
|
s = 0
|
||||||
|
for i = k; i < rows; i++ {
|
||||||
|
s += qr.Get(i, k) * qr.Get(i, j)
|
||||||
|
}
|
||||||
|
s = -s / qr.Get(k, k)
|
||||||
|
for i = k; i < rows; i++ {
|
||||||
|
qr.Set(i, j, qr.Get(i, j)+s*qr.Get(i, k))
|
||||||
|
|
||||||
|
if i < j {
|
||||||
|
r.Set(i, j, qr.Get(i, j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Set(k, k, -norm)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Q Matrix:
|
||||||
|
i, j, k = 0, 0, 0
|
||||||
|
|
||||||
|
for k = cols - 1; k >= 0; k-- {
|
||||||
|
q.Set(k, k, 1.0)
|
||||||
|
for j = k; j < cols; j++ {
|
||||||
|
if qr.Get(k, k) != 0 {
|
||||||
|
s = 0
|
||||||
|
for i = k; i < rows; i++ {
|
||||||
|
s += qr.Get(i, k) * q.Get(i, j)
|
||||||
|
}
|
||||||
|
s = -s / qr.Get(k, k)
|
||||||
|
for i = k; i < rows; i++ {
|
||||||
|
q.Set(i, j, q.Get(i, j)+s*qr.Get(i, k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transpose flips a matrix about its diagonal, returning a new copy.
|
||||||
|
func (m *Matrix) Transpose() *Matrix {
|
||||||
|
rows, cols := m.Size()
|
||||||
|
m2 := Zero(cols, rows)
|
||||||
|
for i := 0; i < rows; i++ {
|
||||||
|
for j := 0; j < cols; j++ {
|
||||||
|
m2.Set(j, i, m.Get(i, j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse returns a matrix such that M*I==1.
|
||||||
|
func (m *Matrix) Inverse() (*Matrix, error) {
|
||||||
|
if !m.IsSymmetric() {
|
||||||
|
return nil, ErrDimensionMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, cols := m.Size()
|
||||||
|
|
||||||
|
aug, _ := m.Augment(Eye(rows))
|
||||||
|
for i := 0; i < rows; i++ {
|
||||||
|
j := i
|
||||||
|
for k := i; k < rows; k++ {
|
||||||
|
if math.Abs(aug.Get(k, i)) > math.Abs(aug.Get(j, i)) {
|
||||||
|
j = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if j != i {
|
||||||
|
aug.SwapRows(i, j)
|
||||||
|
}
|
||||||
|
if aug.Get(i, i) == 0 {
|
||||||
|
return nil, ErrSingularValue
|
||||||
|
}
|
||||||
|
aug.ScaleRow(i, 1.0/aug.Get(i, i))
|
||||||
|
for k := 0; k < rows; k++ {
|
||||||
|
if k == i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
aug.scaleAddRow(k, i, -aug.Get(k, i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aug.SubMatrix(0, cols, rows, cols), nil
|
||||||
|
}
|
45
vendor/github.com/wcharczuk/go-chart/v2/matrix/regression.go
generated
vendored
Normal file
45
vendor/github.com/wcharczuk/go-chart/v2/matrix/regression.go
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package matrix
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPolyRegArraysSameLength is a common error.
|
||||||
|
ErrPolyRegArraysSameLength = errors.New("polynomial array inputs must be the same length")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poly returns the polynomial regress of a given degree over the given values.
|
||||||
|
func Poly(xvalues, yvalues []float64, degree int) ([]float64, error) {
|
||||||
|
if len(xvalues) != len(yvalues) {
|
||||||
|
return nil, ErrPolyRegArraysSameLength
|
||||||
|
}
|
||||||
|
|
||||||
|
m := len(yvalues)
|
||||||
|
n := degree + 1
|
||||||
|
y := New(m, 1, yvalues...)
|
||||||
|
x := Zero(m, n)
|
||||||
|
|
||||||
|
for i := 0; i < m; i++ {
|
||||||
|
ip := float64(1)
|
||||||
|
for j := 0; j < n; j++ {
|
||||||
|
x.Set(i, j, ip)
|
||||||
|
ip *= xvalues[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q, r := x.QR()
|
||||||
|
qty, err := q.Transpose().Times(y)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := make([]float64, n)
|
||||||
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
c[i] = qty.Get(i, 0)
|
||||||
|
for j := i + 1; j < n; j++ {
|
||||||
|
c[i] -= c[j] * r.Get(i, j)
|
||||||
|
}
|
||||||
|
c[i] /= r.Get(i, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
36
vendor/github.com/wcharczuk/go-chart/v2/matrix/util.go
generated
vendored
Normal file
36
vendor/github.com/wcharczuk/go-chart/v2/matrix/util.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package matrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func minInt(values ...int) int {
|
||||||
|
min := math.MaxInt32
|
||||||
|
|
||||||
|
for x := 0; x < len(values); x++ {
|
||||||
|
if values[x] < min {
|
||||||
|
min = values[x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt(values ...int) int {
|
||||||
|
max := math.MinInt32
|
||||||
|
|
||||||
|
for x := 0; x < len(values); x++ {
|
||||||
|
if values[x] > max {
|
||||||
|
max = values[x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func f64s(v float64) string {
|
||||||
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundToEpsilon(value, epsilon float64) float64 {
|
||||||
|
return math.Nextafter(value, value)
|
||||||
|
}
|
17
vendor/github.com/wcharczuk/go-chart/v2/matrix/vector.go
generated
vendored
Normal file
17
vendor/github.com/wcharczuk/go-chart/v2/matrix/vector.go
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package matrix
|
||||||
|
|
||||||
|
// Vector is just an array of values.
|
||||||
|
type Vector []float64
|
||||||
|
|
||||||
|
// DotProduct returns the dot product of two vectors.
|
||||||
|
func (v Vector) DotProduct(v2 Vector) (result float64, err error) {
|
||||||
|
if len(v) != len(v2) {
|
||||||
|
err = ErrDimensionMismatch
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(v); i++ {
|
||||||
|
result = result + (v[i] * v2[i])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
138
vendor/github.com/wcharczuk/go-chart/v2/min_max_series.go
generated
vendored
Normal file
138
vendor/github.com/wcharczuk/go-chart/v2/min_max_series.go
generated
vendored
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MinSeries draws a horizontal line at the minimum value of the inner series.
|
||||||
|
type MinSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
minValue *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (ms MinSeries) GetName() string {
|
||||||
|
return ms.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (ms MinSeries) GetStyle() Style {
|
||||||
|
return ms.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (ms MinSeries) GetYAxis() YAxisType {
|
||||||
|
return ms.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (ms MinSeries) Len() int {
|
||||||
|
return ms.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (ms *MinSeries) GetValues(index int) (x, y float64) {
|
||||||
|
ms.ensureMinValue()
|
||||||
|
x, _ = ms.InnerSeries.GetValues(index)
|
||||||
|
y = *ms.minValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (ms *MinSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := ms.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MinSeries) ensureMinValue() {
|
||||||
|
if ms.minValue == nil {
|
||||||
|
minValue := math.MaxFloat64
|
||||||
|
var y float64
|
||||||
|
for x := 0; x < ms.InnerSeries.Len(); x++ {
|
||||||
|
_, y = ms.InnerSeries.GetValues(x)
|
||||||
|
if y < minValue {
|
||||||
|
minValue = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ms.minValue = &minValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (ms *MinSeries) Validate() error {
|
||||||
|
if ms.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("min series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxSeries draws a horizontal line at the maximum value of the inner series.
|
||||||
|
type MaxSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
maxValue *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (ms MaxSeries) GetName() string {
|
||||||
|
return ms.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (ms MaxSeries) GetStyle() Style {
|
||||||
|
return ms.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (ms MaxSeries) GetYAxis() YAxisType {
|
||||||
|
return ms.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (ms MaxSeries) Len() int {
|
||||||
|
return ms.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (ms *MaxSeries) GetValues(index int) (x, y float64) {
|
||||||
|
ms.ensureMaxValue()
|
||||||
|
x, _ = ms.InnerSeries.GetValues(index)
|
||||||
|
y = *ms.maxValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (ms *MaxSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := ms.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MaxSeries) ensureMaxValue() {
|
||||||
|
if ms.maxValue == nil {
|
||||||
|
maxValue := -math.MaxFloat64
|
||||||
|
var y float64
|
||||||
|
for x := 0; x < ms.InnerSeries.Len(); x++ {
|
||||||
|
_, y = ms.InnerSeries.GetValues(x)
|
||||||
|
if y > maxValue {
|
||||||
|
maxValue = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ms.maxValue = &maxValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (ms *MaxSeries) Validate() error {
|
||||||
|
if ms.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("max series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
40
vendor/github.com/wcharczuk/go-chart/v2/parse.go
generated
vendored
Normal file
40
vendor/github.com/wcharczuk/go-chart/v2/parse.go
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseFloats parses a list of floats.
|
||||||
|
func ParseFloats(values ...string) ([]float64, error) {
|
||||||
|
var output []float64
|
||||||
|
var parsedValue float64
|
||||||
|
var err error
|
||||||
|
var cleaned string
|
||||||
|
for _, value := range values {
|
||||||
|
cleaned = strings.TrimSpace(strings.Replace(value, ",", "", -1))
|
||||||
|
if cleaned == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parsedValue, err = strconv.ParseFloat(cleaned, 64); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
output = append(output, parsedValue)
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTimes parses a list of times with a given format.
|
||||||
|
func ParseTimes(layout string, values ...string) ([]time.Time, error) {
|
||||||
|
var output []time.Time
|
||||||
|
var parsedValue time.Time
|
||||||
|
var err error
|
||||||
|
for _, value := range values {
|
||||||
|
if parsedValue, err = time.Parse(layout, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
output = append(output, parsedValue)
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
89
vendor/github.com/wcharczuk/go-chart/v2/percent_change_series.go
generated
vendored
Normal file
89
vendor/github.com/wcharczuk/go-chart/v2/percent_change_series.go
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*PercentChangeSeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*PercentChangeSeries)(nil)
|
||||||
|
_ LastValuesProvider = (*PercentChangeSeries)(nil)
|
||||||
|
_ ValueFormatterProvider = (*PercentChangeSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// PercentChangeSeriesSource is a series that
|
||||||
|
// can be used with a PercentChangeSeries
|
||||||
|
type PercentChangeSeriesSource interface {
|
||||||
|
Series
|
||||||
|
FirstValuesProvider
|
||||||
|
LastValuesProvider
|
||||||
|
ValuesProvider
|
||||||
|
ValueFormatterProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// PercentChangeSeries applies a
|
||||||
|
// percentage difference function to a given continuous series.
|
||||||
|
type PercentChangeSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
InnerSeries PercentChangeSeriesSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (pcs PercentChangeSeries) GetName() string {
|
||||||
|
return pcs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (pcs PercentChangeSeries) GetStyle() Style {
|
||||||
|
return pcs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len implements part of Series.
|
||||||
|
func (pcs PercentChangeSeries) Len() int {
|
||||||
|
return pcs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues implements FirstValuesProvider.
|
||||||
|
func (pcs PercentChangeSeries) GetFirstValues() (x, y float64) {
|
||||||
|
return pcs.InnerSeries.GetFirstValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets x, y values at a given index.
|
||||||
|
func (pcs PercentChangeSeries) GetValues(index int) (x, y float64) {
|
||||||
|
_, fy := pcs.InnerSeries.GetFirstValues()
|
||||||
|
x0, y0 := pcs.InnerSeries.GetValues(index)
|
||||||
|
x = x0
|
||||||
|
y = PercentDifference(fy, y0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueFormatters returns value formatter defaults for the series.
|
||||||
|
func (pcs PercentChangeSeries) GetValueFormatters() (x, y ValueFormatter) {
|
||||||
|
x, _ = pcs.InnerSeries.GetValueFormatters()
|
||||||
|
y = PercentValueFormatter
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (pcs PercentChangeSeries) GetYAxis() YAxisType {
|
||||||
|
return pcs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues gets the last values.
|
||||||
|
func (pcs PercentChangeSeries) GetLastValues() (x, y float64) {
|
||||||
|
_, fy := pcs.InnerSeries.GetFirstValues()
|
||||||
|
x0, y0 := pcs.InnerSeries.GetLastValues()
|
||||||
|
x = x0
|
||||||
|
y = PercentDifference(fy, y0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (pcs PercentChangeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := pcs.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, pcs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (pcs PercentChangeSeries) Validate() error {
|
||||||
|
return pcs.InnerSeries.Validate()
|
||||||
|
}
|
311
vendor/github.com/wcharczuk/go-chart/v2/pie_chart.go
generated
vendored
Normal file
311
vendor/github.com/wcharczuk/go-chart/v2/pie_chart.go
generated
vendored
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PieChart is a chart that draws sections of a circle based on percentages.
|
||||||
|
type PieChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
SliceStyle Style
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Values []Value
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (pc PieChart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if pc.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return pc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (pc PieChart) GetFont() *truetype.Font {
|
||||||
|
if pc.Font == nil {
|
||||||
|
return pc.defaultFont
|
||||||
|
}
|
||||||
|
return pc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (pc PieChart) GetWidth() int {
|
||||||
|
if pc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return pc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (pc PieChart) GetHeight() int {
|
||||||
|
if pc.Height == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return pc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(pc.Values) == 0 {
|
||||||
|
return errors.New("please provide at least one value")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(pc.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
canvasBox := pc.getDefaultCanvasBox()
|
||||||
|
canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
|
||||||
|
|
||||||
|
pc.drawBackground(r)
|
||||||
|
pc.drawCanvas(r, canvasBox)
|
||||||
|
|
||||||
|
finalValues, err := pc.finalizeValues(pc.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pc.drawSlices(r, canvasBox, finalValues)
|
||||||
|
pc.drawTitle(r)
|
||||||
|
for _, a := range pc.Elements {
|
||||||
|
a(r, canvasBox, pc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: pc.GetWidth(),
|
||||||
|
Bottom: pc.GetHeight(),
|
||||||
|
}, pc.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, pc.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawTitle(r Renderer) {
|
||||||
|
if len(pc.Title) > 0 && !pc.TitleStyle.Hidden {
|
||||||
|
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
||||||
|
cx, cy := canvasBox.Center()
|
||||||
|
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
|
radius := float64(diameter >> 1)
|
||||||
|
labelRadius := (radius * 2.0) / 3.0
|
||||||
|
|
||||||
|
// draw the pie slices
|
||||||
|
var rads, delta, delta2, total float64
|
||||||
|
var lx, ly int
|
||||||
|
|
||||||
|
if len(values) == 1 {
|
||||||
|
pc.stylePieChartValue(0).WriteToRenderer(r)
|
||||||
|
r.MoveTo(cx, cy)
|
||||||
|
r.Circle(radius, cx, cy)
|
||||||
|
} else {
|
||||||
|
for index, v := range values {
|
||||||
|
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(cx, cy)
|
||||||
|
rads = PercentToRadians(total)
|
||||||
|
delta = PercentToRadians(v.Value)
|
||||||
|
|
||||||
|
r.ArcTo(cx, cy, radius, radius, rads, delta)
|
||||||
|
|
||||||
|
r.LineTo(cx, cy)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
total = total + v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the labels
|
||||||
|
total = 0
|
||||||
|
for index, v := range values {
|
||||||
|
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||||
|
if len(v.Label) > 0 {
|
||||||
|
delta2 = PercentToRadians(total + (v.Value / 2.0))
|
||||||
|
delta2 = RadianAdd(delta2, _pi2)
|
||||||
|
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
|
||||||
|
|
||||||
|
tb := r.MeasureText(v.Label)
|
||||||
|
lx = lx - (tb.Width() >> 1)
|
||||||
|
ly = ly + (tb.Height() >> 1)
|
||||||
|
|
||||||
|
if lx < 0 {
|
||||||
|
lx = 0
|
||||||
|
}
|
||||||
|
if ly < 0 {
|
||||||
|
lx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Text(v.Label, lx, ly)
|
||||||
|
}
|
||||||
|
total = total + v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) finalizeValues(values []Value) ([]Value, error) {
|
||||||
|
finalValues := Values(values).Normalize()
|
||||||
|
if len(finalValues) == 0 {
|
||||||
|
return nil, fmt.Errorf("pie chart must contain at least (1) non-zero value")
|
||||||
|
}
|
||||||
|
return finalValues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getDefaultCanvasBox() Box {
|
||||||
|
return pc.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
|
||||||
|
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
|
|
||||||
|
square := Box{
|
||||||
|
Right: circleDiameter,
|
||||||
|
Bottom: circleDiameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.Fit(square)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getBackgroundStyle() Style {
|
||||||
|
return pc.Background.InheritFrom(pc.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getCanvasStyle() Style {
|
||||||
|
return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: pc.GetColorPalette().CanvasColor(),
|
||||||
|
StrokeColor: pc.GetColorPalette().CanvasStrokeColor(),
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsPieChartValue() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: pc.GetColorPalette().TextColor(),
|
||||||
|
StrokeWidth: 5.0,
|
||||||
|
FillColor: pc.GetColorPalette().TextColor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) stylePieChartValue(index int) Style {
|
||||||
|
return pc.SliceStyle.InheritFrom(Style{
|
||||||
|
StrokeColor: ColorWhite,
|
||||||
|
StrokeWidth: 5.0,
|
||||||
|
FillColor: pc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
FontSize: pc.getScaledFontSize(),
|
||||||
|
FontColor: pc.GetColorPalette().TextColor(),
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getScaledFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48.0
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24.0
|
||||||
|
} else if effectiveDimension > 512 {
|
||||||
|
return 18.0
|
||||||
|
} else if effectiveDimension > 256 {
|
||||||
|
return 12.0
|
||||||
|
}
|
||||||
|
return 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: pc.GetColorPalette().BackgroundColor(),
|
||||||
|
StrokeColor: pc.GetColorPalette().BackgroundStrokeColor(),
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsTitle() Style {
|
||||||
|
return pc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: pc.GetColorPalette().TextColor(),
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
FontSize: pc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24
|
||||||
|
} else if effectiveDimension >= 512 {
|
||||||
|
return 18
|
||||||
|
} else if effectiveDimension >= 256 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (pc PieChart) GetColorPalette() ColorPalette {
|
||||||
|
if pc.ColorPalette != nil {
|
||||||
|
return pc.ColorPalette
|
||||||
|
}
|
||||||
|
return AlternateColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (pc PieChart) Box() Box {
|
||||||
|
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||||
|
dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||||
|
Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
|
||||||
|
Right: pc.GetWidth() - dpr,
|
||||||
|
Bottom: pc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
177
vendor/github.com/wcharczuk/go-chart/v2/polynomial_regression_series.go
generated
vendored
Normal file
177
vendor/github.com/wcharczuk/go-chart/v2/polynomial_regression_series.go
generated
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2/matrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*PolynomialRegressionSeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*PolynomialRegressionSeries)(nil)
|
||||||
|
_ LastValuesProvider = (*PolynomialRegressionSeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolynomialRegressionSeries implements a polynomial regression over a given
|
||||||
|
// inner series.
|
||||||
|
type PolynomialRegressionSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
Degree int
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
|
||||||
|
coeffs []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (prs PolynomialRegressionSeries) GetName() string {
|
||||||
|
return prs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (prs PolynomialRegressionSeries) GetStyle() Style {
|
||||||
|
return prs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (prs PolynomialRegressionSeries) GetYAxis() YAxisType {
|
||||||
|
return prs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (prs PolynomialRegressionSeries) Len() int {
|
||||||
|
return MinInt(prs.GetLimit(), prs.InnerSeries.Len()-prs.GetOffset())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLimit returns the window size.
|
||||||
|
func (prs PolynomialRegressionSeries) GetLimit() int {
|
||||||
|
if prs.Limit == 0 {
|
||||||
|
return prs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
return prs.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEndIndex returns the effective limit end.
|
||||||
|
func (prs PolynomialRegressionSeries) GetEndIndex() int {
|
||||||
|
windowEnd := prs.GetOffset() + prs.GetLimit()
|
||||||
|
innerSeriesLastIndex := prs.InnerSeries.Len() - 1
|
||||||
|
return MinInt(windowEnd, innerSeriesLastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOffset returns the data offset.
|
||||||
|
func (prs PolynomialRegressionSeries) GetOffset() int {
|
||||||
|
if prs.Offset == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prs.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (prs *PolynomialRegressionSeries) Validate() error {
|
||||||
|
if prs.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("linear regression series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
endIndex := prs.GetEndIndex()
|
||||||
|
if endIndex >= prs.InnerSeries.Len() {
|
||||||
|
return fmt.Errorf("invalid window; inner series has length %d but end index is %d", prs.InnerSeries.Len(), endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues returns the series value for a given index.
|
||||||
|
func (prs *PolynomialRegressionSeries) GetValues(index int) (x, y float64) {
|
||||||
|
if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prs.coeffs == nil {
|
||||||
|
coeffs, err := prs.computeCoefficients()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
prs.coeffs = coeffs
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := prs.GetOffset()
|
||||||
|
effectiveIndex := MinInt(index+offset, prs.InnerSeries.Len())
|
||||||
|
x, y = prs.InnerSeries.GetValues(effectiveIndex)
|
||||||
|
y = prs.apply(x)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues computes the first poly regression value.
|
||||||
|
func (prs *PolynomialRegressionSeries) GetFirstValues() (x, y float64) {
|
||||||
|
if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prs.coeffs == nil {
|
||||||
|
coeffs, err := prs.computeCoefficients()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
prs.coeffs = coeffs
|
||||||
|
}
|
||||||
|
x, y = prs.InnerSeries.GetValues(0)
|
||||||
|
y = prs.apply(x)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues computes the last poly regression value.
|
||||||
|
func (prs *PolynomialRegressionSeries) GetLastValues() (x, y float64) {
|
||||||
|
if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prs.coeffs == nil {
|
||||||
|
coeffs, err := prs.computeCoefficients()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
prs.coeffs = coeffs
|
||||||
|
}
|
||||||
|
endIndex := prs.GetEndIndex()
|
||||||
|
x, y = prs.InnerSeries.GetValues(endIndex)
|
||||||
|
y = prs.apply(x)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prs *PolynomialRegressionSeries) apply(v float64) (out float64) {
|
||||||
|
for index, coeff := range prs.coeffs {
|
||||||
|
out = out + (coeff * math.Pow(v, float64(index)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prs *PolynomialRegressionSeries) computeCoefficients() ([]float64, error) {
|
||||||
|
xvalues, yvalues := prs.values()
|
||||||
|
return matrix.Poly(xvalues, yvalues, prs.Degree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prs *PolynomialRegressionSeries) values() (xvalues, yvalues []float64) {
|
||||||
|
startIndex := prs.GetOffset()
|
||||||
|
endIndex := prs.GetEndIndex()
|
||||||
|
|
||||||
|
xvalues = make([]float64, endIndex-startIndex)
|
||||||
|
yvalues = make([]float64, endIndex-startIndex)
|
||||||
|
|
||||||
|
for index := startIndex; index < endIndex; index++ {
|
||||||
|
x, y := prs.InnerSeries.GetValues(index)
|
||||||
|
xvalues[index-startIndex] = x
|
||||||
|
yvalues[index-startIndex] = y
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (prs *PolynomialRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := prs.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, prs)
|
||||||
|
}
|
92
vendor/github.com/wcharczuk/go-chart/v2/random_sequence.go
generated
vendored
Normal file
92
vendor/github.com/wcharczuk/go-chart/v2/random_sequence.go
generated
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Sequence = (*RandomSeq)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomValues returns an array of random values.
|
||||||
|
func RandomValues(count int) []float64 {
|
||||||
|
return Seq{NewRandomSequence().WithLen(count)}.Values()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomValuesWithMax returns an array of random values with a given average.
|
||||||
|
func RandomValuesWithMax(count int, max float64) []float64 {
|
||||||
|
return Seq{NewRandomSequence().WithMax(max).WithLen(count)}.Values()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandomSequence creates a new random seq.
|
||||||
|
func NewRandomSequence() *RandomSeq {
|
||||||
|
return &RandomSeq{
|
||||||
|
rnd: rand.New(rand.NewSource(time.Now().Unix())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomSeq is a random number seq generator.
|
||||||
|
type RandomSeq struct {
|
||||||
|
rnd *rand.Rand
|
||||||
|
max *float64
|
||||||
|
min *float64
|
||||||
|
len *int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements that will be generated.
|
||||||
|
func (r *RandomSeq) Len() int {
|
||||||
|
if r.len != nil {
|
||||||
|
return *r.len
|
||||||
|
}
|
||||||
|
return math.MaxInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value.
|
||||||
|
func (r *RandomSeq) GetValue(_ int) float64 {
|
||||||
|
if r.min != nil && r.max != nil {
|
||||||
|
var delta float64
|
||||||
|
|
||||||
|
if *r.max > *r.min {
|
||||||
|
delta = *r.max - *r.min
|
||||||
|
} else {
|
||||||
|
delta = *r.min - *r.max
|
||||||
|
}
|
||||||
|
|
||||||
|
return *r.min + (r.rnd.Float64() * delta)
|
||||||
|
} else if r.max != nil {
|
||||||
|
return r.rnd.Float64() * *r.max
|
||||||
|
} else if r.min != nil {
|
||||||
|
return *r.min + (r.rnd.Float64())
|
||||||
|
}
|
||||||
|
return r.rnd.Float64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLen sets a maximum len
|
||||||
|
func (r *RandomSeq) WithLen(length int) *RandomSeq {
|
||||||
|
r.len = &length
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min returns the minimum value.
|
||||||
|
func (r RandomSeq) Min() *float64 {
|
||||||
|
return r.min
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMin sets the scale and returns the Random.
|
||||||
|
func (r *RandomSeq) WithMin(min float64) *RandomSeq {
|
||||||
|
r.min = &min
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the maximum value.
|
||||||
|
func (r RandomSeq) Max() *float64 {
|
||||||
|
return r.max
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMax sets the average and returns the Random.
|
||||||
|
func (r *RandomSeq) WithMax(max float64) *RandomSeq {
|
||||||
|
r.max = &max
|
||||||
|
return r
|
||||||
|
}
|
43
vendor/github.com/wcharczuk/go-chart/v2/range.go
generated
vendored
Normal file
43
vendor/github.com/wcharczuk/go-chart/v2/range.go
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// NameProvider is a type that returns a name.
|
||||||
|
type NameProvider interface {
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleProvider is a type that returns a style.
|
||||||
|
type StyleProvider interface {
|
||||||
|
GetStyle() Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZeroable is a type that returns if it's been set or not.
|
||||||
|
type IsZeroable interface {
|
||||||
|
IsZero() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stringable is a type that has a string representation.
|
||||||
|
type Stringable interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range is a common interface for a range of values.
|
||||||
|
type Range interface {
|
||||||
|
Stringable
|
||||||
|
IsZeroable
|
||||||
|
|
||||||
|
GetMin() float64
|
||||||
|
SetMin(min float64)
|
||||||
|
|
||||||
|
GetMax() float64
|
||||||
|
SetMax(max float64)
|
||||||
|
|
||||||
|
GetDelta() float64
|
||||||
|
|
||||||
|
GetDomain() int
|
||||||
|
SetDomain(domain int)
|
||||||
|
|
||||||
|
IsDescending() bool
|
||||||
|
|
||||||
|
// Translate the range to the domain.
|
||||||
|
Translate(value float64) int
|
||||||
|
}
|
230
vendor/github.com/wcharczuk/go-chart/v2/raster_renderer.go
generated
vendored
Normal file
230
vendor/github.com/wcharczuk/go-chart/v2/raster_renderer.go
generated
vendored
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PNG returns a new png/raster renderer.
|
||||||
|
func PNG(width, height int) (Renderer, error) {
|
||||||
|
i := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
gc, err := drawing.NewRasterGraphicContext(i)
|
||||||
|
if err == nil {
|
||||||
|
return &rasterRenderer{
|
||||||
|
i: i,
|
||||||
|
gc: gc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// rasterRenderer renders chart commands to a bitmap.
|
||||||
|
type rasterRenderer struct {
|
||||||
|
i *image.RGBA
|
||||||
|
gc *drawing.RasterGraphicContext
|
||||||
|
|
||||||
|
rotateRadians *float64
|
||||||
|
|
||||||
|
s Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *rasterRenderer) ResetStyle() {
|
||||||
|
rr.s = Style{Font: rr.s.Font}
|
||||||
|
rr.ClearTextRotation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi.
|
||||||
|
func (rr *rasterRenderer) GetDPI() float64 {
|
||||||
|
return rr.gc.GetDPI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDPI implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetDPI(dpi float64) {
|
||||||
|
rr.gc.SetDPI(dpi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClassName implements the interface method. However, PNGs have no classes.
|
||||||
|
func (rr *rasterRenderer) SetClassName(_ string) {}
|
||||||
|
|
||||||
|
// SetStrokeColor implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) {
|
||||||
|
rr.s.StrokeColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLineWidth implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetStrokeWidth(width float64) {
|
||||||
|
rr.s.StrokeWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeDashArray sets the stroke dash array.
|
||||||
|
func (rr *rasterRenderer) SetStrokeDashArray(dashArray []float64) {
|
||||||
|
rr.s.StrokeDashArray = dashArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFillColor implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetFillColor(c drawing.Color) {
|
||||||
|
rr.s.FillColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo implements the interface method.
|
||||||
|
func (rr *rasterRenderer) MoveTo(x, y int) {
|
||||||
|
rr.gc.MoveTo(float64(x), float64(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineTo implements the interface method.
|
||||||
|
func (rr *rasterRenderer) LineTo(x, y int) {
|
||||||
|
rr.gc.LineTo(float64(x), float64(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo implements the interface method.
|
||||||
|
func (rr *rasterRenderer) QuadCurveTo(cx, cy, x, y int) {
|
||||||
|
rr.gc.QuadCurveTo(float64(cx), float64(cy), float64(x), float64(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArcTo implements the interface method.
|
||||||
|
func (rr *rasterRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
|
||||||
|
rr.gc.ArcTo(float64(cx), float64(cy), rx, ry, startAngle, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the interface method.
|
||||||
|
func (rr *rasterRenderer) Close() {
|
||||||
|
rr.gc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stroke implements the interface method.
|
||||||
|
func (rr *rasterRenderer) Stroke() {
|
||||||
|
rr.gc.SetStrokeColor(rr.s.StrokeColor)
|
||||||
|
rr.gc.SetLineWidth(rr.s.StrokeWidth)
|
||||||
|
rr.gc.SetLineDash(rr.s.StrokeDashArray, 0)
|
||||||
|
rr.gc.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill implements the interface method.
|
||||||
|
func (rr *rasterRenderer) Fill() {
|
||||||
|
rr.gc.SetFillColor(rr.s.FillColor)
|
||||||
|
rr.gc.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillStroke implements the interface method.
|
||||||
|
func (rr *rasterRenderer) FillStroke() {
|
||||||
|
rr.gc.SetFillColor(rr.s.FillColor)
|
||||||
|
rr.gc.SetStrokeColor(rr.s.StrokeColor)
|
||||||
|
rr.gc.SetLineWidth(rr.s.StrokeWidth)
|
||||||
|
rr.gc.SetLineDash(rr.s.StrokeDashArray, 0)
|
||||||
|
rr.gc.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circle fully draws a circle at a given point but does not apply the fill or stroke.
|
||||||
|
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
|
||||||
|
xf := float64(x)
|
||||||
|
yf := float64(y)
|
||||||
|
|
||||||
|
rr.gc.MoveTo(xf-radius, yf) //9
|
||||||
|
rr.gc.QuadCurveTo(xf-radius, yf-radius, xf, yf-radius) //12
|
||||||
|
rr.gc.QuadCurveTo(xf+radius, yf-radius, xf+radius, yf) //3
|
||||||
|
rr.gc.QuadCurveTo(xf+radius, yf+radius, xf, yf+radius) //6
|
||||||
|
rr.gc.QuadCurveTo(xf-radius, yf+radius, xf-radius, yf) //9
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFont implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetFont(f *truetype.Font) {
|
||||||
|
rr.s.Font = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontSize implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetFontSize(size float64) {
|
||||||
|
rr.s.FontSize = size
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFontColor implements the interface method.
|
||||||
|
func (rr *rasterRenderer) SetFontColor(c drawing.Color) {
|
||||||
|
rr.s.FontColor = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text implements the interface method.
|
||||||
|
func (rr *rasterRenderer) Text(body string, x, y int) {
|
||||||
|
xf, yf := rr.getCoords(x, y)
|
||||||
|
rr.gc.SetFont(rr.s.Font)
|
||||||
|
rr.gc.SetFontSize(rr.s.FontSize)
|
||||||
|
rr.gc.SetFillColor(rr.s.FontColor)
|
||||||
|
rr.gc.CreateStringPath(body, float64(xf), float64(yf))
|
||||||
|
rr.gc.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeasureText returns the height and width in pixels of a string.
|
||||||
|
func (rr *rasterRenderer) MeasureText(body string) Box {
|
||||||
|
rr.gc.SetFont(rr.s.Font)
|
||||||
|
rr.gc.SetFontSize(rr.s.FontSize)
|
||||||
|
rr.gc.SetFillColor(rr.s.FontColor)
|
||||||
|
l, t, r, b, err := rr.gc.GetStringBounds(body)
|
||||||
|
if err != nil {
|
||||||
|
return Box{}
|
||||||
|
}
|
||||||
|
if l < 0 {
|
||||||
|
r = r - l // equivalent to r+(-1*l)
|
||||||
|
l = 0
|
||||||
|
}
|
||||||
|
if t < 0 {
|
||||||
|
b = b - t
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if l > 0 {
|
||||||
|
r = r + l
|
||||||
|
l = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if t > 0 {
|
||||||
|
b = b + t
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
textBox := Box{
|
||||||
|
Top: int(math.Ceil(t)),
|
||||||
|
Left: int(math.Ceil(l)),
|
||||||
|
Right: int(math.Ceil(r)),
|
||||||
|
Bottom: int(math.Ceil(b)),
|
||||||
|
}
|
||||||
|
if rr.rotateRadians == nil {
|
||||||
|
return textBox
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBox.Corners().Rotate(RadiansToDegrees(*rr.rotateRadians)).Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTextRotation sets a text rotation.
|
||||||
|
func (rr *rasterRenderer) SetTextRotation(radians float64) {
|
||||||
|
rr.rotateRadians = &radians
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) {
|
||||||
|
if rr.rotateRadians == nil {
|
||||||
|
xf = x
|
||||||
|
yf = y
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rr.gc.Translate(float64(x), float64(y))
|
||||||
|
rr.gc.Rotate(*rr.rotateRadians)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearTextRotation clears text rotation.
|
||||||
|
func (rr *rasterRenderer) ClearTextRotation() {
|
||||||
|
rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix())
|
||||||
|
rr.rotateRadians = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save implements the interface method.
|
||||||
|
func (rr *rasterRenderer) Save(w io.Writer) error {
|
||||||
|
if typed, isTyped := w.(RGBACollector); isTyped {
|
||||||
|
typed.SetRGBA(rr.i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return png.Encode(w, rr.i)
|
||||||
|
}
|
4
vendor/github.com/wcharczuk/go-chart/v2/renderable.go
generated
vendored
Normal file
4
vendor/github.com/wcharczuk/go-chart/v2/renderable.go
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// Renderable is a function that can be called to render custom elements on the chart.
|
||||||
|
type Renderable func(r Renderer, canvasBox Box, defaults Style)
|
89
vendor/github.com/wcharczuk/go-chart/v2/renderer.go
generated
vendored
Normal file
89
vendor/github.com/wcharczuk/go-chart/v2/renderer.go
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Renderer represents the basic methods required to draw a chart.
|
||||||
|
type Renderer interface {
|
||||||
|
// ResetStyle should reset any style related settings on the renderer.
|
||||||
|
ResetStyle()
|
||||||
|
|
||||||
|
// GetDPI gets the DPI for the renderer.
|
||||||
|
GetDPI() float64
|
||||||
|
|
||||||
|
// SetDPI sets the DPI for the renderer.
|
||||||
|
SetDPI(dpi float64)
|
||||||
|
|
||||||
|
// SetClassName sets the current class name.
|
||||||
|
SetClassName(string)
|
||||||
|
|
||||||
|
// SetStrokeColor sets the current stroke color.
|
||||||
|
SetStrokeColor(drawing.Color)
|
||||||
|
|
||||||
|
// SetFillColor sets the current fill color.
|
||||||
|
SetFillColor(drawing.Color)
|
||||||
|
|
||||||
|
// SetStrokeWidth sets the stroke width.
|
||||||
|
SetStrokeWidth(width float64)
|
||||||
|
|
||||||
|
// SetStrokeDashArray sets the stroke dash array.
|
||||||
|
SetStrokeDashArray(dashArray []float64)
|
||||||
|
|
||||||
|
// MoveTo moves the cursor to a given point.
|
||||||
|
MoveTo(x, y int)
|
||||||
|
|
||||||
|
// LineTo both starts a shape and draws a line to a given point
|
||||||
|
// from the previous point.
|
||||||
|
LineTo(x, y int)
|
||||||
|
|
||||||
|
// QuadCurveTo draws a quad curve.
|
||||||
|
// cx and cy represent the bezier "control points".
|
||||||
|
QuadCurveTo(cx, cy, x, y int)
|
||||||
|
|
||||||
|
// ArcTo draws an arc with a given center (cx,cy)
|
||||||
|
// a given set of radii (rx,ry), a startAngle and delta (in radians).
|
||||||
|
ArcTo(cx, cy int, rx, ry, startAngle, delta float64)
|
||||||
|
|
||||||
|
// Close finalizes a shape as drawn by LineTo.
|
||||||
|
Close()
|
||||||
|
|
||||||
|
// Stroke strokes the path.
|
||||||
|
Stroke()
|
||||||
|
|
||||||
|
// Fill fills the path, but does not stroke.
|
||||||
|
Fill()
|
||||||
|
|
||||||
|
// FillStroke fills and strokes a path.
|
||||||
|
FillStroke()
|
||||||
|
|
||||||
|
// Circle draws a circle at the given coords with a given radius.
|
||||||
|
Circle(radius float64, x, y int)
|
||||||
|
|
||||||
|
// SetFont sets a font for a text field.
|
||||||
|
SetFont(*truetype.Font)
|
||||||
|
|
||||||
|
// SetFontColor sets a font's color
|
||||||
|
SetFontColor(drawing.Color)
|
||||||
|
|
||||||
|
// SetFontSize sets the font size for a text field.
|
||||||
|
SetFontSize(size float64)
|
||||||
|
|
||||||
|
// Text draws a text blob.
|
||||||
|
Text(body string, x, y int)
|
||||||
|
|
||||||
|
// MeasureText measures text.
|
||||||
|
MeasureText(body string) Box
|
||||||
|
|
||||||
|
// SetTextRotatation sets a rotation for drawing elements.
|
||||||
|
SetTextRotation(radians float64)
|
||||||
|
|
||||||
|
// ClearTextRotation clears rotation.
|
||||||
|
ClearTextRotation()
|
||||||
|
|
||||||
|
// Save writes the image to the given writer.
|
||||||
|
Save(w io.Writer) error
|
||||||
|
}
|
4
vendor/github.com/wcharczuk/go-chart/v2/renderer_provider.go
generated
vendored
Normal file
4
vendor/github.com/wcharczuk/go-chart/v2/renderer_provider.go
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// RendererProvider is a function that returns a renderer.
|
||||||
|
type RendererProvider func(int, int) (Renderer, error)
|
5
vendor/github.com/wcharczuk/go-chart/v2/roboto/roboto.go
generated
vendored
Normal file
5
vendor/github.com/wcharczuk/go-chart/v2/roboto/roboto.go
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
275
vendor/github.com/wcharczuk/go-chart/v2/seq.go
generated
vendored
Normal file
275
vendor/github.com/wcharczuk/go-chart/v2/seq.go
generated
vendored
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValueSequence returns a sequence for a given values set.
|
||||||
|
func ValueSequence(values ...float64) Seq {
|
||||||
|
return Seq{NewArray(values...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequence is a provider for values for a seq.
|
||||||
|
type Sequence interface {
|
||||||
|
Len() int
|
||||||
|
GetValue(int) float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seq is a utility wrapper for seq providers.
|
||||||
|
type Seq struct {
|
||||||
|
Sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values enumerates the seq into a slice.
|
||||||
|
func (s Seq) Values() (output []float64) {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output = make([]float64, s.Len())
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
output[i] = s.GetValue(i)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each applies the `mapfn` to all values in the value provider.
|
||||||
|
func (s Seq) Each(mapfn func(int, float64)) {
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
mapfn(i, s.GetValue(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map applies the `mapfn` to all values in the value provider,
|
||||||
|
// returning a new seq.
|
||||||
|
func (s Seq) Map(mapfn func(i int, v float64) float64) Seq {
|
||||||
|
output := make([]float64, s.Len())
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
mapfn(i, s.GetValue(i))
|
||||||
|
}
|
||||||
|
return Seq{Array(output)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FoldLeft collapses a seq from left to right.
|
||||||
|
func (s Seq) FoldLeft(mapfn func(i int, v0, v float64) float64) (v0 float64) {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Len() == 1 {
|
||||||
|
return s.GetValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
v0 = s.GetValue(0)
|
||||||
|
for i := 1; i < s.Len(); i++ {
|
||||||
|
v0 = mapfn(i, v0, s.GetValue(i))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FoldRight collapses a seq from right to left.
|
||||||
|
func (s Seq) FoldRight(mapfn func(i int, v0, v float64) float64) (v0 float64) {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Len() == 1 {
|
||||||
|
return s.GetValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
v0 = s.GetValue(s.Len() - 1)
|
||||||
|
for i := s.Len() - 2; i >= 0; i-- {
|
||||||
|
v0 = mapfn(i, v0, s.GetValue(i))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min returns the minimum value in the seq.
|
||||||
|
func (s Seq) Min() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
min := s.GetValue(0)
|
||||||
|
var value float64
|
||||||
|
for i := 1; i < s.Len(); i++ {
|
||||||
|
value = s.GetValue(i)
|
||||||
|
if value < min {
|
||||||
|
min = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the maximum value in the seq.
|
||||||
|
func (s Seq) Max() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
max := s.GetValue(0)
|
||||||
|
var value float64
|
||||||
|
for i := 1; i < s.Len(); i++ {
|
||||||
|
value = s.GetValue(i)
|
||||||
|
if value > max {
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinMax returns the minimum and the maximum in one pass.
|
||||||
|
func (s Seq) MinMax() (min, max float64) {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
min = s.GetValue(0)
|
||||||
|
max = min
|
||||||
|
var value float64
|
||||||
|
for i := 1; i < s.Len(); i++ {
|
||||||
|
value = s.GetValue(i)
|
||||||
|
if value < min {
|
||||||
|
min = value
|
||||||
|
}
|
||||||
|
if value > max {
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort returns the seq sorted in ascending order.
|
||||||
|
// This fully enumerates the seq.
|
||||||
|
func (s Seq) Sort() Seq {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
values := s.Values()
|
||||||
|
sort.Float64s(values)
|
||||||
|
return Seq{Array(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse reverses the sequence
|
||||||
|
func (s Seq) Reverse() Seq {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
values := s.Values()
|
||||||
|
valuesLen := len(values)
|
||||||
|
valuesLen1 := len(values) - 1
|
||||||
|
valuesLen2 := valuesLen >> 1
|
||||||
|
var i, j float64
|
||||||
|
for index := 0; index < valuesLen2; index++ {
|
||||||
|
i = values[index]
|
||||||
|
j = values[valuesLen1-index]
|
||||||
|
values[index] = j
|
||||||
|
values[valuesLen1-index] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
return Seq{Array(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Median returns the median or middle value in the sorted seq.
|
||||||
|
func (s Seq) Median() (median float64) {
|
||||||
|
l := s.Len()
|
||||||
|
if l == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted := s.Sort()
|
||||||
|
if l%2 == 0 {
|
||||||
|
v0 := sorted.GetValue(l/2 - 1)
|
||||||
|
v1 := sorted.GetValue(l/2 + 1)
|
||||||
|
median = (v0 + v1) / 2
|
||||||
|
} else {
|
||||||
|
median = float64(sorted.GetValue(l << 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum adds all the elements of a series together.
|
||||||
|
func (s Seq) Sum() (accum float64) {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
accum += s.GetValue(i)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average returns the float average of the values in the buffer.
|
||||||
|
func (s Seq) Average() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Sum() / float64(s.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variance computes the variance of the buffer.
|
||||||
|
func (s Seq) Variance() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
m := s.Average()
|
||||||
|
var variance, v float64
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
v = s.GetValue(i)
|
||||||
|
variance += (v - m) * (v - m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return variance / float64(s.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdDev returns the standard deviation.
|
||||||
|
func (s Seq) StdDev() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Pow(s.Variance(), 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Percentile finds the relative standing in a slice of floats.
|
||||||
|
// `percent` should be given on the interval [0,1.0).
|
||||||
|
func (s Seq) Percentile(percent float64) (percentile float64) {
|
||||||
|
l := s.Len()
|
||||||
|
if l == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if percent < 0 || percent > 1.0 {
|
||||||
|
panic("percent out of range [0.0, 1.0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted := s.Sort()
|
||||||
|
index := percent * float64(l)
|
||||||
|
if index == float64(int64(index)) {
|
||||||
|
i := f64i(index)
|
||||||
|
ci := sorted.GetValue(i - 1)
|
||||||
|
c := sorted.GetValue(i)
|
||||||
|
percentile = (ci + c) / 2.0
|
||||||
|
} else {
|
||||||
|
i := f64i(index)
|
||||||
|
percentile = sorted.GetValue(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return percentile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize maps every value to the interval [0, 1.0].
|
||||||
|
func (s Seq) Normalize() Seq {
|
||||||
|
min, max := s.MinMax()
|
||||||
|
|
||||||
|
delta := max - min
|
||||||
|
output := make([]float64, s.Len())
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
output[i] = (s.GetValue(i) - min) / delta
|
||||||
|
}
|
||||||
|
|
||||||
|
return Seq{Array(output)}
|
||||||
|
}
|
10
vendor/github.com/wcharczuk/go-chart/v2/series.go
generated
vendored
Normal file
10
vendor/github.com/wcharczuk/go-chart/v2/series.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
// Series is an alias to Renderable.
|
||||||
|
type Series interface {
|
||||||
|
GetName() string
|
||||||
|
GetYAxis() YAxisType
|
||||||
|
GetStyle() Style
|
||||||
|
Validate() error
|
||||||
|
Render(r Renderer, canvasBox Box, xrange, yrange Range, s Style)
|
||||||
|
}
|
120
vendor/github.com/wcharczuk/go-chart/v2/sma_series.go
generated
vendored
Normal file
120
vendor/github.com/wcharczuk/go-chart/v2/sma_series.go
generated
vendored
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultSimpleMovingAveragePeriod is the default number of values to average.
|
||||||
|
DefaultSimpleMovingAveragePeriod = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface Assertions.
|
||||||
|
var (
|
||||||
|
_ Series = (*SMASeries)(nil)
|
||||||
|
_ FirstValuesProvider = (*SMASeries)(nil)
|
||||||
|
_ LastValuesProvider = (*SMASeries)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// SMASeries is a computed series.
|
||||||
|
type SMASeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
Period int
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (sma SMASeries) GetName() string {
|
||||||
|
return sma.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (sma SMASeries) GetStyle() Style {
|
||||||
|
return sma.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (sma SMASeries) GetYAxis() YAxisType {
|
||||||
|
return sma.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (sma SMASeries) Len() int {
|
||||||
|
return sma.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeriod returns the window size.
|
||||||
|
func (sma SMASeries) GetPeriod(defaults ...int) int {
|
||||||
|
if sma.Period == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultSimpleMovingAveragePeriod
|
||||||
|
}
|
||||||
|
return sma.Period
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues gets a value at a given index.
|
||||||
|
func (sma SMASeries) GetValues(index int) (x, y float64) {
|
||||||
|
if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
px, _ := sma.InnerSeries.GetValues(index)
|
||||||
|
x = px
|
||||||
|
y = sma.getAverage(index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstValues computes the first moving average value.
|
||||||
|
func (sma SMASeries) GetFirstValues() (x, y float64) {
|
||||||
|
if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
px, _ := sma.InnerSeries.GetValues(0)
|
||||||
|
x = px
|
||||||
|
y = sma.getAverage(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValues computes the last moving average value but walking back window size samples,
|
||||||
|
// and recomputing the last moving average chunk.
|
||||||
|
func (sma SMASeries) GetLastValues() (x, y float64) {
|
||||||
|
if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seriesLen := sma.InnerSeries.Len()
|
||||||
|
px, _ := sma.InnerSeries.GetValues(seriesLen - 1)
|
||||||
|
x = px
|
||||||
|
y = sma.getAverage(seriesLen - 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sma SMASeries) getAverage(index int) float64 {
|
||||||
|
period := sma.GetPeriod()
|
||||||
|
floor := MaxInt(0, index-period)
|
||||||
|
var accum float64
|
||||||
|
var count float64
|
||||||
|
for x := index; x >= floor; x-- {
|
||||||
|
_, vy := sma.InnerSeries.GetValues(x)
|
||||||
|
accum += vy
|
||||||
|
count += 1.0
|
||||||
|
}
|
||||||
|
return accum / count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := sma.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, sma)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (sma SMASeries) Validate() error {
|
||||||
|
if sma.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("sma series requires InnerSeries to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
610
vendor/github.com/wcharczuk/go-chart/v2/stacked_bar_chart.go
generated
vendored
Normal file
610
vendor/github.com/wcharczuk/go-chart/v2/stacked_bar_chart.go
generated
vendored
Normal file
@ -0,0 +1,610 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackedBar is a bar within a StackedBarChart.
|
||||||
|
type StackedBar struct {
|
||||||
|
Name string
|
||||||
|
Width int
|
||||||
|
Values []Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the width of the bar.
|
||||||
|
func (sb StackedBar) GetWidth() int {
|
||||||
|
if sb.Width == 0 {
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
return sb.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackedBarChart is a chart that draws sections of a bar based on percentages.
|
||||||
|
type StackedBarChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
ColorPalette ColorPalette
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
|
||||||
|
XAxis Style
|
||||||
|
YAxis Style
|
||||||
|
|
||||||
|
BarSpacing int
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
IsHorizontal bool
|
||||||
|
|
||||||
|
Bars []StackedBar
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (sbc StackedBarChart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if sbc.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return sbc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (sbc StackedBarChart) GetFont() *truetype.Font {
|
||||||
|
if sbc.Font == nil {
|
||||||
|
return sbc.defaultFont
|
||||||
|
}
|
||||||
|
return sbc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (sbc StackedBarChart) GetWidth() int {
|
||||||
|
if sbc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return sbc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (sbc StackedBarChart) GetHeight() int {
|
||||||
|
if sbc.Height == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return sbc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBarSpacing returns the spacing between bars.
|
||||||
|
func (sbc StackedBarChart) GetBarSpacing() int {
|
||||||
|
if sbc.BarSpacing == 0 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return sbc.BarSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(sbc.Bars) == 0 {
|
||||||
|
return errors.New("please provide at least one bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(sbc.GetWidth(), sbc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if sbc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sbc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(sbc.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
var canvasBox Box
|
||||||
|
if sbc.IsHorizontal {
|
||||||
|
canvasBox = sbc.getHorizontalAdjustedCanvasBox(r, sbc.getDefaultCanvasBox())
|
||||||
|
sbc.drawCanvas(r, canvasBox)
|
||||||
|
sbc.drawHorizontalBars(r, canvasBox)
|
||||||
|
sbc.drawHorizontalXAxis(r, canvasBox)
|
||||||
|
sbc.drawHorizontalYAxis(r, canvasBox)
|
||||||
|
} else {
|
||||||
|
canvasBox = sbc.getAdjustedCanvasBox(r, sbc.getDefaultCanvasBox())
|
||||||
|
sbc.drawCanvas(r, canvasBox)
|
||||||
|
sbc.drawBars(r, canvasBox)
|
||||||
|
sbc.drawXAxis(r, canvasBox)
|
||||||
|
sbc.drawYAxis(r, canvasBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
sbc.drawTitle(r)
|
||||||
|
for _, a := range sbc.Elements {
|
||||||
|
a(r, canvasBox, sbc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, sbc.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawBars(r Renderer, canvasBox Box) {
|
||||||
|
xoffset := canvasBox.Left
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
sbc.drawBar(r, canvasBox, xoffset, bar)
|
||||||
|
xoffset += (sbc.GetBarSpacing() + bar.GetWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawHorizontalBars(r Renderer, canvasBox Box) {
|
||||||
|
yOffset := canvasBox.Top
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
sbc.drawHorizontalBar(r, canvasBox, yOffset, bar)
|
||||||
|
yOffset += sbc.GetBarSpacing() + bar.GetWidth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar StackedBar) int {
|
||||||
|
barSpacing2 := sbc.GetBarSpacing() >> 1
|
||||||
|
bxl := xoffset + barSpacing2
|
||||||
|
bxr := bxl + bar.GetWidth()
|
||||||
|
|
||||||
|
normalizedBarComponents := Values(bar.Values).Normalize()
|
||||||
|
yoffset := canvasBox.Top
|
||||||
|
for index, bv := range normalizedBarComponents {
|
||||||
|
barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Height())))
|
||||||
|
barBox := Box{
|
||||||
|
Top: yoffset,
|
||||||
|
Left: bxl,
|
||||||
|
Right: bxr,
|
||||||
|
Bottom: MinInt(yoffset+barHeight, canvasBox.Bottom-DefaultStrokeWidth),
|
||||||
|
}
|
||||||
|
Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)))
|
||||||
|
yoffset += barHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the labels
|
||||||
|
yoffset = canvasBox.Top
|
||||||
|
var lx, ly int
|
||||||
|
for index, bv := range normalizedBarComponents {
|
||||||
|
barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Height())))
|
||||||
|
|
||||||
|
if len(bv.Label) > 0 {
|
||||||
|
lx = bxl + ((bxr - bxl) / 2)
|
||||||
|
ly = yoffset + (barHeight / 2)
|
||||||
|
|
||||||
|
bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)).WriteToRenderer(r)
|
||||||
|
tb := r.MeasureText(bv.Label)
|
||||||
|
lx = lx - (tb.Width() >> 1)
|
||||||
|
ly = ly + (tb.Height() >> 1)
|
||||||
|
|
||||||
|
if lx < 0 {
|
||||||
|
lx = 0
|
||||||
|
}
|
||||||
|
if ly < 0 {
|
||||||
|
lx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Text(bv.Label, lx, ly)
|
||||||
|
}
|
||||||
|
yoffset += barHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return bxr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawHorizontalBar(r Renderer, canvasBox Box, yoffset int, bar StackedBar) {
|
||||||
|
halfBarSpacing := sbc.GetBarSpacing() >> 1
|
||||||
|
|
||||||
|
boxTop := yoffset + halfBarSpacing
|
||||||
|
boxBottom := boxTop + bar.GetWidth()
|
||||||
|
|
||||||
|
normalizedBarComponents := Values(bar.Values).Normalize()
|
||||||
|
|
||||||
|
xOffset := canvasBox.Right
|
||||||
|
for index, bv := range normalizedBarComponents {
|
||||||
|
barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Width())))
|
||||||
|
barBox := Box{
|
||||||
|
Top: boxTop,
|
||||||
|
Left: MinInt(xOffset-barHeight, canvasBox.Left+DefaultStrokeWidth),
|
||||||
|
Right: xOffset,
|
||||||
|
Bottom: boxBottom,
|
||||||
|
}
|
||||||
|
Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)))
|
||||||
|
xOffset -= barHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the labels
|
||||||
|
xOffset = canvasBox.Right
|
||||||
|
var lx, ly int
|
||||||
|
for index, bv := range normalizedBarComponents {
|
||||||
|
barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Width())))
|
||||||
|
|
||||||
|
if len(bv.Label) > 0 {
|
||||||
|
lx = xOffset - (barHeight / 2)
|
||||||
|
ly = boxTop + ((boxBottom - boxTop) / 2)
|
||||||
|
|
||||||
|
bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)).WriteToRenderer(r)
|
||||||
|
tb := r.MeasureText(bv.Label)
|
||||||
|
lx = lx - (tb.Width() >> 1)
|
||||||
|
ly = ly + (tb.Height() >> 1)
|
||||||
|
|
||||||
|
if lx < 0 {
|
||||||
|
lx = 0
|
||||||
|
}
|
||||||
|
if ly < 0 {
|
||||||
|
lx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Text(bv.Label, lx, ly)
|
||||||
|
}
|
||||||
|
xOffset -= barHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) {
|
||||||
|
if !sbc.XAxis.Hidden {
|
||||||
|
axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
|
||||||
|
Bottom: sbc.GetHeight(),
|
||||||
|
}
|
||||||
|
if len(bar.Name) > 0 {
|
||||||
|
Draw.TextWithin(r, bar.Name, barLabelBox, axisStyle)
|
||||||
|
}
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
cursor += bar.GetWidth() + sbc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawHorizontalXAxis(r Renderer, canvasBox Box) {
|
||||||
|
if !sbc.XAxis.Hidden {
|
||||||
|
axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
ticks := LinearRangeWithStep(0.0, 1.0, 0.2)
|
||||||
|
for _, t := range ticks {
|
||||||
|
axisStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
|
tx := canvasBox.Left + int(t*float64(canvasBox.Width()))
|
||||||
|
r.MoveTo(tx, canvasBox.Bottom)
|
||||||
|
r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
text := fmt.Sprintf("%0.0f%%", t*100)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(text)
|
||||||
|
textX := tx - (textBox.Width() >> 1)
|
||||||
|
textY := canvasBox.Bottom + DefaultXAxisMargin + 10
|
||||||
|
|
||||||
|
if t == 1 {
|
||||||
|
textX = canvasBox.Right - textBox.Width()
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw.Text(r, text, textX, textY, axisStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) {
|
||||||
|
if !sbc.YAxis.Hidden {
|
||||||
|
axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
r.MoveTo(canvasBox.Right, canvasBox.Top)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
ticks := LinearRangeWithStep(0.0, 1.0, 0.2)
|
||||||
|
for _, t := range ticks {
|
||||||
|
axisStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
|
ty := canvasBox.Bottom - int(t*float64(canvasBox.Height()))
|
||||||
|
r.MoveTo(canvasBox.Right, ty)
|
||||||
|
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
text := fmt.Sprintf("%0.0f%%", t*100)
|
||||||
|
|
||||||
|
tb := r.MeasureText(text)
|
||||||
|
Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawHorizontalYAxis(r Renderer, canvasBox Box) {
|
||||||
|
if !sbc.YAxis.Hidden {
|
||||||
|
axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsHorizontalAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left, canvasBox.Top)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left-DefaultHorizontalTickWidth, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
cursor := canvasBox.Top
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: cursor,
|
||||||
|
Left: 0,
|
||||||
|
Right: canvasBox.Left - DefaultYAxisMargin,
|
||||||
|
Bottom: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
|
||||||
|
}
|
||||||
|
if len(bar.Name) > 0 {
|
||||||
|
Draw.TextWithin(r, bar.Name, barLabelBox, axisStyle)
|
||||||
|
}
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
r.MoveTo(canvasBox.Left, barLabelBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left-DefaultHorizontalTickWidth, barLabelBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
cursor += bar.GetWidth() + sbc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawTitle(r Renderer) {
|
||||||
|
if len(sbc.Title) > 0 && !sbc.TitleStyle.Hidden {
|
||||||
|
r.SetFont(sbc.TitleStyle.GetFont(sbc.GetFont()))
|
||||||
|
r.SetFontColor(sbc.TitleStyle.GetFontColor(sbc.GetColorPalette().TextColor()))
|
||||||
|
titleFontSize := sbc.TitleStyle.GetFontSize(DefaultTitleFontSize)
|
||||||
|
r.SetFontSize(titleFontSize)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(sbc.Title)
|
||||||
|
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
textHeight := textBox.Height()
|
||||||
|
|
||||||
|
titleX := (sbc.GetWidth() >> 1) - (textWidth >> 1)
|
||||||
|
titleY := sbc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||||
|
|
||||||
|
r.Text(sbc.Title, titleX, titleY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getCanvasStyle() Style {
|
||||||
|
return sbc.Canvas.InheritFrom(sbc.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: sbc.GetColorPalette().CanvasColor(),
|
||||||
|
StrokeColor: sbc.GetColorPalette().CanvasStrokeColor(),
|
||||||
|
StrokeWidth: DefaultCanvasStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPalette returns the color palette for the chart.
|
||||||
|
func (sbc StackedBarChart) GetColorPalette() ColorPalette {
|
||||||
|
if sbc.ColorPalette != nil {
|
||||||
|
return sbc.ColorPalette
|
||||||
|
}
|
||||||
|
return AlternateColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getDefaultCanvasBox() Box {
|
||||||
|
return sbc.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box {
|
||||||
|
var totalWidth int
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
totalWidth += bar.GetWidth() + sbc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sbc.XAxis.Hidden {
|
||||||
|
xaxisHeight := DefaultVerticalTickHeight
|
||||||
|
|
||||||
|
axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
if len(bar.Name) > 0 {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
|
||||||
|
Bottom: sbc.GetHeight(),
|
||||||
|
}
|
||||||
|
lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
|
xaxisHeight = MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Left + totalWidth,
|
||||||
|
Bottom: sbc.GetHeight() - xaxisHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Left + totalWidth,
|
||||||
|
Bottom: canvasBox.Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getHorizontalAdjustedCanvasBox(r Renderer, canvasBox Box) Box {
|
||||||
|
var totalHeight int
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
totalHeight += bar.GetWidth() + sbc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sbc.YAxis.Hidden {
|
||||||
|
yAxisWidth := DefaultHorizontalTickWidth
|
||||||
|
|
||||||
|
axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsHorizontalAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
cursor := canvasBox.Top
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
if len(bar.Name) > 0 {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: cursor,
|
||||||
|
Left: 0,
|
||||||
|
Right: canvasBox.Left + DefaultYAxisMargin,
|
||||||
|
Bottom: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
|
||||||
|
}
|
||||||
|
lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
|
yAxisWidth = MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), yAxisWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left + yAxisWidth,
|
||||||
|
Right: canvasBox.Right,
|
||||||
|
Bottom: canvasBox.Top + totalHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Right,
|
||||||
|
Bottom: canvasBox.Top + totalHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (sbc StackedBarChart) Box() Box {
|
||||||
|
dpr := sbc.Background.Padding.GetRight(10)
|
||||||
|
dpb := sbc.Background.Padding.GetBottom(50)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: sbc.Background.Padding.GetTop(20),
|
||||||
|
Left: sbc.Background.Padding.GetLeft(20),
|
||||||
|
Right: sbc.GetWidth() - dpr,
|
||||||
|
Bottom: sbc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsStackedBarValue(index int) Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: sbc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
StrokeWidth: 3.0,
|
||||||
|
FillColor: sbc.GetColorPalette().GetSeriesColor(index),
|
||||||
|
FontSize: sbc.getScaledFontSize(),
|
||||||
|
FontColor: sbc.GetColorPalette().TextColor(),
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsTitle() Style {
|
||||||
|
return sbc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
FontSize: sbc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getScaledFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(sbc.GetWidth(), sbc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48.0
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24.0
|
||||||
|
} else if effectiveDimension > 512 {
|
||||||
|
return 18.0
|
||||||
|
} else if effectiveDimension > 256 {
|
||||||
|
return 12.0
|
||||||
|
}
|
||||||
|
return 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := MinInt(sbc.GetWidth(), sbc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24
|
||||||
|
} else if effectiveDimension >= 512 {
|
||||||
|
return 18
|
||||||
|
} else if effectiveDimension >= 256 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsAxes() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
FontColor: DefaultAxisColor,
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsHorizontalAxes() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
FontColor: DefaultAxisColor,
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignMiddle,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
57
vendor/github.com/wcharczuk/go-chart/v2/stringutil.go
generated
vendored
Normal file
57
vendor/github.com/wcharczuk/go-chart/v2/stringutil.go
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// SplitCSV splits a corpus by the `,`, dropping leading or trailing whitespace unless quoted.
|
||||||
|
func SplitCSV(text string) (output []string) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var state int
|
||||||
|
var word []rune
|
||||||
|
var opened rune
|
||||||
|
for _, r := range text {
|
||||||
|
switch state {
|
||||||
|
case 0: // word
|
||||||
|
if isQuote(r) {
|
||||||
|
opened = r
|
||||||
|
state = 1
|
||||||
|
} else if isCSVDelim(r) {
|
||||||
|
output = append(output, strings.TrimSpace(string(word)))
|
||||||
|
word = nil
|
||||||
|
} else {
|
||||||
|
word = append(word, r)
|
||||||
|
}
|
||||||
|
case 1: // we're in a quoted section
|
||||||
|
if matchesQuote(opened, r) {
|
||||||
|
state = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
word = append(word, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(word) > 0 {
|
||||||
|
output = append(output, strings.TrimSpace(string(word)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCSVDelim(r rune) bool {
|
||||||
|
return r == rune(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
func isQuote(r rune) bool {
|
||||||
|
return r == '"' || r == '\'' || r == '“' || r == '”' || r == '`'
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesQuote(a, b rune) bool {
|
||||||
|
if a == '“' && b == '”' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a == '”' && b == '“' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return a == b
|
||||||
|
}
|
480
vendor/github.com/wcharczuk/go-chart/v2/style.go
generated
vendored
Normal file
480
vendor/github.com/wcharczuk/go-chart/v2/style.go
generated
vendored
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Disabled indicates if the value should be interpreted as set intentionally to zero.
|
||||||
|
// this is because golang optionals aren't here yet.
|
||||||
|
Disabled = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hidden is a prebuilt style with the `Hidden` property set to true.
|
||||||
|
func Hidden() Style {
|
||||||
|
return Style{
|
||||||
|
Hidden: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shown is a prebuilt style with the `Hidden` property set to false.
|
||||||
|
// You can also think of this as the default.
|
||||||
|
func Shown() Style {
|
||||||
|
return Style{
|
||||||
|
Hidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleTextDefaults returns a style for drawing outside a
|
||||||
|
// chart context.
|
||||||
|
func StyleTextDefaults() Style {
|
||||||
|
font, _ := GetDefaultFont()
|
||||||
|
return Style{
|
||||||
|
Hidden: false,
|
||||||
|
Font: font,
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
FontSize: DefaultTitleFontSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style is a simple style set.
|
||||||
|
type Style struct {
|
||||||
|
Hidden bool
|
||||||
|
Padding Box
|
||||||
|
|
||||||
|
ClassName string
|
||||||
|
|
||||||
|
StrokeWidth float64
|
||||||
|
StrokeColor drawing.Color
|
||||||
|
StrokeDashArray []float64
|
||||||
|
|
||||||
|
DotColor drawing.Color
|
||||||
|
DotWidth float64
|
||||||
|
|
||||||
|
DotWidthProvider SizeProvider
|
||||||
|
DotColorProvider DotColorProvider
|
||||||
|
|
||||||
|
FillColor drawing.Color
|
||||||
|
|
||||||
|
FontSize float64
|
||||||
|
FontColor drawing.Color
|
||||||
|
Font *truetype.Font
|
||||||
|
|
||||||
|
TextHorizontalAlign TextHorizontalAlign
|
||||||
|
TextVerticalAlign TextVerticalAlign
|
||||||
|
TextWrap TextWrap
|
||||||
|
TextLineSpacing int
|
||||||
|
TextRotationDegrees float64 //0 is unset or normal
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the object is set or not.
|
||||||
|
func (s Style) IsZero() bool {
|
||||||
|
return !s.Hidden &&
|
||||||
|
s.StrokeColor.IsZero() &&
|
||||||
|
s.StrokeWidth == 0 &&
|
||||||
|
s.DotColor.IsZero() &&
|
||||||
|
s.DotWidth == 0 &&
|
||||||
|
s.FillColor.IsZero() &&
|
||||||
|
s.FontColor.IsZero() &&
|
||||||
|
s.FontSize == 0 &&
|
||||||
|
s.Font == nil &&
|
||||||
|
s.ClassName == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a text representation of the style.
|
||||||
|
func (s Style) String() string {
|
||||||
|
if s.IsZero() {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
var output []string
|
||||||
|
if s.Hidden {
|
||||||
|
output = []string{"\"hidden\": true"}
|
||||||
|
} else {
|
||||||
|
output = []string{"\"hidden\": false"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ClassName != "" {
|
||||||
|
output = append(output, fmt.Sprintf("\"class_name\": %s", s.ClassName))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"class_name\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Padding.IsZero() {
|
||||||
|
output = append(output, fmt.Sprintf("\"padding\": %s", s.Padding.String()))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"padding\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.StrokeWidth >= 0 {
|
||||||
|
output = append(output, fmt.Sprintf("\"stroke_width\": %0.2f", s.StrokeWidth))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"stroke_width\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.StrokeColor.IsZero() {
|
||||||
|
output = append(output, fmt.Sprintf("\"stroke_color\": %s", s.StrokeColor.String()))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"stroke_color\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.StrokeDashArray) > 0 {
|
||||||
|
var elements []string
|
||||||
|
for _, v := range s.StrokeDashArray {
|
||||||
|
elements = append(elements, fmt.Sprintf("%.2f", v))
|
||||||
|
}
|
||||||
|
dashArray := strings.Join(elements, ", ")
|
||||||
|
output = append(output, fmt.Sprintf("\"stroke_dash_array\": [%s]", dashArray))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"stroke_dash_array\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.DotWidth >= 0 {
|
||||||
|
output = append(output, fmt.Sprintf("\"dot_width\": %0.2f", s.DotWidth))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"dot_width\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.DotColor.IsZero() {
|
||||||
|
output = append(output, fmt.Sprintf("\"dot_color\": %s", s.DotColor.String()))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"dot_color\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.FillColor.IsZero() {
|
||||||
|
output = append(output, fmt.Sprintf("\"fill_color\": %s", s.FillColor.String()))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"fill_color\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.FontSize != 0 {
|
||||||
|
output = append(output, fmt.Sprintf("\"font_size\": \"%0.2fpt\"", s.FontSize))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"font_size\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.FontColor.IsZero() {
|
||||||
|
output = append(output, fmt.Sprintf("\"font_color\": %s", s.FontColor.String()))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"font_color\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Font != nil {
|
||||||
|
output = append(output, fmt.Sprintf("\"font\": \"%s\"", s.Font.Name(truetype.NameIDFontFamily)))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"font_color\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{" + strings.Join(output, ", ") + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClassName returns the class name or a default.
|
||||||
|
func (s Style) GetClassName(defaults ...string) string {
|
||||||
|
if s.ClassName == "" {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.ClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrokeColor returns the stroke color.
|
||||||
|
func (s Style) GetStrokeColor(defaults ...drawing.Color) drawing.Color {
|
||||||
|
if s.StrokeColor.IsZero() {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return drawing.ColorTransparent
|
||||||
|
}
|
||||||
|
return s.StrokeColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFillColor returns the fill color.
|
||||||
|
func (s Style) GetFillColor(defaults ...drawing.Color) drawing.Color {
|
||||||
|
if s.FillColor.IsZero() {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return drawing.ColorTransparent
|
||||||
|
}
|
||||||
|
return s.FillColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDotColor returns the stroke color.
|
||||||
|
func (s Style) GetDotColor(defaults ...drawing.Color) drawing.Color {
|
||||||
|
if s.DotColor.IsZero() {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return drawing.ColorTransparent
|
||||||
|
}
|
||||||
|
return s.DotColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrokeWidth returns the stroke width.
|
||||||
|
func (s Style) GetStrokeWidth(defaults ...float64) float64 {
|
||||||
|
if s.StrokeWidth == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultStrokeWidth
|
||||||
|
}
|
||||||
|
return s.StrokeWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDotWidth returns the dot width for scatter plots.
|
||||||
|
func (s Style) GetDotWidth(defaults ...float64) float64 {
|
||||||
|
if s.DotWidth == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDotWidth
|
||||||
|
}
|
||||||
|
return s.DotWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrokeDashArray returns the stroke dash array.
|
||||||
|
func (s Style) GetStrokeDashArray(defaults ...[]float64) []float64 {
|
||||||
|
if len(s.StrokeDashArray) == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.StrokeDashArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFontSize gets the font size.
|
||||||
|
func (s Style) GetFontSize(defaults ...float64) float64 {
|
||||||
|
if s.FontSize == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultFontSize
|
||||||
|
}
|
||||||
|
return s.FontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFontColor gets the font size.
|
||||||
|
func (s Style) GetFontColor(defaults ...drawing.Color) drawing.Color {
|
||||||
|
if s.FontColor.IsZero() {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return drawing.ColorTransparent
|
||||||
|
}
|
||||||
|
return s.FontColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the font face.
|
||||||
|
func (s Style) GetFont(defaults ...*truetype.Font) *truetype.Font {
|
||||||
|
if s.Font == nil {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPadding returns the padding.
|
||||||
|
func (s Style) GetPadding(defaults ...Box) Box {
|
||||||
|
if s.Padding.IsZero() {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return Box{}
|
||||||
|
}
|
||||||
|
return s.Padding
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextHorizontalAlign returns the horizontal alignment.
|
||||||
|
func (s Style) GetTextHorizontalAlign(defaults ...TextHorizontalAlign) TextHorizontalAlign {
|
||||||
|
if s.TextHorizontalAlign == TextHorizontalAlignUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TextHorizontalAlignUnset
|
||||||
|
}
|
||||||
|
return s.TextHorizontalAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextVerticalAlign returns the vertical alignment.
|
||||||
|
func (s Style) GetTextVerticalAlign(defaults ...TextVerticalAlign) TextVerticalAlign {
|
||||||
|
if s.TextVerticalAlign == TextVerticalAlignUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TextVerticalAlignUnset
|
||||||
|
}
|
||||||
|
return s.TextVerticalAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextWrap returns the word wrap.
|
||||||
|
func (s Style) GetTextWrap(defaults ...TextWrap) TextWrap {
|
||||||
|
if s.TextWrap == TextWrapUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TextWrapUnset
|
||||||
|
}
|
||||||
|
return s.TextWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextLineSpacing returns the spacing in pixels between lines of text (vertically).
|
||||||
|
func (s Style) GetTextLineSpacing(defaults ...int) int {
|
||||||
|
if s.TextLineSpacing == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultLineSpacing
|
||||||
|
}
|
||||||
|
return s.TextLineSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextRotationDegrees returns the text rotation in degrees.
|
||||||
|
func (s Style) GetTextRotationDegrees(defaults ...float64) float64 {
|
||||||
|
if s.TextRotationDegrees == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.TextRotationDegrees
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToRenderer passes the style's options to a renderer.
|
||||||
|
func (s Style) WriteToRenderer(r Renderer) {
|
||||||
|
r.SetClassName(s.GetClassName())
|
||||||
|
r.SetStrokeColor(s.GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(s.GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||||
|
r.SetFillColor(s.GetFillColor())
|
||||||
|
r.SetFont(s.GetFont())
|
||||||
|
r.SetFontColor(s.GetFontColor())
|
||||||
|
r.SetFontSize(s.GetFontSize())
|
||||||
|
|
||||||
|
r.ClearTextRotation()
|
||||||
|
if s.GetTextRotationDegrees() != 0 {
|
||||||
|
r.SetTextRotation(DegreesToRadians(s.GetTextRotationDegrees()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
|
||||||
|
func (s Style) WriteDrawingOptionsToRenderer(r Renderer) {
|
||||||
|
r.SetClassName(s.GetClassName())
|
||||||
|
r.SetStrokeColor(s.GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(s.GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||||
|
r.SetFillColor(s.GetFillColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTextOptionsToRenderer passes just the text style options to a renderer.
|
||||||
|
func (s Style) WriteTextOptionsToRenderer(r Renderer) {
|
||||||
|
r.SetClassName(s.GetClassName())
|
||||||
|
r.SetFont(s.GetFont())
|
||||||
|
r.SetFontColor(s.GetFontColor())
|
||||||
|
r.SetFontSize(s.GetFontSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// InheritFrom coalesces two styles into a new style.
|
||||||
|
func (s Style) InheritFrom(defaults Style) (final Style) {
|
||||||
|
final.ClassName = s.GetClassName(defaults.ClassName)
|
||||||
|
|
||||||
|
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
|
||||||
|
final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
|
||||||
|
final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray)
|
||||||
|
|
||||||
|
final.DotColor = s.GetDotColor(defaults.DotColor)
|
||||||
|
final.DotWidth = s.GetDotWidth(defaults.DotWidth)
|
||||||
|
|
||||||
|
final.DotWidthProvider = s.DotWidthProvider
|
||||||
|
final.DotColorProvider = s.DotColorProvider
|
||||||
|
|
||||||
|
final.FillColor = s.GetFillColor(defaults.FillColor)
|
||||||
|
final.FontColor = s.GetFontColor(defaults.FontColor)
|
||||||
|
final.FontSize = s.GetFontSize(defaults.FontSize)
|
||||||
|
final.Font = s.GetFont(defaults.Font)
|
||||||
|
final.Padding = s.GetPadding(defaults.Padding)
|
||||||
|
final.TextHorizontalAlign = s.GetTextHorizontalAlign(defaults.TextHorizontalAlign)
|
||||||
|
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
|
||||||
|
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
||||||
|
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
||||||
|
final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrokeOptions returns the stroke components.
|
||||||
|
func (s Style) GetStrokeOptions() Style {
|
||||||
|
return Style{
|
||||||
|
ClassName: s.ClassName,
|
||||||
|
StrokeDashArray: s.StrokeDashArray,
|
||||||
|
StrokeColor: s.StrokeColor,
|
||||||
|
StrokeWidth: s.StrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFillOptions returns the fill components.
|
||||||
|
func (s Style) GetFillOptions() Style {
|
||||||
|
return Style{
|
||||||
|
ClassName: s.ClassName,
|
||||||
|
FillColor: s.FillColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDotOptions returns the dot components.
|
||||||
|
func (s Style) GetDotOptions() Style {
|
||||||
|
return Style{
|
||||||
|
ClassName: s.ClassName,
|
||||||
|
StrokeDashArray: nil,
|
||||||
|
FillColor: s.DotColor,
|
||||||
|
StrokeColor: s.DotColor,
|
||||||
|
StrokeWidth: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFillAndStrokeOptions returns the fill and stroke components.
|
||||||
|
func (s Style) GetFillAndStrokeOptions() Style {
|
||||||
|
return Style{
|
||||||
|
ClassName: s.ClassName,
|
||||||
|
StrokeDashArray: s.StrokeDashArray,
|
||||||
|
FillColor: s.FillColor,
|
||||||
|
StrokeColor: s.StrokeColor,
|
||||||
|
StrokeWidth: s.StrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextOptions returns just the text components of the style.
|
||||||
|
func (s Style) GetTextOptions() Style {
|
||||||
|
return Style{
|
||||||
|
ClassName: s.ClassName,
|
||||||
|
FontColor: s.FontColor,
|
||||||
|
FontSize: s.FontSize,
|
||||||
|
Font: s.Font,
|
||||||
|
TextHorizontalAlign: s.TextHorizontalAlign,
|
||||||
|
TextVerticalAlign: s.TextVerticalAlign,
|
||||||
|
TextWrap: s.TextWrap,
|
||||||
|
TextLineSpacing: s.TextLineSpacing,
|
||||||
|
TextRotationDegrees: s.TextRotationDegrees,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldDrawStroke tells drawing functions if they should draw the stroke.
|
||||||
|
func (s Style) ShouldDrawStroke() bool {
|
||||||
|
return !s.StrokeColor.IsZero() && s.StrokeWidth > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldDrawDot tells drawing functions if they should draw the dot.
|
||||||
|
func (s Style) ShouldDrawDot() bool {
|
||||||
|
return (!s.DotColor.IsZero() && s.DotWidth > 0) || s.DotColorProvider != nil || s.DotWidthProvider != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldDrawFill tells drawing functions if they should draw the stroke.
|
||||||
|
func (s Style) ShouldDrawFill() bool {
|
||||||
|
return !s.FillColor.IsZero()
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user