feat: support mulit y axis
This commit is contained in:
parent
c0bb1654c2
commit
fd05250305
16 changed files with 393 additions and 96 deletions
5
axis.go
5
axis.go
|
|
@ -78,14 +78,17 @@ func NewAxis(d *Draw, data AxisDataList, style AxisOption) *axis {
|
|||
|
||||
}
|
||||
|
||||
// GetLabelMargin returns the label margin value
|
||||
func (as *AxisOption) GetLabelMargin() int {
|
||||
return getDefaultInt(as.LabelMargin, 8)
|
||||
}
|
||||
|
||||
// GetTickLength returns the tick length value
|
||||
func (as *AxisOption) GetTickLength() int {
|
||||
return getDefaultInt(as.TickLength, 5)
|
||||
}
|
||||
|
||||
// Style returns the style of axis
|
||||
func (as *AxisOption) Style(f *truetype.Font) chart.Style {
|
||||
s := chart.Style{
|
||||
ClassName: as.ClassName,
|
||||
|
|
@ -109,6 +112,7 @@ type AxisData struct {
|
|||
}
|
||||
type AxisDataList []AxisData
|
||||
|
||||
// TextList returns the text list of axis data
|
||||
func (l AxisDataList) TextList() []string {
|
||||
textList := make([]string, len(l))
|
||||
for index, item := range l {
|
||||
|
|
@ -125,6 +129,7 @@ type axisRenderOption struct {
|
|||
modValue int
|
||||
}
|
||||
|
||||
// NewAxisDataListFromStringList creates a new axis data list from string list
|
||||
func NewAxisDataListFromStringList(textList []string) AxisDataList {
|
||||
list := make(AxisDataList, len(textList))
|
||||
for index, text := range textList {
|
||||
|
|
|
|||
27
bar_chart.go
27
bar_chart.go
|
|
@ -37,7 +37,7 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
yRange := result.yRange
|
||||
// yRange := result.yRange
|
||||
xRange := result.xRange
|
||||
x0, x1 := xRange.GetRange(0)
|
||||
width := int(x1 - x0)
|
||||
|
|
@ -50,7 +50,7 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
|||
// 总的宽度-两个margin-(总数-1)的barMargin
|
||||
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList)
|
||||
|
||||
barMaxHeight := yRange.Size
|
||||
barMaxHeight := result.getYRange(0).Size
|
||||
theme := NewTheme(opt.Theme)
|
||||
|
||||
seriesNames := opt.Legend.Data
|
||||
|
|
@ -58,6 +58,9 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
|||
r := d.Render
|
||||
|
||||
for i, series := range opt.SeriesList {
|
||||
yRange := result.getYRange(series.YAxisIndex)
|
||||
points := make([]Point, len(series.Data))
|
||||
seriesColor := theme.GetSeriesColor(i)
|
||||
for j, item := range series.Data {
|
||||
x0, _ := xRange.GetRange(j)
|
||||
x := int(x0)
|
||||
|
|
@ -67,26 +70,32 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
|||
}
|
||||
|
||||
h := int(yRange.getHeight(item.Value))
|
||||
fillColor := theme.GetSeriesColor(i)
|
||||
fillColor := seriesColor
|
||||
if !item.Style.FillColor.IsZero() {
|
||||
fillColor = item.Style.FillColor
|
||||
}
|
||||
|
||||
top := barMaxHeight - h
|
||||
d.Bar(chart.Box{
|
||||
Top: barMaxHeight - h,
|
||||
Top: top,
|
||||
Left: x,
|
||||
Right: x + barWidth,
|
||||
Bottom: barMaxHeight - 1,
|
||||
}, BarStyle{
|
||||
FillColor: fillColor,
|
||||
})
|
||||
// 用于生成marker point
|
||||
points[j] = Point{
|
||||
// 居中的位置
|
||||
X: x + barWidth>>1,
|
||||
Y: top,
|
||||
}
|
||||
if !series.Label.Show {
|
||||
continue
|
||||
}
|
||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
||||
labelStyle := chart.Style{
|
||||
FontColor: theme.GetTextColor(),
|
||||
FontSize: 10,
|
||||
FontSize: labelFontSize,
|
||||
Font: opt.Font,
|
||||
}
|
||||
if !series.Label.Color.IsZero() {
|
||||
|
|
@ -96,6 +105,12 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
|||
textBox := r.MeasureText(text)
|
||||
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5)
|
||||
}
|
||||
markPointRender(d, markPointRenderOption{
|
||||
FillColor: seriesColor,
|
||||
Font: opt.Font,
|
||||
Points: points,
|
||||
Series: &series,
|
||||
})
|
||||
}
|
||||
|
||||
return result.d, nil
|
||||
|
|
|
|||
63
chart.go
63
chart.go
|
|
@ -42,6 +42,8 @@ type Point struct {
|
|||
Y int
|
||||
}
|
||||
|
||||
const labelFontSize = 10
|
||||
|
||||
type ChartOption struct {
|
||||
Type string
|
||||
Font *truetype.Font
|
||||
|
|
@ -49,7 +51,7 @@ type ChartOption struct {
|
|||
Title TitleOption
|
||||
Legend LegendOption
|
||||
XAxis XAxisOption
|
||||
YAxis YAxisOption
|
||||
YAxisList []YAxisOption
|
||||
Width int
|
||||
Height int
|
||||
Parent *Draw
|
||||
|
|
@ -61,6 +63,17 @@ type ChartOption struct {
|
|||
|
||||
func (o *ChartOption) FillDefault(theme string) {
|
||||
t := NewTheme(theme)
|
||||
// 如果为空,初始化
|
||||
yAxisCount := 1
|
||||
for _, series := range o.SeriesList {
|
||||
if series.YAxisIndex >= yAxisCount {
|
||||
yAxisCount++
|
||||
}
|
||||
}
|
||||
yAxisList := make([]YAxisOption, yAxisCount)
|
||||
copy(yAxisList, o.YAxisList)
|
||||
o.YAxisList = yAxisList
|
||||
|
||||
if o.Font == nil {
|
||||
o.Font, _ = chart.GetDefaultFont()
|
||||
}
|
||||
|
|
@ -136,9 +149,13 @@ func (o *ChartOption) getHeight() int {
|
|||
return o.Height
|
||||
}
|
||||
|
||||
func (o *ChartOption) getYRange(axisIndex int) Range {
|
||||
func (o *ChartOption) newYRange(axisIndex int) Range {
|
||||
min := math.MaxFloat64
|
||||
max := -math.MaxFloat64
|
||||
if axisIndex >= len(o.YAxisList) {
|
||||
axisIndex = 0
|
||||
}
|
||||
yAxis := o.YAxisList[axisIndex]
|
||||
|
||||
for _, series := range o.SeriesList {
|
||||
if series.YAxisIndex != axisIndex {
|
||||
|
|
@ -155,21 +172,21 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
|
|||
}
|
||||
min = min * 0.9
|
||||
max = max * 1.1
|
||||
if o.YAxis.Min != nil {
|
||||
min = *o.YAxis.Min
|
||||
if yAxis.Min != nil {
|
||||
min = *yAxis.Min
|
||||
}
|
||||
if o.YAxis.Max != nil {
|
||||
max = *o.YAxis.Max
|
||||
if yAxis.Max != nil {
|
||||
max = *yAxis.Max
|
||||
}
|
||||
divideCount := 6
|
||||
// y轴分设置默认划分为6块
|
||||
r := NewRange(min, max, divideCount)
|
||||
|
||||
// 由于NewRange会重新计算min max
|
||||
if o.YAxis.Min != nil {
|
||||
if yAxis.Min != nil {
|
||||
r.Min = min
|
||||
}
|
||||
if o.YAxis.Max != nil {
|
||||
if yAxis.Max != nil {
|
||||
r.Max = max
|
||||
}
|
||||
|
||||
|
|
@ -178,11 +195,18 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
|
|||
|
||||
type basicRenderResult struct {
|
||||
xRange *Range
|
||||
yRange *Range
|
||||
yRangeList []*Range
|
||||
d *Draw
|
||||
titleBox chart.Box
|
||||
}
|
||||
|
||||
func (r *basicRenderResult) getYRange(index int) *Range {
|
||||
if index >= len(r.yRangeList) {
|
||||
index = 0
|
||||
}
|
||||
return r.yRangeList[index]
|
||||
}
|
||||
|
||||
func Render(opt ChartOption) (*Draw, error) {
|
||||
if len(opt.SeriesList) == 0 {
|
||||
return nil, errors.New("series can not be nil")
|
||||
|
|
@ -206,7 +230,9 @@ func Render(opt ChartOption) (*Draw, error) {
|
|||
// pie不需要axis
|
||||
if isPieChart {
|
||||
opt.XAxis.Hidden = true
|
||||
opt.YAxis.Hidden = true
|
||||
for index := range opt.YAxisList {
|
||||
opt.YAxisList[index].Hidden = true
|
||||
}
|
||||
}
|
||||
result, err := chartBasicRender(&opt)
|
||||
if err != nil {
|
||||
|
|
@ -284,6 +310,9 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
|||
}
|
||||
|
||||
opt.FillDefault(opt.Theme)
|
||||
if len(opt.YAxisList) > 2 {
|
||||
return nil, errors.New("y axis should not be gt 2")
|
||||
}
|
||||
if opt.Parent == nil {
|
||||
d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor)
|
||||
}
|
||||
|
|
@ -299,25 +328,29 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
|||
|
||||
if !opt.XAxis.Hidden {
|
||||
// xAxis
|
||||
xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis)
|
||||
xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis, len(opt.YAxisList))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 暂时仅支持单一yaxis
|
||||
yRangeList := make([]*Range, len(opt.YAxisList))
|
||||
|
||||
for index, yAxis := range opt.YAxisList {
|
||||
var yRange *Range
|
||||
if !opt.YAxis.Hidden {
|
||||
yRange, err = drawYAxis(d, opt, xAxisHeight, chart.Box{
|
||||
if !yAxis.Hidden {
|
||||
yRange, err = drawYAxis(d, opt, index, xAxisHeight, chart.Box{
|
||||
Top: titleBox.Height(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
yRangeList[index] = yRange
|
||||
}
|
||||
}
|
||||
return &basicRenderResult{
|
||||
xRange: xRange,
|
||||
yRange: yRange,
|
||||
yRangeList: yRangeList,
|
||||
d: d,
|
||||
titleBox: titleBox,
|
||||
}, nil
|
||||
|
|
|
|||
55
draw.go
55
draw.go
|
|
@ -173,7 +173,60 @@ func (d *Draw) pin(x, y, width int) {
|
|||
cx := x
|
||||
cy := y + int(r*2.5)
|
||||
d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
|
||||
d.Render.Fill()
|
||||
d.Render.Stroke()
|
||||
}
|
||||
|
||||
func (d *Draw) arrowLeft(x, y, width, height int) {
|
||||
d.arrow(x, y, width, height, PositionLeft)
|
||||
}
|
||||
|
||||
func (d *Draw) arrowRight(x, y, width, height int) {
|
||||
d.arrow(x, y, width, height, PositionRight)
|
||||
}
|
||||
|
||||
func (d *Draw) arrowTop(x, y, width, height int) {
|
||||
d.arrow(x, y, width, height, PositionTop)
|
||||
}
|
||||
func (d *Draw) arrowBottom(x, y, width, height int) {
|
||||
d.arrow(x, y, width, height, PositionBottom)
|
||||
}
|
||||
|
||||
func (d *Draw) arrow(x, y, width, height int, direction string) {
|
||||
halfWidth := width >> 1
|
||||
halfHeight := height >> 1
|
||||
if direction == PositionTop || direction == PositionBottom {
|
||||
x0 := x - halfWidth
|
||||
x1 := x0 + width
|
||||
dy := -height / 3
|
||||
y0 := y
|
||||
y1 := y0 - height
|
||||
if direction == PositionBottom {
|
||||
y0 = y - height
|
||||
y1 = y
|
||||
dy = 2 * dy
|
||||
}
|
||||
d.moveTo(x0, y0)
|
||||
d.lineTo(x0+halfWidth, y1)
|
||||
d.lineTo(x1, y0)
|
||||
d.lineTo(x0+halfWidth, y+dy)
|
||||
d.lineTo(x0, y0)
|
||||
} else {
|
||||
x0 := x + width
|
||||
x1 := x0 - width
|
||||
y0 := y - halfHeight
|
||||
dx := -width / 3
|
||||
if direction == PositionRight {
|
||||
x0 = x - width
|
||||
dx = -dx
|
||||
x1 = x0 + width
|
||||
}
|
||||
d.moveTo(x0, y0)
|
||||
d.lineTo(x1, y0+halfHeight)
|
||||
d.lineTo(x0, y0+height)
|
||||
d.lineTo(x0+dx, y0+halfHeight)
|
||||
d.lineTo(x0, y0)
|
||||
}
|
||||
d.Render.Stroke()
|
||||
}
|
||||
|
||||
func (d *Draw) circle(radius float64, x, y int) {
|
||||
|
|
|
|||
89
draw_test.go
89
draw_test.go
|
|
@ -239,6 +239,7 @@ func TestDraw(t *testing.T) {
|
|||
},
|
||||
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 205 110\nA 100 100 90.00 0 1 105 210\nZ\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,255,1.0)\"/></svg>",
|
||||
},
|
||||
// pin
|
||||
{
|
||||
fn: func(d *Draw) {
|
||||
chart.Style{
|
||||
|
|
@ -260,6 +261,94 @@ func TestDraw(t *testing.T) {
|
|||
},
|
||||
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 32 47\nA 15 15 330.00 1 1 38 47\nL 35 33\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 20 33\nQ35,70 50,33\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||
},
|
||||
// arrow left
|
||||
{
|
||||
fn: func(d *Draw) {
|
||||
chart.Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
}.WriteToRenderer(d.Render)
|
||||
d.arrowLeft(30, 30, 16, 10)
|
||||
},
|
||||
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 51 35\nL 35 40\nL 51 45\nL 46 40\nL 51 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||
},
|
||||
// arrow right
|
||||
{
|
||||
fn: func(d *Draw) {
|
||||
chart.Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
}.WriteToRenderer(d.Render)
|
||||
d.arrowRight(30, 30, 16, 10)
|
||||
},
|
||||
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 19 35\nL 35 40\nL 19 45\nL 24 40\nL 19 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||
},
|
||||
// arrow top
|
||||
{
|
||||
fn: func(d *Draw) {
|
||||
chart.Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
}.WriteToRenderer(d.Render)
|
||||
d.arrowTop(30, 30, 10, 16)
|
||||
},
|
||||
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 30 40\nL 35 24\nL 40 40\nL 35 35\nL 30 40\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||
},
|
||||
// arrow bottom
|
||||
{
|
||||
fn: func(d *Draw) {
|
||||
chart.Style{
|
||||
StrokeWidth: 1,
|
||||
StrokeColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
FillColor: drawing.Color{
|
||||
R: 84,
|
||||
G: 112,
|
||||
B: 198,
|
||||
A: 255,
|
||||
},
|
||||
}.WriteToRenderer(d.Render)
|
||||
d.arrowBottom(30, 30, 10, 16)
|
||||
},
|
||||
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 30 24\nL 35 40\nL 40 24\nL 35 30\nL 30 24\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
d, err := NewDraw(DrawOption{
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@
|
|||
package charts
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
|
@ -45,36 +43,25 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
|||
seriesNames := opt.Legend.Data
|
||||
|
||||
r := d.Render
|
||||
yRange := result.yRange
|
||||
xRange := result.xRange
|
||||
for i, series := range opt.SeriesList {
|
||||
points := make([]Point, 0)
|
||||
minIndex := -1
|
||||
maxIndex := -1
|
||||
minValue := math.MaxFloat64
|
||||
maxValue := -math.MaxFloat64
|
||||
yRange := result.getYRange(series.YAxisIndex)
|
||||
points := make([]Point, len(series.Data))
|
||||
|
||||
for j, item := range series.Data {
|
||||
if item.Value < minValue {
|
||||
minIndex = j
|
||||
minValue = item.Value
|
||||
}
|
||||
if item.Value > maxValue {
|
||||
maxIndex = j
|
||||
maxValue = item.Value
|
||||
}
|
||||
y := yRange.getRestHeight(item.Value)
|
||||
x := xRange.getWidth(float64(j))
|
||||
points = append(points, Point{
|
||||
points[j] = Point{
|
||||
Y: y,
|
||||
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,
|
||||
FontSize: labelFontSize,
|
||||
Font: opt.Font,
|
||||
}
|
||||
if !series.Label.Color.IsZero() {
|
||||
|
|
@ -101,33 +88,12 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
|||
DotFillColor: dotFillColor,
|
||||
})
|
||||
// draw mark point
|
||||
symbolSize := 30
|
||||
if series.MarkPoint.SymbolSize > 0 {
|
||||
symbolSize = series.MarkPoint.SymbolSize
|
||||
}
|
||||
for _, markPointData := range series.MarkPoint.Data {
|
||||
p := points[minIndex]
|
||||
value := minValue
|
||||
switch markPointData.Type {
|
||||
case SeriesMarkPointDataTypeMax:
|
||||
p = points[maxIndex]
|
||||
value = maxValue
|
||||
}
|
||||
chart.Style{
|
||||
markPointRender(d, markPointRenderOption{
|
||||
FillColor: seriesColor,
|
||||
}.WriteToRenderer(r)
|
||||
d.pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
||||
|
||||
chart.Style{
|
||||
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
||||
FontSize: 10,
|
||||
StrokeWidth: 1,
|
||||
Font: opt.Font,
|
||||
}.WriteTextOptionsToRenderer(d.Render)
|
||||
text := commafWithDigits(value)
|
||||
textBox := r.MeasureText(text)
|
||||
d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
||||
}
|
||||
Points: points,
|
||||
Series: &series,
|
||||
})
|
||||
}
|
||||
|
||||
return result.d, nil
|
||||
|
|
|
|||
75
mark_point.go
Normal file
75
mark_point.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// 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 (
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
type markPointRenderOption struct {
|
||||
FillColor drawing.Color
|
||||
Font *truetype.Font
|
||||
Series *Series
|
||||
Points []Point
|
||||
}
|
||||
|
||||
func markPointRender(d *Draw, opt markPointRenderOption) {
|
||||
s := opt.Series
|
||||
if len(s.MarkPoint.Data) == 0 {
|
||||
return
|
||||
}
|
||||
points := opt.Points
|
||||
summary := s.Summary()
|
||||
symbolSize := s.MarkPoint.SymbolSize
|
||||
if symbolSize == 0 {
|
||||
symbolSize = 30
|
||||
}
|
||||
r := d.Render
|
||||
// 设置填充样式
|
||||
chart.Style{
|
||||
FillColor: opt.FillColor,
|
||||
}.WriteToRenderer(r)
|
||||
// 设置文本样式
|
||||
chart.Style{
|
||||
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
||||
FontSize: 10,
|
||||
StrokeWidth: 1,
|
||||
Font: opt.Font,
|
||||
}.WriteTextOptionsToRenderer(r)
|
||||
for _, markPointData := range s.MarkPoint.Data {
|
||||
p := points[summary.MinIndex]
|
||||
value := summary.MinValue
|
||||
switch markPointData.Type {
|
||||
case SeriesMarkPointDataTypeMax:
|
||||
p = points[summary.MaxIndex]
|
||||
value = summary.MaxValue
|
||||
}
|
||||
|
||||
d.pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
||||
text := commafWithDigits(value)
|
||||
textBox := r.MeasureText(text)
|
||||
d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
|||
r.Stroke()
|
||||
textStyle := chart.Style{
|
||||
FontColor: theme.GetTextColor(),
|
||||
FontSize: 10,
|
||||
FontSize: labelFontSize,
|
||||
Font: opt.Font,
|
||||
}
|
||||
if !series.Label.Color.IsZero() {
|
||||
|
|
|
|||
6
range.go
6
range.go
|
|
@ -40,9 +40,15 @@ func NewRange(min, max float64, divideCount int) Range {
|
|||
// 最小单位计算
|
||||
unit := 2
|
||||
if r > 10 {
|
||||
unit = 4
|
||||
}
|
||||
if r > 30 {
|
||||
unit = 5
|
||||
}
|
||||
if r > 100 {
|
||||
unit = 10
|
||||
}
|
||||
if r > 200 {
|
||||
unit = 20
|
||||
}
|
||||
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
|
||||
|
|
|
|||
|
|
@ -37,12 +37,16 @@ func TestRange(t *testing.T) {
|
|||
|
||||
r = NewRange(0, 12, 6)
|
||||
assert.Equal(0.0, r.Min)
|
||||
assert.Equal(30.0, r.Max)
|
||||
assert.Equal(24.0, r.Max)
|
||||
|
||||
r = NewRange(-13, 18, 6)
|
||||
assert.Equal(-20.0, r.Min)
|
||||
assert.Equal(40.0, r.Max)
|
||||
|
||||
r = NewRange(0, 150, 6)
|
||||
assert.Equal(0.0, r.Min)
|
||||
assert.Equal(180.0, r.Max)
|
||||
|
||||
r = NewRange(0, 400, 6)
|
||||
assert.Equal(0.0, r.Min)
|
||||
assert.Equal(480.0, r.Max)
|
||||
|
|
|
|||
35
series.go
35
series.go
|
|
@ -23,6 +23,7 @@
|
|||
package charts
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
|
@ -86,6 +87,40 @@ type Series struct {
|
|||
MarkPoint SeriesMarkPoint
|
||||
}
|
||||
|
||||
type seriesSummary struct {
|
||||
MaxIndex int
|
||||
MaxValue float64
|
||||
MinIndex int
|
||||
MinValue float64
|
||||
AverageValue float64
|
||||
}
|
||||
|
||||
func (s *Series) Summary() seriesSummary {
|
||||
minIndex := -1
|
||||
maxIndex := -1
|
||||
minValue := math.MaxFloat64
|
||||
maxValue := -math.MaxFloat64
|
||||
sum := float64(0)
|
||||
for j, item := range s.Data {
|
||||
if item.Value < minValue {
|
||||
minIndex = j
|
||||
minValue = item.Value
|
||||
}
|
||||
if item.Value > maxValue {
|
||||
maxIndex = j
|
||||
maxValue = item.Value
|
||||
}
|
||||
sum += item.Value
|
||||
}
|
||||
return seriesSummary{
|
||||
MaxIndex: maxIndex,
|
||||
MaxValue: maxValue,
|
||||
MinIndex: minIndex,
|
||||
MinValue: minValue,
|
||||
AverageValue: sum / float64(len(s.Data)),
|
||||
}
|
||||
}
|
||||
|
||||
type LabelFormatter func(index int, value float64, percent float64) string
|
||||
|
||||
func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
||||
|
|
|
|||
6
util.go
6
util.go
|
|
@ -106,7 +106,7 @@ func isFalse(flag *bool) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func toFloatPoint(f float64) *float64 {
|
||||
func NewFloatPoint(f float64) *float64 {
|
||||
v := f
|
||||
return &v
|
||||
}
|
||||
|
|
@ -114,11 +114,11 @@ func commafWithDigits(value float64) string {
|
|||
decimals := 2
|
||||
m := float64(1000 * 1000)
|
||||
if value >= m {
|
||||
return humanize.CommafWithDigits(value/m, decimals) + " M"
|
||||
return humanize.CommafWithDigits(value/m, decimals) + "M"
|
||||
}
|
||||
k := float64(1000)
|
||||
if value >= k {
|
||||
return humanize.CommafWithDigits(value/k, decimals) + " K"
|
||||
return humanize.CommafWithDigits(value/k, decimals) + "k"
|
||||
}
|
||||
return humanize.CommafWithDigits(value, decimals)
|
||||
}
|
||||
|
|
|
|||
11
xaxis.go
11
xaxis.go
|
|
@ -22,7 +22,9 @@
|
|||
|
||||
package charts
|
||||
|
||||
import "github.com/wcharczuk/go-chart/v2"
|
||||
import (
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
type XAxisOption struct {
|
||||
// The boundary gap on both sides of a coordinate axis.
|
||||
|
|
@ -39,16 +41,19 @@ type XAxisOption struct {
|
|||
}
|
||||
|
||||
// drawXAxis draws x axis, and returns the height, range of if.
|
||||
func drawXAxis(p *Draw, opt *XAxisOption) (int, *Range, error) {
|
||||
func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) {
|
||||
if opt.Hidden {
|
||||
return 0, nil, nil
|
||||
}
|
||||
left := YAxisWidth
|
||||
right := (yAxisCount - 1) * YAxisWidth
|
||||
dXAxis, err := NewDraw(
|
||||
DrawOption{
|
||||
Parent: p,
|
||||
},
|
||||
PaddingOption(chart.Box{
|
||||
Left: YAxisWidth,
|
||||
Left: left,
|
||||
Right: right,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func TestDrawXAxis(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
d := tt.newDraw()
|
||||
height, _, err := drawXAxis(d, tt.newOption())
|
||||
height, _, err := drawXAxis(d, tt.newOption(), 1)
|
||||
assert.Nil(err)
|
||||
assert.Equal(25, height)
|
||||
data, err := d.Bytes()
|
||||
|
|
|
|||
17
yaxis.go
17
yaxis.go
|
|
@ -41,11 +41,11 @@ type YAxisOption struct {
|
|||
|
||||
const YAxisWidth = 40
|
||||
|
||||
func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*Range, error) {
|
||||
func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) {
|
||||
theme := NewTheme(opt.Theme)
|
||||
yRange := opt.getYRange(0)
|
||||
yRange := opt.newYRange(axisIndex)
|
||||
values := yRange.Values()
|
||||
formatter := opt.YAxis.Formatter
|
||||
formatter := opt.YAxisList[axisIndex].Formatter
|
||||
if len(formatter) != 0 {
|
||||
for index, text := range values {
|
||||
values[index] = strings.ReplaceAll(formatter, "{value}", text)
|
||||
|
|
@ -64,12 +64,21 @@ func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*
|
|||
}
|
||||
width := NewAxis(p, data, style).measureAxis()
|
||||
|
||||
yAxisCount := len(opt.YAxisList)
|
||||
boxWidth := p.Box.Width()
|
||||
if axisIndex > 0 {
|
||||
style.SplitLineShow = false
|
||||
style.Position = PositionRight
|
||||
padding.Right += (axisIndex - 1) * YAxisWidth
|
||||
} else {
|
||||
boxWidth = p.Box.Width() - (yAxisCount-1)*YAxisWidth
|
||||
padding.Left += (YAxisWidth - width)
|
||||
}
|
||||
|
||||
dYAxis, err := NewDraw(
|
||||
DrawOption{
|
||||
Parent: p,
|
||||
Width: p.Box.Width(),
|
||||
Width: boxWidth,
|
||||
// 减去x轴的高
|
||||
Height: p.Box.Height() - xAxisHeight,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ func TestDrawYAxis(t *testing.T) {
|
|||
newDraw: newDraw,
|
||||
newOption: func() *ChartOption {
|
||||
return &ChartOption{
|
||||
YAxis: YAxisOption{
|
||||
Max: toFloatPoint(20),
|
||||
YAxisList: []YAxisOption{
|
||||
{
|
||||
Max: NewFloatPoint(20),
|
||||
},
|
||||
},
|
||||
SeriesList: []Series{
|
||||
{
|
||||
|
|
@ -72,7 +74,7 @@ func TestDrawYAxis(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
d := tt.newDraw()
|
||||
r, err := drawYAxis(d, tt.newOption(), tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
||||
r, err := drawYAxis(d, tt.newOption(), 0, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
||||
assert.Nil(err)
|
||||
assert.Equal(&Range{
|
||||
divideCount: 6,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue