feat: support radar chart
This commit is contained in:
parent
6209a9ce63
commit
570828d35f
9 changed files with 337 additions and 35 deletions
32
chart.go
32
chart.go
|
|
@ -33,9 +33,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChartTypeLine = "line"
|
ChartTypeLine = "line"
|
||||||
ChartTypeBar = "bar"
|
ChartTypeBar = "bar"
|
||||||
ChartTypePie = "pie"
|
ChartTypePie = "pie"
|
||||||
|
ChartTypeRadar = "radar"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -49,6 +50,8 @@ type Point struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFontSize = 10
|
const labelFontSize = 10
|
||||||
|
const defaultDotWidth = 2.0
|
||||||
|
const defaultStrokeWidth = 2.0
|
||||||
|
|
||||||
var defaultChartWidth = 600
|
var defaultChartWidth = 600
|
||||||
var defaultChartHeight = 400
|
var defaultChartHeight = 400
|
||||||
|
|
@ -82,6 +85,8 @@ type ChartOption struct {
|
||||||
Box chart.Box
|
Box chart.Box
|
||||||
// The series list
|
// The series list
|
||||||
SeriesList SeriesList
|
SeriesList SeriesList
|
||||||
|
// The radar indicator list
|
||||||
|
RadarIndicators []RadarIndicator
|
||||||
// The background color of chart
|
// The background color of chart
|
||||||
BackgroundColor drawing.Color
|
BackgroundColor drawing.Color
|
||||||
// The child charts
|
// The child charts
|
||||||
|
|
@ -283,11 +288,14 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
lineSeries := make([]Series, 0)
|
lineSeries := make([]Series, 0)
|
||||||
barSeries := make([]Series, 0)
|
barSeries := make([]Series, 0)
|
||||||
isPieChart := false
|
isPieChart := false
|
||||||
|
isRadarChart := false
|
||||||
for index, item := range opt.SeriesList {
|
for index, item := range opt.SeriesList {
|
||||||
item.index = index
|
item.index = index
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case ChartTypePie:
|
case ChartTypePie:
|
||||||
isPieChart = true
|
isPieChart = true
|
||||||
|
case ChartTypeRadar:
|
||||||
|
isRadarChart = true
|
||||||
case ChartTypeBar:
|
case ChartTypeBar:
|
||||||
barSeries = append(barSeries, item)
|
barSeries = append(barSeries, item)
|
||||||
default:
|
default:
|
||||||
|
|
@ -296,7 +304,8 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
}
|
}
|
||||||
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
|
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
|
||||||
// pie不需要axis
|
// pie不需要axis
|
||||||
if isPieChart {
|
// radar 同样处理
|
||||||
|
if isPieChart || isRadarChart {
|
||||||
opt.XAxis.Hidden = true
|
opt.XAxis.Hidden = true
|
||||||
for index := range opt.YAxisList {
|
for index := range opt.YAxisList {
|
||||||
opt.YAxisList[index].Hidden = true
|
opt.YAxisList[index].Hidden = true
|
||||||
|
|
@ -313,12 +322,23 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
if !isPieChart {
|
if !isPieChart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := pieChartRender(pieChartOption{
|
return pieChartRender(pieChartOption{
|
||||||
SeriesList: opt.SeriesList,
|
SeriesList: opt.SeriesList,
|
||||||
Theme: opt.Theme,
|
Theme: opt.Theme,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}, result)
|
}, result)
|
||||||
return err
|
},
|
||||||
|
// radar render
|
||||||
|
func() error {
|
||||||
|
if !isRadarChart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return radarChartRender(radarChartOption{
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Font: opt.Font,
|
||||||
|
Indicators: opt.RadarIndicators,
|
||||||
|
}, result)
|
||||||
},
|
},
|
||||||
// bar render
|
// bar render
|
||||||
func() error {
|
func() error {
|
||||||
|
|
|
||||||
13
draw.go
13
draw.go
|
|
@ -309,3 +309,16 @@ func (d *Draw) setBackground(width, height int, color drawing.Color) {
|
||||||
r.LineTo(0, 0)
|
r.LineTo(0, 0)
|
||||||
r.FillStroke()
|
r.FillStroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Draw) polygon(center Point, radius float64, sides int) {
|
||||||
|
points := getPolygonPoints(center, radius, sides)
|
||||||
|
for i, p := range points {
|
||||||
|
if i == 0 {
|
||||||
|
d.moveTo(p.X, p.Y)
|
||||||
|
} else {
|
||||||
|
d.lineTo(p.X, p.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.lineTo(points[0].X, points[0].Y)
|
||||||
|
d.Render.Stroke()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -496,6 +496,66 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
Radius: "35%",
|
Radius: "35%",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
// 雷达图
|
||||||
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Basic Radar Chart",
|
||||||
|
},
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"Allocated Budget",
|
||||||
|
"Actual Spending",
|
||||||
|
}),
|
||||||
|
RadarIndicators: []charts.RadarIndicator{
|
||||||
|
{
|
||||||
|
Name: "Sales",
|
||||||
|
Max: 6500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Administration",
|
||||||
|
Max: 16000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Information Technology",
|
||||||
|
Max: 30000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Customer Support",
|
||||||
|
Max: 38000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Development",
|
||||||
|
Max: 52000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Marketing",
|
||||||
|
Max: 25000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SeriesList: charts.SeriesList{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeRadar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
4200,
|
||||||
|
3000,
|
||||||
|
20000,
|
||||||
|
35000,
|
||||||
|
50000,
|
||||||
|
18000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeRadar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
5000,
|
||||||
|
14000,
|
||||||
|
28000,
|
||||||
|
26000,
|
||||||
|
42000,
|
||||||
|
21000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// 多图展示
|
// 多图展示
|
||||||
{
|
{
|
||||||
Legend: charts.LegendOption{
|
Legend: charts.LegendOption{
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPoin
|
||||||
StrokeColor: seriesColor,
|
StrokeColor: seriesColor,
|
||||||
StrokeWidth: 2,
|
StrokeWidth: 2,
|
||||||
DotColor: seriesColor,
|
DotColor: seriesColor,
|
||||||
DotWidth: 2,
|
DotWidth: defaultDotWidth,
|
||||||
DotFillColor: dotFillColor,
|
DotFillColor: dotFillColor,
|
||||||
})
|
})
|
||||||
// draw mark point
|
// draw mark point
|
||||||
|
|
|
||||||
21
pie_chart.go
21
pie_chart.go
|
|
@ -24,14 +24,11 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultRadiusPercent = 0.4
|
|
||||||
|
|
||||||
func getPieStyle(theme *Theme, index int) chart.Style {
|
func getPieStyle(theme *Theme, index int) chart.Style {
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
seriesColor := theme.GetSeriesColor(index)
|
||||||
return chart.Style{
|
return chart.Style{
|
||||||
|
|
@ -47,22 +44,6 @@ type pieChartOption struct {
|
||||||
SeriesList SeriesList
|
SeriesList SeriesList
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPieRadius(diameter float64, radiusValue string) float64 {
|
|
||||||
var radius float64
|
|
||||||
if len(radiusValue) != 0 {
|
|
||||||
v := convertPercent(radiusValue)
|
|
||||||
if v != -1 {
|
|
||||||
radius = float64(diameter) * v
|
|
||||||
} else {
|
|
||||||
radius, _ = strconv.ParseFloat(radiusValue, 64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if radius <= 0 {
|
|
||||||
radius = float64(diameter) * defaultRadiusPercent
|
|
||||||
}
|
|
||||||
return radius
|
|
||||||
}
|
|
||||||
|
|
||||||
func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewDraw(DrawOption{
|
||||||
Parent: result.d,
|
Parent: result.d,
|
||||||
|
|
@ -95,7 +76,7 @@ func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
cy := box.Height() >> 1
|
cy := box.Height() >> 1
|
||||||
|
|
||||||
diameter := chart.MinInt(box.Width(), box.Height())
|
diameter := chart.MinInt(box.Width(), box.Height())
|
||||||
radius := getPieRadius(float64(diameter), radiusValue)
|
radius := getRadius(float64(diameter), radiusValue)
|
||||||
|
|
||||||
labelLineWidth := 15
|
labelLineWidth := 15
|
||||||
if radius < 50 {
|
if radius < 50 {
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,6 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetPieRadius(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal(50.0, getPieRadius(100, "50%"))
|
|
||||||
assert.Equal(30.0, getPieRadius(100, "30"))
|
|
||||||
assert.Equal(40.0, getPieRadius(100, ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPieChartRender(t *testing.T) {
|
func TestPieChartRender(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
|
|
||||||
183
radar_chart.go
Normal file
183
radar_chart.go
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
// MIT License
|
||||||
|
|
||||||
|
// Copyright (c) 2022 Tree Xie
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 线 E0E6F1
|
||||||
|
// 填充 rgb(210,219,238) fill-opacity="0.2"
|
||||||
|
|
||||||
|
type RadarIndicator struct {
|
||||||
|
Name string
|
||||||
|
Max float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type radarChartOption struct {
|
||||||
|
Theme string
|
||||||
|
Font *truetype.Font
|
||||||
|
SeriesList SeriesList
|
||||||
|
Indicators []RadarIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
||||||
|
sides := len(opt.Indicators)
|
||||||
|
if sides < 3 {
|
||||||
|
return errors.New("The count of indicator should be >= 3")
|
||||||
|
}
|
||||||
|
for _, indicator := range opt.Indicators {
|
||||||
|
if indicator.Max <= 0 {
|
||||||
|
return errors.New("The max of indicator should be > 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d, err := NewDraw(DrawOption{
|
||||||
|
Parent: result.d,
|
||||||
|
}, PaddingOption(chart.Box{
|
||||||
|
Top: result.titleBox.Height(),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
radiusValue := ""
|
||||||
|
for _, series := range opt.SeriesList {
|
||||||
|
if len(series.Radius) != 0 {
|
||||||
|
radiusValue = series.Radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
box := d.Box
|
||||||
|
cx := box.Width() >> 1
|
||||||
|
cy := box.Height() >> 1
|
||||||
|
diameter := chart.MinInt(box.Width(), box.Height())
|
||||||
|
radius := getRadius(float64(diameter), radiusValue)
|
||||||
|
|
||||||
|
theme := NewTheme(opt.Theme)
|
||||||
|
|
||||||
|
divideCount := 5
|
||||||
|
divideRadius := float64(int(radius / float64(divideCount)))
|
||||||
|
radius = divideRadius * float64(divideCount)
|
||||||
|
|
||||||
|
style := chart.Style{
|
||||||
|
StrokeColor: theme.GetAxisSplitLineColor(),
|
||||||
|
StrokeWidth: 1,
|
||||||
|
}
|
||||||
|
r := d.Render
|
||||||
|
style.WriteToRenderer(r)
|
||||||
|
center := Point{
|
||||||
|
X: cx,
|
||||||
|
Y: cy,
|
||||||
|
}
|
||||||
|
for i := 0; i < divideCount; i++ {
|
||||||
|
d.polygon(center, divideRadius*float64(i+1), sides)
|
||||||
|
}
|
||||||
|
points := getPolygonPoints(center, radius, sides)
|
||||||
|
for _, p := range points {
|
||||||
|
d.moveTo(center.X, center.Y)
|
||||||
|
d.lineTo(p.X, p.Y)
|
||||||
|
d.Render.Stroke()
|
||||||
|
}
|
||||||
|
// 文本
|
||||||
|
textStyle := chart.Style{
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
FontSize: labelFontSize,
|
||||||
|
Font: opt.Font,
|
||||||
|
}
|
||||||
|
textStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
offset := 5
|
||||||
|
// 文本生成
|
||||||
|
for index, p := range points {
|
||||||
|
name := opt.Indicators[index].Name
|
||||||
|
b := r.MeasureText(name)
|
||||||
|
isXCenter := p.X == center.X
|
||||||
|
isYCenter := p.Y == center.Y
|
||||||
|
isRight := p.X > center.X
|
||||||
|
isLeft := p.X < center.X
|
||||||
|
isTop := p.Y < center.Y
|
||||||
|
isBottom := p.Y > center.Y
|
||||||
|
x := p.X
|
||||||
|
y := p.Y
|
||||||
|
if isXCenter {
|
||||||
|
x -= b.Width() >> 1
|
||||||
|
if isTop {
|
||||||
|
y -= b.Height()
|
||||||
|
} else {
|
||||||
|
y += b.Height()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isYCenter {
|
||||||
|
y += b.Height() >> 1
|
||||||
|
}
|
||||||
|
if isTop {
|
||||||
|
y += offset
|
||||||
|
}
|
||||||
|
if isBottom {
|
||||||
|
y += offset
|
||||||
|
}
|
||||||
|
if isRight {
|
||||||
|
x += offset
|
||||||
|
}
|
||||||
|
if isLeft {
|
||||||
|
x -= (b.Width() + offset)
|
||||||
|
}
|
||||||
|
d.text(name, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 雷达图
|
||||||
|
angles := getPolygonPointAngles(sides)
|
||||||
|
maxCount := len(opt.Indicators)
|
||||||
|
for i, series := range opt.SeriesList {
|
||||||
|
linePoints := make([]Point, 0, maxCount)
|
||||||
|
for j, item := range series.Data {
|
||||||
|
if j >= maxCount {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
percent := item.Value / opt.Indicators[j].Max
|
||||||
|
r := percent * radius
|
||||||
|
p := getPolygonPoint(center, r, angles[j])
|
||||||
|
linePoints = append(linePoints, p)
|
||||||
|
}
|
||||||
|
color := theme.GetSeriesColor(i)
|
||||||
|
dotFillColor := drawing.ColorWhite
|
||||||
|
if theme.IsDark() {
|
||||||
|
dotFillColor = color
|
||||||
|
}
|
||||||
|
linePoints = append(linePoints, linePoints[0])
|
||||||
|
s := LineStyle{
|
||||||
|
StrokeColor: color,
|
||||||
|
StrokeWidth: defaultStrokeWidth,
|
||||||
|
DotWidth: defaultDotWidth,
|
||||||
|
DotColor: color,
|
||||||
|
DotFillColor: dotFillColor,
|
||||||
|
FillColor: color.WithAlpha(20),
|
||||||
|
}
|
||||||
|
d.lineStroke(linePoints, s)
|
||||||
|
d.lineFill(linePoints, s)
|
||||||
|
d.lineDot(linePoints[0:len(linePoints)-1], s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
45
util.go
45
util.go
|
|
@ -23,6 +23,7 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -168,3 +169,47 @@ func parseColor(color string) drawing.Color {
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultRadiusPercent = 0.4
|
||||||
|
|
||||||
|
func getRadius(diameter float64, radiusValue string) float64 {
|
||||||
|
var radius float64
|
||||||
|
if len(radiusValue) != 0 {
|
||||||
|
v := convertPercent(radiusValue)
|
||||||
|
if v != -1 {
|
||||||
|
radius = float64(diameter) * v
|
||||||
|
} else {
|
||||||
|
radius, _ = strconv.ParseFloat(radiusValue, 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if radius <= 0 {
|
||||||
|
radius = float64(diameter) * defaultRadiusPercent
|
||||||
|
}
|
||||||
|
return radius
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPolygonPointAngles(sides int) []float64 {
|
||||||
|
angles := make([]float64, sides)
|
||||||
|
for i := 0; i < sides; i++ {
|
||||||
|
angle := 2*math.Pi/float64(sides)*float64(i) - (math.Pi / 2)
|
||||||
|
angles[i] = angle
|
||||||
|
}
|
||||||
|
return angles
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPolygonPoint(center Point, radius, angle float64) Point {
|
||||||
|
x := center.X + int(radius*math.Cos(angle))
|
||||||
|
y := center.Y + int(radius*math.Sin(angle))
|
||||||
|
return Point{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPolygonPoints(center Point, radius float64, sides int) []Point {
|
||||||
|
points := make([]Point, sides)
|
||||||
|
for i, angle := range getPolygonPointAngles(sides) {
|
||||||
|
points[i] = getPolygonPoint(center, radius, angle)
|
||||||
|
}
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,14 @@ func TestAutoDivide(t *testing.T) {
|
||||||
}, autoDivide(600, 7))
|
}, autoDivide(600, 7))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetRadius(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Equal(50.0, getRadius(100, "50%"))
|
||||||
|
assert.Equal(30.0, getRadius(100, "30"))
|
||||||
|
assert.Equal(40.0, getRadius(100, ""))
|
||||||
|
}
|
||||||
|
|
||||||
func TestMeasureTextMaxWidthHeight(t *testing.T) {
|
func TestMeasureTextMaxWidthHeight(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
r, err := chart.SVG(400, 300)
|
r, err := chart.SVG(400, 300)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue