feat: support line chart render function

This commit is contained in:
vicanso 2022-06-12 11:55:37 +08:00
parent b394e1b49f
commit c4045cfbbe
11 changed files with 1012 additions and 46 deletions

120
axis.go
View file

@ -39,6 +39,8 @@ func NewAxisPainter(p *Painter, opt AxisPainterOption) *axisPainter {
} }
type AxisPainterOption struct { type AxisPainterOption struct {
// The theme of chart
Theme ColorPalette
// The label of axis // The label of axis
Data []string Data []string
// The boundary gap on both sides of a coordinate axis. // The boundary gap on both sides of a coordinate axis.
@ -70,13 +72,31 @@ type AxisPainterOption struct {
func (a *axisPainter) Render() (Box, error) { func (a *axisPainter) Render() (Box, error) {
opt := a.opt opt := a.opt
p := a.p top := a.p
theme := opt.Theme
strokeWidth := opt.StrokeWidth strokeWidth := opt.StrokeWidth
if strokeWidth == 0 { if strokeWidth == 0 {
strokeWidth = 1 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 tickCount := opt.SplitNumber
if tickCount == 0 { if tickCount == 0 {
tickCount = len(opt.Data) tickCount = len(opt.Data)
@ -86,12 +106,17 @@ func (a *axisPainter) Render() (Box, error) {
if opt.BoundaryGap != nil && !*opt.BoundaryGap { if opt.BoundaryGap != nil && !*opt.BoundaryGap {
boundaryGap = false boundaryGap = false
} }
isVertical := opt.Position == PositionLeft ||
opt.Position == PositionRight
labelPosition := "" labelPosition := ""
if !boundaryGap { if !boundaryGap {
tickCount-- tickCount--
labelPosition = PositionLeft labelPosition = PositionLeft
} }
if isVertical && boundaryGap {
labelPosition = PositionCenter
}
// TODO 计算unit // TODO 计算unit
unit := 1 unit := 1
@ -99,71 +124,88 @@ func (a *axisPainter) Render() (Box, error) {
tickLength := getDefaultInt(opt.TickLength, 5) tickLength := getDefaultInt(opt.TickLength, 5)
labelMargin := getDefaultInt(opt.LabelMargin, 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 width := 0
height := 0 height := 0
// 垂直 // 垂直
if opt.Position == PositionLeft || if isVertical {
opt.Position == PositionRight {
width = textMaxWidth + tickLength<<1 width = textMaxWidth + tickLength<<1
height = p.Height() height = top.Height()
} else { } else {
width = p.Width() width = top.Width()
height = tickLength<<1 + textMaxHeight height = tickLength<<1 + textMaxHeight
} }
padding := Box{} padding := Box{}
switch opt.Position { switch opt.Position {
case PositionTop: case PositionTop:
padding.Top = p.Height() - height padding.Top = top.Height() - height
case PositionLeft: 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{ p := top.Child(PainterPaddingOption(padding))
StrokeColor: opt.StrokeColor,
StrokeWidth: strokeWidth,
}).OverrideTextStyle(Style{
Font: opt.Font,
FontColor: opt.FontColor,
FontSize: opt.FontSize,
})
x0 := 0 x0 := 0
y0 := 0 y0 := 0
x1 := 0 x1 := 0
y1 := 0 y1 := 0
ticksPadding := 0 ticksPaddingTop := 0
labelPadding := 0 ticksPaddingLeft := 0
labelPaddingTop := 0
labelPaddingLeft := 0
labelPaddingRight := 0
orient := "" orient := ""
textAlign := "" textAlign := ""
switch opt.Position { switch opt.Position {
case PositionTop: case PositionTop:
labelPadding = labelMargin labelPaddingTop = labelMargin
x1 = p.Width() x1 = p.Width()
y0 = labelMargin + int(opt.FontSize) y0 = labelMargin + int(opt.FontSize)
ticksPadding = int(opt.FontSize) ticksPaddingTop = int(opt.FontSize)
y1 = y0 y1 = y0
orient = OrientHorizontal orient = OrientHorizontal
case PositionLeft: case PositionLeft:
x0 = p.Width()
y0 = 0
x1 = p.Width()
y1 = p.Height()
orient = OrientVertical orient = OrientVertical
textAlign = AlignRight textAlign = AlignRight
ticksPaddingLeft = textMaxWidth + tickLength
labelPaddingRight = width - textMaxWidth
case PositionRight:
orient = OrientVertical
y1 = p.Height()
labelPaddingLeft = width - textMaxWidth
default: default:
labelPadding = height labelPaddingTop = height
x1 = p.Width() x1 = p.Width()
orient = OrientHorizontal orient = OrientHorizontal
} }
if strokeWidth > 0 {
p.Child(PainterPaddingOption(Box{ p.Child(PainterPaddingOption(Box{
Top: ticksPadding, Top: ticksPaddingTop,
Left: ticksPaddingLeft,
})).Ticks(TicksOption{ })).Ticks(TicksOption{
Count: tickCount, Count: tickCount,
Length: tickLength, Length: tickLength,
Unit: unit, Unit: unit,
Orient: orient, Orient: orient,
}) })
p.LineStroke([]Point{ p.LineStroke([]Point{
{ {
X: x0, X: x0,
@ -174,9 +216,12 @@ func (a *axisPainter) Render() (Box, error) {
Y: y1, Y: y1,
}, },
}) })
}
p.Child(PainterPaddingOption(Box{ p.Child(PainterPaddingOption(Box{
Top: labelPadding, Left: labelPaddingLeft,
Top: labelPaddingTop,
Right: labelPaddingRight,
})).MultiText(MultiTextOption{ })).MultiText(MultiTextOption{
Align: textAlign, Align: textAlign,
TextList: opt.Data, TextList: opt.Data,
@ -184,6 +229,31 @@ func (a *axisPainter) Render() (Box, error) {
Unit: unit, Unit: unit,
Position: labelPosition, 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{ return Box{
Bottom: height, Bottom: height,

View file

@ -25,3 +25,26 @@ package charts
type Renderer interface { type Renderer interface {
Render() (Box, error) 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
View 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)
}
}

View file

@ -409,6 +409,7 @@ func main() {
Bottom: 20, Bottom: 20,
}) })
// grid
top += 50 top += 50
charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
@ -422,6 +423,7 @@ func main() {
StrokeColor: drawing.ColorBlue, StrokeColor: drawing.ColorBlue,
}).Render() }).Render()
// legend
top += 100 top += 100
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
@ -440,6 +442,7 @@ func main() {
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).Render()
// legend
top += 30 top += 30
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
@ -460,6 +463,7 @@ func main() {
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).Render()
// legend
top += 30 top += 30
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
@ -479,6 +483,7 @@ func main() {
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).Render()
// axis bottom
top += 100 top += 100
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
@ -500,6 +505,7 @@ func main() {
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).Render()
// axis top
top += 50 top += 50
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
@ -523,11 +529,12 @@ func main() {
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).Render()
// axis left
top += 50 top += 50
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top, Top: top,
Left: 10, Left: 10,
Right: p.Width() - 1, Right: 60,
Bottom: top + 200, Bottom: top + 200,
})), charts.AxisPainterOption{ })), charts.AxisPainterOption{
Position: charts.PositionLeft, Position: charts.PositionLeft,
@ -544,6 +551,51 @@ func main() {
FontSize: 12, FontSize: 12,
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).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() buf, err := p.Bytes()
if err != nil { if err != nil {

134
line_chart.go Normal file
View 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
}

View file

@ -636,7 +636,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
x := 0 x := 0
y := 0 y := 0
if isVertical { if isVertical {
y = start - box.Height()>>1 y = start + box.Height()>>1
switch opt.Align { switch opt.Align {
case AlignRight: case AlignRight:
x = width - box.Width() x = width - box.Width()
@ -700,7 +700,7 @@ func (p *Painter) Grid(opt GridOption) *Painter {
func (p *Painter) Dots(points []Point) *Painter { func (p *Painter) Dots(points []Point) *Painter {
for _, item := range points { for _, item := range points {
p.Circle(5, item.X, item.Y) p.Circle(3, item.X, item.Y)
} }
p.FillStroke() p.FillStroke()
return p return p

127
range.go Normal file
View 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)
// 如果是小于0int的时候向上取整了因此调整
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
View 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
}
}

View file

@ -22,7 +22,11 @@
package charts 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 ThemeDark = "dark"
const ThemeLight = "light" const ThemeLight = "light"
@ -36,6 +40,8 @@ type ColorPalette interface {
GetSeriesColor(int) Color GetSeriesColor(int) Color
GetBackgroundColor() Color GetBackgroundColor() Color
GetTextColor() Color GetTextColor() Color
GetFontSize() float64
GetFont() *truetype.Font
} }
type themeColorPalette struct { type themeColorPalette struct {
@ -45,10 +51,14 @@ type themeColorPalette struct {
backgroundColor Color backgroundColor Color
textColor Color textColor Color
seriesColors []Color seriesColors []Color
fontSize float64
font *truetype.Font
} }
var palettes = map[string]ColorPalette{} var palettes = map[string]ColorPalette{}
const defaultFontSize = 12.0
func init() { func init() {
echartSeriesColors := []Color{ echartSeriesColors := []Color{
parseColor("#5470c6"), parseColor("#5470c6"),
@ -233,3 +243,18 @@ func (t *themeColorPalette) GetBackgroundColor() Color {
func (t *themeColorPalette) GetTextColor() Color { func (t *themeColorPalette) GetTextColor() Color {
return t.textColor 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
View 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
View 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())
}