Add response time badge and chart

This commit is contained in:
TwinProduction 2021-08-21 17:38:23 -04:00 committed by Chris
parent bab69478dd
commit 470e3a3ebc
140 changed files with 26995 additions and 106 deletions

View File

@ -3,6 +3,7 @@ package controller
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
@ -11,19 +12,27 @@ import (
"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
// Pattern for {identifier}: <KEY>.svg
func badgeHandler(writer http.ResponseWriter, request *http.Request) {
func uptimeBadgeHandler(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(-time.Hour * 24 * 7)
from = time.Now().Add(-7 * 24 * time.Hour)
case "24h":
from = time.Now().Add(-time.Hour * 24)
from = time.Now().Add(-24 * time.Hour)
case "1h":
from = time.Now().Add(-time.Hour)
default:
@ -31,8 +40,13 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
return
}
identifier := variables["identifier"]
key := strings.TrimSuffix(identifier, ".svg")
var key string
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())
if err != nil {
if err == common.ErrServiceNotFound {
@ -50,10 +64,10 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Date", formattedDate)
writer.Header().Set("Expires", formattedDate)
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
switch duration {
case "7d":
@ -106,15 +120,118 @@ func generateSVG(duration string, uptime float64) []byte {
func getBadgeColorFromUptime(uptime float64) string {
if uptime >= 0.975 {
return "#40cc11"
return badgeColorHexAwesome
} else if uptime >= 0.95 {
return "#94cc11"
return badgeColorHexGreat
} else if uptime >= 0.9 {
return "#ccc311"
return badgeColorHexGood
} else if uptime >= 0.8 {
return "#ccb311"
return badgeColorHexPassable
} 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
}

View File

@ -1,32 +1,116 @@
package controller
import (
"strconv"
"testing"
)
func TestGetBadgeColorFromUptime(t *testing.T) {
if getBadgeColorFromUptime(1) != "#40cc11" {
t.Error("expected #40cc11 from an uptime of 1, got", getBadgeColorFromUptime(1))
scenarios := []struct {
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" {
t.Error("expected #94cc11 from an uptime of 0.95, got", getBadgeColorFromUptime(0.95))
}
if getBadgeColorFromUptime(0.9) != "#ccc311" {
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" {
t.Error("expected #cc8111 from an uptime of 0.75, got", getBadgeColorFromUptime(0.75))
}
if getBadgeColorFromUptime(0.6) != "#c7130a" {
t.Error("expected #c7130a from an uptime of 0.6, got", getBadgeColorFromUptime(0.6))
}
if getBadgeColorFromUptime(0.25) != "#c7130a" {
t.Error("expected #c7130a from an uptime of 0.25, got", getBadgeColorFromUptime(0.25))
}
if getBadgeColorFromUptime(0) != "#c7130a" {
t.Error("expected #c7130a from an uptime of 0, got", getBadgeColorFromUptime(0))
for _, scenario := range scenarios {
t.Run("uptime-"+strconv.Itoa(int(scenario.Uptime*100)), func(t *testing.T) {
if getBadgeColorFromUptime(scenario.Uptime) != scenario.ExpectedColor {
t.Errorf("expected %s from %f, got %v", scenario.ExpectedColor, scenario.Uptime, getBadgeColorFromUptime(scenario.Uptime))
}
})
}
}
func TestGetBadgeColorFromResponseTime(t *testing.T) {
scenarios := []struct {
ResponseTime int
ExpectedColor string
}{
{
ResponseTime: 10,
ExpectedColor: badgeColorHexAwesome,
},
{
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
View 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
}
}

View File

@ -75,9 +75,18 @@ func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Rou
}
router.Handle("/health", health.Handler().WithJSON(true)).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/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
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
// Everything else falls back on static content

View File

@ -123,60 +123,167 @@ func TestCreateRouter(t *testing.T) {
ExpectedCode: http.StatusOK,
},
{
Name: "badges-1h",
Name: "old-badge-1h",
Path: "/api/v1/badges/uptime/1h/core_frontend.svg",
ExpectedCode: http.StatusOK,
},
{
Name: "badges-24h",
Name: "old-badge-24h",
Path: "/api/v1/badges/uptime/24h/core_backend.svg",
ExpectedCode: http.StatusOK,
},
{
Name: "badges-7d",
Name: "old-badge-7d",
Path: "/api/v1/badges/uptime/7d/core_frontend.svg",
ExpectedCode: http.StatusOK,
},
{
Name: "badges-with-invalid-duration",
Name: "old-badge-with-invalid-duration",
Path: "/api/v1/badges/uptime/3d/core_backend.svg",
ExpectedCode: http.StatusBadRequest,
},
{
Name: "badges-for-invalid-key",
Name: "old-badge-for-invalid-key",
Path: "/api/v1/badges/uptime/7d/invalid_key.svg",
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",
ExpectedCode: http.StatusOK,
},
{
Name: "service-statuses-gzip",
Name: "old-service-statuses-gzip",
Path: "/api/v1/statuses",
ExpectedCode: http.StatusOK,
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",
Path: "/api/v1/statuses?page=1&pageSize=20",
Path: "/api/v1/services/statuses?page=1&pageSize=20",
ExpectedCode: http.StatusOK,
},
{
Name: "service-status",
Path: "/api/v1/statuses/core_frontend",
Path: "/api/v1/services/core_frontend/statuses",
ExpectedCode: http.StatusOK,
},
{
Name: "service-status-gzip",
Path: "/api/v1/statuses/core_frontend",
Path: "/api/v1/services/core_frontend/statuses",
ExpectedCode: http.StatusOK,
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",
Path: "/api/v1/statuses/invalid_key",
Path: "/api/v1/services/invalid_key/statuses",
ExpectedCode: http.StatusNotFound,
},
{

20
vendor/github.com/golang/freetype/AUTHORS generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
// |xy″-yx″| / (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
View 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
View 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

File diff suppressed because it is too large Load Diff

289
vendor/github.com/golang/freetype/truetype/opcodes.go generated vendored Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
29.02

21
vendor/github.com/wcharczuk/go-chart/v2/LICENSE generated vendored Normal file
View 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
View File

@ -0,0 +1,10 @@
all: new-install test
new-install:
@go get -v -u ./...
generate:
@go generate ./...
test:
@go test ./...

View 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
View File

@ -0,0 +1,95 @@
go-chart
========
[![CircleCI](https://circleci.com/gh/wcharczuk/go-chart.svg?style=svg)](https://circleci.com/gh/wcharczuk/go-chart) [![Go Report Card](https://goreportcard.com/badge/github.com/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:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/tvix_ltm.png)
Single axis:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/goog_ltm.png)
Two axis:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/two_axis.png)
# Other Chart Types
Pie Chart:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/pie_chart.png)
The code for this chart can be found in `examples/pie_chart/main.go`.
Stacked Bar:
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/_images/stacked_bar.png)
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.

View 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
View 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
View 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
View 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
}

View 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
}

View 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
View 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
View 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
View 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)
}

View 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
}

View 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)))
}

View 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
View 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
View 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
View 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()
}
}

View 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).

View 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)
}

View File

@ -0,0 +1,6 @@
package drawing
const (
// DefaultDPI is the default image DPI.
DefaultDPI = 96.0
)

View 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)
}
}

View 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
}

View 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()
}
}

View 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
}

View 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
}

View 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() {}

View 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)
}

View 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
)

View 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
}
}
}

View 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
}

View 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
View 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
}

View 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)
}

View 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
}
}

View 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:]...)
}

View 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,
}
}

View 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()
}

View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
}

View 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
}

View 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
View 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
}

View 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
View 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++
}
}
}
}

View 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
}

View 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)
}

View 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
}

View 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
View 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
View 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
View 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)
}

View 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
}

View 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
View 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)
}

View 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
}

View 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
View 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
}

View 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
View 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,
}
}

View 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)
}

View 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
View 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
}

View 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)
}

View 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
View 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
}

View File

@ -0,0 +1,4 @@
package chart
// RendererProvider is a function that returns a renderer.
type RendererProvider func(int, int) (Renderer, error)

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
View 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
View 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
View 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
}

View 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
View 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
View 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