feat: support line chart render function
This commit is contained in:
parent
b394e1b49f
commit
c4045cfbbe
11 changed files with 1012 additions and 46 deletions
120
axis.go
120
axis.go
|
|
@ -39,6 +39,8 @@ func NewAxisPainter(p *Painter, opt AxisPainterOption) *axisPainter {
|
|||
}
|
||||
|
||||
type AxisPainterOption struct {
|
||||
// The theme of chart
|
||||
Theme ColorPalette
|
||||
// The label of axis
|
||||
Data []string
|
||||
// The boundary gap on both sides of a coordinate axis.
|
||||
|
|
@ -70,13 +72,31 @@ type AxisPainterOption struct {
|
|||
|
||||
func (a *axisPainter) Render() (Box, error) {
|
||||
opt := a.opt
|
||||
p := a.p
|
||||
top := a.p
|
||||
theme := opt.Theme
|
||||
|
||||
strokeWidth := opt.StrokeWidth
|
||||
if strokeWidth == 0 {
|
||||
strokeWidth = 1
|
||||
}
|
||||
|
||||
font := opt.Font
|
||||
if font == nil {
|
||||
font = theme.GetFont()
|
||||
}
|
||||
fontColor := opt.FontColor
|
||||
if fontColor.IsZero() {
|
||||
fontColor = theme.GetTextColor()
|
||||
}
|
||||
fontSize := opt.FontSize
|
||||
if fontSize == 0 {
|
||||
fontSize = theme.GetFontSize()
|
||||
}
|
||||
strokeColor := opt.StrokeColor
|
||||
if strokeColor.IsZero() {
|
||||
strokeColor = theme.GetAxisStrokeColor()
|
||||
}
|
||||
|
||||
tickCount := opt.SplitNumber
|
||||
if tickCount == 0 {
|
||||
tickCount = len(opt.Data)
|
||||
|
|
@ -86,12 +106,17 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
|
||||
boundaryGap = false
|
||||
}
|
||||
isVertical := opt.Position == PositionLeft ||
|
||||
opt.Position == PositionRight
|
||||
|
||||
labelPosition := ""
|
||||
if !boundaryGap {
|
||||
tickCount--
|
||||
labelPosition = PositionLeft
|
||||
}
|
||||
if isVertical && boundaryGap {
|
||||
labelPosition = PositionCenter
|
||||
}
|
||||
|
||||
// TODO 计算unit
|
||||
unit := 1
|
||||
|
|
@ -99,71 +124,88 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
tickLength := getDefaultInt(opt.TickLength, 5)
|
||||
labelMargin := getDefaultInt(opt.LabelMargin, 5)
|
||||
|
||||
textMaxWidth, textMaxHeight := p.MeasureTextMaxWidthHeight(opt.Data)
|
||||
style := Style{
|
||||
StrokeColor: strokeColor,
|
||||
StrokeWidth: strokeWidth,
|
||||
Font: font,
|
||||
FontColor: fontColor,
|
||||
FontSize: fontSize,
|
||||
}
|
||||
top.SetDrawingStyle(style).OverrideTextStyle(style)
|
||||
|
||||
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(opt.Data)
|
||||
|
||||
width := 0
|
||||
height := 0
|
||||
// 垂直
|
||||
if opt.Position == PositionLeft ||
|
||||
opt.Position == PositionRight {
|
||||
if isVertical {
|
||||
width = textMaxWidth + tickLength<<1
|
||||
height = p.Height()
|
||||
height = top.Height()
|
||||
} else {
|
||||
width = p.Width()
|
||||
width = top.Width()
|
||||
height = tickLength<<1 + textMaxHeight
|
||||
}
|
||||
padding := Box{}
|
||||
switch opt.Position {
|
||||
case PositionTop:
|
||||
padding.Top = p.Height() - height
|
||||
padding.Top = top.Height() - height
|
||||
case PositionLeft:
|
||||
padding.Right = p.Width() - width
|
||||
padding.Right = top.Width() - width
|
||||
case PositionRight:
|
||||
padding.Left = top.Width() - width
|
||||
}
|
||||
p = p.Child(PainterPaddingOption(padding))
|
||||
p.SetDrawingStyle(Style{
|
||||
StrokeColor: opt.StrokeColor,
|
||||
StrokeWidth: strokeWidth,
|
||||
}).OverrideTextStyle(Style{
|
||||
Font: opt.Font,
|
||||
FontColor: opt.FontColor,
|
||||
FontSize: opt.FontSize,
|
||||
})
|
||||
|
||||
p := top.Child(PainterPaddingOption(padding))
|
||||
|
||||
x0 := 0
|
||||
y0 := 0
|
||||
x1 := 0
|
||||
y1 := 0
|
||||
ticksPadding := 0
|
||||
labelPadding := 0
|
||||
ticksPaddingTop := 0
|
||||
ticksPaddingLeft := 0
|
||||
labelPaddingTop := 0
|
||||
labelPaddingLeft := 0
|
||||
labelPaddingRight := 0
|
||||
orient := ""
|
||||
textAlign := ""
|
||||
|
||||
switch opt.Position {
|
||||
case PositionTop:
|
||||
labelPadding = labelMargin
|
||||
labelPaddingTop = labelMargin
|
||||
x1 = p.Width()
|
||||
y0 = labelMargin + int(opt.FontSize)
|
||||
ticksPadding = int(opt.FontSize)
|
||||
ticksPaddingTop = int(opt.FontSize)
|
||||
y1 = y0
|
||||
orient = OrientHorizontal
|
||||
case PositionLeft:
|
||||
x0 = p.Width()
|
||||
y0 = 0
|
||||
x1 = p.Width()
|
||||
y1 = p.Height()
|
||||
orient = OrientVertical
|
||||
textAlign = AlignRight
|
||||
ticksPaddingLeft = textMaxWidth + tickLength
|
||||
labelPaddingRight = width - textMaxWidth
|
||||
case PositionRight:
|
||||
orient = OrientVertical
|
||||
y1 = p.Height()
|
||||
labelPaddingLeft = width - textMaxWidth
|
||||
default:
|
||||
labelPadding = height
|
||||
labelPaddingTop = height
|
||||
x1 = p.Width()
|
||||
orient = OrientHorizontal
|
||||
}
|
||||
|
||||
if strokeWidth > 0 {
|
||||
p.Child(PainterPaddingOption(Box{
|
||||
Top: ticksPadding,
|
||||
Top: ticksPaddingTop,
|
||||
Left: ticksPaddingLeft,
|
||||
})).Ticks(TicksOption{
|
||||
Count: tickCount,
|
||||
Length: tickLength,
|
||||
Unit: unit,
|
||||
Orient: orient,
|
||||
})
|
||||
|
||||
p.LineStroke([]Point{
|
||||
{
|
||||
X: x0,
|
||||
|
|
@ -174,9 +216,12 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
Y: y1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
p.Child(PainterPaddingOption(Box{
|
||||
Top: labelPadding,
|
||||
Left: labelPaddingLeft,
|
||||
Top: labelPaddingTop,
|
||||
Right: labelPaddingRight,
|
||||
})).MultiText(MultiTextOption{
|
||||
Align: textAlign,
|
||||
TextList: opt.Data,
|
||||
|
|
@ -184,6 +229,31 @@ func (a *axisPainter) Render() (Box, error) {
|
|||
Unit: unit,
|
||||
Position: labelPosition,
|
||||
})
|
||||
// 显示辅助线
|
||||
if opt.SplitLineShow {
|
||||
style.StrokeColor = opt.SplitLineColor
|
||||
top.OverrideDrawingStyle(style)
|
||||
if isVertical {
|
||||
x0 := p.Width()
|
||||
x1 := top.Width()
|
||||
if opt.Position == PositionRight {
|
||||
x0 = 0
|
||||
x1 = top.Width() - p.Width()
|
||||
}
|
||||
for _, y := range autoDivide(height, tickCount) {
|
||||
top.LineStroke([]Point{
|
||||
{
|
||||
X: x0,
|
||||
Y: y,
|
||||
},
|
||||
{
|
||||
X: x1,
|
||||
Y: y,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Box{
|
||||
Bottom: height,
|
||||
|
|
|
|||
23
charts.go
23
charts.go
|
|
@ -25,3 +25,26 @@ package charts
|
|||
type Renderer interface {
|
||||
Render() (Box, error)
|
||||
}
|
||||
|
||||
type defaultRenderOption struct {
|
||||
Theme ColorPalette
|
||||
Padding Box
|
||||
}
|
||||
|
||||
func defaultRender(p *Painter, opt defaultRenderOption) *Painter {
|
||||
p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor())
|
||||
if !opt.Padding.IsZero() {
|
||||
p = p.Child(PainterPaddingOption(opt.Padding))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func doRender(renderers ...Renderer) error {
|
||||
for _, r := range renderers {
|
||||
_, err := r.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
111
examples/line_chart/main.go
Normal file
111
examples/line_chart/main.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/vicanso/go-charts"
|
||||
)
|
||||
|
||||
func writeFile(buf []byte) error {
|
||||
tmpPath := "./tmp"
|
||||
err := os.MkdirAll(tmpPath, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(tmpPath, "line-chart.png")
|
||||
err = ioutil.WriteFile(file, buf, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
p, err := charts.NewPainter(charts.PainterOptions{
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
Type: charts.ChartOutputPNG,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = charts.NewLineChart(p, charts.LineChartOption{
|
||||
Padding: charts.Box{
|
||||
Left: 10,
|
||||
Top: 10,
|
||||
Right: 10,
|
||||
Bottom: 10,
|
||||
},
|
||||
XAxis: charts.NewXAxisOption([]string{
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
"Sun",
|
||||
}),
|
||||
SeriesList: charts.SeriesList{
|
||||
charts.NewSeriesFromValues([]float64{
|
||||
120,
|
||||
132,
|
||||
101,
|
||||
134,
|
||||
90,
|
||||
230,
|
||||
210,
|
||||
}),
|
||||
charts.NewSeriesFromValues([]float64{
|
||||
220,
|
||||
182,
|
||||
191,
|
||||
234,
|
||||
290,
|
||||
330,
|
||||
310,
|
||||
}),
|
||||
charts.NewSeriesFromValues([]float64{
|
||||
150,
|
||||
232,
|
||||
201,
|
||||
154,
|
||||
190,
|
||||
330,
|
||||
410,
|
||||
}),
|
||||
charts.NewSeriesFromValues([]float64{
|
||||
320,
|
||||
332,
|
||||
301,
|
||||
334,
|
||||
390,
|
||||
330,
|
||||
320,
|
||||
}),
|
||||
charts.NewSeriesFromValues([]float64{
|
||||
820,
|
||||
932,
|
||||
901,
|
||||
934,
|
||||
1290,
|
||||
1330,
|
||||
1320,
|
||||
}),
|
||||
},
|
||||
}).Render()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = writeFile(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -409,6 +409,7 @@ func main() {
|
|||
Bottom: 20,
|
||||
})
|
||||
|
||||
// grid
|
||||
top += 50
|
||||
charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
|
|
@ -422,6 +423,7 @@ func main() {
|
|||
StrokeColor: drawing.ColorBlue,
|
||||
}).Render()
|
||||
|
||||
// legend
|
||||
top += 100
|
||||
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
|
|
@ -440,6 +442,7 @@ func main() {
|
|||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
|
||||
// legend
|
||||
top += 30
|
||||
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
|
|
@ -460,6 +463,7 @@ func main() {
|
|||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
|
||||
// legend
|
||||
top += 30
|
||||
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
|
|
@ -479,6 +483,7 @@ func main() {
|
|||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
|
||||
// axis bottom
|
||||
top += 100
|
||||
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
|
|
@ -500,6 +505,7 @@ func main() {
|
|||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
|
||||
// axis top
|
||||
top += 50
|
||||
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
|
|
@ -523,11 +529,12 @@ func main() {
|
|||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
|
||||
// axis left
|
||||
top += 50
|
||||
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
Left: 10,
|
||||
Right: p.Width() - 1,
|
||||
Right: 60,
|
||||
Bottom: top + 200,
|
||||
})), charts.AxisPainterOption{
|
||||
Position: charts.PositionLeft,
|
||||
|
|
@ -544,6 +551,51 @@ func main() {
|
|||
FontSize: 12,
|
||||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
// axis right
|
||||
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
Left: 100,
|
||||
Right: 150,
|
||||
Bottom: top + 200,
|
||||
})), charts.AxisPainterOption{
|
||||
Position: charts.PositionRight,
|
||||
Data: []string{
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
"Sun",
|
||||
},
|
||||
StrokeColor: drawing.ColorBlack,
|
||||
FontSize: 12,
|
||||
FontColor: drawing.ColorBlack,
|
||||
}).Render()
|
||||
|
||||
// axis left no tick
|
||||
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||
Top: top,
|
||||
Left: 150,
|
||||
Right: 300,
|
||||
Bottom: top + 200,
|
||||
})), charts.AxisPainterOption{
|
||||
BoundaryGap: charts.FalseFlag(),
|
||||
Position: charts.PositionLeft,
|
||||
Data: []string{
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
"Sun",
|
||||
},
|
||||
FontSize: 12,
|
||||
FontColor: drawing.ColorBlack,
|
||||
SplitLineShow: true,
|
||||
SplitLineColor: drawing.ColorBlack.WithAlpha(100),
|
||||
}).Render()
|
||||
|
||||
buf, err := p.Bytes()
|
||||
if err != nil {
|
||||
|
|
|
|||
134
line_chart.go
Normal file
134
line_chart.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// 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/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
type lineChart struct {
|
||||
p *Painter
|
||||
opt *LineChartOption
|
||||
}
|
||||
|
||||
func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
|
||||
if opt.Theme == nil {
|
||||
opt.Theme = NewTheme("")
|
||||
}
|
||||
return &lineChart{
|
||||
p: p,
|
||||
opt: &opt,
|
||||
}
|
||||
}
|
||||
|
||||
type LineChartOption struct {
|
||||
Theme ColorPalette
|
||||
// The data series list
|
||||
SeriesList SeriesList
|
||||
// The x axis option
|
||||
XAxis XAxisOption
|
||||
// The padding of line chart
|
||||
Padding Box
|
||||
// The y axis option
|
||||
YAxis YAxisOption
|
||||
}
|
||||
|
||||
func (l *lineChart) Render() (Box, error) {
|
||||
p := l.p
|
||||
opt := l.opt
|
||||
p = defaultRender(p, defaultRenderOption{
|
||||
Theme: opt.Theme,
|
||||
Padding: opt.Padding,
|
||||
})
|
||||
|
||||
seriesList := opt.SeriesList
|
||||
seriesList.init()
|
||||
// 过滤前先计算最大最小值
|
||||
max, min := seriesList.GetMaxMin()
|
||||
|
||||
seriesList = seriesList.Filter(ChartTypeLine)
|
||||
|
||||
// Y轴
|
||||
yr := NewRange(AxisRangeOption{
|
||||
Min: min,
|
||||
Max: max,
|
||||
// 高度需要减去x轴的高度
|
||||
Size: p.Height() - defaultXAxisHeight,
|
||||
DivideCount: defaultAxisDivideCount,
|
||||
})
|
||||
if opt.YAxis.Theme == nil {
|
||||
opt.YAxis.Theme = opt.Theme
|
||||
}
|
||||
opt.YAxis.Data = yr.Values()
|
||||
reverseStringSlice(opt.YAxis.Data)
|
||||
yAxis := NewLeftYAxis(p, opt.YAxis)
|
||||
yAxisBox, err := yAxis.Render()
|
||||
if err != nil {
|
||||
return chart.BoxZero, err
|
||||
}
|
||||
seriesPainter := p.Child(PainterPaddingOption(Box{
|
||||
Bottom: defaultXAxisHeight,
|
||||
Left: yAxisBox.Width(),
|
||||
}))
|
||||
|
||||
if opt.XAxis.Theme == nil {
|
||||
opt.XAxis.Theme = opt.Theme
|
||||
}
|
||||
xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
|
||||
Left: yAxisBox.Width(),
|
||||
})), opt.XAxis)
|
||||
|
||||
xDivideValues := autoDivide(seriesPainter.Width(), len(opt.XAxis.Data))
|
||||
xValues := make([]int, len(xDivideValues)-1)
|
||||
for i := 0; i < len(xDivideValues)-1; i++ {
|
||||
xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1
|
||||
}
|
||||
for index, series := range seriesList {
|
||||
seriesColor := opt.Theme.GetSeriesColor(index)
|
||||
seriesPainter.SetDrawingStyle(Style{
|
||||
StrokeColor: seriesColor,
|
||||
StrokeWidth: 2,
|
||||
FillColor: seriesColor,
|
||||
})
|
||||
points := make([]Point, 0)
|
||||
for i, item := range series.Data {
|
||||
h := yr.getRestHeight(item.Value)
|
||||
p := Point{
|
||||
X: xValues[i],
|
||||
Y: h,
|
||||
}
|
||||
points = append(points, p)
|
||||
}
|
||||
seriesPainter.LineStroke(points)
|
||||
seriesPainter.Dots(points)
|
||||
}
|
||||
|
||||
err = doRender(
|
||||
xAxis,
|
||||
)
|
||||
if err != nil {
|
||||
return chart.BoxZero, err
|
||||
}
|
||||
|
||||
return p.box, nil
|
||||
}
|
||||
|
|
@ -636,7 +636,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
|
|||
x := 0
|
||||
y := 0
|
||||
if isVertical {
|
||||
y = start - box.Height()>>1
|
||||
y = start + box.Height()>>1
|
||||
switch opt.Align {
|
||||
case AlignRight:
|
||||
x = width - box.Width()
|
||||
|
|
@ -700,7 +700,7 @@ func (p *Painter) Grid(opt GridOption) *Painter {
|
|||
|
||||
func (p *Painter) Dots(points []Point) *Painter {
|
||||
for _, item := range points {
|
||||
p.Circle(5, item.X, item.Y)
|
||||
p.Circle(3, item.X, item.Y)
|
||||
}
|
||||
p.FillStroke()
|
||||
return p
|
||||
|
|
|
|||
127
range.go
Normal file
127
range.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// 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 (
|
||||
"math"
|
||||
)
|
||||
|
||||
const defaultAxisDivideCount = 6
|
||||
|
||||
type axisRange struct {
|
||||
divideCount int
|
||||
min float64
|
||||
max float64
|
||||
size int
|
||||
boundary bool
|
||||
}
|
||||
|
||||
type AxisRangeOption struct {
|
||||
Min float64
|
||||
Max float64
|
||||
Size int
|
||||
Boundary bool
|
||||
DivideCount int
|
||||
}
|
||||
|
||||
func NewRange(opt AxisRangeOption) axisRange {
|
||||
max := opt.Max
|
||||
min := opt.Min
|
||||
|
||||
max += math.Abs(max * 0.1)
|
||||
min -= math.Abs(min * 0.1)
|
||||
divideCount := opt.DivideCount
|
||||
r := math.Abs(max - min)
|
||||
|
||||
// 最小单位计算
|
||||
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
|
||||
|
||||
if min != 0 {
|
||||
isLessThanZero := min < 0
|
||||
min = float64(int(min/float64(unit)) * unit)
|
||||
// 如果是小于0,int的时候向上取整了,因此调整
|
||||
if min < 0 ||
|
||||
(isLessThanZero && min == 0) {
|
||||
min -= float64(unit)
|
||||
}
|
||||
}
|
||||
max = min + float64(unit*divideCount)
|
||||
return axisRange{
|
||||
divideCount: divideCount,
|
||||
min: min,
|
||||
max: max,
|
||||
size: opt.Size,
|
||||
boundary: opt.Boundary,
|
||||
}
|
||||
}
|
||||
|
||||
func (r axisRange) 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 := commafWithDigits(v)
|
||||
values = append(values, value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func (r *axisRange) getHeight(value float64) int {
|
||||
v := (value - r.min) / (r.max - r.min)
|
||||
return int(v * float64(r.size))
|
||||
}
|
||||
|
||||
func (r *axisRange) getRestHeight(value float64) int {
|
||||
return r.size - r.getHeight(value)
|
||||
}
|
||||
|
||||
func (r *axisRange) GetRange(index int) (float64, float64) {
|
||||
unit := float64(r.size) / float64(r.divideCount)
|
||||
return unit * float64(index), unit * float64(index+1)
|
||||
}
|
||||
func (r *axisRange) AutoDivide() []int {
|
||||
return autoDivide(r.size, r.divideCount)
|
||||
}
|
||||
|
||||
func (r *axisRange) getWidth(value float64) int {
|
||||
v := value / (r.max - r.min)
|
||||
// 移至居中
|
||||
if r.boundary &&
|
||||
r.divideCount != 0 {
|
||||
v += 1 / float64(r.divideCount*2)
|
||||
}
|
||||
return int(v * float64(r.size))
|
||||
}
|
||||
270
series.go
Normal file
270
series.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// 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 (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
type SeriesData struct {
|
||||
// The value of series data
|
||||
Value float64
|
||||
// The style of series data
|
||||
Style 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 {
|
||||
// Data label formatter, which supports string template.
|
||||
// {b}: the name of a data item.
|
||||
// {c}: the value of a data item.
|
||||
// {d}: the percent of a data item(pie chart).
|
||||
Formatter string
|
||||
// The color for label
|
||||
Color Color
|
||||
// Show flag for label
|
||||
Show bool
|
||||
// Distance to the host graphic element.
|
||||
Distance int
|
||||
}
|
||||
|
||||
const (
|
||||
SeriesMarkDataTypeMax = "max"
|
||||
SeriesMarkDataTypeMin = "min"
|
||||
SeriesMarkDataTypeAverage = "average"
|
||||
)
|
||||
|
||||
type SeriesMarkData struct {
|
||||
// The mark data type, it can be "max", "min", "average".
|
||||
// The "average" is only for mark line
|
||||
Type string
|
||||
}
|
||||
type SeriesMarkPoint struct {
|
||||
// The width of symbol, default value is 30
|
||||
SymbolSize int
|
||||
// The mark data of series mark point
|
||||
Data []SeriesMarkData
|
||||
}
|
||||
type SeriesMarkLine struct {
|
||||
// The mark data of series mark line
|
||||
Data []SeriesMarkData
|
||||
}
|
||||
type Series struct {
|
||||
index int
|
||||
// The type of series, it can be "line", "bar" or "pie".
|
||||
// Default value is "line"
|
||||
Type string
|
||||
// The data list of series
|
||||
Data []SeriesData
|
||||
// The Y axis index, it should be 0 or 1.
|
||||
// Default value is 1
|
||||
YAxisIndex int
|
||||
// The style for series
|
||||
Style chart.Style
|
||||
// The label for series
|
||||
Label SeriesLabel
|
||||
// The name of series
|
||||
Name string
|
||||
// Radius for Pie chart, e.g.: 40%, default is "40%"
|
||||
Radius string
|
||||
// Mark point for series
|
||||
MarkPoint SeriesMarkPoint
|
||||
// Make line for series
|
||||
MarkLine SeriesMarkLine
|
||||
// Max value of series
|
||||
Min *float64
|
||||
// Min value of series
|
||||
Max *float64
|
||||
}
|
||||
type SeriesList []Series
|
||||
|
||||
func (sl SeriesList) init() {
|
||||
if sl[len(sl)-1].index != 0 {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(sl); i++ {
|
||||
if sl[i].Type == "" {
|
||||
sl[i].Type = ChartTypeLine
|
||||
}
|
||||
sl[i].index = i
|
||||
}
|
||||
}
|
||||
|
||||
func (sl SeriesList) Filter(chartType string) SeriesList {
|
||||
arr := make(SeriesList, 0)
|
||||
for index, item := range sl {
|
||||
if item.Type == chartType {
|
||||
arr = append(arr, sl[index])
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// GetMaxMin get max and min value of series list
|
||||
func (sl SeriesList) GetMaxMin() (float64, float64) {
|
||||
min := math.MaxFloat64
|
||||
max := -math.MaxFloat64
|
||||
for _, series := range sl {
|
||||
for _, item := range series.Data {
|
||||
if item.Value > max {
|
||||
max = item.Value
|
||||
}
|
||||
if item.Value < min {
|
||||
min = item.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return max, min
|
||||
}
|
||||
|
||||
type PieSeriesOption struct {
|
||||
Radius string
|
||||
Label SeriesLabel
|
||||
Names []string
|
||||
}
|
||||
|
||||
func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
|
||||
result := make([]Series, len(values))
|
||||
var opt PieSeriesOption
|
||||
if len(opts) != 0 {
|
||||
opt = opts[0]
|
||||
}
|
||||
for index, v := range values {
|
||||
name := ""
|
||||
if index < len(opt.Names) {
|
||||
name = opt.Names[index]
|
||||
}
|
||||
s := Series{
|
||||
Type: ChartTypePie,
|
||||
Data: []SeriesData{
|
||||
{
|
||||
Value: v,
|
||||
},
|
||||
},
|
||||
Radius: opt.Radius,
|
||||
Label: opt.Label,
|
||||
Name: name,
|
||||
}
|
||||
result[index] = s
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
func (sl SeriesList) Names() []string {
|
||||
names := make([]string, len(sl))
|
||||
for index, s := range sl {
|
||||
names[index] = s.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
27
theme.go
27
theme.go
|
|
@ -22,7 +22,11 @@
|
|||
|
||||
package charts
|
||||
|
||||
import "github.com/wcharczuk/go-chart/v2/drawing"
|
||||
import (
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
const ThemeDark = "dark"
|
||||
const ThemeLight = "light"
|
||||
|
|
@ -36,6 +40,8 @@ type ColorPalette interface {
|
|||
GetSeriesColor(int) Color
|
||||
GetBackgroundColor() Color
|
||||
GetTextColor() Color
|
||||
GetFontSize() float64
|
||||
GetFont() *truetype.Font
|
||||
}
|
||||
|
||||
type themeColorPalette struct {
|
||||
|
|
@ -45,10 +51,14 @@ type themeColorPalette struct {
|
|||
backgroundColor Color
|
||||
textColor Color
|
||||
seriesColors []Color
|
||||
fontSize float64
|
||||
font *truetype.Font
|
||||
}
|
||||
|
||||
var palettes = map[string]ColorPalette{}
|
||||
|
||||
const defaultFontSize = 12.0
|
||||
|
||||
func init() {
|
||||
echartSeriesColors := []Color{
|
||||
parseColor("#5470c6"),
|
||||
|
|
@ -233,3 +243,18 @@ func (t *themeColorPalette) GetBackgroundColor() Color {
|
|||
func (t *themeColorPalette) GetTextColor() Color {
|
||||
return t.textColor
|
||||
}
|
||||
|
||||
func (t *themeColorPalette) GetFontSize() float64 {
|
||||
if t.fontSize != 0 {
|
||||
return t.fontSize
|
||||
}
|
||||
return defaultFontSize
|
||||
}
|
||||
|
||||
func (t *themeColorPalette) GetFont() *truetype.Font {
|
||||
if t.font != nil {
|
||||
return t.font
|
||||
}
|
||||
f, _ := chart.GetDefaultFont()
|
||||
return f
|
||||
}
|
||||
|
|
|
|||
88
xaxis.go
Normal file
88
xaxis.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
type XAxisOption struct {
|
||||
// The font of x axis
|
||||
Font *truetype.Font
|
||||
// The boundary gap on both sides of a coordinate axis.
|
||||
// Nil or *true means the center part of two axis ticks
|
||||
BoundaryGap *bool
|
||||
// The data value of x axis
|
||||
Data []string
|
||||
// The theme of chart
|
||||
Theme ColorPalette
|
||||
// The font size of x axis label
|
||||
FontSize float64
|
||||
// Hidden x axis
|
||||
Hidden bool
|
||||
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
||||
SplitNumber int
|
||||
// The position of axis, it can be 'top' or 'bottom'
|
||||
Position string
|
||||
// The line color of axis
|
||||
StrokeColor Color
|
||||
// The color of label
|
||||
FontColor Color
|
||||
}
|
||||
|
||||
const defaultXAxisHeight = 30
|
||||
|
||||
func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
|
||||
opt := XAxisOption{
|
||||
Data: data,
|
||||
}
|
||||
if len(boundaryGap) != 0 {
|
||||
opt.BoundaryGap = boundaryGap[0]
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *XAxisOption) ToAxisPainterOption() AxisPainterOption {
|
||||
position := PositionBottom
|
||||
if opt.Position == PositionTop {
|
||||
position = PositionTop
|
||||
}
|
||||
return AxisPainterOption{
|
||||
Theme: opt.Theme,
|
||||
Data: opt.Data,
|
||||
BoundaryGap: opt.BoundaryGap,
|
||||
Position: position,
|
||||
SplitNumber: opt.SplitNumber,
|
||||
StrokeColor: opt.StrokeColor,
|
||||
FontSize: opt.FontSize,
|
||||
Font: opt.Font,
|
||||
FontColor: opt.FontColor,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter {
|
||||
p = p.Child(PainterPaddingOption(Box{
|
||||
Top: p.Height() - defaultXAxisHeight,
|
||||
}))
|
||||
return NewAxisPainter(p, opt.ToAxisPainterOption())
|
||||
}
|
||||
66
yaxis.go
Normal file
66
yaxis.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// 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"
|
||||
|
||||
type YAxisOption struct {
|
||||
// The font of y axis
|
||||
Font *truetype.Font
|
||||
// The data value of x axis
|
||||
Data []string
|
||||
// The theme of chart
|
||||
Theme ColorPalette
|
||||
// The font size of x axis label
|
||||
FontSize float64
|
||||
// The position of axis, it can be 'left' or 'right'
|
||||
Position string
|
||||
// The color of label
|
||||
FontColor Color
|
||||
}
|
||||
|
||||
func (opt *YAxisOption) ToAxisPainterOption() AxisPainterOption {
|
||||
position := PositionLeft
|
||||
if opt.Position == PositionRight {
|
||||
position = PositionRight
|
||||
}
|
||||
return AxisPainterOption{
|
||||
Theme: opt.Theme,
|
||||
Data: opt.Data,
|
||||
Position: position,
|
||||
FontSize: opt.FontSize,
|
||||
StrokeWidth: -1,
|
||||
Font: opt.Font,
|
||||
FontColor: opt.FontColor,
|
||||
BoundaryGap: FalseFlag(),
|
||||
SplitLineShow: true,
|
||||
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||
p = p.Child(PainterPaddingOption(Box{
|
||||
Bottom: defaultXAxisHeight,
|
||||
}))
|
||||
return NewAxisPainter(p, opt.ToAxisPainterOption())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue