feat: support radius for pie chart
This commit is contained in:
parent
e07cb90607
commit
524eb79a8e
12 changed files with 376 additions and 66 deletions
3
axis.go
3
axis.go
|
|
@ -277,6 +277,9 @@ func (a *axis) axisTick(opt *axisRenderOption) {
|
||||||
height := d.Box.Height()
|
height := d.Box.Height()
|
||||||
data := *a.data
|
data := *a.data
|
||||||
tickCount := len(data)
|
tickCount := len(data)
|
||||||
|
if tickCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
if !opt.boundaryGap {
|
if !opt.boundaryGap {
|
||||||
tickCount--
|
tickCount--
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
bar_chart.go
25
bar_chart.go
|
|
@ -53,6 +53,10 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
barMaxHeight := yRange.Size
|
barMaxHeight := yRange.Size
|
||||||
theme := NewTheme(opt.Theme)
|
theme := NewTheme(opt.Theme)
|
||||||
|
|
||||||
|
seriesNames := opt.Legend.Data
|
||||||
|
|
||||||
|
r := d.Render
|
||||||
|
|
||||||
for i, series := range opt.SeriesList {
|
for i, series := range opt.SeriesList {
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
x0, _ := xRange.GetRange(j)
|
x0, _ := xRange.GetRange(j)
|
||||||
|
|
@ -63,6 +67,10 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
h := int(yRange.getHeight(item.Value))
|
h := int(yRange.getHeight(item.Value))
|
||||||
|
fillColor := theme.GetSeriesColor(i)
|
||||||
|
if !item.Style.FillColor.IsZero() {
|
||||||
|
fillColor = item.Style.FillColor
|
||||||
|
}
|
||||||
|
|
||||||
d.Bar(chart.Box{
|
d.Bar(chart.Box{
|
||||||
Top: barMaxHeight - h,
|
Top: barMaxHeight - h,
|
||||||
|
|
@ -70,8 +78,23 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
Right: x + barWidth,
|
Right: x + barWidth,
|
||||||
Bottom: barMaxHeight - 1,
|
Bottom: barMaxHeight - 1,
|
||||||
}, BarStyle{
|
}, BarStyle{
|
||||||
FillColor: theme.GetSeriesColor(i),
|
FillColor: fillColor,
|
||||||
})
|
})
|
||||||
|
if !series.Label.Show {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
||||||
|
labelStyle := chart.Style{
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
FontSize: 10,
|
||||||
|
Font: opt.Font,
|
||||||
|
}
|
||||||
|
if !series.Label.Color.IsZero() {
|
||||||
|
labelStyle.FontColor = series.Label.Color
|
||||||
|
}
|
||||||
|
labelStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
textBox := r.MeasureText(text)
|
||||||
|
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
102
chart.go
102
chart.go
|
|
@ -26,7 +26,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
|
@ -38,23 +37,11 @@ const (
|
||||||
ChartTypePie = "pie"
|
ChartTypePie = "pie"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SeriesData struct {
|
|
||||||
Value float64
|
|
||||||
Style chart.Style
|
|
||||||
}
|
|
||||||
type Point struct {
|
type Point struct {
|
||||||
X int
|
X int
|
||||||
Y int
|
Y int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Series struct {
|
|
||||||
index int
|
|
||||||
Type string
|
|
||||||
Data []SeriesData
|
|
||||||
YAxisIndex int
|
|
||||||
Style chart.Style
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartOption struct {
|
type ChartOption struct {
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
Theme string
|
Theme string
|
||||||
|
|
@ -68,6 +55,7 @@ type ChartOption struct {
|
||||||
Padding chart.Box
|
Padding chart.Box
|
||||||
SeriesList []Series
|
SeriesList []Series
|
||||||
BackgroundColor drawing.Color
|
BackgroundColor drawing.Color
|
||||||
|
Children []ChartOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ChartOption) FillDefault(theme string) {
|
func (o *ChartOption) FillDefault(theme string) {
|
||||||
|
|
@ -115,6 +103,13 @@ func (o *ChartOption) FillDefault(theme string) {
|
||||||
if o.Legend.Left == "" {
|
if o.Legend.Left == "" {
|
||||||
o.Legend.Left = PositionCenter
|
o.Legend.Left = PositionCenter
|
||||||
}
|
}
|
||||||
|
if len(o.Legend.Data) == 0 {
|
||||||
|
names := make([]string, len(o.SeriesList))
|
||||||
|
for index, item := range o.SeriesList {
|
||||||
|
names[index] = item.Name
|
||||||
|
}
|
||||||
|
o.Legend.Data = names
|
||||||
|
}
|
||||||
if o.Legend.Style.Font == nil {
|
if o.Legend.Style.Font == nil {
|
||||||
o.Legend.Style.Font = o.Font
|
o.Legend.Style.Font = o.Font
|
||||||
}
|
}
|
||||||
|
|
@ -165,20 +160,19 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
|
||||||
if o.YAxis.Max != nil {
|
if o.YAxis.Max != nil {
|
||||||
max = *o.YAxis.Max
|
max = *o.YAxis.Max
|
||||||
}
|
}
|
||||||
|
divideCount := 6
|
||||||
// y轴分设置默认划分为6块
|
// y轴分设置默认划分为6块
|
||||||
r := NewRange(min, max, 6)
|
r := NewRange(min, max, divideCount)
|
||||||
return r
|
|
||||||
|
// 由于NewRange会重新计算min max
|
||||||
|
if o.YAxis.Min != nil {
|
||||||
|
r.Min = min
|
||||||
|
}
|
||||||
|
if o.YAxis.Max != nil {
|
||||||
|
r.Max = max
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Range) Values() []string {
|
return r
|
||||||
offset := (r.Max - r.Min) / float64(r.divideCount)
|
|
||||||
values := make([]string, 0)
|
|
||||||
for i := 0; i <= r.divideCount; i++ {
|
|
||||||
v := r.Min + float64(i)*offset
|
|
||||||
value := humanize.CommafWithDigits(v, 2)
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type basicRenderResult struct {
|
type basicRenderResult struct {
|
||||||
|
|
@ -188,7 +182,7 @@ type basicRenderResult struct {
|
||||||
titleBox chart.Box
|
titleBox chart.Box
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChartRender(opt ChartOption) (*Draw, error) {
|
func Render(opt ChartOption) (*Draw, error) {
|
||||||
if len(opt.SeriesList) == 0 {
|
if len(opt.SeriesList) == 0 {
|
||||||
return nil, errors.New("series can not be nil")
|
return nil, errors.New("series can not be nil")
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +201,7 @@ func ChartRender(opt ChartOption) (*Draw, error) {
|
||||||
lineSeries = append(lineSeries, item)
|
lineSeries = append(lineSeries, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果指定了pie,则以pie的形式处理,不支持多类型图表
|
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
|
||||||
// pie不需要axis
|
// pie不需要axis
|
||||||
if isPieChart {
|
if isPieChart {
|
||||||
opt.XAxis.Hidden = true
|
opt.XAxis.Hidden = true
|
||||||
|
|
@ -217,21 +211,56 @@ func ChartRender(opt ChartOption) (*Draw, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if isPieChart {
|
fns := []func() error{
|
||||||
return pieChartRender(opt, result)
|
// pie render
|
||||||
|
func() error {
|
||||||
|
if !isPieChart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := pieChartRender(opt, result)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
// bar render
|
||||||
|
func() error {
|
||||||
|
// 如果是pie或者无bar类型的series
|
||||||
|
if isPieChart || len(barSeries) == 0 {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if len(barSeries) != 0 {
|
|
||||||
o := opt
|
o := opt
|
||||||
o.SeriesList = barSeries
|
o.SeriesList = barSeries
|
||||||
_, err = barChartRender(o, result)
|
_, err := barChartRender(o, result)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
// line render
|
||||||
|
func() error {
|
||||||
|
// 如果是pie或者无line类型的series
|
||||||
|
if isPieChart || len(lineSeries) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
o := opt
|
||||||
|
o.SeriesList = lineSeries
|
||||||
|
_, err := lineChartRender(o, result)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
// legend需要在顶层,因此最后render
|
||||||
|
func() error {
|
||||||
|
_, err := NewLegend(result.d, opt.Legend).Render()
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range fns {
|
||||||
|
err = fn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(lineSeries) != 0 {
|
for _, child := range opt.Children {
|
||||||
o := opt
|
child.Parent = result.d
|
||||||
o.SeriesList = lineSeries
|
if len(child.Theme) == 0 {
|
||||||
_, err = lineChartRender(o, result)
|
child.Theme = opt.Theme
|
||||||
|
}
|
||||||
|
_, err = Render(child)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -263,11 +292,6 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = NewLegend(d, opt.Legend).Render()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
xAxisHeight := 0
|
xAxisHeight := 0
|
||||||
var xRange *Range
|
var xRange *Range
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -253,7 +252,6 @@ func TestDraw(t *testing.T) {
|
||||||
tt.fn(d)
|
tt.fn(d)
|
||||||
data, err := d.Bytes()
|
data, err := d.Bytes()
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
fmt.Println(string(data))
|
|
||||||
assert.Equal(tt.result, string(data))
|
assert.Equal(tt.result, string(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,16 +40,36 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
seriesNames := opt.Legend.Data
|
||||||
|
|
||||||
|
r := d.Render
|
||||||
yRange := result.yRange
|
yRange := result.yRange
|
||||||
xRange := result.xRange
|
xRange := result.xRange
|
||||||
for i, series := range opt.SeriesList {
|
for i, series := range opt.SeriesList {
|
||||||
points := make([]Point, 0)
|
points := make([]Point, 0)
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
y := yRange.getRestHeight(item.Value)
|
y := yRange.getRestHeight(item.Value)
|
||||||
|
x := xRange.getWidth(float64(j))
|
||||||
points = append(points, Point{
|
points = append(points, Point{
|
||||||
Y: y,
|
Y: y,
|
||||||
X: xRange.getWidth(float64(j)),
|
X: x,
|
||||||
})
|
})
|
||||||
|
if !series.Label.Show {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
||||||
|
labelStyle := chart.Style{
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
FontSize: 10,
|
||||||
|
Font: opt.Font,
|
||||||
|
}
|
||||||
|
if !series.Label.Color.IsZero() {
|
||||||
|
labelStyle.FontColor = series.Label.Color
|
||||||
|
}
|
||||||
|
labelStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
textBox := r.MeasureText(text)
|
||||||
|
d.text(text, x-textBox.Width()>>1, y-5)
|
||||||
|
}
|
||||||
index := series.index
|
index := series.index
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
index = i
|
index = i
|
||||||
|
|
@ -67,7 +87,6 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
DotFillColor: dotFillColor,
|
DotFillColor: dotFillColor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result.d, nil
|
return result.d, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
pie_chart.go
35
pie_chart.go
|
|
@ -24,8 +24,8 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -53,7 +53,11 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
|
|
||||||
values := make([]float64, len(opt.SeriesList))
|
values := make([]float64, len(opt.SeriesList))
|
||||||
total := float64(0)
|
total := float64(0)
|
||||||
|
radiusValue := ""
|
||||||
for index, series := range opt.SeriesList {
|
for index, series := range opt.SeriesList {
|
||||||
|
if len(series.Radius) != 0 {
|
||||||
|
radiusValue = series.Radius
|
||||||
|
}
|
||||||
value := float64(0)
|
value := float64(0)
|
||||||
for _, item := range series.Data {
|
for _, item := range series.Data {
|
||||||
value += item.Value
|
value += item.Value
|
||||||
|
|
@ -69,9 +73,23 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
cy := box.Height() >> 1
|
cy := box.Height() >> 1
|
||||||
|
|
||||||
diameter := chart.MinInt(box.Width(), box.Height())
|
diameter := chart.MinInt(box.Width(), box.Height())
|
||||||
radius := float64(diameter) * defaultRadiusPercent
|
|
||||||
|
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
|
||||||
|
}
|
||||||
labelRadius := radius + 20
|
labelRadius := radius + 20
|
||||||
|
|
||||||
|
seriesNames := opt.Legend.Data
|
||||||
|
|
||||||
if len(values) == 1 {
|
if len(values) == 1 {
|
||||||
getPieStyle(theme, 0).WriteToRenderer(r)
|
getPieStyle(theme, 0).WriteToRenderer(r)
|
||||||
d.moveTo(cx, cy)
|
d.moveTo(cx, cy)
|
||||||
|
|
@ -79,6 +97,7 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
} else {
|
} else {
|
||||||
currentValue := float64(0)
|
currentValue := float64(0)
|
||||||
for index, v := range values {
|
for index, v := range values {
|
||||||
|
|
||||||
pieStyle := getPieStyle(theme, index)
|
pieStyle := getPieStyle(theme, index)
|
||||||
pieStyle.WriteToRenderer(r)
|
pieStyle.WriteToRenderer(r)
|
||||||
d.moveTo(cx, cy)
|
d.moveTo(cx, cy)
|
||||||
|
|
@ -91,6 +110,13 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
r.Close()
|
r.Close()
|
||||||
r.FillStroke()
|
r.FillStroke()
|
||||||
|
|
||||||
|
series := opt.SeriesList[index]
|
||||||
|
// 是否显示label
|
||||||
|
showLabel := series.Label.Show
|
||||||
|
if !showLabel {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// label的角度为饼块中间
|
// label的角度为饼块中间
|
||||||
angle := start + delta/2
|
angle := start + delta/2
|
||||||
startx := cx + int(radius*math.Cos(angle))
|
startx := cx + int(radius*math.Cos(angle))
|
||||||
|
|
@ -113,8 +139,11 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
FontSize: 10,
|
FontSize: 10,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}
|
}
|
||||||
|
if !series.Label.Color.IsZero() {
|
||||||
|
textStyle.FontColor = series.Label.Color
|
||||||
|
}
|
||||||
textStyle.GetTextOptions().WriteToRenderer(r)
|
textStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
text := humanize.FtoaWithDigits(percent*100, 2) + "%"
|
text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent)
|
||||||
textBox := r.MeasureText(text)
|
textBox := r.MeasureText(text)
|
||||||
textMargin := 3
|
textMargin := 3
|
||||||
x := endx + textMargin
|
x := endx + textMargin
|
||||||
|
|
|
||||||
13
range.go
13
range.go
|
|
@ -24,6 +24,8 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Range struct {
|
type Range struct {
|
||||||
|
|
@ -62,6 +64,17 @@ func NewRange(min, max float64, divideCount int) Range {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r Range) Values() []string {
|
||||||
|
offset := (r.Max - r.Min) / float64(r.divideCount)
|
||||||
|
values := make([]string, 0)
|
||||||
|
for i := 0; i <= r.divideCount; i++ {
|
||||||
|
v := r.Min + float64(i)*offset
|
||||||
|
value := humanize.CommafWithDigits(v, 2)
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Range) getHeight(value float64) int {
|
func (r *Range) getHeight(value float64) int {
|
||||||
v := (value - r.Min) / (r.Max - r.Min)
|
v := (value - r.Min) / (r.Max - r.Min)
|
||||||
return int(v * float64(r.Size))
|
return int(v * float64(r.Size))
|
||||||
|
|
|
||||||
108
series.go
Normal file
108
series.go
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// 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 (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SeriesData struct {
|
||||||
|
Value float64
|
||||||
|
Style chart.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSeriesFromValues(values []float64, chartType ...string) Series {
|
||||||
|
s := Series{
|
||||||
|
Data: NewSeriesDataFromValues(values),
|
||||||
|
}
|
||||||
|
if len(chartType) != 0 {
|
||||||
|
s.Type = chartType[0]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSeriesDataFromValues(values []float64) []SeriesData {
|
||||||
|
data := make([]SeriesData, len(values))
|
||||||
|
for index, value := range values {
|
||||||
|
data[index] = SeriesData{
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeriesLabel struct {
|
||||||
|
Formatter string
|
||||||
|
Color drawing.Color
|
||||||
|
Show bool
|
||||||
|
}
|
||||||
|
type Series struct {
|
||||||
|
index int
|
||||||
|
Type string
|
||||||
|
Data []SeriesData
|
||||||
|
YAxisIndex int
|
||||||
|
Style chart.Style
|
||||||
|
Label SeriesLabel
|
||||||
|
Name string
|
||||||
|
// Radius of Pie chart, e.g.: 40%
|
||||||
|
Radius string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabelFormatter func(index int, value float64, percent float64) string
|
||||||
|
|
||||||
|
func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
||||||
|
if len(layout) == 0 {
|
||||||
|
layout = "{b}: {d}"
|
||||||
|
}
|
||||||
|
return NewLabelFormatter(seriesNames, layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter {
|
||||||
|
if len(layout) == 0 {
|
||||||
|
layout = "{c}"
|
||||||
|
}
|
||||||
|
return NewLabelFormatter(seriesNames, layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
||||||
|
return func(index int, value, percent float64) string {
|
||||||
|
// 如果无percent的则设置为<0
|
||||||
|
percentText := ""
|
||||||
|
if percent >= 0 {
|
||||||
|
percentText = humanize.FtoaWithDigits(percent*100, 2) + "%"
|
||||||
|
}
|
||||||
|
valueText := humanize.FtoaWithDigits(value, 2)
|
||||||
|
name := ""
|
||||||
|
if len(seriesNames) > index {
|
||||||
|
name = seriesNames[index]
|
||||||
|
}
|
||||||
|
text := strings.ReplaceAll(layout, "{c}", valueText)
|
||||||
|
text = strings.ReplaceAll(text, "{d}", percentText)
|
||||||
|
text = strings.ReplaceAll(text, "{b}", name)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -138,7 +137,6 @@ func TestDrawTitle(t *testing.T) {
|
||||||
data, err := d.Bytes()
|
data, err := d.Bytes()
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.NotEmpty(data)
|
assert.NotEmpty(data)
|
||||||
fmt.Println(string(data))
|
|
||||||
assert.Equal(tt.result, string(data))
|
assert.Equal(tt.result, string(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
util.go
5
util.go
|
|
@ -104,3 +104,8 @@ func isFalse(flag *bool) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toFloatPoint(f float64) *float64 {
|
||||||
|
v := f
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
|
||||||
3
yaxis.go
3
yaxis.go
|
|
@ -27,8 +27,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type YAxisOption struct {
|
type YAxisOption struct {
|
||||||
|
// The minimun value of axis.
|
||||||
Min *float64
|
Min *float64
|
||||||
|
// The maximum value of axis.
|
||||||
Max *float64
|
Max *float64
|
||||||
|
// Hidden y axis
|
||||||
Hidden bool
|
Hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
87
yaxis_test.go
Normal file
87
yaxis_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDrawYAxis(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
newDraw := func() *Draw {
|
||||||
|
d, _ := NewDraw(DrawOption{
|
||||||
|
Width: 400,
|
||||||
|
Height: 300,
|
||||||
|
})
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
newDraw func() *Draw
|
||||||
|
newOption func() *ChartOption
|
||||||
|
xAxisHeight int
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
newDraw: newDraw,
|
||||||
|
newOption: func() *ChartOption {
|
||||||
|
return &ChartOption{
|
||||||
|
YAxis: YAxisOption{
|
||||||
|
Max: toFloatPoint(20),
|
||||||
|
},
|
||||||
|
SeriesList: []Series{
|
||||||
|
{
|
||||||
|
Data: []SeriesData{
|
||||||
|
{
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 50 10\nL 50 290\" style=\"stroke-width:1;stroke:none;fill:none\"/><path d=\"M 50 10\nL 390 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 57\nL 390 57\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 104\nL 390 104\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 151\nL 390 151\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 198\nL 390 198\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 244\nL 390 244\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"36\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0</text><text x=\"18\" y=\"248\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3.33</text><text x=\"18\" y=\"202\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6.66</text><text x=\"29\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10</text><text x=\"11\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">13.33</text><text x=\"11\" y=\"61\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">16.66</text><text x=\"29\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text></svg>",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
d := tt.newDraw()
|
||||||
|
r, err := drawYAxis(d, tt.newOption(), tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(&Range{
|
||||||
|
divideCount: 6,
|
||||||
|
Max: 20,
|
||||||
|
Size: 280,
|
||||||
|
}, r)
|
||||||
|
|
||||||
|
data, err := d.Bytes()
|
||||||
|
assert.Nil(err)
|
||||||
|
assert.Equal(tt.result, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue