diff --git a/assets/go-charts.png b/assets/go-charts.png
index f556abf..12d0cad 100644
Binary files a/assets/go-charts.png and b/assets/go-charts.png differ
diff --git a/assets/go-line-chart.png b/assets/go-line-chart.png
new file mode 100644
index 0000000..71c9eb1
Binary files /dev/null and b/assets/go-line-chart.png differ
diff --git a/axis.go b/axis.go
index c632815..46292e4 100644
--- a/axis.go
+++ b/axis.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -25,184 +25,444 @@ package charts
import (
"math"
- "github.com/dustin/go-humanize"
+ "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
)
-type (
- // AxisData string
- XAxis struct {
- // data value of axis
- Data []string
- // number of segments
- SplitNumber int
- }
-)
-
-type YAxisOption struct {
- // formater of axis
- Formater chart.ValueFormatter
- // disabled axis
- Disabled bool
- // min value of axis
- Min *float64
- // max value of axis
- Max *float64
+type AxisOption struct {
+ // The boundary gap on both sides of a coordinate axis.
+ // Nil or *true means the center part of two axis ticks
+ BoundaryGap *bool
+ // The flag for show axis, set this to *false will hide axis
+ Show *bool
+ // The position of axis, it can be 'left', 'top', 'right' or 'bottom'
+ Position string
+ // Number of segments that the axis is split into. Note that this number serves only as a recommendation.
+ SplitNumber int
+ ClassName string
+ // The line color of axis
+ StrokeColor drawing.Color
+ // The line width
+ StrokeWidth float64
+ // The length of the axis tick
+ TickLength int
+ // The flag for show axis tick, set this to *false will hide axis tick
+ TickShow *bool
+ // The margin value of label
+ LabelMargin int
+ // The font size of label
+ FontSize float64
+ // The font of label
+ Font *truetype.Font
+ // The color of label
+ FontColor drawing.Color
+ // The flag for show axis split line, set this to true will show axis split line
+ SplitLineShow bool
+ // The color of split line
+ SplitLineColor drawing.Color
}
-const axisStrokeWidth = 1
+type axis struct {
+ d *Draw
+ data *AxisDataList
+ option *AxisOption
+}
+type axisMeasurement struct {
+ Width int
+ Height int
+}
-func maxInt(values ...int) int {
- result := 0
- for _, v := range values {
- if v > result {
- result = v
+// NewAxis creates a new axis with data and style options
+func NewAxis(d *Draw, data AxisDataList, option AxisOption) *axis {
+ return &axis{
+ d: d,
+ data: &data,
+ option: &option,
+ }
+
+}
+
+// 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,
+ StrokeColor: as.StrokeColor,
+ StrokeWidth: as.StrokeWidth,
+ FontSize: as.FontSize,
+ FontColor: as.FontColor,
+ Font: as.Font,
+ }
+ if s.FontSize == 0 {
+ s.FontSize = chart.DefaultFontSize
+ }
+ if s.Font == nil {
+ s.Font = f
+ }
+ return s
+}
+
+type AxisData struct {
+ // The text value of axis
+ Text string
+}
+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 {
+ textList[index] = item.Text
+ }
+ return textList
+}
+
+type axisRenderOption struct {
+ textMaxWith int
+ textMaxHeight int
+ boundaryGap bool
+ unitCount int
+ 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 {
+ list[index] = AxisData{
+ Text: text,
}
}
- return result
+ return list
}
-// GetXAxisAndValues returns x axis by theme, and the values of axis.
-func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme string) (chart.XAxis, []float64) {
- data := xAxis.Data
- originalSize := len(data)
- // 如果居中,则需要多添加一个值
- if tickPosition == chart.TickPositionBetweenTicks {
- data = append([]string{
- "",
- }, data...)
- }
-
- size := len(data)
-
- xValues := make([]float64, size)
- ticks := make([]chart.Tick, 0)
-
- // tick width
- maxTicks := maxInt(xAxis.SplitNumber, 10)
-
- // 计息最多每个unit至少放多个
- minUnitSize := originalSize / maxTicks
- if originalSize%maxTicks != 0 {
- minUnitSize++
- }
- unitSize := minUnitSize
- // 尽可能选择一格展示更多的块
- for i := minUnitSize; i < 2*minUnitSize; i++ {
- if originalSize%i == 0 {
- unitSize = i
- }
- }
-
- for index, key := range data {
- f := float64(index)
- xValues[index] = f
- if index%unitSize == 0 || index == size-1 {
- ticks = append(ticks, chart.Tick{
- Value: f,
- Label: key,
- })
- }
- }
- return chart.XAxis{
- Ticks: ticks,
- TickPosition: tickPosition,
- Style: chart.Style{
- FontColor: getAxisColor(theme),
- StrokeColor: getAxisColor(theme),
- StrokeWidth: axisStrokeWidth,
- },
- }, xValues
-}
-
-func defaultFloatFormater(v interface{}) string {
- value, ok := v.(float64)
- if !ok {
- return ""
- }
- // 大于10的则直接取整展示
- if value >= 10 {
- return humanize.CommafWithDigits(value, 0)
- }
- return humanize.CommafWithDigits(value, 2)
-}
-
-func newYContinuousRange(option *YAxisOption) *YContinuousRange {
- m := YContinuousRange{}
- m.Min = -math.MaxFloat64
- m.Max = math.MaxFloat64
- if option != nil {
- if option.Min != nil {
- m.Min = *option.Min
- }
- if option.Max != nil {
- m.Max = *option.Max
- }
- }
- return &m
-}
-
-// GetSecondaryYAxis returns the secondary y axis by theme
-func GetSecondaryYAxis(theme string, option *YAxisOption) chart.YAxis {
- strokeColor := getGridColor(theme)
- yAxis := chart.YAxis{
- Range: newYContinuousRange(option),
- ValueFormatter: defaultFloatFormater,
- AxisType: chart.YAxisSecondary,
- GridMajorStyle: chart.Style{
- StrokeColor: strokeColor,
- StrokeWidth: axisStrokeWidth,
- },
- GridMinorStyle: chart.Style{
- StrokeColor: strokeColor,
- StrokeWidth: axisStrokeWidth,
- },
- Style: chart.Style{
- FontColor: getAxisColor(theme),
- // alpha 0,隐藏
- StrokeColor: hiddenColor,
- StrokeWidth: axisStrokeWidth,
- },
- }
- setYAxisOption(&yAxis, option)
- return yAxis
-}
-
-func setYAxisOption(yAxis *chart.YAxis, option *YAxisOption) {
- if option == nil {
+func (a *axis) axisLabel(renderOpt *axisRenderOption) {
+ option := a.option
+ data := *a.data
+ d := a.d
+ if option.FontColor.IsZero() || len(data) == 0 {
return
}
- if option.Formater != nil {
- yAxis.ValueFormatter = option.Formater
+ r := d.Render
+
+ s := option.Style(d.Font)
+ s.GetTextOptions().WriteTextOptionsToRenderer(r)
+
+ width := d.Box.Width()
+ height := d.Box.Height()
+ textList := data.TextList()
+ count := len(textList)
+
+ boundaryGap := renderOpt.boundaryGap
+ if !boundaryGap {
+ count--
+ }
+
+ unitCount := renderOpt.unitCount
+ modValue := renderOpt.modValue
+ labelMargin := option.GetLabelMargin()
+
+ // 轴线
+ labelHeight := labelMargin + renderOpt.textMaxHeight
+ labelWidth := labelMargin + renderOpt.textMaxWith
+
+ // 坐标轴文本
+ position := option.Position
+ switch position {
+ case PositionLeft:
+ fallthrough
+ case PositionRight:
+ values := autoDivide(height, count)
+ textList := data.TextList()
+ // 由下往上
+ reverseIntSlice(values)
+ for index, text := range textList {
+ y := values[index] - 2
+ b := r.MeasureText(text)
+ if boundaryGap {
+ height := y - values[index+1]
+ y -= (height - b.Height()) >> 1
+ } else {
+ y += b.Height() >> 1
+ }
+ // 左右位置的x不一样
+ x := width - renderOpt.textMaxWith
+ if position == PositionLeft {
+ x = labelWidth - b.Width() - 1
+ }
+ d.text(text, x, y)
+ }
+ default:
+ // 定位bottom,重新计算y0的定位
+ y0 := height - labelMargin
+ if position == PositionTop {
+ y0 = labelHeight - labelMargin
+ }
+ values := autoDivide(width, count)
+ for index, text := range data.TextList() {
+ if unitCount != 0 && index%unitCount != modValue {
+ continue
+ }
+ x := values[index]
+ leftOffset := 0
+ b := r.MeasureText(text)
+ if boundaryGap {
+ width := values[index+1] - x
+ leftOffset = (width - b.Width()) >> 1
+ } else {
+ // 左移文本长度
+ leftOffset = -b.Width() >> 1
+ }
+ d.text(text, x+leftOffset, y0)
+ }
}
}
-// GetYAxis returns the primary y axis by theme
-func GetYAxis(theme string, option *YAxisOption) chart.YAxis {
- disabled := false
- if option != nil {
- disabled = option.Disabled
- }
- hidden := chart.Hidden()
+func (a *axis) axisLine(renderOpt *axisRenderOption) {
+ d := a.d
+ r := d.Render
+ option := a.option
+ s := option.Style(d.Font)
+ s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
- yAxis := chart.YAxis{
- Range: newYContinuousRange(option),
- ValueFormatter: defaultFloatFormater,
- AxisType: chart.YAxisPrimary,
- GridMajorStyle: hidden,
- GridMinorStyle: hidden,
- Style: chart.Style{
- FontColor: getAxisColor(theme),
- // alpha 0,隐藏
- StrokeColor: hiddenColor,
- StrokeWidth: axisStrokeWidth,
- },
+ x0 := 0
+ y0 := 0
+ x1 := 0
+ y1 := 0
+ width := d.Box.Width()
+ height := d.Box.Height()
+ labelMargin := option.GetLabelMargin()
+
+ // 轴线
+ labelHeight := labelMargin + renderOpt.textMaxHeight
+ labelWidth := labelMargin + renderOpt.textMaxWith
+ tickLength := option.GetTickLength()
+ switch option.Position {
+ case PositionLeft:
+ x0 = tickLength + labelWidth
+ x1 = x0
+ y0 = 0
+ y1 = height
+ case PositionRight:
+ x0 = width - labelWidth
+ x1 = x0
+ y0 = 0
+ y1 = height
+ case PositionTop:
+ x0 = 0
+ x1 = width
+ y0 = labelHeight
+ y1 = y0
+ // bottom
+ default:
+ x0 = 0
+ x1 = width
+ y0 = height - tickLength - labelHeight
+ y1 = y0
}
- // 如果禁用,则默认为隐藏,并设置range
- if disabled {
- yAxis.Range = &HiddenRange{}
- yAxis.Style.Hidden = true
- }
- setYAxisOption(&yAxis, option)
- return yAxis
+
+ d.moveTo(x0, y0)
+ d.lineTo(x1, y1)
+ r.FillStroke()
+}
+
+func (a *axis) axisTick(renderOpt *axisRenderOption) {
+ d := a.d
+ r := d.Render
+
+ option := a.option
+ s := option.Style(d.Font)
+ s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
+
+ width := d.Box.Width()
+ height := d.Box.Height()
+ data := *a.data
+ tickCount := len(data)
+ if tickCount == 0 {
+ return
+ }
+ if !renderOpt.boundaryGap {
+ tickCount--
+ }
+ labelMargin := option.GetLabelMargin()
+ tickShow := true
+ if isFalse(option.TickShow) {
+ tickShow = false
+ }
+ unitCount := renderOpt.unitCount
+
+ tickLengthValue := option.GetTickLength()
+ labelHeight := labelMargin + renderOpt.textMaxHeight
+ labelWidth := labelMargin + renderOpt.textMaxWith
+ position := option.Position
+ switch position {
+ case PositionLeft:
+ fallthrough
+ case PositionRight:
+ values := autoDivide(height, tickCount)
+ // 左右仅是x0的位置不一样
+ x0 := width - labelWidth
+ if option.Position == PositionLeft {
+ x0 = labelWidth
+ }
+ if tickShow {
+ for _, v := range values {
+ x := x0
+ y := v
+ d.moveTo(x, y)
+ d.lineTo(x+tickLengthValue, y)
+ r.Stroke()
+ }
+ }
+ // 辅助线
+ if option.SplitLineShow && !option.SplitLineColor.IsZero() {
+ r.SetStrokeColor(option.SplitLineColor)
+ splitLineWidth := width - labelWidth - tickLengthValue
+ x0 = labelWidth + tickLengthValue
+ if position == PositionRight {
+ x0 = 0
+ splitLineWidth = width - labelWidth - 1
+ }
+ for _, v := range values[0 : len(values)-1] {
+ x := x0
+ y := v
+ d.moveTo(x, y)
+ d.lineTo(x+splitLineWidth, y)
+ r.Stroke()
+ }
+ }
+ default:
+ values := autoDivide(width, tickCount)
+ // 上下仅是y0的位置不一样
+ y0 := height - labelHeight
+ if position == PositionTop {
+ y0 = labelHeight
+ }
+ if tickShow {
+ for index, v := range values {
+ if index%unitCount != 0 {
+ continue
+ }
+ x := v
+ y := y0
+ d.moveTo(x, y-tickLengthValue)
+ d.lineTo(x, y)
+ r.Stroke()
+ }
+ }
+ // 辅助线
+ if option.SplitLineShow && !option.SplitLineColor.IsZero() {
+ r.SetStrokeColor(option.SplitLineColor)
+ y0 = 0
+ splitLineHeight := height - labelHeight - tickLengthValue
+ if position == PositionTop {
+ y0 = labelHeight
+ splitLineHeight = height - labelHeight
+ }
+
+ for index, v := range values {
+ if index%unitCount != 0 {
+ continue
+ }
+ x := v
+ y := y0
+
+ d.moveTo(x, y)
+ d.lineTo(x, y0+splitLineHeight)
+ r.Stroke()
+ }
+ }
+ }
+}
+
+func (a *axis) measureTextMaxWidthHeight() (int, int) {
+ d := a.d
+ r := d.Render
+ s := a.option.Style(d.Font)
+ data := a.data
+ s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
+ s.GetTextOptions().WriteTextOptionsToRenderer(r)
+ return measureTextMaxWidthHeight(data.TextList(), r)
+}
+
+// measure returns the measurement of axis.
+// Width will be textMaxWidth + labelMargin + tickLength for position left or right.
+// Height will be textMaxHeight + labelMargin + tickLength for position top or bottom.
+func (a *axis) measure() axisMeasurement {
+ option := a.option
+ value := option.GetLabelMargin() + option.GetTickLength()
+ textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
+ info := axisMeasurement{}
+ if option.Position == PositionLeft ||
+ option.Position == PositionRight {
+ info.Width = textMaxWidth + value
+ } else {
+ info.Height = textMaxHeight + value
+ }
+ return info
+}
+
+// Render renders the axis for chart
+func (a *axis) Render() {
+ option := a.option
+ if isFalse(option.Show) {
+ return
+ }
+ textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
+ opt := &axisRenderOption{
+ textMaxWith: textMaxWidth,
+ textMaxHeight: textMaxHeight,
+ boundaryGap: true,
+ }
+ if isFalse(option.BoundaryGap) {
+ opt.boundaryGap = false
+ }
+
+ unitCount := chart.MaxInt(option.SplitNumber, 1)
+ width := a.d.Box.Width()
+ textList := a.data.TextList()
+ count := len(textList)
+
+ position := option.Position
+ switch position {
+ case PositionLeft:
+ fallthrough
+ case PositionRight:
+ default:
+ maxCount := width / (opt.textMaxWith + 10)
+ // 可以显示所有
+ if maxCount >= count {
+ unitCount = 1
+ } else if maxCount < count/unitCount {
+ unitCount = int(math.Ceil(float64(count) / float64(maxCount)))
+ }
+ }
+
+ boundaryGap := opt.boundaryGap
+ modValue := 0
+ if boundaryGap && unitCount > 1 {
+ // 如果是居中,unit count需要设置为奇数
+ if unitCount%2 == 0 {
+ unitCount++
+ }
+ modValue = unitCount / 2
+ }
+ opt.modValue = modValue
+ opt.unitCount = unitCount
+
+ // 坐标轴线
+ a.axisLine(opt)
+ a.axisTick(opt)
+ // 坐标文本
+ a.axisLabel(opt)
}
diff --git a/axis_test.go b/axis_test.go
index 43779e9..37c8314 100644
--- a/axis_test.go
+++ b/axis_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,150 +23,237 @@
package charts
import (
- "strconv"
"testing"
+ "github.com/golang/freetype/truetype"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
)
-func TestGetXAxisAndValues(t *testing.T) {
+func TestAxisOption(t *testing.T) {
assert := assert.New(t)
- genLabels := func(count int) []string {
- arr := make([]string, count)
- for i := 0; i < count; i++ {
- arr[i] = strconv.Itoa(i)
- }
- return arr
- }
- genValues := func(count int, betweenTicks bool) []float64 {
- if betweenTicks {
- count++
- }
- arr := make([]float64, count)
- for i := 0; i < count; i++ {
- arr[i] = float64(i)
- }
- return arr
- }
- genTicks := func(count int, betweenTicks bool) []chart.Tick {
- arr := make([]chart.Tick, 0)
- offset := 0
- if betweenTicks {
- offset = 1
- arr = append(arr, chart.Tick{})
- }
- for i := 0; i < count; i++ {
- arr = append(arr, chart.Tick{
- Value: float64(i + offset),
- Label: strconv.Itoa(i),
- })
- }
- return arr
- }
+ as := AxisOption{}
- tests := []struct {
- xAxis XAxis
- tickPosition chart.TickPosition
- theme string
- result chart.XAxis
- values []float64
- }{
- {
- xAxis: XAxis{
- Data: genLabels(5),
- },
- values: genValues(5, false),
- result: chart.XAxis{
- Ticks: genTicks(5, false),
- },
- },
- // 居中
- {
- xAxis: XAxis{
- Data: genLabels(5),
- },
- tickPosition: chart.TickPositionBetweenTicks,
- // 居中因此value多一个
- values: genValues(5, true),
- result: chart.XAxis{
- Ticks: genTicks(5, true),
- },
- },
- {
- xAxis: XAxis{
- Data: genLabels(20),
- },
- // 居中因此value多一个
- values: genValues(20, false),
- result: chart.XAxis{
- Ticks: []chart.Tick{
- {Value: 0, Label: "0"}, {Value: 2, Label: "2"}, {Value: 4, Label: "4"}, {Value: 6, Label: "6"}, {Value: 8, Label: "8"}, {Value: 10, Label: "10"}, {Value: 12, Label: "12"}, {Value: 14, Label: "14"}, {Value: 16, Label: "16"}, {Value: 18, Label: "18"}, {Value: 19, Label: "19"}},
- },
- },
- }
+ assert.Equal(8, as.GetLabelMargin())
+ as.LabelMargin = 10
+ assert.Equal(10, as.GetLabelMargin())
- for _, tt := range tests {
- xAxis, values := GetXAxisAndValues(tt.xAxis, tt.tickPosition, tt.theme)
+ assert.Equal(5, as.GetTickLength())
+ as.TickLength = 6
+ assert.Equal(6, as.GetTickLength())
- assert.Equal(tt.result.Ticks, xAxis.Ticks)
- assert.Equal(tt.tickPosition, xAxis.TickPosition)
- assert.Equal(tt.values, values)
- }
+ f := &truetype.Font{}
+ style := as.Style(f)
+ assert.Equal(float64(10), style.FontSize)
+ assert.Equal(f, style.Font)
}
-func TestDefaultFloatFormater(t *testing.T) {
+func TestAxisDataList(t *testing.T) {
assert := assert.New(t)
- assert.Equal("", defaultFloatFormater(1))
-
- assert.Equal("0.1", defaultFloatFormater(0.1))
- assert.Equal("0.12", defaultFloatFormater(0.123))
- assert.Equal("10", defaultFloatFormater(10.1))
+ textList := []string{
+ "a",
+ "b",
+ }
+ data := NewAxisDataListFromStringList(textList)
+ assert.Equal(textList, data.TextList())
}
-func TestSetYAxisOption(t *testing.T) {
- assert := assert.New(t)
- min := 10.0
- max := 20.0
- opt := &YAxisOption{
- Formater: func(v interface{}) string {
- return ""
- },
- Min: &min,
- Max: &max,
- }
- yAxis := &chart.YAxis{
- Range: newYContinuousRange(opt),
- }
- setYAxisOption(yAxis, opt)
-
- assert.NotEmpty(yAxis.ValueFormatter)
- assert.Equal(max, yAxis.Range.GetMax())
- assert.Equal(min, yAxis.Range.GetMin())
-}
-
-func TestGetYAxis(t *testing.T) {
+func TestAxis(t *testing.T) {
assert := assert.New(t)
- yAxis := GetYAxis(ThemeDark, nil)
-
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.False(yAxis.Style.Hidden)
-
- yAxis = GetYAxis(ThemeDark, &YAxisOption{
- Disabled: true,
+ axisData := NewAxisDataListFromStringList([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
})
+ getDefaultOption := func() AxisOption {
+ return AxisOption{
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ FontColor: drawing.ColorBlack,
+ Show: TrueFlag(),
+ TickShow: TrueFlag(),
+ SplitLineShow: true,
+ SplitLineColor: drawing.ColorBlack.WithAlpha(60),
+ }
+ }
+ tests := []struct {
+ newOption func() AxisOption
+ newData func() AxisDataList
+ result string
+ }{
+ // 文本按起始位置展示
+ // axis位于bottom
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.BoundaryGap = FalseFlag()
+ return opt
+ },
+ result: "",
+ },
+ // 文本居中展示
+ // axis位于bottom
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ return opt
+ },
+ result: "",
+ },
+ // 文本按起始位置展示
+ // axis位于top
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionTop
+ opt.BoundaryGap = FalseFlag()
+ return opt
+ },
+ result: "",
+ },
+ // 文本居中展示
+ // axis位于top
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionTop
+ return opt
+ },
+ result: "",
+ },
+ // 文本按起始位置展示
+ // axis位于left
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionLeft
+ opt.BoundaryGap = FalseFlag()
+ return opt
+ },
+ result: "",
+ },
+ // 文本居中展示
+ // axis位于left
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionLeft
+ return opt
+ },
+ result: "",
+ },
+ // 文本按起始位置展示
+ // axis位于right
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionRight
+ opt.BoundaryGap = FalseFlag()
+ return opt
+ },
+ result: "",
+ },
+ // 文本居中展示
+ // axis位于right
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionRight
+ return opt
+ },
+ result: "",
+ },
+ // text较多,仅展示部分
+ {
+ newOption: func() AxisOption {
+ opt := getDefaultOption()
+ opt.Position = PositionBottom
+ return opt
+ },
+ newData: func() AxisDataList {
+ return NewAxisDataListFromStringList([]string{
+ "01-01",
+ "01-02",
+ "01-03",
+ "01-04",
+ "01-05",
+ "01-06",
+ "01-07",
+ "01-08",
+ "01-09",
+ "01-10",
+ "01-11",
+ "01-12",
+ "01-13",
+ "01-14",
+ "01-15",
+ "01-16",
+ "01-17",
+ "01-18",
+ "01-19",
+ "01-20",
+ "01-21",
+ })
+ },
+ result: "",
+ },
+ }
+ for _, tt := range tests {
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ }, PaddingOption(chart.Box{
+ Left: 5,
+ Top: 5,
+ Right: 5,
+ Bottom: 5,
+ }))
+ assert.Nil(err)
+ style := tt.newOption()
+ data := axisData
+ if tt.newData != nil {
+ data = tt.newData()
+ }
+ NewAxis(d, data, style).Render()
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.Style.Hidden)
-
- // secondary yAxis
- yAxis = GetSecondaryYAxis(ThemeDark, nil)
- assert.False(yAxis.GridMajorStyle.Hidden)
- assert.False(yAxis.GridMajorStyle.Hidden)
- assert.True(yAxis.Style.StrokeColor.IsTransparent())
+ result, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal(tt.result, string(result))
+ }
+}
+
+func TestMeasureAxis(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+ data := NewAxisDataListFromStringList([]string{
+ "Mon",
+ "Sun",
+ })
+ f, _ := chart.GetDefaultFont()
+ width := NewAxis(d, data, AxisOption{
+ FontSize: 12,
+ Font: f,
+ Position: PositionLeft,
+ }).measure().Width
+ assert.Equal(44, width)
+
+ height := NewAxis(d, data, AxisOption{
+ FontSize: 12,
+ Font: f,
+ Position: PositionTop,
+ }).measure().Height
+ assert.Equal(28, height)
}
diff --git a/line_series.go b/bar.go
similarity index 61%
rename from line_series.go
rename to bar.go
index 93a1479..1090f6b 100644
--- a/line_series.go
+++ b/bar.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -24,26 +24,35 @@ package charts
import (
"github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
)
-type LineSeries struct {
- BaseSeries
+type BarStyle struct {
+ ClassName string
+ StrokeDashArray []float64
+ FillColor drawing.Color
}
-func (ls LineSeries) getXRange(xrange chart.Range) chart.Range {
- if ls.TickPosition != chart.TickPositionBetweenTicks {
- return xrange
+func (bs *BarStyle) Style() chart.Style {
+ return chart.Style{
+ ClassName: bs.ClassName,
+ StrokeDashArray: bs.StrokeDashArray,
+ StrokeColor: bs.FillColor,
+ StrokeWidth: 1,
+ FillColor: bs.FillColor,
}
- // 如果是居中,画线时重新调整
- return wrapRange(xrange, ls.TickPosition)
}
-func (ls LineSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) {
- style := ls.Style.InheritFrom(defaults)
- xrange = ls.getXRange(xrange)
- chart.Draw.LineSeries(r, canvasBox, xrange, yrange, style, ls)
- lr := LabelRenderer{
- Options: ls.Label,
- }
- lr.Render(r, canvasBox, xrange, yrange, style, ls)
+// Bar renders bar for chart
+func (d *Draw) Bar(b chart.Box, style BarStyle) {
+ s := style.Style()
+
+ r := d.Render
+ s.GetFillAndStrokeOptions().WriteToRenderer(r)
+ d.moveTo(b.Left, b.Top)
+ d.lineTo(b.Right, b.Top)
+ d.lineTo(b.Right, b.Bottom)
+ d.lineTo(b.Left, b.Bottom)
+ d.lineTo(b.Left, b.Top)
+ d.Render.FillStroke()
}
diff --git a/bar_chart.go b/bar_chart.go
new file mode 100644
index 0000000..e008baf
--- /dev/null
+++ b/bar_chart.go
@@ -0,0 +1,163 @@
+// 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"
+)
+
+type barChartOption struct {
+ // The series list fo bar chart
+ SeriesList SeriesList
+ // The theme
+ Theme string
+ // The font
+ Font *truetype.Font
+}
+
+func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointRenderOption, error) {
+ d, err := NewDraw(DrawOption{
+ Parent: result.d,
+ }, PaddingOption(chart.Box{
+ Top: result.titleBox.Height(),
+ // TODO 后续考虑是否需要根据左侧是否展示y轴再生成对应的left
+ Left: YAxisWidth,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ xRange := result.xRange
+ x0, x1 := xRange.GetRange(0)
+ width := int(x1 - x0)
+ // 每一块之间的margin
+ margin := 10
+ // 每一个bar之间的margin
+ barMargin := 5
+ if width < 20 {
+ margin = 2
+ barMargin = 2
+ } else if width < 50 {
+ margin = 5
+ barMargin = 3
+ }
+
+ seriesCount := len(opt.SeriesList)
+ // 总的宽度-两个margin-(总数-1)的barMargin
+ barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList)
+
+ barMaxHeight := result.getYRange(0).Size
+ theme := NewTheme(opt.Theme)
+
+ seriesNames := opt.SeriesList.Names()
+
+ r := d.Render
+
+ markPointRenderOptions := make([]markPointRenderOption, 0)
+
+ for i, s := range opt.SeriesList {
+ // 由于series是for range,为同一个数据,因此需要clone
+ // 后续需要使用,如mark point
+ series := s
+ yRange := result.getYRange(series.YAxisIndex)
+ points := make([]Point, len(series.Data))
+ index := series.index
+ if index == 0 {
+ index = i
+ }
+ seriesColor := theme.GetSeriesColor(index)
+ // mark line
+ markLineRender(markLineRenderOption{
+ Draw: d,
+ FillColor: seriesColor,
+ FontColor: theme.GetTextColor(),
+ StrokeColor: seriesColor,
+ Font: opt.Font,
+ Series: &series,
+ Range: yRange,
+ })
+ divideValues := xRange.AutoDivide()
+ for j, item := range series.Data {
+ if j >= len(divideValues) {
+ continue
+ }
+ x := divideValues[j]
+ x += margin
+ if i != 0 {
+ x += i * (barWidth + barMargin)
+ }
+
+ h := int(yRange.getHeight(item.Value))
+ fillColor := seriesColor
+ if !item.Style.FillColor.IsZero() {
+ fillColor = item.Style.FillColor
+ }
+ top := barMaxHeight - h
+ d.Bar(chart.Box{
+ Top: top,
+ Left: x,
+ Right: x + barWidth,
+ Bottom: barMaxHeight - 1,
+ }, BarStyle{
+ FillColor: fillColor,
+ })
+ // 用于生成marker point
+ points[j] = Point{
+ // 居中的位置
+ X: x + barWidth>>1,
+ Y: top,
+ }
+ // 如果label不需要展示,则返回
+ if !series.Label.Show {
+ continue
+ }
+ distance := series.Label.Distance
+ if distance == 0 {
+ distance = 5
+ }
+ text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
+ labelStyle := chart.Style{
+ FontColor: theme.GetTextColor(),
+ FontSize: labelFontSize,
+ Font: opt.Font,
+ }
+ if !series.Label.Color.IsZero() {
+ labelStyle.FontColor = series.Label.Color
+ }
+ labelStyle.GetTextOptions().WriteToRenderer(r)
+ textBox := r.MeasureText(text)
+ d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance)
+ }
+
+ // 生成mark point的参数
+ markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{
+ Draw: d,
+ FillColor: seriesColor,
+ Font: opt.Font,
+ Points: points,
+ Series: &series,
+ })
+ }
+
+ return markPointRenderOptions, nil
+}
diff --git a/bar_chart_test.go b/bar_chart_test.go
new file mode 100644
index 0000000..f10a1bc
--- /dev/null
+++ b/bar_chart_test.go
@@ -0,0 +1,131 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestBarChartRender(t *testing.T) {
+ assert := assert.New(t)
+
+ width := 400
+ height := 300
+ d, err := NewDraw(DrawOption{
+ Width: width,
+ Height: height,
+ })
+ assert.Nil(err)
+
+ result := basicRenderResult{
+ xRange: &Range{
+ Min: 0,
+ Max: 4,
+ divideCount: 4,
+ Size: width,
+ Boundary: true,
+ },
+ yRangeList: []*Range{
+ {
+ divideCount: 6,
+ Max: 100,
+ Min: 0,
+ Size: height,
+ },
+ },
+ d: d,
+ }
+ f, _ := chart.GetDefaultFont()
+
+ markPointOptions, err := barChartRender(barChartOption{
+ Font: f,
+ SeriesList: SeriesList{
+ {
+ Label: SeriesLabel{
+ Show: true,
+ Color: drawing.ColorBlue,
+ },
+ MarkLine: NewMarkLine(
+ SeriesMarkDataTypeMin,
+ ),
+ Data: []SeriesData{
+ {
+ Value: 20,
+ },
+ {
+ Value: 60,
+ Style: chart.Style{
+ FillColor: drawing.ColorRed,
+ },
+ },
+ {
+ Value: 90,
+ },
+ },
+ },
+ NewSeriesFromValues([]float64{
+ 80,
+ 30,
+ 70,
+ }),
+ },
+ }, &result)
+ assert.Nil(err)
+ assert.Equal(2, len(markPointOptions))
+ assert.Equal([]Point{
+ {
+ X: 28,
+ Y: 240,
+ },
+ {
+ X: 128,
+ Y: 120,
+ },
+ {
+ X: 228,
+ Y: 30,
+ },
+ }, markPointOptions[0].Points)
+ assert.Equal([]Point{
+ {
+ X: 70,
+ Y: 60,
+ },
+ {
+ X: 170,
+ Y: 210,
+ },
+ {
+ X: 270,
+ Y: 90,
+ },
+ }, markPointOptions[1].Points)
+
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/bar_series.go b/bar_series.go
deleted file mode 100644
index b9e8fc1..0000000
--- a/bar_series.go
+++ /dev/null
@@ -1,148 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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/dustin/go-humanize"
- "github.com/wcharczuk/go-chart/v2"
-)
-
-const defaultBarMargin = 10
-
-type BarSeriesCustomStyle struct {
- PointIndex int
- Index int
- Style chart.Style
-}
-
-type BarSeries struct {
- BaseSeries
- Count int
- Index int
- // 间隔
- Margin int
- // 偏移量
- Offset int
- // 宽度
- BarWidth int
- CustomStyles []BarSeriesCustomStyle
-}
-
-type barSeriesWidthValues struct {
- columnWidth int
- columnMargin int
- margin int
- barWidth int
-}
-
-func (bs BarSeries) GetBarStyle(index, pointIndex int) chart.Style {
- // 指定样式
- for _, item := range bs.CustomStyles {
- if item.Index == index && item.PointIndex == pointIndex {
- return item.Style
- }
- }
- // 其它非指定样式
- return chart.Style{}
-}
-
-func (bs BarSeries) getWidthValues(width int) barSeriesWidthValues {
- columnWidth := width / bs.Len()
- // 块间隔
- columnMargin := columnWidth / 10
- minColumnMargin := 2
- if columnMargin < minColumnMargin {
- columnMargin = minColumnMargin
- }
- margin := bs.Margin
- if margin <= 0 {
- margin = defaultBarMargin
- }
- // 如果margin大于column margin
- if margin > columnMargin {
- margin = columnMargin
- }
-
- allBarMarginWidth := (bs.Count - 1) * margin
- barWidth := ((columnWidth - 2*columnMargin) - allBarMarginWidth) / bs.Count
- if bs.BarWidth > 0 && bs.BarWidth < barWidth {
- barWidth = bs.BarWidth
- // 重新计息columnMargin
- columnMargin = (columnWidth - allBarMarginWidth - (bs.Count * barWidth)) / 2
- }
- return barSeriesWidthValues{
- columnWidth: columnWidth,
- columnMargin: columnMargin,
- margin: margin,
- barWidth: barWidth,
- }
-}
-
-func (bs BarSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) {
- if bs.Len() == 0 || bs.Count <= 0 {
- return
- }
- style := bs.Style.InheritFrom(defaults)
- style.FillColor = style.StrokeColor
- if !style.ShouldDrawStroke() {
- return
- }
-
- cb := canvasBox.Bottom
- cl := canvasBox.Left
- widthValues := bs.getWidthValues(canvasBox.Width())
- labelValues := make([]LabelValue, 0)
-
- for i := 0; i < bs.Len(); i++ {
- vx, vy := bs.GetValues(i)
- customStyle := bs.GetBarStyle(bs.Index, i)
- cloneStyle := style
- if !customStyle.IsZero() {
- cloneStyle.FillColor = customStyle.FillColor
- cloneStyle.StrokeColor = customStyle.StrokeColor
- }
-
- x := cl + xrange.Translate(vx)
- // 由于bar是居中展示,因此需要往前移一个显示块
- x += (-widthValues.columnWidth + widthValues.columnMargin)
- // 计算是第几个bar,位置右偏
- x += bs.Index * (widthValues.margin + widthValues.barWidth)
- y := cb - yrange.Translate(vy)
-
- chart.Draw.Box(r, chart.Box{
- Left: x,
- Top: y,
- Right: x + widthValues.barWidth,
- Bottom: canvasBox.Bottom - 1,
- }, cloneStyle)
- labelValues = append(labelValues, LabelValue{
- Left: x + widthValues.barWidth/2,
- Top: y,
- Text: humanize.CommafWithDigits(vy, 2),
- })
- }
- lr := LabelRenderer{
- Options: bs.Label,
- }
- lr.CustomizeRender(r, style, labelValues)
-}
diff --git a/bar_series_test.go b/bar_series_test.go
deleted file mode 100644
index 8703367..0000000
--- a/bar_series_test.go
+++ /dev/null
@@ -1,166 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
- "github.com/wcharczuk/go-chart/v2/drawing"
-)
-
-func TestBarSeries(t *testing.T) {
- assert := assert.New(t)
-
- customStyle := chart.Style{
- StrokeColor: drawing.ColorBlue,
- }
- bs := BarSeries{
- CustomStyles: []BarSeriesCustomStyle{
- {
- PointIndex: 1,
- Style: customStyle,
- },
- },
- }
-
- assert.Equal(customStyle, bs.GetBarStyle(0, 1))
-
- assert.True(bs.GetBarStyle(1, 0).IsZero())
-}
-
-func TestBarSeriesGetWidthValues(t *testing.T) {
- assert := assert.New(t)
-
- bs := BarSeries{
- Count: 1,
- BaseSeries: BaseSeries{
- XValues: []float64{
- 1,
- 2,
- 3,
- },
- },
- }
- widthValues := bs.getWidthValues(300)
- assert.Equal(barSeriesWidthValues{
- columnWidth: 100,
- columnMargin: 10,
- margin: 10,
- barWidth: 80,
- }, widthValues)
-
- // 指定margin
- bs.Margin = 5
- widthValues = bs.getWidthValues(300)
- assert.Equal(barSeriesWidthValues{
- columnWidth: 100,
- columnMargin: 10,
- margin: 5,
- barWidth: 80,
- }, widthValues)
-
- // 指定bar的宽度
- bs.BarWidth = 60
- widthValues = bs.getWidthValues(300)
- assert.Equal(barSeriesWidthValues{
- columnWidth: 100,
- columnMargin: 20,
- margin: 5,
- barWidth: 60,
- }, widthValues)
-}
-
-func TestBarSeriesRender(t *testing.T) {
- assert := assert.New(t)
-
- width := 800
- height := 400
-
- r, err := chart.SVG(width, height)
- assert.Nil(err)
-
- bs := BarSeries{
- Count: 1,
- CustomStyles: []BarSeriesCustomStyle{
- {
- Index: 0,
- PointIndex: 1,
- Style: chart.Style{
- StrokeColor: SeriesColorsLight[1],
- },
- },
- },
- BaseSeries: BaseSeries{
- TickPosition: chart.TickPositionBetweenTicks,
- Style: chart.Style{
- StrokeColor: SeriesColorsLight[0],
- StrokeWidth: 1,
- },
- XValues: []float64{
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- },
- YValues: []float64{
- // 第一个点为占位点
- 0,
- 120,
- 200,
- 150,
- 80,
- 70,
- 110,
- 130,
- },
- },
- }
- xrange := &chart.ContinuousRange{
- Min: 0,
- Max: 7,
- Domain: 753,
- }
- yrange := &chart.ContinuousRange{
- Min: 70,
- Max: 200,
- Domain: 362,
- }
- bs.Render(r, chart.Box{
- Top: 11,
- Left: 42,
- Right: 795,
- Bottom: 373,
- }, xrange, yrange, chart.Style{})
-
- buffer := bytes.Buffer{}
- err = r.Save(&buffer)
- assert.Nil(err)
- assert.Equal("", buffer.String())
-}
diff --git a/bar_test.go b/bar_test.go
new file mode 100644
index 0000000..01b6d3c
--- /dev/null
+++ b/bar_test.go
@@ -0,0 +1,78 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestBarStyle(t *testing.T) {
+ assert := assert.New(t)
+
+ bs := BarStyle{
+ ClassName: "test",
+ StrokeDashArray: []float64{
+ 1.0,
+ },
+ FillColor: drawing.ColorBlack,
+ }
+
+ assert.Equal(chart.Style{
+ ClassName: "test",
+ StrokeDashArray: []float64{
+ 1.0,
+ },
+ StrokeWidth: 1,
+ FillColor: drawing.ColorBlack,
+ StrokeColor: drawing.ColorBlack,
+ }, bs.Style())
+}
+
+func TestDrawBar(t *testing.T) {
+ assert := assert.New(t)
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ }, PaddingOption(chart.Box{
+ Left: 10,
+ Top: 20,
+ Right: 30,
+ Bottom: 40,
+ }))
+ assert.Nil(err)
+ d.Bar(chart.Box{
+ Left: 0,
+ Top: 0,
+ Right: 20,
+ Bottom: 200,
+ }, BarStyle{
+ FillColor: drawing.ColorBlack,
+ })
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/base_series.go b/base_series.go
deleted file mode 100644
index 37e3689..0000000
--- a/base_series.go
+++ /dev/null
@@ -1,140 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "fmt"
-
- "github.com/wcharczuk/go-chart/v2"
-)
-
-// Interface Assertions.
-var (
- _ chart.Series = (*BaseSeries)(nil)
- _ chart.FirstValuesProvider = (*BaseSeries)(nil)
- _ chart.LastValuesProvider = (*BaseSeries)(nil)
-)
-
-type SeriesLabel struct {
- Show bool
- Offset chart.Box
-}
-
-// BaseSeries represents a line on a chart.
-type BaseSeries struct {
- Name string
- Style chart.Style
- TickPosition chart.TickPosition
-
- YAxis chart.YAxisType
-
- XValueFormatter chart.ValueFormatter
- YValueFormatter chart.ValueFormatter
-
- XValues []float64
- YValues []float64
-
- Label SeriesLabel
-}
-
-// GetName returns the name of the time series.
-func (bs BaseSeries) GetName() string {
- return bs.Name
-}
-
-// GetStyle returns the line style.
-func (bs BaseSeries) GetStyle() chart.Style {
- return bs.Style
-}
-
-// Len returns the number of elements in the series.
-func (bs BaseSeries) Len() int {
- offset := 0
- if bs.TickPosition == chart.TickPositionBetweenTicks {
- offset = -1
- }
- return len(bs.XValues) + offset
-}
-
-// GetValues gets the x,y values at a given index.
-func (bs BaseSeries) GetValues(index int) (float64, float64) {
- if bs.TickPosition == chart.TickPositionBetweenTicks {
- index++
- }
- return bs.XValues[index], bs.YValues[index]
-}
-
-// GetFirstValues gets the first x,y values.
-func (bs BaseSeries) GetFirstValues() (float64, float64) {
- index := 0
- if bs.TickPosition == chart.TickPositionBetweenTicks {
- index++
- }
- return bs.XValues[index], bs.YValues[index]
-}
-
-// GetLastValues gets the last x,y values.
-func (bs BaseSeries) GetLastValues() (float64, float64) {
- return bs.XValues[len(bs.XValues)-1], bs.YValues[len(bs.YValues)-1]
-}
-
-// GetValueFormatters returns value formatter defaults for the series.
-func (bs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) {
- if bs.XValueFormatter != nil {
- x = bs.XValueFormatter
- } else {
- x = chart.FloatValueFormatter
- }
- if bs.YValueFormatter != nil {
- y = bs.YValueFormatter
- } else {
- y = chart.FloatValueFormatter
- }
- return
-}
-
-// GetYAxis returns which YAxis the series draws on.
-func (bs BaseSeries) GetYAxis() chart.YAxisType {
- return bs.YAxis
-}
-
-// Render renders the series.
-func (bs BaseSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) {
- fmt.Println("should be override the function")
-}
-
-// Validate validates the series.
-func (bs BaseSeries) Validate() error {
- if len(bs.XValues) == 0 {
- return fmt.Errorf("continuous series; must have xvalues set")
- }
-
- if len(bs.YValues) == 0 {
- return fmt.Errorf("continuous series; must have yvalues set")
- }
-
- if len(bs.XValues) != len(bs.YValues) {
- return fmt.Errorf("continuous series; must have same length xvalues as yvalues")
- }
- return nil
-}
diff --git a/base_series_test.go b/base_series_test.go
deleted file mode 100644
index 0c9b5d1..0000000
--- a/base_series_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "reflect"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
-)
-
-func TestBaseSeries(t *testing.T) {
- assert := assert.New(t)
-
- bs := BaseSeries{
- XValues: []float64{
- 1,
- 2,
- 3,
- },
- YValues: []float64{
- 10,
- 20,
- 30,
- },
- }
- assert.Equal(3, bs.Len())
- bs.TickPosition = chart.TickPositionBetweenTicks
- assert.Equal(2, bs.Len())
-
- bs.TickPosition = chart.TickPositionUnset
- x, y := bs.GetValues(1)
- assert.Equal(float64(2), x)
- assert.Equal(float64(20), y)
- bs.TickPosition = chart.TickPositionBetweenTicks
- x, y = bs.GetValues(1)
- assert.Equal(float64(3), x)
- assert.Equal(float64(30), y)
-
- bs.TickPosition = chart.TickPositionUnset
- x, y = bs.GetFirstValues()
- assert.Equal(float64(1), x)
- assert.Equal(float64(10), y)
- bs.TickPosition = chart.TickPositionBetweenTicks
- x, y = bs.GetFirstValues()
- assert.Equal(float64(2), x)
- assert.Equal(float64(20), y)
-
- bs.TickPosition = chart.TickPositionUnset
- x, y = bs.GetLastValues()
- assert.Equal(float64(3), x)
- assert.Equal(float64(30), y)
- bs.TickPosition = chart.TickPositionBetweenTicks
- x, y = bs.GetLastValues()
- assert.Equal(float64(3), x)
- assert.Equal(float64(30), y)
-
- xFormater, yFormater := bs.GetValueFormatters()
- assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(xFormater).Pointer())
- assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(yFormater).Pointer())
- formater := func(v interface{}) string {
- return ""
- }
- bs.XValueFormatter = formater
- bs.YValueFormatter = formater
- xFormater, yFormater = bs.GetValueFormatters()
- assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(xFormater).Pointer())
- assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(yFormater).Pointer())
-
- assert.Equal(chart.YAxisPrimary, bs.GetYAxis())
-
- assert.Nil(bs.Validate())
-}
diff --git a/chart.go b/chart.go
new file mode 100644
index 0000000..5178b04
--- /dev/null
+++ b/chart.go
@@ -0,0 +1,450 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "errors"
+ "math"
+ "strings"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+const (
+ ChartTypeLine = "line"
+ ChartTypeBar = "bar"
+ ChartTypePie = "pie"
+)
+
+const (
+ ChartOutputSVG = "svg"
+ ChartOutputPNG = "png"
+)
+
+type Point struct {
+ X int
+ Y int
+}
+
+const labelFontSize = 10
+
+var defaultChartWidth = 600
+var defaultChartHeight = 400
+
+type ChartOption struct {
+ // The output type of chart, "svg" or "png", default value is "svg"
+ Type string
+ // The font family, which should be installed first
+ FontFamily string
+ // The font of chart, the default font is "roboto"
+ Font *truetype.Font
+ // The theme of chart, "light" and "dark".
+ // The default theme is "light"
+ Theme string
+ // The title option
+ Title TitleOption
+ // The legend option
+ Legend LegendOption
+ // The x axis option
+ XAxis XAxisOption
+ // The y axis option list
+ YAxisList []YAxisOption
+ // The width of chart, default width is 600
+ Width int
+ // The height of chart, default height is 400
+ Height int
+ Parent *Draw
+ // The padding for chart, default padding is [20, 10, 10, 10]
+ Padding chart.Box
+ // The canvas box for chart
+ Box chart.Box
+ // The series list
+ SeriesList SeriesList
+ // The background color of chart
+ BackgroundColor drawing.Color
+ // The child charts
+ Children []ChartOption
+}
+
+// FillDefault fills the default value for chart option
+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()
+ }
+ if o.BackgroundColor.IsZero() {
+ o.BackgroundColor = t.GetBackgroundColor()
+ }
+ if o.Padding.IsZero() {
+ o.Padding = chart.Box{
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
+ }
+ }
+
+ // 标题的默认值
+ if o.Title.Style.FontColor.IsZero() {
+ o.Title.Style.FontColor = t.GetTextColor()
+ }
+ if o.Title.Style.FontSize == 0 {
+ o.Title.Style.FontSize = 14
+ }
+ if o.Title.Style.Font == nil {
+ o.Title.Style.Font = o.Font
+ }
+ if o.Title.Style.Padding.IsZero() {
+ o.Title.Style.Padding = chart.Box{
+ Bottom: 10,
+ }
+ }
+ // 副标题
+ if o.Title.SubtextStyle.FontColor.IsZero() {
+ o.Title.SubtextStyle.FontColor = o.Title.Style.FontColor.WithAlpha(180)
+ }
+ if o.Title.SubtextStyle.FontSize == 0 {
+ o.Title.SubtextStyle.FontSize = labelFontSize
+ }
+ if o.Title.SubtextStyle.Font == nil {
+ o.Title.SubtextStyle.Font = o.Font
+ }
+
+ o.Legend.theme = theme
+ if o.Legend.Style.FontSize == 0 {
+ o.Legend.Style.FontSize = labelFontSize
+ }
+ if o.Legend.Left == "" {
+ o.Legend.Left = PositionCenter
+ }
+ // legend与series name的关联
+ if len(o.Legend.Data) == 0 {
+ o.Legend.Data = o.SeriesList.Names()
+ } else {
+ seriesCount := len(o.SeriesList)
+ for index, name := range o.Legend.Data {
+ if index < seriesCount {
+ o.SeriesList[index].Name = name
+ }
+ }
+ }
+ // 如果无legend数据,则隐藏
+ if len(strings.Join(o.Legend.Data, "")) == 0 {
+ o.Legend.Show = FalseFlag()
+ }
+ if o.Legend.Style.Font == nil {
+ o.Legend.Style.Font = o.Font
+ }
+ if o.Legend.Style.FontColor.IsZero() {
+ o.Legend.Style.FontColor = t.GetTextColor()
+ }
+ if o.XAxis.Theme == "" {
+ o.XAxis.Theme = theme
+ }
+}
+
+func (o *ChartOption) getWidth() int {
+ if o.Width != 0 {
+ return o.Width
+ }
+ if o.Parent != nil {
+ return o.Parent.Box.Width()
+ }
+ return defaultChartWidth
+}
+
+func SetDefaultWidth(width int) {
+ if width > 0 {
+ defaultChartWidth = width
+ }
+}
+func SetDefaultHeight(height int) {
+ if height > 0 {
+ defaultChartHeight = height
+ }
+}
+
+func (o *ChartOption) getHeight() int {
+
+ if o.Height != 0 {
+ return o.Height
+ }
+ if o.Parent != nil {
+ return o.Parent.Box.Height()
+ }
+ return defaultChartHeight
+}
+
+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 {
+ continue
+ }
+ for _, item := range series.Data {
+ if item.Value > max {
+ max = item.Value
+ }
+ if item.Value < min {
+ min = item.Value
+ }
+ }
+ }
+ min = min * 0.9
+ max = max * 1.1
+ if yAxis.Min != nil {
+ min = *yAxis.Min
+ }
+ if yAxis.Max != nil {
+ max = *yAxis.Max
+ }
+ divideCount := 6
+ // y轴分设置默认划分为6块
+ r := NewRange(min, max, divideCount)
+
+ // 由于NewRange会重新计算min max
+ if yAxis.Min != nil {
+ r.Min = min
+ }
+ if yAxis.Max != nil {
+ r.Max = max
+ }
+
+ return r
+}
+
+type basicRenderResult struct {
+ xRange *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]
+}
+
+// Render renders the chart by option
+func Render(opt ChartOption) (*Draw, error) {
+ if len(opt.SeriesList) == 0 {
+ return nil, errors.New("series can not be nil")
+ }
+ if len(opt.FontFamily) != 0 {
+ f, err := GetFont(opt.FontFamily)
+ if err != nil {
+ return nil, err
+ }
+ opt.Font = f
+ }
+ opt.FillDefault(opt.Theme)
+
+ lineSeries := make([]Series, 0)
+ barSeries := make([]Series, 0)
+ isPieChart := false
+ for index, item := range opt.SeriesList {
+ item.index = index
+ switch item.Type {
+ case ChartTypePie:
+ isPieChart = true
+ case ChartTypeBar:
+ barSeries = append(barSeries, item)
+ default:
+ lineSeries = append(lineSeries, item)
+ }
+ }
+ // 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
+ // pie不需要axis
+ if isPieChart {
+ opt.XAxis.Hidden = true
+ for index := range opt.YAxisList {
+ opt.YAxisList[index].Hidden = true
+ }
+ }
+ result, err := chartBasicRender(&opt)
+ if err != nil {
+ return nil, err
+ }
+ markPointRenderOptions := make([]markPointRenderOption, 0)
+ fns := []func() error{
+ // pie render
+ func() error {
+ if !isPieChart {
+ return nil
+ }
+ err := pieChartRender(pieChartOption{
+ SeriesList: opt.SeriesList,
+ Theme: opt.Theme,
+ Font: opt.Font,
+ }, result)
+ return err
+ },
+ // bar render
+ func() error {
+ // 如果是pie或者无bar类型的series
+ if isPieChart || len(barSeries) == 0 {
+ return nil
+ }
+ options, err := barChartRender(barChartOption{
+ SeriesList: barSeries,
+ Theme: opt.Theme,
+ Font: opt.Font,
+ }, result)
+ if err != nil {
+ return err
+ }
+ markPointRenderOptions = append(markPointRenderOptions, options...)
+ return nil
+ },
+ // line render
+ func() error {
+ // 如果是pie或者无line类型的series
+ if isPieChart || len(lineSeries) == 0 {
+ return nil
+ }
+ options, err := lineChartRender(lineChartOption{
+ Theme: opt.Theme,
+ SeriesList: lineSeries,
+ Font: opt.Font,
+ }, result)
+ if err != nil {
+ return err
+ }
+ markPointRenderOptions = append(markPointRenderOptions, options...)
+ return nil
+ },
+ // legend需要在顶层,因此此处render
+ func() error {
+ _, err := NewLegend(result.d, opt.Legend).Render()
+ return err
+ },
+ // mark point最后render
+ func() error {
+ // mark point render不会出错
+ for _, opt := range markPointRenderOptions {
+ markPointRender(opt)
+ }
+ return nil
+ },
+ }
+
+ for _, fn := range fns {
+ err = fn()
+ if err != nil {
+ return nil, err
+ }
+ }
+ for _, child := range opt.Children {
+ child.Parent = result.d
+ if len(child.Theme) == 0 {
+ child.Theme = opt.Theme
+ }
+ _, err = Render(child)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return result.d, nil
+}
+
+func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
+ d, err := NewDraw(
+ DrawOption{
+ Type: opt.Type,
+ Parent: opt.Parent,
+ Width: opt.getWidth(),
+ Height: opt.getHeight(),
+ },
+ PaddingOption(opt.Padding),
+ BoxOption(opt.Box),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ 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)
+ }
+
+ // 标题
+ titleBox, err := drawTitle(d, &opt.Title)
+ if err != nil {
+ return nil, err
+ }
+
+ xAxisHeight := 0
+ var xRange *Range
+
+ if !opt.XAxis.Hidden {
+ // xAxis
+ xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis, len(opt.YAxisList))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ yRangeList := make([]*Range, len(opt.YAxisList))
+
+ for index, yAxis := range opt.YAxisList {
+ var yRange *Range
+ 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,
+ yRangeList: yRangeList,
+ d: d,
+ titleBox: titleBox,
+ }, nil
+}
diff --git a/chart_test.go b/chart_test.go
new file mode 100644
index 0000000..4fc3d20
--- /dev/null
+++ b/chart_test.go
@@ -0,0 +1,504 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestChartSetDefaultWidthHeight(t *testing.T) {
+ assert := assert.New(t)
+
+ width := defaultChartWidth
+ height := defaultChartHeight
+ defer SetDefaultWidth(width)
+ defer SetDefaultHeight(height)
+
+ SetDefaultWidth(60)
+ assert.Equal(60, defaultChartWidth)
+ SetDefaultHeight(40)
+ assert.Equal(40, defaultChartHeight)
+}
+
+func TestChartFillDefault(t *testing.T) {
+ assert := assert.New(t)
+ // default value
+ opt := ChartOption{}
+ opt.FillDefault("")
+ // padding
+ assert.Equal(chart.Box{
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
+ }, opt.Padding)
+ // background color
+ assert.Equal(drawing.ColorWhite, opt.BackgroundColor)
+ // title font color
+ assert.Equal(drawing.Color{
+ R: 70,
+ G: 70,
+ B: 70,
+ A: 255,
+ }, opt.Title.Style.FontColor)
+ // title font size
+ assert.Equal(float64(14), opt.Title.Style.FontSize)
+ // sub title font color
+ assert.Equal(drawing.Color{
+ R: 70,
+ G: 70,
+ B: 70,
+ A: 180,
+ }, opt.Title.SubtextStyle.FontColor)
+ // sub title font size
+ assert.Equal(float64(10), opt.Title.SubtextStyle.FontSize)
+ // legend font size
+ assert.Equal(float64(10), opt.Legend.Style.FontSize)
+ // legend position
+ assert.Equal("center", opt.Legend.Left)
+ assert.Equal(drawing.Color{
+ R: 70,
+ G: 70,
+ B: 70,
+ A: 255,
+ }, opt.Legend.Style.FontColor)
+
+ // y axis
+ opt = ChartOption{
+ SeriesList: SeriesList{
+ {
+ YAxisIndex: 1,
+ },
+ },
+ }
+ opt.FillDefault("")
+ assert.Equal([]YAxisOption{
+ {},
+ {},
+ }, opt.YAxisList)
+ opt = ChartOption{}
+ opt.FillDefault("")
+ assert.Equal([]YAxisOption{
+ {},
+ }, opt.YAxisList)
+
+ // legend get from series's name
+
+ opt = ChartOption{
+ SeriesList: SeriesList{
+ {
+ Name: "a",
+ },
+ {
+ Name: "b",
+ },
+ },
+ }
+ opt.FillDefault("")
+ assert.Equal([]string{
+ "a",
+ "b",
+ }, opt.Legend.Data)
+ // series name set by legend
+ opt = ChartOption{
+ Legend: LegendOption{
+ Data: []string{
+ "a",
+ "b",
+ },
+ },
+ SeriesList: SeriesList{
+ {},
+ {},
+ },
+ }
+ opt.FillDefault("")
+ assert.Equal("a", opt.SeriesList[0].Name)
+ assert.Equal("b", opt.SeriesList[1].Name)
+}
+
+func TestChartGetWidthHeight(t *testing.T) {
+ assert := assert.New(t)
+
+ opt := ChartOption{
+ Width: 10,
+ }
+ assert.Equal(10, opt.getWidth())
+ opt.Width = 0
+ assert.Equal(600, opt.getWidth())
+ opt.Parent = &Draw{
+ Box: chart.Box{
+ Left: 10,
+ Right: 50,
+ },
+ }
+ assert.Equal(40, opt.getWidth())
+
+ opt = ChartOption{
+ Height: 20,
+ }
+ assert.Equal(20, opt.getHeight())
+ opt.Height = 0
+ assert.Equal(400, opt.getHeight())
+ opt.Parent = &Draw{
+ Box: chart.Box{
+ Top: 20,
+ Bottom: 80,
+ },
+ }
+ assert.Equal(60, opt.getHeight())
+}
+
+func TestChartRender(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := Render(ChartOption{
+ Width: 800,
+ Height: 600,
+ Legend: LegendOption{
+ Top: "-90",
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Padding: chart.Box{
+ Top: 100,
+ },
+ XAxis: NewXAxisOption([]string{
+ "2012",
+ "2013",
+ "2014",
+ "2015",
+ "2016",
+ "2017",
+ }),
+ YAxisList: []YAxisOption{
+ {
+
+ Min: NewFloatPoint(0),
+ Max: NewFloatPoint(90),
+ },
+ },
+ SeriesList: []Series{
+ NewSeriesFromValues([]float64{
+ 56.5,
+ 82.1,
+ 88.7,
+ 70.1,
+ 53.4,
+ 85.1,
+ }),
+ NewSeriesFromValues([]float64{
+ 51.1,
+ 51.4,
+ 55.1,
+ 53.3,
+ 73.8,
+ 68.7,
+ }),
+ NewSeriesFromValues([]float64{
+ 40.1,
+ 62.2,
+ 69.5,
+ 36.4,
+ 45.2,
+ 32.5,
+ }, ChartTypeBar),
+ NewSeriesFromValues([]float64{
+ 25.2,
+ 37.1,
+ 41.2,
+ 18,
+ 33.9,
+ 49.1,
+ }, ChartTypeBar),
+ },
+ Children: []ChartOption{
+ {
+ Legend: LegendOption{
+ Show: FalseFlag(),
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Box: chart.Box{
+ Top: 20,
+ Left: 400,
+ Right: 500,
+ Bottom: 120,
+ },
+ SeriesList: NewPieSeriesList([]float64{
+ 435.9,
+ 354.3,
+ 285.9,
+ 204.5,
+ }, PieSeriesOption{
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "35%",
+ }),
+ },
+ },
+ })
+ assert.Nil(err)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func BenchmarkMultiChartPNGRender(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ opt := ChartOption{
+ Type: ChartOutputPNG,
+ Legend: LegendOption{
+ Top: "-90",
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Padding: chart.Box{
+ Top: 100,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
+ },
+ XAxis: NewXAxisOption([]string{
+ "2012",
+ "2013",
+ "2014",
+ "2015",
+ "2016",
+ "2017",
+ }),
+ YAxisList: []YAxisOption{
+ {
+
+ Min: NewFloatPoint(0),
+ Max: NewFloatPoint(90),
+ },
+ },
+ SeriesList: []Series{
+ NewSeriesFromValues([]float64{
+ 56.5,
+ 82.1,
+ 88.7,
+ 70.1,
+ 53.4,
+ 85.1,
+ }),
+ NewSeriesFromValues([]float64{
+ 51.1,
+ 51.4,
+ 55.1,
+ 53.3,
+ 73.8,
+ 68.7,
+ }),
+ NewSeriesFromValues([]float64{
+ 40.1,
+ 62.2,
+ 69.5,
+ 36.4,
+ 45.2,
+ 32.5,
+ }, ChartTypeBar),
+ NewSeriesFromValues([]float64{
+ 25.2,
+ 37.1,
+ 41.2,
+ 18,
+ 33.9,
+ 49.1,
+ }, ChartTypeBar),
+ },
+ Children: []ChartOption{
+ {
+ Legend: LegendOption{
+ Show: FalseFlag(),
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Box: chart.Box{
+ Top: 20,
+ Left: 400,
+ Right: 500,
+ Bottom: 120,
+ },
+ SeriesList: NewPieSeriesList([]float64{
+ 435.9,
+ 354.3,
+ 285.9,
+ 204.5,
+ }, PieSeriesOption{
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "35%",
+ }),
+ },
+ },
+ }
+ d, err := Render(opt)
+ if err != nil {
+ panic(err)
+ }
+ buf, err := d.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ if len(buf) == 0 {
+ panic(errors.New("data is nil"))
+ }
+ }
+}
+
+func BenchmarkMultiChartSVGRender(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ opt := ChartOption{
+ Legend: LegendOption{
+ Top: "-90",
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Padding: chart.Box{
+ Top: 100,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
+ },
+ XAxis: NewXAxisOption([]string{
+ "2012",
+ "2013",
+ "2014",
+ "2015",
+ "2016",
+ "2017",
+ }),
+ YAxisList: []YAxisOption{
+ {
+
+ Min: NewFloatPoint(0),
+ Max: NewFloatPoint(90),
+ },
+ },
+ SeriesList: []Series{
+ NewSeriesFromValues([]float64{
+ 56.5,
+ 82.1,
+ 88.7,
+ 70.1,
+ 53.4,
+ 85.1,
+ }),
+ NewSeriesFromValues([]float64{
+ 51.1,
+ 51.4,
+ 55.1,
+ 53.3,
+ 73.8,
+ 68.7,
+ }),
+ NewSeriesFromValues([]float64{
+ 40.1,
+ 62.2,
+ 69.5,
+ 36.4,
+ 45.2,
+ 32.5,
+ }, ChartTypeBar),
+ NewSeriesFromValues([]float64{
+ 25.2,
+ 37.1,
+ 41.2,
+ 18,
+ 33.9,
+ 49.1,
+ }, ChartTypeBar),
+ },
+ Children: []ChartOption{
+ {
+ Legend: LegendOption{
+ Show: FalseFlag(),
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Box: chart.Box{
+ Top: 20,
+ Left: 400,
+ Right: 500,
+ Bottom: 120,
+ },
+ SeriesList: NewPieSeriesList([]float64{
+ 435.9,
+ 354.3,
+ 285.9,
+ 204.5,
+ }, PieSeriesOption{
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "35%",
+ }),
+ },
+ },
+ }
+ d, err := Render(opt)
+ if err != nil {
+ panic(err)
+ }
+ buf, err := d.Bytes()
+ if err != nil {
+ panic(err)
+ }
+ if len(buf) == 0 {
+ panic(errors.New("data is nil"))
+ }
+ }
+}
diff --git a/charts.go b/charts.go
deleted file mode 100644
index 5957fdb..0000000
--- a/charts.go
+++ /dev/null
@@ -1,292 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 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 (
- "bytes"
- "errors"
- "io"
- "sync"
-
- "github.com/dustin/go-humanize"
- "github.com/golang/freetype/truetype"
- "github.com/wcharczuk/go-chart/v2"
- "github.com/wcharczuk/go-chart/v2/drawing"
-)
-
-const (
- ThemeLight = "light"
- ThemeDark = "dark"
-)
-
-const (
- DefaultChartWidth = 800
- DefaultChartHeight = 400
-)
-
-type (
- Title struct {
- Text string
- Style chart.Style
- Font *truetype.Font
- Left string
- Top string
- }
- Legend struct {
- Data []string
- Align string
- Padding chart.Box
- Left string
- Right string
- Top string
- Bottom string
- }
- Options struct {
- Padding chart.Box
- Width int
- Height int
- Theme string
- XAxis XAxis
- YAxisOptions []*YAxisOption
- Series []Series
- Title Title
- Legend Legend
- TickPosition chart.TickPosition
- Log chart.Logger
- Font *truetype.Font
- }
-)
-
-var fonts = sync.Map{}
-var ErrFontNotExists = errors.New("font is not exists")
-
-// InstallFont installs the font for charts
-func InstallFont(fontFamily string, data []byte) error {
- font, err := truetype.Parse(data)
- if err != nil {
- return err
- }
- fonts.Store(fontFamily, font)
- return nil
-}
-
-// GetFont returns the font of font family
-func GetFont(fontFamily string) (*truetype.Font, error) {
- value, ok := fonts.Load(fontFamily)
- if !ok {
- return nil, ErrFontNotExists
- }
- f, ok := value.(*truetype.Font)
- if !ok {
- return nil, ErrFontNotExists
- }
- return f, nil
-}
-
-type Graph interface {
- Render(rp chart.RendererProvider, w io.Writer) error
-}
-
-func (o *Options) validate() error {
- if len(o.Series) == 0 {
- return errors.New("series can not be empty")
- }
- xAxisCount := len(o.XAxis.Data)
-
- for _, item := range o.Series {
- if item.Type != SeriesPie && len(item.Data) != xAxisCount {
- return errors.New("series and xAxis is not matched")
- }
- }
- return nil
-}
-
-func (o *Options) getWidth() int {
- width := o.Width
- if width <= 0 {
- width = DefaultChartWidth
- }
- return width
-}
-
-func (o *Options) getHeight() int {
- height := o.Height
- if height <= 0 {
- height = DefaultChartHeight
- }
- return height
-}
-
-func (o *Options) getBackground() chart.Style {
- bg := chart.Style{
- Padding: o.Padding,
- }
- return bg
-}
-
-func render(g Graph, rp chart.RendererProvider) ([]byte, error) {
- buf := bytes.Buffer{}
- err := g.Render(rp, &buf)
- if err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
-
-func ToPNG(g Graph) ([]byte, error) {
- return render(g, chart.PNG)
-}
-
-func ToSVG(g Graph) ([]byte, error) {
- return render(g, chart.SVG)
-}
-
-func newTitleRenderable(title Title, font *truetype.Font, textColor drawing.Color) chart.Renderable {
- if title.Text == "" || title.Style.Hidden {
- return nil
- }
- title.Font = font
- if title.Style.FontColor.IsZero() {
- title.Style.FontColor = textColor
- }
- return NewTitleCustomize(title)
-}
-
-func newPieChart(opt Options) *chart.PieChart {
- values := make(chart.Values, len(opt.Series))
- for index, item := range opt.Series {
- label := item.Name
- if item.Label.Show {
- label += ":" + humanize.CommafWithDigits(item.Data[0].Value, 2)
- }
- values[index] = chart.Value{
- Value: item.Data[0].Value,
- Label: label,
- }
- }
- p := &chart.PieChart{
- Font: opt.Font,
- Background: opt.getBackground(),
- Width: opt.getWidth(),
- Height: opt.getHeight(),
- Values: values,
- ColorPalette: &PieThemeColorPalette{
- ThemeColorPalette: ThemeColorPalette{
- Theme: opt.Theme,
- },
- },
- }
- // pie 图表默认设置为居中
- if opt.Title.Left == "" {
- opt.Title.Left = "center"
- }
- titleColor := drawing.ColorBlack
- if opt.Theme == ThemeDark {
- titleColor = drawing.ColorWhite
- }
- titleRender := newTitleRenderable(opt.Title, p.GetFont(), titleColor)
- if titleRender != nil {
- p.Elements = []chart.Renderable{
- titleRender,
- }
- }
- return p
-}
-
-func newChart(opt Options) *chart.Chart {
- tickPosition := opt.TickPosition
-
- xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme)
-
- legendSize := len(opt.Legend.Data)
- for index, item := range opt.Series {
- if len(item.XValues) == 0 {
- opt.Series[index].XValues = xValues
- }
- if index < legendSize && opt.Series[index].Name == "" {
- opt.Series[index].Name = opt.Legend.Data[index]
- }
- }
-
- var secondaryYAxisOption *YAxisOption
- if len(opt.YAxisOptions) != 0 {
- secondaryYAxisOption = opt.YAxisOptions[0]
- }
-
- yAxisOption := &YAxisOption{
- Disabled: true,
- }
- if len(opt.YAxisOptions) > 1 {
- yAxisOption = opt.YAxisOptions[1]
- }
-
- c := &chart.Chart{
- Font: opt.Font,
- Log: opt.Log,
- Background: opt.getBackground(),
- ColorPalette: &ThemeColorPalette{
- Theme: opt.Theme,
- },
- Width: opt.getWidth(),
- Height: opt.getHeight(),
- XAxis: xAxis,
- YAxis: GetYAxis(opt.Theme, yAxisOption),
- YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption),
- Series: GetSeries(opt.Series, tickPosition, opt.Theme),
- }
-
- elements := make([]chart.Renderable, 0)
-
- if legendSize != 0 {
- elements = append(elements, NewLegendCustomize(c.Series, LegendOption{
- Theme: opt.Theme,
- IconDraw: DefaultLegendIconDraw,
- Align: opt.Legend.Align,
- Padding: opt.Legend.Padding,
- Left: opt.Legend.Left,
- Right: opt.Legend.Right,
- Top: opt.Legend.Top,
- Bottom: opt.Legend.Bottom,
- }))
- }
- titleRender := newTitleRenderable(opt.Title, c.GetFont(), c.GetColorPalette().TextColor())
- if titleRender != nil {
- elements = append(elements, titleRender)
- }
- if len(elements) != 0 {
- c.Elements = elements
- }
- return c
-}
-
-func New(opt Options) (Graph, error) {
- err := opt.validate()
- if err != nil {
- return nil, err
- }
- if opt.Series[0].Type == SeriesPie {
- return newPieChart(opt), nil
- }
-
- return newChart(opt), nil
-}
diff --git a/charts_test.go b/charts_test.go
deleted file mode 100644
index 98a7288..0000000
--- a/charts_test.go
+++ /dev/null
@@ -1,156 +0,0 @@
-// MIT License
-
-// Copyright (c) 2021 Tree Xie
-
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-package charts
-
-import (
- "errors"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
- "github.com/wcharczuk/go-chart/v2/roboto"
-)
-
-func TestFont(t *testing.T) {
- assert := assert.New(t)
-
- fontFamily := "roboto"
- err := InstallFont(fontFamily, roboto.Roboto)
- assert.Nil(err)
-
- font, err := GetFont(fontFamily)
- assert.Nil(err)
- assert.NotNil(font)
-}
-
-func TestChartsOptions(t *testing.T) {
- assert := assert.New(t)
-
- o := Options{}
-
- assert.Equal(errors.New("series can not be empty"), o.validate())
-
- o.Series = []Series{
- {
- Data: []SeriesData{
- {
- Value: 1,
- },
- },
- },
- }
- assert.Equal(errors.New("series and xAxis is not matched"), o.validate())
- o.XAxis.Data = []string{
- "1",
- }
- assert.Nil(o.validate())
-
- assert.Equal(DefaultChartWidth, o.getWidth())
- o.Width = 10
- assert.Equal(10, o.getWidth())
-
- assert.Equal(DefaultChartHeight, o.getHeight())
- o.Height = 10
- assert.Equal(10, o.getHeight())
-
- padding := chart.NewBox(10, 10, 10, 10)
- o.Padding = padding
- assert.Equal(padding, o.getBackground().Padding)
-}
-
-func TestNewPieChart(t *testing.T) {
- assert := assert.New(t)
-
- data := []Series{
- {
- Data: []SeriesData{
- {
- Value: 10,
- },
- },
- Name: "chrome",
- },
- {
- Data: []SeriesData{
- {
- Value: 2,
- },
- },
- Name: "edge",
- },
- }
- pie := newPieChart(Options{
- Series: data,
- })
- for index, item := range pie.Values {
- assert.Equal(data[index].Name, item.Label)
- assert.Equal(data[index].Data[0].Value, item.Value)
- }
-}
-
-func TestNewChart(t *testing.T) {
- assert := assert.New(t)
-
- data := []Series{
- {
- Data: []SeriesData{
- {
- Value: 10,
- },
- {
- Value: 20,
- },
- },
- Name: "chrome",
- },
- {
- Data: []SeriesData{
- {
- Value: 2,
- },
- {
- Value: 3,
- },
- },
- Name: "edge",
- },
- }
-
- c := newChart(Options{
- Series: data,
- })
- assert.Empty(c.Elements)
- for index, series := range c.Series {
- assert.Equal(data[index].Name, series.GetName())
- }
-
- c = newChart(Options{
- Legend: Legend{
- Data: []string{
- "chrome",
- "edge",
- },
- },
- })
- assert.Equal(1, len(c.Elements))
-}
diff --git a/draw.go b/draw.go
new file mode 100644
index 0000000..bc3d9e8
--- /dev/null
+++ b/draw.go
@@ -0,0 +1,311 @@
+// 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 (
+ "bytes"
+ "errors"
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+const (
+ PositionLeft = "left"
+ PositionRight = "right"
+ PositionCenter = "center"
+ PositionTop = "top"
+ PositionBottom = "bottom"
+)
+
+const (
+ OrientHorizontal = "horizontal"
+ OrientVertical = "vertical"
+)
+
+type Draw struct {
+ // Render
+ Render chart.Renderer
+ // The canvas box
+ Box chart.Box
+ // The font for draw
+ Font *truetype.Font
+ // The parent of draw
+ parent *Draw
+}
+
+type DrawOption struct {
+ // Draw type, "svg" or "png", default type is "svg"
+ Type string
+ // Parent of draw
+ Parent *Draw
+ // The width of draw canvas
+ Width int
+ // The height of draw canvas
+ Height int
+}
+
+type Option func(*Draw) error
+
+// PaddingOption sets the padding of draw canvas
+func PaddingOption(padding chart.Box) Option {
+ return func(d *Draw) error {
+ d.Box.Left += padding.Left
+ d.Box.Top += padding.Top
+ d.Box.Right -= padding.Right
+ d.Box.Bottom -= padding.Bottom
+ return nil
+ }
+}
+
+// BoxOption set the box of draw canvas
+func BoxOption(box chart.Box) Option {
+ return func(d *Draw) error {
+ if box.IsZero() {
+ return nil
+ }
+ d.Box = box
+ return nil
+ }
+}
+
+// NewDraw returns a new draw canvas
+func NewDraw(opt DrawOption, opts ...Option) (*Draw, error) {
+ if opt.Parent == nil && (opt.Width <= 0 || opt.Height <= 0) {
+ return nil, errors.New("parent and width/height can not be nil")
+ }
+ font, _ := chart.GetDefaultFont()
+ d := &Draw{
+ Font: font,
+ }
+ width := opt.Width
+ height := opt.Height
+ if opt.Parent != nil {
+ d.parent = opt.Parent
+ d.Render = d.parent.Render
+ d.Box = opt.Parent.Box.Clone()
+ }
+ if width != 0 && height != 0 {
+ d.Box.Right = width + d.Box.Left
+ d.Box.Bottom = height + d.Box.Top
+ }
+ // 创建render
+ if d.parent == nil {
+ fn := chart.SVG
+ if opt.Type == ChartOutputPNG {
+ fn = chart.PNG
+ }
+ r, err := fn(d.Box.Right, d.Box.Bottom)
+ if err != nil {
+ return nil, err
+ }
+ d.Render = r
+ }
+
+ for _, o := range opts {
+ err := o(d)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return d, nil
+}
+
+// Parent returns the parent of draw
+func (d *Draw) Parent() *Draw {
+ return d.parent
+}
+
+// Top returns the top parent of draw
+func (d *Draw) Top() *Draw {
+ if d.parent == nil {
+ return nil
+ }
+ t := d.parent
+ // 限制最多查询次数,避免嵌套引用
+ for i := 50; i > 0; i-- {
+ if t.parent == nil {
+ break
+ }
+ t = t.parent
+ }
+ return t
+}
+
+// Bytes returns the data of draw canvas
+func (d *Draw) Bytes() ([]byte, error) {
+ buffer := bytes.Buffer{}
+ err := d.Render.Save(&buffer)
+ if err != nil {
+ return nil, err
+ }
+ return buffer.Bytes(), err
+}
+
+func (d *Draw) moveTo(x, y int) {
+ d.Render.MoveTo(x+d.Box.Left, y+d.Box.Top)
+}
+
+func (d *Draw) arcTo(cx, cy int, rx, ry, startAngle, delta float64) {
+ d.Render.ArcTo(cx+d.Box.Left, cy+d.Box.Top, rx, ry, startAngle, delta)
+}
+
+func (d *Draw) lineTo(x, y int) {
+ d.Render.LineTo(x+d.Box.Left, y+d.Box.Top)
+}
+
+func (d *Draw) pin(x, y, width int) {
+ r := float64(width) / 2
+ y -= width / 4
+ angle := chart.DegreesToRadians(15)
+
+ startAngle := math.Pi/2 + angle
+ delta := 2*math.Pi - 2*angle
+ d.arcTo(x, y, r, r, startAngle, delta)
+ d.lineTo(x, y)
+ d.Render.Close()
+ d.Render.FillStroke()
+
+ startX := x - int(r)
+ startY := y
+ endX := x + int(r)
+ endY := y
+ d.moveTo(startX, startY)
+
+ left := d.Box.Left
+ top := d.Box.Top
+ cx := x
+ cy := y + int(r*2.5)
+ d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
+ d.Render.Close()
+ d.Render.Fill()
+}
+
+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.FillStroke()
+}
+
+func (d *Draw) makeLine(x, y, width int) {
+ arrowWidth := 16
+ arrowHeight := 10
+ endX := x + width
+ d.circle(3, x, y)
+ d.Render.Fill()
+ d.moveTo(x+5, y)
+ d.lineTo(endX-arrowWidth, y)
+ d.Render.Stroke()
+ d.Render.SetStrokeDashArray([]float64{})
+ d.arrowRight(endX, y, arrowWidth, arrowHeight)
+}
+
+func (d *Draw) circle(radius float64, x, y int) {
+ d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top)
+}
+
+func (d *Draw) text(body string, x, y int) {
+ d.Render.Text(body, x+d.Box.Left, y+d.Box.Top)
+}
+
+func (d *Draw) lineStroke(points []Point, style LineStyle) {
+ s := style.Style()
+ if !s.ShouldDrawStroke() {
+ return
+ }
+ r := d.Render
+ s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
+ for index, point := range points {
+ x := point.X
+ y := point.Y
+ if index == 0 {
+ d.moveTo(x, y)
+ } else {
+ d.lineTo(x, y)
+ }
+ }
+ r.Stroke()
+}
+
+func (d *Draw) setBackground(width, height int, color drawing.Color) {
+ r := d.Render
+ s := chart.Style{
+ FillColor: color,
+ }
+ s.WriteToRenderer(r)
+ r.MoveTo(0, 0)
+ r.LineTo(width, 0)
+ r.LineTo(width, height)
+ r.LineTo(0, height)
+ r.LineTo(0, 0)
+ r.FillStroke()
+}
diff --git a/draw_test.go b/draw_test.go
new file mode 100644
index 0000000..712641a
--- /dev/null
+++ b/draw_test.go
@@ -0,0 +1,427 @@
+// 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"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestParentOption(t *testing.T) {
+ assert := assert.New(t)
+ p, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+
+ d, err := NewDraw(DrawOption{
+ Parent: p,
+ })
+ assert.Nil(err)
+ assert.Equal(p, d.parent)
+}
+
+func TestWidthHeightOption(t *testing.T) {
+ assert := assert.New(t)
+
+ // no parent
+ width := 300
+ height := 200
+ d, err := NewDraw(DrawOption{
+ Width: width,
+ Height: height,
+ })
+ assert.Nil(err)
+ assert.Equal(chart.Box{
+ Top: 0,
+ Left: 0,
+ Right: width,
+ Bottom: height,
+ }, d.Box)
+
+ width = 500
+ height = 600
+ // with parent
+ p, err := NewDraw(
+ DrawOption{
+ Width: width,
+ Height: height,
+ },
+ PaddingOption(chart.NewBox(5, 5, 5, 5)),
+ )
+ assert.Nil(err)
+ d, err = NewDraw(
+ DrawOption{
+ Parent: p,
+ },
+ PaddingOption(chart.NewBox(1, 2, 3, 4)),
+ )
+ assert.Nil(err)
+ assert.Equal(chart.Box{
+ Top: 6,
+ Left: 7,
+ Right: 492,
+ Bottom: 591,
+ }, d.Box)
+}
+
+func TestBoxOption(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+
+ err = BoxOption(chart.Box{
+ Left: 10,
+ Top: 20,
+ Right: 50,
+ Bottom: 100,
+ })(d)
+ assert.Nil(err)
+ assert.Equal(chart.Box{
+ Left: 10,
+ Top: 20,
+ Right: 50,
+ Bottom: 100,
+ }, d.Box)
+
+ // zero box will be ignored
+ err = BoxOption(chart.Box{})(d)
+ assert.Nil(err)
+ assert.Equal(chart.Box{
+ Left: 10,
+ Top: 20,
+ Right: 50,
+ Bottom: 100,
+ }, d.Box)
+}
+
+func TestPaddingOption(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+
+ // 默认的box
+ assert.Equal(chart.Box{
+ Right: 400,
+ Bottom: 300,
+ }, d.Box)
+
+ // 设置padding之后的box
+ d, err = NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ }, PaddingOption(chart.Box{
+ Left: 1,
+ Top: 2,
+ Right: 3,
+ Bottom: 4,
+ }))
+ assert.Nil(err)
+ assert.Equal(chart.Box{
+ Top: 2,
+ Left: 1,
+ Right: 397,
+ Bottom: 296,
+ }, d.Box)
+
+ p := d
+ // 设置父元素之后的box
+ d, err = NewDraw(
+ DrawOption{
+ Parent: p,
+ },
+ PaddingOption(chart.Box{
+ Left: 1,
+ Top: 2,
+ Right: 3,
+ Bottom: 4,
+ }),
+ )
+ assert.Nil(err)
+ assert.Equal(chart.Box{
+ Top: 4,
+ Left: 2,
+ Right: 394,
+ Bottom: 292,
+ }, d.Box)
+}
+
+func TestParentTop(t *testing.T) {
+ assert := assert.New(t)
+ d1, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+
+ d2, err := NewDraw(DrawOption{
+ Parent: d1,
+ })
+ assert.Nil(err)
+
+ d3, err := NewDraw(DrawOption{
+ Parent: d2,
+ })
+ assert.Nil(err)
+
+ assert.Equal(d2, d3.Parent())
+ assert.Equal(d1, d2.Parent())
+ assert.Equal(d1, d3.Top())
+ assert.Equal(d1, d2.Top())
+}
+
+func TestDraw(t *testing.T) {
+ assert := assert.New(t)
+
+ tests := []struct {
+ fn func(d *Draw)
+ result string
+ }{
+ // moveTo, lineTo
+ {
+ fn: func(d *Draw) {
+ d.moveTo(1, 1)
+ d.lineTo(2, 2)
+ d.Render.Stroke()
+ },
+ result: "",
+ },
+ // circle
+ {
+ fn: func(d *Draw) {
+ d.circle(5, 2, 3)
+ },
+ result: "",
+ },
+ // text
+ {
+ fn: func(d *Draw) {
+ d.text("hello world!", 3, 6)
+ },
+ result: "",
+ },
+ // line stroke
+ {
+ fn: func(d *Draw) {
+ d.lineStroke([]Point{
+ {
+ X: 1,
+ Y: 2,
+ },
+ {
+ X: 3,
+ Y: 4,
+ },
+ }, LineStyle{
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ })
+ },
+ result: "",
+ },
+ // set background
+ {
+ fn: func(d *Draw) {
+ d.setBackground(400, 300, chart.ColorWhite)
+ },
+ result: "",
+ },
+ // arcTo
+ {
+ fn: func(d *Draw) {
+ chart.Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.ColorBlack,
+ FillColor: drawing.ColorBlue,
+ }.WriteToRenderer(d.Render)
+ d.arcTo(100, 100, 100, 100, 0, math.Pi/2)
+ d.Render.Close()
+ d.Render.FillStroke()
+ },
+ result: "",
+ },
+ // pin
+ {
+ 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.pin(30, 30, 30)
+ },
+ result: "",
+ },
+ // 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: "",
+ },
+ // 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: "",
+ },
+ // 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: "",
+ },
+ // 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: "",
+ },
+ // mark line
+ {
+ 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,
+ },
+ StrokeDashArray: []float64{
+ 4,
+ 2,
+ },
+ }.WriteToRenderer(d.Render)
+ d.makeLine(0, 20, 300)
+ },
+ result: "",
+ },
+ }
+ for _, tt := range tests {
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ }, PaddingOption(chart.Box{
+ Left: 5,
+ Top: 10,
+ }))
+ assert.Nil(err)
+ tt.fn(d)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal(tt.result, string(data))
+ }
+}
diff --git a/echarts.go b/echarts.go
index 7e1884c..dc2e761 100644
--- a/echarts.go
+++ b/echarts.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -28,21 +28,10 @@ import (
"fmt"
"regexp"
"strconv"
- "strings"
"github.com/wcharczuk/go-chart/v2"
)
-type EChartStyle struct {
- Color string `json:"color"`
-}
-type ECharsSeriesData struct {
- Value float64 `json:"value"`
- Name string `json:"name"`
- ItemStyle EChartStyle `json:"itemStyle"`
-}
-type _ECharsSeriesData ECharsSeriesData
-
func convertToArray(data []byte) []byte {
data = bytes.TrimSpace(data)
if len(data) == 0 {
@@ -54,12 +43,47 @@ func convertToArray(data []byte) []byte {
return data
}
-func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
- data = bytes.TrimSpace(data)
+type EChartsPosition string
+
+func (p *EChartsPosition) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
if regexp.MustCompile(`^\d+`).Match(data) {
+ data = []byte(fmt.Sprintf(`"%s"`, string(data)))
+ }
+ s := (*string)(p)
+ return json.Unmarshal(data, s)
+}
+
+type EChartStyle struct {
+ Color string `json:"color"`
+}
+
+func (es *EChartStyle) ToStyle() chart.Style {
+ color := parseColor(es.Color)
+ return chart.Style{
+ FillColor: color,
+ FontColor: color,
+ StrokeColor: color,
+ }
+}
+
+type EChartsSeriesData struct {
+ Value float64 `json:"value"`
+ Name string `json:"name"`
+ ItemStyle EChartStyle `json:"itemStyle"`
+}
+type _EChartsSeriesData EChartsSeriesData
+
+var numericRep = regexp.MustCompile("^[-+]?[0-9]+(?:\\.[0-9]+)?$")
+
+func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error {
+ data = bytes.TrimSpace(data)
+ if len(data) == 0 {
+ return nil
+ }
+ if numericRep.Match(data) {
v, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return err
@@ -67,7 +91,7 @@ func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
es.Value = v
return nil
}
- v := _ECharsSeriesData{}
+ v := _EChartsSeriesData{}
err := json.Unmarshal(data, &v)
if err != nil {
return err
@@ -78,24 +102,53 @@ func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
return nil
}
-type EChartsPadding struct {
- box chart.Box
+type EChartsXAxisData struct {
+ BoundaryGap *bool `json:"boundaryGap"`
+ SplitNumber int `json:"splitNumber"`
+ Data []string `json:"data"`
+}
+type EChartsXAxis struct {
+ Data []EChartsXAxisData
}
-type Position string
-
-func (lp *Position) UnmarshalJSON(data []byte) error {
+func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
+ data = convertToArray(data)
if len(data) == 0 {
return nil
}
- if regexp.MustCompile(`^\d+`).Match(data) {
- data = []byte(fmt.Sprintf(`"%s"`, string(data)))
- }
- s := (*string)(lp)
- return json.Unmarshal(data, s)
+ return json.Unmarshal(data, &ex.Data)
}
-func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
+type EChartsAxisLabel struct {
+ Formatter string `json:"formatter"`
+}
+type EChartsYAxisData struct {
+ Min *float64 `json:"min"`
+ Max *float64 `json:"max"`
+ AxisLabel EChartsAxisLabel `json:"axisLabel"`
+ AxisLine struct {
+ LineStyle struct {
+ Color string
+ }
+ }
+}
+type EChartsYAxis struct {
+ Data []EChartsYAxisData `json:"data"`
+}
+
+func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
+ data = convertToArray(data)
+ if len(data) == 0 {
+ return nil
+ }
+ return json.Unmarshal(data, &ey.Data)
+}
+
+type EChartsPadding struct {
+ Box chart.Box
+}
+
+func (eb *EChartsPadding) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
if len(data) == 0 {
return nil
@@ -110,14 +163,14 @@ func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
}
switch len(arr) {
case 1:
- ep.box = chart.Box{
+ eb.Box = chart.Box{
Left: arr[0],
Top: arr[0],
Bottom: arr[0],
Right: arr[0],
}
case 2:
- ep.box = chart.Box{
+ eb.Box = chart.Box{
Top: arr[0],
Bottom: arr[0],
Left: arr[1],
@@ -130,7 +183,7 @@ func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
result[3] = result[1]
}
// 上右下左
- ep.box = chart.Box{
+ eb.Box = chart.Box{
Top: result[0],
Right: result[1],
Bottom: result[2],
@@ -140,264 +193,247 @@ func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
return nil
}
-type EChartsYAxis struct {
- Data []struct {
- Min *float64 `json:"min"`
- Max *float64 `json:"max"`
- // Interval int `json:"interval"`
- AxisLabel struct {
- Formatter string `json:"formatter"`
- } `json:"axisLabel"`
+type EChartsLabelOption struct {
+ Show bool `json:"show"`
+ Distance int `json:"distance"`
+ Color string `json:"color"`
+}
+type EChartsLegend struct {
+ Show *bool `json:"show"`
+ Data []string `json:"data"`
+ Align string `json:"align"`
+ Orient string `json:"orient"`
+ Padding EChartsPadding `json:"padding"`
+ Left EChartsPosition `json:"left"`
+ Top EChartsPosition `json:"top"`
+ TextStyle EChartsTextStyle `json:"textStyle"`
+}
+
+type EChartsMarkPoint struct {
+ SymbolSize int `json:"symbolSize"`
+ Data []struct {
+ Type string `json:"type"`
} `json:"data"`
}
-func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
- data = convertToArray(data)
- if len(data) == 0 {
- return nil
+func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint {
+ sp := SeriesMarkPoint{
+ SymbolSize: emp.SymbolSize,
}
- return json.Unmarshal(data, &ey.Data)
+ if len(emp.Data) == 0 {
+ return sp
+ }
+ data := make([]SeriesMarkData, len(emp.Data))
+ for index, item := range emp.Data {
+ data[index] = SeriesMarkData{
+ Type: item.Type,
+ }
+ }
+ sp.Data = data
+ return sp
}
-type EChartsXAxis struct {
+type EChartsMarkLine struct {
Data []struct {
- // Type string `json:"type"`
- BoundaryGap *bool `json:"boundaryGap"`
- SplitNumber int `json:"splitNumber"`
- Data []string `json:"data"`
- }
+ Type string `json:"type"`
+ } `json:"data"`
}
-func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
- data = convertToArray(data)
- if len(data) == 0 {
- return nil
+func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine {
+ sl := SeriesMarkLine{}
+ if len(eml.Data) == 0 {
+ return sl
}
- return json.Unmarshal(data, &ex.Data)
+ data := make([]SeriesMarkData, len(eml.Data))
+ for index, item := range eml.Data {
+ data[index] = SeriesMarkData{
+ Type: item.Type,
+ }
+ }
+ sl.Data = data
+ return sl
}
-type EChartsLabelOption struct {
- Show bool `json:"show"`
- Distance int `json:"distance"`
+type EChartsSeries struct {
+ Data []EChartsSeriesData `json:"data"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Radius string `json:"radius"`
+ YAxisIndex int `json:"yAxisIndex"`
+ ItemStyle EChartStyle `json:"itemStyle"`
+ // label的配置
+ Label EChartsLabelOption `json:"label"`
+ MarkPoint EChartsMarkPoint `json:"markPoint"`
+ MarkLine EChartsMarkLine `json:"markLine"`
}
+type EChartsSeriesList []EChartsSeries
-func (elo EChartsLabelOption) ToLabel() SeriesLabel {
- if !elo.Show {
- return SeriesLabel{}
- }
- return SeriesLabel{
- Show: true,
- Offset: chart.Box{
- // 默认位置为top,因此设置为负
- Top: -elo.Distance,
- },
- }
-}
-
-type ECharsOptions struct {
- Theme string `json:"theme"`
- Padding EChartsPadding `json:"padding"`
- Title struct {
- Text string `json:"text"`
- Left Position `json:"left"`
- Top Position `json:"top"`
- TextStyle struct {
- Color string `json:"color"`
- FontFamily string `json:"fontFamily"`
- FontSize float64 `json:"fontSize"`
- Height float64 `json:"height"`
- } `json:"textStyle"`
- } `json:"title"`
- XAxis EChartsXAxis `json:"xAxis"`
- YAxis EChartsYAxis `json:"yAxis"`
- Legend struct {
- Data []string `json:"data"`
- Align string `json:"align"`
- Padding EChartsPadding `json:"padding"`
- Left Position `json:"left"`
- Right Position `json:"right"`
- // Top string `json:"top"`
- // Bottom string `json:"bottom"`
- } `json:"legend"`
- Series []struct {
- Data []ECharsSeriesData `json:"data"`
- Type string `json:"type"`
- YAxisIndex int `json:"yAxisIndex"`
- ItemStyle EChartStyle `json:"itemStyle"`
- // label的配置
- Label EChartsLabelOption `json:"label"`
- } `json:"series"`
-}
-
-func convertEChartsSeries(e *ECharsOptions) ([]Series, chart.TickPosition) {
- tickPosition := chart.TickPositionUnset
-
- if len(e.Series) == 0 {
- return nil, tickPosition
- }
- seriesType := e.Series[0].Type
- if seriesType == SeriesPie {
- series := make([]Series, len(e.Series[0].Data))
- label := e.Series[0].Label.ToLabel()
- for index, item := range e.Series[0].Data {
- style := chart.Style{}
- if item.ItemStyle.Color != "" {
- c := parseColor(item.ItemStyle.Color)
- style.FillColor = c
- style.StrokeColor = c
- }
-
- series[index] = Series{
- Style: style,
- Data: []SeriesData{
- {
- Value: item.Value,
+func (esList EChartsSeriesList) ToSeriesList() SeriesList {
+ seriesList := make(SeriesList, 0, len(esList))
+ for _, item := range esList {
+ // 如果是pie,则每个子荐生成一个series
+ if item.Type == ChartTypePie {
+ for _, dataItem := range item.Data {
+ seriesList = append(seriesList, Series{
+ Type: ChartTypePie,
+ Name: dataItem.Name,
+ Label: SeriesLabel{
+ Show: true,
},
- },
- Type: seriesType,
- Name: item.Name,
- Label: label,
+ Radius: item.Radius,
+ Data: []SeriesData{
+ {
+ Value: dataItem.Value,
+ },
+ },
+ })
}
- }
- return series, tickPosition
- }
- series := make([]Series, len(e.Series))
- for index, item := range e.Series {
- // bar默认tick居中
- if item.Type == SeriesBar {
- tickPosition = chart.TickPositionBetweenTicks
- }
- style := chart.Style{}
- if item.ItemStyle.Color != "" {
- c := parseColor(item.ItemStyle.Color)
- style.FillColor = c
- style.StrokeColor = c
+ continue
}
data := make([]SeriesData, len(item.Data))
- for j, itemData := range item.Data {
- sd := SeriesData{
- Value: itemData.Value,
+ for j, dataItem := range item.Data {
+ data[j] = SeriesData{
+ Value: dataItem.Value,
+ Style: dataItem.ItemStyle.ToStyle(),
}
- if itemData.ItemStyle.Color != "" {
- c := parseColor(itemData.ItemStyle.Color)
- sd.Style.FillColor = c
- sd.Style.StrokeColor = c
- }
- data[j] = sd
}
- series[index] = Series{
- Style: style,
- YAxisIndex: item.YAxisIndex,
- Data: data,
+ seriesList = append(seriesList, Series{
Type: item.Type,
- Label: item.Label.ToLabel(),
- }
+ Data: data,
+ YAxisIndex: item.YAxisIndex,
+ Style: item.ItemStyle.ToStyle(),
+ Label: SeriesLabel{
+ Color: parseColor(item.Label.Color),
+ Show: item.Label.Show,
+ Distance: item.Label.Distance,
+ },
+ Name: item.Name,
+ MarkPoint: item.MarkPoint.ToSeriesMarkPoint(),
+ MarkLine: item.MarkLine.ToSeriesMarkLine(),
+ })
}
- return series, tickPosition
+ return seriesList
}
-func (e *ECharsOptions) ToOptions() Options {
- o := Options{
- Theme: e.Theme,
- Padding: e.Padding.box,
- }
+type EChartsTextStyle struct {
+ Color string `json:"color"`
+ FontFamily string `json:"fontFamily"`
+ FontSize float64 `json:"fontSize"`
+}
- titleTextStyle := e.Title.TextStyle
- o.Title = Title{
- Text: e.Title.Text,
- Left: string(e.Title.Left),
- Top: string(e.Title.Top),
- Style: chart.Style{
- FontColor: parseColor(titleTextStyle.Color),
- FontSize: titleTextStyle.FontSize,
+func (et *EChartsTextStyle) ToStyle() chart.Style {
+ s := chart.Style{
+ FontSize: et.FontSize,
+ FontColor: parseColor(et.Color),
+ }
+ if et.FontFamily != "" {
+ s.Font, _ = GetFont(et.FontFamily)
+ }
+ return s
+}
+
+type EChartsOption struct {
+ Type string `json:"type"`
+ Theme string `json:"theme"`
+ FontFamily string `json:"fontFamily"`
+ Padding EChartsPadding `json:"padding"`
+ Box chart.Box `json:"box"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Title struct {
+ Text string `json:"text"`
+ Subtext string `json:"subtext"`
+ Left EChartsPosition `json:"left"`
+ Top EChartsPosition `json:"top"`
+ TextStyle EChartsTextStyle `json:"textStyle"`
+ SubtextStyle EChartsTextStyle `json:"subtextStyle"`
+ } `json:"title"`
+ XAxis EChartsXAxis `json:"xAxis"`
+ YAxis EChartsYAxis `json:"yAxis"`
+ Legend EChartsLegend `json:"legend"`
+ Series EChartsSeriesList `json:"series"`
+ Children []EChartsOption `json:"children"`
+}
+
+func (eo *EChartsOption) ToOption() ChartOption {
+ fontFamily := eo.FontFamily
+ if len(fontFamily) == 0 {
+ fontFamily = eo.Title.TextStyle.FontFamily
+ }
+ o := ChartOption{
+ Type: eo.Type,
+ FontFamily: fontFamily,
+ Theme: eo.Theme,
+ Title: TitleOption{
+ Text: eo.Title.Text,
+ Subtext: eo.Title.Subtext,
+ Style: eo.Title.TextStyle.ToStyle(),
+ SubtextStyle: eo.Title.SubtextStyle.ToStyle(),
+ Left: string(eo.Title.Left),
+ Top: string(eo.Title.Top),
},
+ Legend: LegendOption{
+ Show: eo.Legend.Show,
+ Style: eo.Legend.TextStyle.ToStyle(),
+ Data: eo.Legend.Data,
+ Left: string(eo.Legend.Left),
+ Top: string(eo.Legend.Top),
+ Align: eo.Legend.Align,
+ Orient: eo.Legend.Orient,
+ },
+ Width: eo.Width,
+ Height: eo.Height,
+ Padding: eo.Padding.Box,
+ Box: eo.Box,
+ SeriesList: eo.Series.ToSeriesList(),
}
- if e.Title.TextStyle.FontFamily != "" {
- // 如果获取字体失败忽略
- o.Font, _ = GetFont(e.Title.TextStyle.FontFamily)
- }
-
- if titleTextStyle.FontSize != 0 && titleTextStyle.Height > titleTextStyle.FontSize {
- padding := int(titleTextStyle.Height-titleTextStyle.FontSize) / 2
- o.Title.Style.Padding.Top = padding
- o.Title.Style.Padding.Bottom = padding
- }
-
- boundaryGap := false
- if len(e.XAxis.Data) != 0 {
- xAxis := e.XAxis.Data[0]
- o.XAxis = XAxis{
- Data: xAxis.Data,
- SplitNumber: xAxis.SplitNumber,
- }
- if xAxis.BoundaryGap == nil || *xAxis.BoundaryGap {
- boundaryGap = true
+ if len(eo.XAxis.Data) != 0 {
+ xAxisData := eo.XAxis.Data[0]
+ o.XAxis = XAxisOption{
+ BoundaryGap: xAxisData.BoundaryGap,
+ Data: xAxisData.Data,
+ SplitNumber: xAxisData.SplitNumber,
}
}
-
- o.Legend = Legend{
- Data: e.Legend.Data,
- Align: e.Legend.Align,
- Padding: e.Legend.Padding.box,
- Left: string(e.Legend.Left),
- Right: string(e.Legend.Right),
- }
- if len(e.YAxis.Data) != 0 {
- yAxisOptions := make([]*YAxisOption, len(e.YAxis.Data))
- for index, item := range e.YAxis.Data {
- opt := &YAxisOption{
- Max: item.Max,
- Min: item.Min,
- }
- template := item.AxisLabel.Formatter
- if template != "" {
- opt.Formater = func(v interface{}) string {
- str := defaultFloatFormater(v)
- return strings.ReplaceAll(template, "{value}", str)
- }
- }
- yAxisOptions[index] = opt
+ yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data))
+ for index, item := range eo.YAxis.Data {
+ yAxisOptions[index] = YAxisOption{
+ Min: item.Min,
+ Max: item.Max,
+ Formatter: item.AxisLabel.Formatter,
+ Color: parseColor(item.AxisLine.LineStyle.Color),
}
- o.YAxisOptions = yAxisOptions
}
+ o.YAxisList = yAxisOptions
- series, tickPosition := convertEChartsSeries(e)
-
- o.Series = series
-
- if boundaryGap {
- tickPosition = chart.TickPositionBetweenTicks
+ if len(eo.Children) != 0 {
+ o.Children = make([]ChartOption, len(eo.Children))
+ for index, item := range eo.Children {
+ o.Children[index] = item.ToOption()
+ }
}
- o.TickPosition = tickPosition
return o
}
-func ParseECharsOptions(options string) (Options, error) {
- e := ECharsOptions{}
- err := json.Unmarshal([]byte(options), &e)
- if err != nil {
- return Options{}, err
- }
-
- return e.ToOptions(), nil
-}
-
-func echartsRender(options string, rp chart.RendererProvider) ([]byte, error) {
- o, err := ParseECharsOptions(options)
+func renderEcharts(options, outputType string) ([]byte, error) {
+ o := EChartsOption{}
+ err := json.Unmarshal([]byte(options), &o)
if err != nil {
return nil, err
}
- g, err := New(o)
+ opt := o.ToOption()
+ opt.Type = outputType
+ d, err := Render(opt)
if err != nil {
return nil, err
}
- return render(g, rp)
+ return d.Bytes()
}
func RenderEChartsToPNG(options string) ([]byte, error) {
- return echartsRender(options, chart.PNG)
+ return renderEcharts(options, "png")
}
func RenderEChartsToSVG(options string) ([]byte, error) {
- return echartsRender(options, chart.SVG)
+ return renderEcharts(options, "svg")
}
diff --git a/echarts_test.go b/echarts_test.go
index 6dcc4d0..d80ecbb 100644
--- a/echarts_test.go
+++ b/echarts_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -31,425 +31,529 @@ import (
"github.com/wcharczuk/go-chart/v2/drawing"
)
-func TestConvertToArray(t *testing.T) {
+func TestEChartsPosition(t *testing.T) {
assert := assert.New(t)
- assert.Nil(convertToArray([]byte(" ")))
+ var p EChartsPosition
+ err := p.UnmarshalJSON([]byte("12"))
+ assert.Nil(err)
+ assert.Equal("12", string(p))
- assert.Equal([]byte("[{}]"), convertToArray([]byte("{}")))
- assert.Equal([]byte("[{}]"), convertToArray([]byte("[{}]")))
+ err = p.UnmarshalJSON([]byte(`"12%"`))
+ assert.Nil(err)
+ assert.Equal("12%", string(p))
+}
+func TestEChartStyle(t *testing.T) {
+ assert := assert.New(t)
+
+ s := EChartStyle{
+ Color: "#aaa",
+ }
+ r := drawing.Color{
+ R: 170,
+ G: 170,
+ B: 170,
+ A: 255,
+ }
+ assert.Equal(chart.Style{
+ FillColor: r,
+ FontColor: r,
+ StrokeColor: r,
+ }, s.ToStyle())
}
-func TestECharsSeriesData(t *testing.T) {
+func TestEChartsXAxis(t *testing.T) {
assert := assert.New(t)
-
- es := ECharsSeriesData{}
- err := es.UnmarshalJSON([]byte(" "))
+ ex := EChartsXAxis{}
+ err := ex.UnmarshalJSON([]byte(`{
+ "boundaryGap": false,
+ "splitNumber": 5,
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ }`))
assert.Nil(err)
- assert.Equal(ECharsSeriesData{}, es)
+ assert.Equal(EChartsXAxis{
+ Data: []EChartsXAxisData{
+ {
+ BoundaryGap: FalseFlag(),
+ SplitNumber: 5,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ },
+ },
+ },
+ }, ex)
+}
- es = ECharsSeriesData{}
- err = es.UnmarshalJSON([]byte("12.1"))
- assert.Nil(err)
- assert.Equal(ECharsSeriesData{
- Value: 12.1,
- }, es)
+func TestEChartsYAxis(t *testing.T) {
+ assert := assert.New(t)
+ ey := EChartsYAxis{}
- es = ECharsSeriesData{}
- err = es.UnmarshalJSON([]byte(`{
- "value": 12.1,
- "name": "test",
- "itemStyle": {
- "color": "#333"
+ err := ey.UnmarshalJSON([]byte(`{
+ "min": 1,
+ "max": 10,
+ "axisLabel": {
+ "formatter": "ab"
}
}`))
assert.Nil(err)
- assert.Equal(ECharsSeriesData{
- Value: 12.1,
- Name: "test",
- ItemStyle: EChartStyle{
- Color: "#333",
+ assert.Equal(EChartsYAxis{
+ Data: []EChartsYAxisData{
+ {
+ Min: NewFloatPoint(1),
+ Max: NewFloatPoint(10),
+ AxisLabel: EChartsAxisLabel{
+ Formatter: "ab",
+ },
+ },
},
- }, es)
+ }, ey)
+
+ ey = EChartsYAxis{}
+ err = ey.UnmarshalJSON([]byte(`[
+ {
+ "min": 1,
+ "max": 10,
+ "axisLabel": {
+ "formatter": "ab"
+ }
+ },
+ {
+ "min": 2,
+ "max": 20,
+ "axisLabel": {
+ "formatter": "cd"
+ }
+ }
+ ]`))
+ assert.Nil(err)
+ assert.Equal(EChartsYAxis{
+ Data: []EChartsYAxisData{
+ {
+ Min: NewFloatPoint(1),
+ Max: NewFloatPoint(10),
+ AxisLabel: EChartsAxisLabel{
+ Formatter: "ab",
+ },
+ },
+ {
+ Min: NewFloatPoint(2),
+ Max: NewFloatPoint(20),
+ AxisLabel: EChartsAxisLabel{
+ Formatter: "cd",
+ },
+ },
+ },
+ }, ey)
}
func TestEChartsPadding(t *testing.T) {
assert := assert.New(t)
ep := EChartsPadding{}
- err := ep.UnmarshalJSON([]byte(" "))
- assert.Nil(err)
- assert.Equal(EChartsPadding{}, ep)
- ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("1"))
- assert.Nil(err)
+ ep.UnmarshalJSON([]byte(`10`))
assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Left: 1,
- Right: 1,
- Bottom: 1,
+ Box: chart.Box{
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
},
}, ep)
ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("[1, 2]"))
- assert.Nil(err)
+ ep.UnmarshalJSON([]byte(`[10, 20]`))
assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Left: 2,
- Right: 2,
- Bottom: 1,
+ Box: chart.Box{
+ Top: 10,
+ Right: 20,
+ Bottom: 10,
+ Left: 20,
},
}, ep)
ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("[1, 2, 3]"))
- assert.Nil(err)
+ ep.UnmarshalJSON([]byte(`[10, 20, 30]`))
assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Right: 2,
- Bottom: 3,
- Left: 2,
+ Box: chart.Box{
+ Top: 10,
+ Right: 20,
+ Bottom: 30,
+ Left: 20,
},
}, ep)
ep = EChartsPadding{}
- err = ep.UnmarshalJSON([]byte("[1, 2, 3, 4]"))
- assert.Nil(err)
+ ep.UnmarshalJSON([]byte(`[10, 20, 30, 40]`))
assert.Equal(EChartsPadding{
- box: chart.Box{
- Top: 1,
- Right: 2,
- Bottom: 3,
- Left: 4,
+ Box: chart.Box{
+ Top: 10,
+ Right: 20,
+ Bottom: 30,
+ Left: 40,
},
}, ep)
+
}
-
-func TestConvertEChartsSeries(t *testing.T) {
+func TestEChartsLegend(t *testing.T) {
assert := assert.New(t)
- seriesList, tickPosition := convertEChartsSeries(&ECharsOptions{})
- assert.Empty(seriesList)
- assert.Equal(chart.TickPositionUnset, tickPosition)
+ el := EChartsLegend{}
- e := ECharsOptions{}
err := json.Unmarshal([]byte(`{
- "title": {
- "text": "Referer of a Website"
+ "data": ["a", "b", "c"],
+ "align": "right",
+ "padding": [10],
+ "left": "20%",
+ "top": 10
+ }`), &el)
+ assert.Nil(err)
+ assert.Equal(EChartsLegend{
+ Data: []string{
+ "a",
+ "b",
+ "c",
},
- "series": [
+ Align: "right",
+ Padding: EChartsPadding{
+ Box: chart.Box{
+ Left: 10,
+ Top: 10,
+ Right: 10,
+ Bottom: 10,
+ },
+ },
+ Left: EChartsPosition("20%"),
+ Top: EChartsPosition("10"),
+ }, el)
+}
+
+func TestEChartsSeriesData(t *testing.T) {
+ assert := assert.New(t)
+
+ esd := EChartsSeriesData{}
+ err := esd.UnmarshalJSON([]byte(`123`))
+ assert.Nil(err)
+ assert.Equal(EChartsSeriesData{
+ Value: 123,
+ }, esd)
+
+ esd = EChartsSeriesData{}
+ err = esd.UnmarshalJSON([]byte(`{
+ "value": 123.12,
+ "name": "test",
+ "itemStyle": {
+ "color": "#aaa"
+ }
+ }`))
+ assert.Nil(err)
+ assert.Equal(EChartsSeriesData{
+ Value: 123.12,
+ Name: "test",
+ ItemStyle: EChartStyle{
+ Color: "#aaa",
+ },
+ }, esd)
+}
+
+func TestEChartsSeries(t *testing.T) {
+ assert := assert.New(t)
+
+ esList := make([]EChartsSeries, 0)
+ err := json.Unmarshal([]byte(`[
+ {
+ "name": "Email",
+ "data": [
+ 120,
+ 132
+ ]
+ },
+ {
+ "name": "Union Ads",
+ "type": "bar",
+ "data": [
+ 220,
+ 182
+ ]
+ }
+ ]`), &esList)
+ assert.Nil(err)
+ assert.Equal([]EChartsSeries{
+ {
+ Name: "Email",
+ Data: []EChartsSeriesData{
+ {
+ Value: 120,
+ },
+ {
+ Value: 132,
+ },
+ },
+ },
+ {
+ Name: "Union Ads",
+ Type: "bar",
+ Data: []EChartsSeriesData{
+ {
+ Value: 220,
+ },
+ {
+ Value: 182,
+ },
+ },
+ },
+ }, esList)
+}
+
+func TestEChartsMarkPoint(t *testing.T) {
+ assert := assert.New(t)
+
+ p := EChartsMarkPoint{}
+
+ err := json.Unmarshal([]byte(`{
+ "symbolSize": 30,
+ "data": [
{
- "name": "Access From",
- "type": "pie",
- "radius": "50%",
- "data": [
- {
- "value": 1048,
- "name": "Search Engine"
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
+ }`), &p)
+ assert.Nil(err)
+ assert.Equal(SeriesMarkPoint{
+ SymbolSize: 30,
+ Data: []SeriesMarkData{
+ {
+ Type: "max",
+ },
+ {
+ Type: "min",
+ },
+ },
+ }, p.ToSeriesMarkPoint())
+}
+
+func TestEChartsMarkLine(t *testing.T) {
+ assert := assert.New(t)
+ l := EChartsMarkLine{}
+
+ err := json.Unmarshal([]byte(`{
+ "data": [
+ {
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
+ }`), &l)
+ assert.Nil(err)
+ assert.Equal(SeriesMarkLine{
+ Data: []SeriesMarkData{
+ {
+ Type: "max",
+ },
+ {
+ Type: "min",
+ },
+ },
+ }, l.ToSeriesMarkLine())
+}
+
+func TestEChartsTextStyle(t *testing.T) {
+ assert := assert.New(t)
+
+ s := EChartsTextStyle{
+ Color: "#aaa",
+ FontFamily: "test",
+ FontSize: 14,
+ }
+ assert.Equal(chart.Style{
+ FontColor: drawing.Color{
+ R: 170,
+ G: 170,
+ B: 170,
+ A: 255,
+ },
+ FontSize: 14,
+ }, s.ToStyle())
+}
+
+func TestEChartsSeriesList(t *testing.T) {
+ assert := assert.New(t)
+
+ // pie
+ es := EChartsSeriesList{
+ {
+ Type: ChartTypePie,
+ Radius: "30%",
+ Data: []EChartsSeriesData{
+ {
+ Name: "1",
+ Value: 1,
+ },
+ {
+ Name: "2",
+ Value: 2,
+ },
+ },
+ },
+ }
+ assert.Equal(SeriesList{
+ {
+ Type: ChartTypePie,
+ Name: "1",
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "30%",
+ Data: []SeriesData{
+ {
+ Value: 1,
+ },
+ },
+ },
+ {
+ Type: ChartTypePie,
+ Name: "2",
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "30%",
+ Data: []SeriesData{
+ {
+ Value: 2,
+ },
+ },
+ },
+ }, es.ToSeriesList())
+
+ es = EChartsSeriesList{
+ {
+ Type: ChartTypeBar,
+ Data: []EChartsSeriesData{
+ {
+ Value: 1,
+ ItemStyle: EChartStyle{
+ Color: "#aaa",
},
- {
- "value": 735,
- "name": "Direct"
- }
- ]
- }
- ]
- }`), &e)
- assert.Nil(err)
- seriesList, tickPosition = convertEChartsSeries(&e)
- assert.Equal(chart.TickPositionUnset, tickPosition)
- assert.Equal([]Series{
- {
- Data: []SeriesData{
+ },
{
- Value: 1048,
+ Value: 2,
},
},
- Type: SeriesPie,
- Name: "Search Engine",
- },
- {
- Data: []SeriesData{
- {
- Value: 735,
- },
- },
- Type: SeriesPie,
- Name: "Direct",
- },
- }, seriesList)
-
- err = json.Unmarshal([]byte(`{
- "series": [
- {
- "name": "Evaporation",
- "type": "bar",
- "data": [2, {
- "value": 4.9,
- "itemStyle": {
- "color": "#a90000"
- }
- }, 7, 23.2, 25.6, 76.7, 135.6]
- },
- {
- "name": "Precipitation",
- "type": "bar",
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6]
- },
- {
- "name": "Temperature",
- "type": "line",
- "yAxisIndex": 1,
- "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
- }
- ]
- }`), &e)
- assert.Nil(err)
- bar1Data := NewSeriesDataListFromFloat([]float64{
- 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6,
- })
- bar1Data[1].Style.FillColor = parseColor("#a90000")
- bar1Data[1].Style.StrokeColor = bar1Data[1].Style.FillColor
-
- seriesList, tickPosition = convertEChartsSeries(&e)
- assert.Equal(chart.TickPositionBetweenTicks, tickPosition)
- assert.Equal([]Series{
- {
- Data: bar1Data,
- Type: SeriesBar,
- },
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6,
- }),
- Type: SeriesBar,
- },
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3,
- }),
- Type: SeriesLine,
YAxisIndex: 1,
},
- }, seriesList)
-
-}
-
-func TestParseECharsOptions(t *testing.T) {
-
- assert := assert.New(t)
- options, err := ParseECharsOptions(`{
- "theme": "dark",
- "padding": [5, 10],
- "title": {
- "text": "Multi Line",
- "textAlign": "left",
- "textStyle": {
- "color": "#333",
- "fontSize": 24,
- "height": 40
- }
- },
- "legend": {
- "align": "left",
- "padding": [5, 0, 0, 50],
- "data": ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"]
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
- "splitNumber": 10
- },
- "yAxis": [
- {
- "min": 0,
- "max": 250
- },
- {
- "min": 0,
- "max": 25
- }
- ],
- "series": [
- {
- "name": "Evaporation",
- "type": "bar",
- "data": [2, {
- "value": 4.9,
- "itemStyle": {
- "color": "#a90000"
- }
- }, 7, 23.2, 25.6, 76.7, 135.6]
- },
- {
- "name": "Precipitation",
- "type": "bar",
- "itemStyle": {
- "color": "#0052d9"
+ {
+ Data: []EChartsSeriesData{
+ {
+ Value: 3,
+ },
+ {
+ Value: 4,
},
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6]
},
- {
- "name": "Temperature",
- "type": "line",
- "yAxisIndex": 1,
- "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
- }
- ]
- }`)
-
- assert.Nil(err)
-
- min1 := float64(0)
- max1 := float64(250)
- min2 := float64(0)
- max2 := float64(25)
-
- bar1Data := NewSeriesDataListFromFloat([]float64{
- 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6,
- })
- bar1Data[1].Style.FillColor = parseColor("#a90000")
- bar1Data[1].Style.StrokeColor = bar1Data[1].Style.FillColor
-
- assert.Equal(Options{
- Theme: ThemeDark,
- Padding: chart.Box{
- Top: 5,
- Bottom: 5,
- Left: 10,
- Right: 10,
+ ItemStyle: EChartStyle{
+ Color: "#ccc",
+ },
+ Label: EChartsLabelOption{
+ Color: "#ddd",
+ Show: true,
+ Distance: 5,
+ },
},
- Title: Title{
- Text: "Multi Line",
+ }
+ assert.Equal(SeriesList{
+ {
+ Type: ChartTypeBar,
+ Data: []SeriesData{
+ {
+ Value: 1,
+ Style: chart.Style{
+ FontColor: drawing.Color{
+ R: 170,
+ G: 170,
+ B: 170,
+ A: 255,
+ },
+ StrokeColor: drawing.Color{
+ R: 170,
+ G: 170,
+ B: 170,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 170,
+ G: 170,
+ B: 170,
+ A: 255,
+ },
+ },
+ },
+ {
+ Value: 2,
+ },
+ },
+ YAxisIndex: 1,
+ },
+ {
+ Data: []SeriesData{
+ {
+ Value: 3,
+ },
+ {
+ Value: 4,
+ },
+ },
Style: chart.Style{
- FontColor: parseColor("#333"),
- FontSize: 24,
- Padding: chart.Box{
- Top: 8,
- Bottom: 8,
+ FontColor: drawing.Color{
+ R: 204,
+ G: 204,
+ B: 204,
+ A: 255,
+ },
+ StrokeColor: drawing.Color{
+ R: 204,
+ G: 204,
+ B: 204,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 204,
+ G: 204,
+ B: 204,
+ A: 255,
},
},
- },
- Legend: Legend{
- Data: []string{
- "Email", "Union Ads", "Video Ads", "Direct", "Search Engine",
- },
- Align: "left",
- Padding: chart.Box{
- Top: 5,
- Right: 0,
- Bottom: 0,
- Left: 50,
- },
- },
- XAxis: XAxis{
- Data: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"},
- SplitNumber: 10,
- },
- TickPosition: chart.TickPositionBetweenTicks,
- YAxisOptions: []*YAxisOption{
- {
- Min: &min1,
- Max: &max1,
- },
- {
- Min: &min2,
- Max: &max2,
- },
- },
- Series: []Series{
- {
- Data: bar1Data,
- Type: SeriesBar,
- },
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6,
- }),
- Type: SeriesBar,
- Style: chart.Style{
- StrokeColor: drawing.Color{
- R: 0,
- G: 82,
- B: 217,
- A: 255,
- },
- FillColor: drawing.Color{
- R: 0,
- G: 82,
- B: 217,
- A: 255,
- },
+ Label: SeriesLabel{
+ Color: drawing.Color{
+ R: 221,
+ G: 221,
+ B: 221,
+ A: 255,
},
+ Show: true,
+ Distance: 5,
},
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3,
- }),
- Type: SeriesLine,
- YAxisIndex: 1,
- },
+ MarkPoint: SeriesMarkPoint{},
+ MarkLine: SeriesMarkLine{},
},
- }, options)
-}
+ }, es.ToSeriesList())
-func TestUnmarshalJSON(t *testing.T) {
- assert := assert.New(t)
- var lp Position
- err := lp.UnmarshalJSON([]byte("123"))
- assert.Nil(err)
- assert.Equal("123", string(lp))
-
- err = lp.UnmarshalJSON([]byte(`"234"`))
- assert.Nil(err)
- assert.Equal("234", string(lp))
-}
-
-func BenchmarkEChartsRenderPNG(b *testing.B) {
- for i := 0; i < b.N; i++ {
- _, err := RenderEChartsToPNG(`{
- "title": {
- "text": "Line"
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260]
- }
- ]
- }`)
- if err != nil {
- panic(err)
- }
- }
-}
-
-func BenchmarkEChartsRenderSVG(b *testing.B) {
- for i := 0; i < b.N; i++ {
- _, err := RenderEChartsToSVG(`{
- "title": {
- "text": "Line"
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260]
- }
- ]
- }`)
- if err != nil {
- panic(err)
- }
- }
}
diff --git a/examples/basic/main.go b/examples/basic/main.go
deleted file mode 100644
index 9efc745..0000000
--- a/examples/basic/main.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package main
-
-import (
- "os"
-
- charts "github.com/vicanso/go-charts"
-)
-
-func main() {
- buf, err := charts.RenderEChartsToPNG(`{
- "title": {
- "text": "Line"
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260]
- }
- ]
- }`)
- if err != nil {
- panic(err)
- }
- file, err := os.Create("output.png")
- if err != nil {
- panic(err)
- }
- defer file.Close()
- file.Write(buf)
-}
diff --git a/examples/charts/main.go b/examples/charts/main.go
deleted file mode 100644
index 1828a52..0000000
--- a/examples/charts/main.go
+++ /dev/null
@@ -1,394 +0,0 @@
-package main
-
-import (
- "bytes"
- "net/http"
-
- charts "github.com/vicanso/go-charts"
-)
-
-var html = `
-
-
-
-
-
-
-
- go-charts
-
-
- {{body}}
-
-
-`
-
-var chartOptions = []map[string]string{
- {
- "title": "折线图",
- "option": `{
- "title": {
- "text": "Line\nHello World",
- "left": "right",
- "textStyle": {
- "fontSize": 24,
- "height": 40
- }
- },
- "yAxis": {
- "min": 0,
- "max": 300
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260],
- "type": "line"
- }
- ]
-}`,
- },
- {
- "title": "多折线图",
- "option": `{
- "title": {
- "text": "Multi Line"
- },
- "legend": {
- "align": "left",
- "right": 0,
- "data": ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"]
- },
- "xAxis": {
- "type": "category",
- "boundaryGap": false,
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "type": "line",
- "data": [120, 132, 101, 134, 90, 230, 210]
- },
- {
- "data": [220, 182, 191, 234, 290, 330, 310]
- },
- {
- "data": [150, 232, 201, 154, 190, 330, 410]
- },
- {
- "data": [320, 332, 301, 334, 390, 330, 320]
- },
- {
- "data": [820, 932, 901, 934, 1290, 1330, 1320]
- }
- ]
-}`,
- },
- {
- "title": "柱状图",
- "option": `{
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [120, 200, 150, 80, 70, 110, 130],
- "type": "bar"
- }
- ]
-}`,
- },
- {
- "title": "柱状图(自定义颜色)",
- "option": `{
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [
- 120,
- {
- "value": 200,
- "itemStyle": {
- "color": "#a90000"
- }
- },
- 150,
- 80,
- 70,
- 110,
- 130
- ],
- "type": "bar"
- }
- ]
-}`,
- },
- {
- "title": "多柱状图",
- "option": `{
- "title": {
- "text": "Rainfall vs Evaporation"
- },
- "legend": {
- "data": ["Rainfall", "Evaporation"]
- },
- "xAxis": {
- "type": "category",
- "splitNumber": 12,
- "data": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
- },
- "series": [
- {
- "name": "Rainfall",
- "type": "bar",
- "data": [2, 4.9, 7, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20, 6.4, 3.3]
- },
- {
- "name": "Evaporation",
- "type": "bar",
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6, 2.3]
- }
- ]
-}`,
- },
- {
- "title": "折柱混合",
- "option": `{
- "legend": {
- "data": [
- "Evaporation",
- "Precipitation",
- "Temperature"
- ]
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "yAxis": [
- {
- "type": "value",
- "name": "Precipitation",
- "min": 0,
- "max": 250,
- "interval": 50,
- "axisLabel": {
- "formatter": "{value} ml"
- }
- },
- {
- "type": "value",
- "name": "Temperature",
- "min": 0,
- "max": 25,
- "interval": 5,
- "axisLabel": {
- "formatter": "{value} °C"
- }
- }
- ],
- "series": [
- {
- "name": "Evaporation",
- "type": "bar",
- "itemStyle": {
- "color": "#0052d9"
- },
- "data": [2, 4.9, 7, 23.2, 25.6, 76.7, 135.6]
- },
- {
- "name": "Precipitation",
- "type": "bar",
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6]
- },
- {
- "name": "Temperature",
- "type": "line",
- "yAxisIndex": 1,
- "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
- }
- ]
-}`,
- },
- {
- "title": "降雨量",
- "option": `{
- "title": {
- "text": "降雨量"
- },
- "legend": {
- "data": ["GZ", "SH"]
- },
- "xAxis": {
- "type": "category",
- "splitNumber": 6,
- "data": ["01-01","01-02","01-03","01-04","01-05","01-06","01-07","01-08","01-09","01-10","01-11","01-12","01-13","01-14","01-15","01-16","01-17","01-18","01-19","01-20","01-21","01-22","01-23","01-24","01-25","01-26","01-27","01-28","01-29","01-30","01-31"]
- },
- "yAxis": {
- "axisLabel": {
- "formatter": "{value} mm"
- }
- },
- "series": [
- {
- "type": "bar",
- "data": [928,821,889,600,547,783,197,853,430,346,63,465,309,334,141,538,792,58,922,807,298,243,744,885,812,231,330,220,984,221,429]
- },
- {
- "type": "bar",
- "data": [749,201,296,579,255,159,902,246,149,158,507,776,186,79,390,222,601,367,221,411,714,620,966,73,203,631,833,610,487,677,596]
- }
- ]
-}`,
- },
- {
- "title": "饼图",
- "option": `{
- "title": {
- "text": "Referer of a Website"
- },
- "series": [
- {
- "name": "Access From",
- "type": "pie",
- "radius": "50%",
- "data": [
- {
- "value": 1048,
- "name": "Search Engine"
- },
- {
- "value": 735,
- "name": "Direct"
- },
- {
- "value": 580,
- "name": "Email"
- },
- {
- "value": 484,
- "name": "Union Ads"
- },
- {
- "value": 300,
- "name": "Video Ads"
- }
- ]
- }
- ]
-}`,
- },
-}
-
-type renderOptions struct {
- theme string
- width int
- height int
- onlyCharts bool
-}
-
-func render(opts renderOptions) ([]byte, error) {
- data := bytes.Buffer{}
- for _, m := range chartOptions {
- chartHTML := []byte(`
-
{{title}}
-
{{option}}
- {{svg}}
-
`)
- if opts.onlyCharts {
- chartHTML = []byte(`
- {{svg}}
-
`)
- }
- o, err := charts.ParseECharsOptions(m["option"])
- if opts.width > 0 {
- o.Width = opts.width
- }
- if opts.height > 0 {
- o.Height = opts.height
- }
-
- o.Theme = opts.theme
- if err != nil {
- return nil, err
- }
- g, err := charts.New(o)
- if err != nil {
- return nil, err
- }
- buf, err := charts.ToSVG(g)
- if err != nil {
- return nil, err
- }
- buf = bytes.ReplaceAll(chartHTML, []byte("{{svg}}"), buf)
- buf = bytes.ReplaceAll(buf, []byte("{{title}}"), []byte(m["title"]))
- buf = bytes.ReplaceAll(buf, []byte("{{option}}"), []byte(m["option"]))
- data.Write(buf)
- }
- return data.Bytes(), nil
-}
-
-func indexHandler(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/" {
- return
- }
- query := r.URL.Query()
- opts := renderOptions{
- theme: query.Get("theme"),
- }
- if query.Get("view") == "grid" {
- opts.width = 400
- opts.height = 200
- opts.onlyCharts = true
- }
-
- buf, err := render(opts)
- if err != nil {
- w.WriteHeader(400)
- w.Write([]byte(err.Error()))
- return
- }
-
- data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), buf)
- w.Header().Set("Content-Type", "text/html")
- w.Write(data)
-}
-
-func main() {
- http.HandleFunc("/", indexHandler)
- http.ListenAndServe(":3012", nil)
-}
diff --git a/examples/demo/main.go b/examples/demo/main.go
index 26866d9..e10c7c0 100644
--- a/examples/demo/main.go
+++ b/examples/demo/main.go
@@ -3,8 +3,11 @@ package main
import (
"bytes"
"net/http"
+ "strconv"
charts "github.com/vicanso/go-charts"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
)
var html = `
@@ -12,14 +15,16 @@ var html = `
-
@@ -56,327 +61,1479 @@ var html = `
`
-var chartOptions = []map[string]string{
- {
- "option": `{
- "title": {
- "text": "Line",
- "left": "center"
- },
- "yAxis": {
- "min": 0,
- "max": 300
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [150, 230, 224, 218, 135, 147, 260],
- "type": "line",
- "label": {
- "show": true,
- "distance": 5
- }
- }
- ]
-}`,
- },
- {
- "option": `{
- "legend": {
- "align": "left",
- "left": 0,
- "data": ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"]
- },
- "xAxis": {
- "type": "category",
- "boundaryGap": false,
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "type": "line",
- "data": [120, 132, 101, 134, 90, 230, 210]
- },
- {
- "data": [220, 182, 191, 234, 290, 330, 310]
- },
- {
- "data": [150, 232, 201, 154, 190, 330, 410]
- },
- {
- "data": [320, 332, 301, 334, 390, 330, 320]
- },
- {
- "data": [820, 932, 901, 934, 1290, 1330, 1320]
- }
- ]
-}`,
- },
- {
- "title": "柱状图(自定义颜色)",
- "option": `{
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "series": [
- {
- "data": [
- 120,
- {
- "value": 200,
- "itemStyle": {
- "color": "#a90000"
- }
- },
- 150,
- 80,
- 70,
- 110,
- 130
- ],
- "type": "bar",
- "label": {
- "show": true,
- "distance": 10
- }
- }
- ]
-}`,
- },
- {
- "title": "多柱状图",
- "option": `{
- "title": {
- "text": "Rainfall vs Evaporation",
- "top": 10
- },
- "legend": {
- "data": ["Rainfall", "Evaporation"]
- },
- "xAxis": {
- "type": "category",
- "splitNumber": 12,
- "data": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
- },
- "series": [
- {
- "name": "Rainfall",
- "type": "bar",
- "data": [2, 4.9, 7, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20, 6.4, 3.3]
- },
- {
- "name": "Evaporation",
- "type": "bar",
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6, 2.3]
- }
- ]
-}`,
- },
- {
- "title": "折柱混合",
- "option": `{
- "legend": {
- "data": [
- "Evaporation",
- "Precipitation",
- "Temperature"
- ]
- },
- "xAxis": {
- "type": "category",
- "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
- },
- "yAxis": [
- {
- "type": "value",
- "name": "Precipitation",
- "min": 0,
- "max": 250,
- "interval": 50,
- "axisLabel": {
- "formatter": "{value} ml"
- }
- },
- {
- "type": "value",
- "name": "Temperature",
- "min": 0,
- "max": 25,
- "interval": 5,
- "axisLabel": {
- "formatter": "{value} °C"
- }
- }
- ],
- "series": [
- {
- "name": "Evaporation",
- "type": "bar",
- "itemStyle": {
- "color": "#0052d9"
- },
- "data": [2, 4.9, 7, 23.2, 25.6, 76.7, 135.6]
- },
- {
- "name": "Precipitation",
- "type": "bar",
- "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6]
- },
- {
- "name": "Temperature",
- "type": "line",
- "yAxisIndex": 1,
- "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
- }
- ]
-}`,
- },
- {
- "title": "降雨量",
- "option": `{
- "title": {
- "text": "Rainfall",
- "left": "right"
- },
- "legend": {
- "data": ["GZ", "SH"]
- },
- "xAxis": {
- "type": "category",
- "splitNumber": 6,
- "data": ["01-01","01-02","01-03","01-04","01-05","01-06","01-07","01-08","01-09","01-10","01-11","01-12","01-13","01-14","01-15","01-16","01-17","01-18","01-19","01-20","01-21","01-22","01-23","01-24","01-25","01-26","01-27","01-28","01-29","01-30","01-31"]
- },
- "yAxis": {
- "axisLabel": {
- "formatter": "{value} mm"
- }
- },
- "series": [
- {
- "type": "bar",
- "data": [928,821,889,600,547,783,197,853,430,346,63,465,309,334,141,538,792,58,922,807,298,243,744,885,812,231,330,220,984,221,429]
- },
- {
- "type": "bar",
- "data": [749,201,296,579,255,159,902,246,149,158,507,776,186,79,390,222,601,367,221,411,714,620,966,73,203,631,833,610,487,677,596]
- }
- ]
-}`,
- },
- {
- "title": "饼图",
- "option": `{
- "title": {
- "text": "Referer of a Website"
- },
- "series": [
- {
- "name": "Access From",
- "type": "pie",
- "radius": "50%",
- "label": {
- "show": true
- },
- "data": [
- {
- "value": 1048,
- "name": "Search Engine"
- },
- {
- "value": 735,
- "name": "Direct"
- },
- {
- "value": 580,
- "name": "Email"
- },
- {
- "value": 484,
- "name": "Union Ads"
- },
- {
- "value": 300,
- "name": "Video Ads"
- }
- ]
- }
- ]
-}`,
- },
-}
-
-type renderOptions struct {
- theme string
- width int
- height int
- onlyCharts bool
-}
-
-func render(opts renderOptions) ([]byte, error) {
- data := bytes.Buffer{}
- for _, m := range chartOptions {
- chartHTML := []byte(`
- {{svg}}
-
`)
- o, err := charts.ParseECharsOptions(m["option"])
+func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.ChartOption, echartsOptions []string) {
+ if req.URL.Path != "/" &&
+ req.URL.Path != "/echarts" {
+ return
+ }
+ query := req.URL.Query()
+ theme := query.Get("theme")
+ width, _ := strconv.Atoi(query.Get("width"))
+ height, _ := strconv.Atoi(query.Get("height"))
+ charts.SetDefaultWidth(width)
+ charts.SetDefaultWidth(height)
+ bytesList := make([][]byte, 0)
+ for _, opt := range chartOptions {
+ opt.Theme = theme
+ d, err := charts.Render(opt)
if err != nil {
- return nil, err
+ panic(err)
}
- if opts.width > 0 {
- o.Width = opts.width
- }
- if opts.height > 0 {
- o.Height = opts.height
- }
-
- for _, theme := range []string{
- charts.ThemeDark,
- charts.ThemeLight,
- } {
- o.Theme = theme
- g, err := charts.New(o)
- if err != nil {
- return nil, err
- }
- buf, err := charts.ToSVG(g)
- if err != nil {
- return nil, err
- }
- buf = bytes.ReplaceAll(chartHTML, []byte("{{svg}}"), buf)
- data.Write(buf)
+ buf, err := d.Bytes()
+ if err != nil {
+ panic(err)
}
+ bytesList = append(bytesList, buf)
}
- return data.Bytes(), nil
-}
-
-func indexHandler(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/" {
- return
- }
- query := r.URL.Query()
- opts := renderOptions{
- theme: query.Get("theme"),
- width: 400,
- height: 200,
- onlyCharts: true,
- }
- buf, err := render(opts)
- if err != nil {
- w.WriteHeader(400)
- w.Write([]byte(err.Error()))
- return
+ for _, opt := range echartsOptions {
+ buf, err := charts.RenderEChartsToSVG(opt)
+ if err != nil {
+ panic(err)
+ }
+ bytesList = append(bytesList, buf)
}
- data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), buf)
+ data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte("")))
w.Header().Set("Content-Type", "text/html")
w.Write(data)
}
+func indexHandler(w http.ResponseWriter, req *http.Request) {
+ chartOptions := []charts.ChartOption{
+ // 普通折线图
+ {
+ Title: charts.TitleOption{
+ Text: "Line",
+ },
+ Legend: charts.NewLegendOption([]string{
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ "Direct",
+ "Search Engine",
+ }),
+ XAxis: charts.NewXAxisOption([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }),
+ SeriesList: []charts.Series{
+ 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,
+ }),
+ },
+ },
+ // 温度折线图
+ {
+ Title: charts.TitleOption{
+ Text: "Temperature Change in the Coming Week",
+ },
+ Padding: chart.Box{
+ Top: 20,
+ Left: 20,
+ Right: 30,
+ Bottom: 20,
+ },
+ Legend: charts.NewLegendOption([]string{
+ "Highest",
+ "Lowest",
+ }, charts.PositionRight),
+ XAxis: charts.NewXAxisOption([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }, charts.FalseFlag()),
+ SeriesList: []charts.Series{
+ {
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 14,
+ 11,
+ 13,
+ 11,
+ 12,
+ 12,
+ 7,
+ }),
+ MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin),
+ MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage),
+ },
+ {
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 1,
+ -2,
+ 2,
+ 5,
+ 3,
+ 2,
+ 0,
+ }),
+ MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage),
+ },
+ },
+ },
+ // 柱状图
+ {
+ Title: charts.TitleOption{
+ Text: "Bar",
+ },
+ XAxis: charts.NewXAxisOption([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }),
+ Legend: charts.NewLegendOption([]string{
+ "Rainfall",
+ "Evaporation",
+ }),
+ SeriesList: []charts.Series{
+ charts.NewSeriesFromValues([]float64{
+ 120,
+ 200,
+ 150,
+ 80,
+ 70,
+ 110,
+ 130,
+ }, charts.ChartTypeBar),
+ {
+ Type: charts.ChartTypeBar,
+ Data: []charts.SeriesData{
+ {
+ Value: 100,
+ },
+ {
+ Value: 190,
+ Style: chart.Style{
+ FillColor: drawing.Color{
+ R: 169,
+ G: 0,
+ B: 0,
+ A: 255,
+ },
+ },
+ },
+ {
+ Value: 230,
+ },
+ {
+ Value: 140,
+ },
+ {
+ Value: 100,
+ },
+ {
+ Value: 200,
+ },
+ {
+ Value: 180,
+ },
+ },
+ },
+ },
+ },
+ // 柱状图+mark
+ {
+ Title: charts.TitleOption{
+ Text: "Rainfall vs Evaporation",
+ Subtext: "Fake Data",
+ },
+ Padding: chart.Box{
+ Top: 20,
+ Right: 20,
+ Bottom: 20,
+ Left: 20,
+ },
+ XAxis: charts.NewXAxisOption([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ Legend: charts.NewLegendOption([]string{
+ "Rainfall",
+ "Evaporation",
+ }, charts.PositionRight),
+ SeriesList: []charts.Series{
+ {
+ Type: charts.ChartTypeBar,
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ }),
+ MarkPoint: charts.NewMarkPoint(
+ charts.SeriesMarkDataTypeMax,
+ charts.SeriesMarkDataTypeMin,
+ ),
+ MarkLine: charts.NewMarkLine(
+ charts.SeriesMarkDataTypeAverage,
+ ),
+ },
+ {
+ Type: charts.ChartTypeBar,
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 2.6,
+ 5.9,
+ 9.0,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6.0,
+ 2.3,
+ }),
+ MarkPoint: charts.NewMarkPoint(
+ charts.SeriesMarkDataTypeMax,
+ charts.SeriesMarkDataTypeMin,
+ ),
+ MarkLine: charts.NewMarkLine(
+ charts.SeriesMarkDataTypeAverage,
+ ),
+ },
+ },
+ },
+ // 双Y轴示例
+ {
+ XAxis: charts.NewXAxisOption([]string{
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ }),
+ Legend: charts.NewLegendOption([]string{
+ "Evaporation",
+ "Precipitation",
+ "Temperature",
+ }),
+ YAxisList: []charts.YAxisOption{
+ {
+ Formatter: "{value}°C",
+ Color: drawing.Color{
+ R: 250,
+ G: 200,
+ B: 88,
+ A: 255,
+ },
+ },
+ {
+ Formatter: "{value}ml",
+ Color: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ },
+ },
+ SeriesList: []charts.Series{
+ {
+ Type: charts.ChartTypeBar,
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 2.0,
+ 4.9,
+ 7.0,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20.0,
+ 6.4,
+ 3.3,
+ }),
+ YAxisIndex: 1,
+ },
+ {
+ Type: charts.ChartTypeBar,
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 2.6,
+ 5.9,
+ 9.0,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6.0,
+ 2.3,
+ }),
+ YAxisIndex: 1,
+ },
+ {
+ Data: charts.NewSeriesDataFromValues([]float64{
+ 2.0,
+ 2.2,
+ 3.3,
+ 4.5,
+ 6.3,
+ 10.2,
+ 20.3,
+ 23.4,
+ 23.0,
+ 16.5,
+ 12.0,
+ 6.2,
+ }),
+ },
+ },
+ },
+ // 饼图
+ {
+ Title: charts.TitleOption{
+ Text: "Referer of a Website",
+ Subtext: "Fake Data",
+ Left: charts.PositionCenter,
+ },
+ Legend: charts.LegendOption{
+ Orient: charts.OrientVertical,
+ Data: []string{
+ "Search Engine",
+ "Direct",
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ },
+ Left: charts.PositionLeft,
+ },
+ SeriesList: charts.NewPieSeriesList([]float64{
+ 1048,
+ 735,
+ 580,
+ 484,
+ 300,
+ }, charts.PieSeriesOption{
+ Label: charts.SeriesLabel{
+ Show: true,
+ },
+ Radius: "35%",
+ }),
+ },
+ // 多图展示
+ {
+ Legend: charts.LegendOption{
+ Top: "-90",
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Padding: chart.Box{
+ Top: 100,
+ Right: 10,
+ Bottom: 10,
+ Left: 10,
+ },
+ XAxis: charts.NewXAxisOption([]string{
+ "2012",
+ "2013",
+ "2014",
+ "2015",
+ "2016",
+ "2017",
+ }),
+ YAxisList: []charts.YAxisOption{
+ {
+
+ Min: charts.NewFloatPoint(0),
+ Max: charts.NewFloatPoint(90),
+ },
+ },
+ SeriesList: []charts.Series{
+ charts.NewSeriesFromValues([]float64{
+ 56.5,
+ 82.1,
+ 88.7,
+ 70.1,
+ 53.4,
+ 85.1,
+ }),
+ charts.NewSeriesFromValues([]float64{
+ 51.1,
+ 51.4,
+ 55.1,
+ 53.3,
+ 73.8,
+ 68.7,
+ }),
+ charts.NewSeriesFromValues([]float64{
+ 40.1,
+ 62.2,
+ 69.5,
+ 36.4,
+ 45.2,
+ 32.5,
+ }, charts.ChartTypeBar),
+ charts.NewSeriesFromValues([]float64{
+ 25.2,
+ 37.1,
+ 41.2,
+ 18,
+ 33.9,
+ 49.1,
+ }, charts.ChartTypeBar),
+ },
+ Children: []charts.ChartOption{
+ {
+ Legend: charts.LegendOption{
+ Show: charts.FalseFlag(),
+ Data: []string{
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie",
+ },
+ },
+ Box: chart.Box{
+ Top: 20,
+ Left: 400,
+ Right: 500,
+ Bottom: 120,
+ },
+ SeriesList: charts.NewPieSeriesList([]float64{
+ 435.9,
+ 354.3,
+ 285.9,
+ 204.5,
+ }, charts.PieSeriesOption{
+ Label: charts.SeriesLabel{
+ Show: true,
+ },
+ Radius: "35%",
+ }),
+ },
+ },
+ },
+ }
+ handler(w, req, chartOptions, nil)
+}
+
+func echartsHandler(w http.ResponseWriter, req *http.Request) {
+ echartsOptions := []string{
+ `{
+ "xAxis": {
+ "type": "category",
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ },
+ "yAxis": {
+ "type": "value"
+ },
+ "series": [
+ {
+ "data": [
+ 150,
+ 230,
+ 224,
+ 218,
+ 135,
+ 147,
+ 260
+ ],
+ "type": "line"
+ }
+ ]
+ }`,
+ `{
+ "title": {
+ "text": "Multiple Line"
+ },
+ "tooltip": {
+ "trigger": "axis"
+ },
+ "legend": {
+ "left": "right",
+ "data": [
+ "Email",
+ "Union Ads",
+ "Video Ads",
+ "Direct",
+ "Search Engine"
+ ]
+ },
+ "grid": {
+ "left": "3%",
+ "right": "4%",
+ "bottom": "3%",
+ "containLabel": true
+ },
+ "toolbox": {
+ "feature": {
+ "saveAsImage": {}
+ }
+ },
+ "xAxis": {
+ "type": "category",
+ "boundaryGap": false,
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ },
+ "yAxis": {
+ "type": "value"
+ },
+ "series": [
+ {
+ "name": "Email",
+ "type": "line",
+ "data": [
+ 120,
+ 132,
+ 101,
+ 134,
+ 90,
+ 230,
+ 210
+ ]
+ },
+ {
+ "name": "Union Ads",
+ "type": "line",
+ "data": [
+ 220,
+ 182,
+ 191,
+ 234,
+ 290,
+ 330,
+ 310
+ ]
+ },
+ {
+ "name": "Video Ads",
+ "type": "line",
+ "data": [
+ 150,
+ 232,
+ 201,
+ 154,
+ 190,
+ 330,
+ 410
+ ]
+ },
+ {
+ "name": "Direct",
+ "type": "line",
+ "data": [
+ 320,
+ 332,
+ 301,
+ 334,
+ 390,
+ 330,
+ 320
+ ]
+ },
+ {
+ "name": "Search Engine",
+ "type": "line",
+ "data": [
+ 820,
+ 932,
+ 901,
+ 934,
+ 1290,
+ 1330,
+ 1320
+ ]
+ }
+ ]
+ }`,
+ `{
+ "title": {
+ "text": "Temperature Change in the Coming Week"
+ },
+ "legend": {
+ "left": "right"
+ },
+ "padding": [10, 30, 10, 10],
+ "xAxis": {
+ "type": "category",
+ "boundaryGap": false,
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ },
+ "yAxis": {
+ "axisLabel": {
+ "formatter": "{value} °C"
+ }
+ },
+ "series": [
+ {
+ "name": "Highest",
+ "type": "line",
+ "data": [
+ 10,
+ 11,
+ 13,
+ 11,
+ 12,
+ 12,
+ 9
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average"
+ }
+ ]
+ }
+ },
+ {
+ "name": "Lowest",
+ "type": "line",
+ "data": [
+ 1,
+ -2,
+ 2,
+ 5,
+ 3,
+ 2,
+ 0
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "min"
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average"
+ },
+ {
+ "type": "max"
+ }
+ ]
+ }
+ }
+ ]
+ }`,
+ `{
+ "xAxis": {
+ "type": "category",
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ },
+ "yAxis": {
+ "type": "value"
+ },
+ "series": [
+ {
+ "data": [
+ 120,
+ 200,
+ 150,
+ 80,
+ 70,
+ 110,
+ 130
+ ],
+ "type": "bar"
+ }
+ ]
+ }`,
+ `{
+ "xAxis": {
+ "type": "category",
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ]
+ },
+ "yAxis": {
+ "type": "value"
+ },
+ "series": [
+ {
+ "data": [
+ 120,
+ {
+ "value": 200,
+ "itemStyle": {
+ "color": "#a90000"
+ }
+ },
+ 150,
+ 80,
+ 70,
+ 110,
+ 130
+ ],
+ "type": "bar"
+ }
+ ]
+ }`,
+ `{
+ "title": {
+ "text": "Rainfall vs Evaporation",
+ "subtext": "Fake Data"
+ },
+ "legend": {
+ "data": [
+ "Rainfall",
+ "Evaporation"
+ ]
+ },
+ "padding": [10, 30, 10, 10],
+ "xAxis": [
+ {
+ "type": "category",
+ "data": [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ]
+ }
+ ],
+ "series": [
+ {
+ "name": "Rainfall",
+ "type": "bar",
+ "data": [
+ 2,
+ 4.9,
+ 7,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20,
+ 6.4,
+ 3.3
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average"
+ }
+ ]
+ }
+ },
+ {
+ "name": "Evaporation",
+ "type": "bar",
+ "data": [
+ 2.6,
+ 5.9,
+ 9,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6,
+ 2.3
+ ],
+ "markPoint": {
+ "data": [
+ {
+ "type": "max"
+ },
+ {
+ "type": "min"
+ }
+ ]
+ },
+ "markLine": {
+ "data": [
+ {
+ "type": "average"
+ }
+ ]
+ }
+ }
+ ]
+ }`,
+ `{
+ "legend": {
+ "data": [
+ "Evaporation",
+ "Precipitation",
+ "Temperature"
+ ]
+ },
+ "xAxis": [
+ {
+ "type": "category",
+ "data": [
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun"
+ ],
+ "axisPointer": {
+ "type": "shadow"
+ }
+ }
+ ],
+ "yAxis": [
+ {
+ "type": "value",
+ "name": "Precipitation",
+ "min": 0,
+ "max": 240,
+ "axisLabel": {
+ "formatter": "{value} ml"
+ }
+ },
+ {
+ "type": "value",
+ "name": "Temperature",
+ "min": 0,
+ "max": 24,
+ "axisLabel": {
+ "formatter": "{value} °C"
+ }
+ }
+ ],
+ "series": [
+ {
+ "name": "Evaporation",
+ "type": "bar",
+ "tooltip": {},
+ "data": [
+ 2,
+ 4.9,
+ 7,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20,
+ 6.4,
+ 3.3
+ ]
+ },
+ {
+ "name": "Precipitation",
+ "type": "bar",
+ "tooltip": {},
+ "data": [
+ 2.6,
+ 5.9,
+ 9,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6,
+ 2.3
+ ]
+ },
+ {
+ "name": "Temperature",
+ "type": "line",
+ "yAxisIndex": 1,
+ "tooltip": {},
+ "data": [
+ 2,
+ 2.2,
+ 3.3,
+ 4.5,
+ 6.3,
+ 10.2,
+ 20.3,
+ 23.4,
+ 23,
+ 16.5,
+ 12,
+ 6.2
+ ]
+ }
+ ]
+ }`,
+ `{
+ "tooltip": {
+ "trigger": "axis",
+ "axisPointer": {
+ "type": "cross"
+ }
+ },
+ "grid": {
+ "right": "20%"
+ },
+ "toolbox": {
+ "feature": {
+ "dataView": {
+ "show": true,
+ "readOnly": false
+ },
+ "restore": {
+ "show": true
+ },
+ "saveAsImage": {
+ "show": true
+ }
+ }
+ },
+ "legend": {
+ "data": [
+ "Evaporation",
+ "Precipitation",
+ "Temperature"
+ ]
+ },
+ "xAxis": [
+ {
+ "type": "category",
+ "axisTick": {
+ "alignWithLabel": true
+ },
+ "data": [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ]
+ }
+ ],
+ "yAxis": [
+ {
+ "type": "value",
+ "name": "温度",
+ "position": "left",
+ "alignTicks": true,
+ "axisLine": {
+ "show": true,
+ "lineStyle": {
+ "color": "#EE6666"
+ }
+ },
+ "axisLabel": {
+ "formatter": "{value} °C"
+ }
+ },
+ {
+ "type": "value",
+ "name": "Evaporation",
+ "position": "right",
+ "alignTicks": true,
+ "axisLine": {
+ "show": true,
+ "lineStyle": {
+ "color": "#5470C6"
+ }
+ },
+ "axisLabel": {
+ "formatter": "{value} ml"
+ }
+ }
+ ],
+ "series": [
+ {
+ "name": "Evaporation",
+ "type": "bar",
+ "yAxisIndex": 1,
+ "data": [
+ 2,
+ 4.9,
+ 7,
+ 23.2,
+ 25.6,
+ 76.7,
+ 135.6,
+ 162.2,
+ 32.6,
+ 20,
+ 6.4,
+ 3.3
+ ]
+ },
+ {
+ "name": "Precipitation",
+ "type": "bar",
+ "yAxisIndex": 1,
+ "data": [
+ 2.6,
+ 5.9,
+ 9,
+ 26.4,
+ 28.7,
+ 70.7,
+ 175.6,
+ 182.2,
+ 48.7,
+ 18.8,
+ 6,
+ 2.3
+ ]
+ },
+ {
+ "name": "Temperature",
+ "type": "line",
+ "data": [
+ 2,
+ 2.2,
+ 3.3,
+ 4.5,
+ 6.3,
+ 10.2,
+ 20.3,
+ 23.4,
+ 23,
+ 16.5,
+ 12,
+ 6.2
+ ]
+ }
+ ]
+ }`,
+ `{
+ "title": {
+ "text": "Referer of a Website",
+ "subtext": "Fake Data",
+ "left": "center"
+ },
+ "tooltip": {
+ "trigger": "item"
+ },
+ "legend": {
+ "orient": "vertical",
+ "left": "left"
+ },
+ "series": [
+ {
+ "name": "Access From",
+ "type": "pie",
+ "radius": "50%",
+ "data": [
+ {
+ "value": 1048,
+ "name": "Search Engine"
+ },
+ {
+ "value": 735,
+ "name": "Direct"
+ },
+ {
+ "value": 580,
+ "name": "Email"
+ },
+ {
+ "value": 484,
+ "name": "Union Ads"
+ },
+ {
+ "value": 300,
+ "name": "Video Ads"
+ }
+ ]
+ }
+ ]
+ }`,
+ `{
+ "title": {
+ "text": "Rainfall"
+ },
+ "padding": [10, 10, 10, 30],
+ "legend": {
+ "data": [
+ "GZ",
+ "SH"
+ ]
+ },
+ "xAxis": {
+ "type": "category",
+ "splitNumber": 6,
+ "data": [
+ "01-01",
+ "01-02",
+ "01-03",
+ "01-04",
+ "01-05",
+ "01-06",
+ "01-07",
+ "01-08",
+ "01-09",
+ "01-10",
+ "01-11",
+ "01-12",
+ "01-13",
+ "01-14",
+ "01-15",
+ "01-16",
+ "01-17",
+ "01-18",
+ "01-19",
+ "01-20",
+ "01-21",
+ "01-22",
+ "01-23",
+ "01-24",
+ "01-25",
+ "01-26",
+ "01-27",
+ "01-28",
+ "01-29",
+ "01-30",
+ "01-31"
+ ]
+ },
+ "yAxis": {
+ "axisLabel": {
+ "formatter": "{value} mm"
+ }
+ },
+ "series": [
+ {
+ "type": "bar",
+ "data": [
+ 928,
+ 821,
+ 889,
+ 600,
+ 547,
+ 783,
+ 197,
+ 853,
+ 430,
+ 346,
+ 63,
+ 465,
+ 309,
+ 334,
+ 141,
+ 538,
+ 792,
+ 58,
+ 922,
+ 807,
+ 298,
+ 243,
+ 744,
+ 885,
+ 812,
+ 231,
+ 330,
+ 220,
+ 984,
+ 221,
+ 429
+ ]
+ },
+ {
+ "type": "bar",
+ "data": [
+ 749,
+ 201,
+ 296,
+ 579,
+ 255,
+ 159,
+ 902,
+ 246,
+ 149,
+ 158,
+ 507,
+ 776,
+ 186,
+ 79,
+ 390,
+ 222,
+ 601,
+ 367,
+ 221,
+ 411,
+ 714,
+ 620,
+ 966,
+ 73,
+ 203,
+ 631,
+ 833,
+ 610,
+ 487,
+ 677,
+ 596
+ ]
+ }
+ ]
+ }`,
+ `{
+ "legend": {
+ "top": "-140",
+ "data": [
+ "Milk Tea",
+ "Matcha Latte",
+ "Cheese Cocoa",
+ "Walnut Brownie"
+ ]
+ },
+ "padding": [
+ 150,
+ 10,
+ 10,
+ 10
+ ],
+ "xAxis": [
+ {
+ "data": [
+ "2012",
+ "2013",
+ "2014",
+ "2015",
+ "2016",
+ "2017"
+ ]
+ }
+ ],
+ "series": [
+ {
+ "data": [
+ 56.5,
+ 82.1,
+ 88.7,
+ 70.1,
+ 53.4,
+ 85.1
+ ]
+ },
+ {
+ "data": [
+ 51.1,
+ 51.4,
+ 55.1,
+ 53.3,
+ 73.8,
+ 68.7
+ ]
+ },
+ {
+ "data": [
+ 40.1,
+ 62.2,
+ 69.5,
+ 36.4,
+ 45.2,
+ 32.5
+ ]
+ },
+ {
+ "data": [
+ 25.2,
+ 37.1,
+ 41.2,
+ 18,
+ 33.9,
+ 49.1
+ ]
+ }
+ ],
+ "children": [
+ {
+ "box": {
+ "left": 0,
+ "top": 30,
+ "right": 600,
+ "bottom": 150
+ },
+ "legend": {
+ "show": false
+ },
+ "series": [
+ {
+ "type": "pie",
+ "radius": "50%",
+ "data": [
+ {
+ "value": 435.9,
+ "name": "Milk Tea"
+ },
+ {
+ "value": 354.3,
+ "name": "Matcha Latte"
+ },
+ {
+ "value": 285.9,
+ "name": "Cheese Cocoa"
+ },
+ {
+ "value": 204.5,
+ "name": "Walnut Brownie"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }`,
+ }
+ handler(w, req, nil, echartsOptions)
+}
+
func main() {
http.HandleFunc("/", indexHandler)
+ http.HandleFunc("/echarts", echartsHandler)
http.ListenAndServe(":3012", nil)
}
diff --git a/font.go b/font.go
new file mode 100644
index 0000000..c40b51e
--- /dev/null
+++ b/font.go
@@ -0,0 +1,61 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "errors"
+ "sync"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/v2/roboto"
+)
+
+var fonts = sync.Map{}
+var ErrFontNotExists = errors.New("font is not exists")
+
+func init() {
+ _ = InstallFont("roboto", roboto.Roboto)
+}
+
+// InstallFont installs the font for charts
+func InstallFont(fontFamily string, data []byte) error {
+ font, err := truetype.Parse(data)
+ if err != nil {
+ return err
+ }
+ fonts.Store(fontFamily, font)
+ return nil
+}
+
+// GetFont get the font by font family
+func GetFont(fontFamily string) (*truetype.Font, error) {
+ value, ok := fonts.Load(fontFamily)
+ if !ok {
+ return nil, ErrFontNotExists
+ }
+ f, ok := value.(*truetype.Font)
+ if !ok {
+ return nil, ErrFontNotExists
+ }
+ return f, nil
+}
diff --git a/line_series_test.go b/font_test.go
similarity index 73%
rename from line_series_test.go
rename to font_test.go
index 27c9371..9dc731c 100644
--- a/line_series_test.go
+++ b/font_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -26,21 +26,17 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/roboto"
)
-func TestLineSeries(t *testing.T) {
+func TestInstallFont(t *testing.T) {
assert := assert.New(t)
- ls := LineSeries{}
+ fontFamily := "test"
+ err := InstallFont(fontFamily, roboto.Roboto)
+ assert.Nil(err)
- originalRange := &chart.ContinuousRange{}
- xrange := ls.getXRange(originalRange)
- assert.Equal(originalRange, xrange)
-
- ls.TickPosition = chart.TickPositionBetweenTicks
- xrange = ls.getXRange(originalRange)
- value, ok := xrange.(*Range)
- assert.True(ok)
- assert.Equal(originalRange, &value.ContinuousRange)
+ font, err := GetFont(fontFamily)
+ assert.Nil(err)
+ assert.NotNil(font)
}
diff --git a/label.go b/label.go
deleted file mode 100644
index 6d77eb2..0000000
--- a/label.go
+++ /dev/null
@@ -1,77 +0,0 @@
-// 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/dustin/go-humanize"
- "github.com/wcharczuk/go-chart/v2"
-)
-
-type LabelRenderer struct {
- Options SeriesLabel
- Offset chart.Box
-}
-type LabelValue struct {
- Left int
- Top int
- Text string
-}
-
-func (l LabelRenderer) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, style chart.Style, vs chart.ValuesProvider) {
- if !l.Options.Show {
- return
- }
- r.SetFontColor(style.FontColor)
- r.SetFontSize(style.FontSize)
- r.SetFont(style.Font)
- offsetX := l.Options.Offset.Left + l.Offset.Left
- offsetY := l.Options.Offset.Top + l.Offset.Top
- for i := 0; i < vs.Len(); i++ {
- vx, vy := vs.GetValues(i)
- x := canvasBox.Left + xrange.Translate(vx) + offsetX
- y := canvasBox.Bottom - yrange.Translate(vy) + offsetY
-
- text := humanize.CommafWithDigits(vy, 2)
- // 往左移一半距离
- x -= r.MeasureText(text).Width() >> 1
- r.Text(text, x, y)
- }
-}
-
-func (l LabelRenderer) CustomizeRender(r chart.Renderer, style chart.Style, values []LabelValue) {
- if !l.Options.Show {
- return
- }
- r.SetFont(style.Font)
- r.SetFontColor(style.FontColor)
- r.SetFontSize(style.FontSize)
- offsetX := l.Options.Offset.Left + l.Offset.Left
- offsetY := l.Options.Offset.Top + l.Offset.Top
- for _, value := range values {
- x := value.Left + offsetX
- y := value.Top + offsetY
- text := value.Text
- x -= r.MeasureText(text).Width() >> 1
- r.Text(text, x, y)
- }
-}
diff --git a/legend.go b/legend.go
index c85564f..7eb33b3 100644
--- a/legend.go
+++ b/legend.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -30,220 +30,179 @@ import (
)
type LegendOption struct {
- Style chart.Style
- Padding chart.Box
- Left string
- Right string
- Top string
- Bottom string
- Align string
- Theme string
- IconDraw LegendIconDraw
-}
-
-type LegendIconDrawOption struct {
- Box chart.Box
+ theme string
+ // Legend show flag, if nil or true, the legend will be shown
+ Show *bool
+ // Legend text style
Style chart.Style
- Index int
- Theme string
+ // Text array of legend
+ Data []string
+ // Distance between legend component and the left side of the container.
+ // It can be pixel value: 20, percentage value: 20%,
+ // or position value: right, center.
+ Left string
+ // Distance between legend component and the top side of the container.
+ // It can be pixel value: 20.
+ Top string
+ // Legend marker and text aligning, it can be left or right, default is left.
+ Align string
+ // The layout orientation of legend, it can be horizontal or vertical, default is horizontal.
+ Orient string
}
-const (
- LegendAlignLeft = "left"
- LegendAlignRight = "right"
-)
-
-type LegendIconDraw func(r chart.Renderer, opt LegendIconDrawOption)
-
-func DefaultLegendIconDraw(r chart.Renderer, opt LegendIconDrawOption) {
- if opt.Box.IsZero() {
- return
+// NewLegendOption creates a new legend option by legend text list
+func NewLegendOption(data []string, position ...string) LegendOption {
+ opt := LegendOption{
+ Data: data,
}
- r.SetStrokeColor(opt.Style.GetStrokeColor())
- strokeWidth := opt.Style.GetStrokeWidth()
- r.SetStrokeWidth(strokeWidth)
- height := opt.Box.Bottom - opt.Box.Top
- ly := opt.Box.Top - (height / 2) + 2
- r.MoveTo(opt.Box.Left, ly)
- r.LineTo(opt.Box.Right, ly)
- r.Stroke()
- r.SetFillColor(getBackgroundColor(opt.Theme))
- r.Circle(5, (opt.Box.Left+opt.Box.Right)/2, ly)
- r.FillStroke()
+ if len(position) != 0 {
+ opt.Left = position[0]
+ }
+ return opt
}
-func convertPercent(value string) float64 {
- if !strings.HasSuffix(value, "%") {
- return -1
+type legend struct {
+ d *Draw
+ opt *LegendOption
+}
+
+func NewLegend(d *Draw, opt LegendOption) *legend {
+ return &legend{
+ d: d,
+ opt: &opt,
}
- v, err := strconv.Atoi(strings.ReplaceAll(value, "%", ""))
+}
+
+func (l *legend) Render() (chart.Box, error) {
+ d := l.d
+ opt := l.opt
+ if len(opt.Data) == 0 || isFalse(opt.Show) {
+ return chart.BoxZero, nil
+ }
+ theme := NewTheme(opt.theme)
+ padding := opt.Style.Padding
+ legendDraw, err := NewDraw(DrawOption{
+ Parent: d,
+ }, PaddingOption(padding))
if err != nil {
- return -1
+ return chart.BoxZero, err
}
- return float64(v) / 100
-}
+ r := legendDraw.Render
+ opt.Style.GetTextOptions().WriteToRenderer(r)
-func getLegendLeft(canvasWidth, legendBoxWidth int, opt LegendOption) int {
- left := (canvasWidth - legendBoxWidth) / 2
- leftValue := opt.Left
- if leftValue == "auto" || leftValue == "center" {
- leftValue = ""
- }
- if leftValue == "left" {
- leftValue = "0"
+ x := 0
+ y := 0
+ top := 0
+ // TODO TOP 暂只支持数值
+ if opt.Top != "" {
+ top, _ = strconv.Atoi(opt.Top)
+ y += top
}
+ legendWidth := 30
+ legendDotHeight := 5
+ textPadding := 5
+ legendMargin := 10
+ // 往下移2倍dot的高度
+ y += 2 * legendDotHeight
- rightValue := opt.Right
- if rightValue == "auto" || leftValue == "center" {
- rightValue = ""
- }
- if rightValue == "right" {
- rightValue = "0"
- }
- if leftValue == "" && rightValue == "" {
- return left
- }
- if leftValue != "" {
- percent := convertPercent(leftValue)
- if percent >= 0 {
- return int(float64(canvasWidth) * percent)
+ widthCount := 0
+ maxTextWidth := 0
+ // 文本宽度
+ for _, text := range opt.Data {
+ b := r.MeasureText(text)
+ if b.Width() > maxTextWidth {
+ maxTextWidth = b.Width()
}
- v, _ := strconv.Atoi(leftValue)
- return v
+ widthCount += b.Width()
}
- if rightValue != "" {
- percent := convertPercent(rightValue)
- if percent >= 0 {
- return canvasWidth - legendBoxWidth - int(float64(canvasWidth)*percent)
- }
- v, _ := strconv.Atoi(rightValue)
- return canvasWidth - legendBoxWidth - v
+ if opt.Orient == OrientVertical {
+ widthCount = maxTextWidth + legendWidth + textPadding
+ } else {
+ // 加上标记
+ widthCount += legendWidth * len(opt.Data)
+ // 文本的padding
+ widthCount += 2 * textPadding * len(opt.Data)
+ // margin的宽度
+ widthCount += legendMargin * (len(opt.Data) - 1)
}
- return left
-}
-func getLegendTop(height, legendBoxHeight int, opt LegendOption) int {
- // TODO 支持top的处理
- return 0
-}
-
-func NewLegendCustomize(series []chart.Series, opt LegendOption) chart.Renderable {
- return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
- legendDefaults := chart.Style{
- FontColor: getTextColor(opt.Theme),
- FontSize: 8.0,
- StrokeColor: chart.DefaultAxisColor,
+ left := 0
+ switch opt.Left {
+ case PositionRight:
+ left = legendDraw.Box.Width() - widthCount
+ case PositionCenter:
+ left = (legendDraw.Box.Width() - widthCount) >> 1
+ default:
+ if strings.HasSuffix(opt.Left, "%") {
+ value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
+ left = legendDraw.Box.Width() * value / 100
+ } else {
+ value, _ := strconv.Atoi(opt.Left)
+ left = value
}
+ }
+ x = left
+ for index, text := range opt.Data {
+ seriesColor := theme.GetSeriesColor(index)
+ fillColor := seriesColor
+ if !theme.IsDark() {
+ fillColor = theme.GetBackgroundColor()
+ }
+ style := chart.Style{
+ StrokeColor: seriesColor,
+ FillColor: fillColor,
+ StrokeWidth: 3,
+ }
+ style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r)
- legendStyle := opt.Style.InheritFrom(chartDefaults.InheritFrom(legendDefaults))
+ textBox := r.MeasureText(text)
+ var renderText func()
+ if opt.Orient == OrientVertical {
+ // 垂直
+ // 重置x的位置
+ x = left
+ renderText = func() {
+ x += textPadding
+ legendDraw.text(text, x, y+legendDotHeight)
+ x += textBox.Width()
+ y += (2*legendDotHeight + legendMargin)
+ }
- r.SetFont(legendStyle.GetFont())
- r.SetFontColor(legendStyle.GetFontColor())
- r.SetFontSize(legendStyle.GetFontSize())
-
- var labels []string
- var lines []chart.Style
- // 计算label和lines
- for _, s := range series {
- if !s.GetStyle().Hidden {
- if _, isAnnotationSeries := s.(chart.AnnotationSeries); !isAnnotationSeries {
- labels = append(labels, s.GetName())
- lines = append(lines, s.GetStyle())
- }
+ } else {
+ // 水平
+ if index != 0 {
+ x += legendMargin
+ }
+ renderText = func() {
+ x += textPadding
+ legendDraw.text(text, x, y+legendDotHeight)
+ x += textBox.Width()
+ x += textPadding
}
}
-
- var textHeight int
- var textBox chart.Box
- labelWidth := 0
- // 计算文本宽度与高度(取最大值)
- for x := 0; x < len(labels); x++ {
- if len(labels[x]) > 0 {
- textBox = r.MeasureText(labels[x])
- labelWidth += textBox.Width()
- textHeight = chart.MaxInt(textBox.Height(), textHeight)
- }
+ if opt.Align == PositionRight {
+ renderText()
}
- legendBoxHeight := textHeight + legendStyle.Padding.Top + legendStyle.Padding.Bottom
- chartPadding := cb.Top
- legendYMargin := (chartPadding - legendBoxHeight) >> 1
+ legendDraw.moveTo(x, y)
+ legendDraw.lineTo(x+legendWidth, y)
+ r.Stroke()
+ legendDraw.circle(float64(legendDotHeight), x+legendWidth>>1, y)
+ r.FillStroke()
+ x += legendWidth
- iconWidth := 25
- lineTextGap := 5
-
- iconAllWidth := iconWidth * len(labels)
- spaceAllWidth := (chart.DefaultMinimumTickHorizontalSpacing + lineTextGap) * (len(labels) - 1)
-
- legendBoxWidth := labelWidth + iconAllWidth + spaceAllWidth
-
- left := getLegendLeft(cb.Width(), legendBoxWidth, opt)
- top := getLegendTop(cb.Height(), legendBoxHeight, opt)
-
- left += (opt.Padding.Left + cb.Left)
- top += (opt.Padding.Top + cb.Top)
-
- legendBox := chart.Box{
- Left: left,
- Right: left + legendBoxWidth,
- Top: top,
- Bottom: top + legendBoxHeight,
- }
-
- chart.Draw.Box(r, legendBox, legendDefaults)
-
- r.SetFont(legendStyle.GetFont())
- r.SetFontColor(legendStyle.GetFontColor())
- r.SetFontSize(legendStyle.GetFontSize())
-
- startX := legendBox.Left + legendStyle.Padding.Left
- ty := top + legendYMargin + legendStyle.Padding.Top + textHeight
- var label string
- var x int
- iconDraw := opt.IconDraw
- if iconDraw == nil {
- iconDraw = DefaultLegendIconDraw
- }
- align := opt.Align
- if align == "" {
- align = LegendAlignLeft
- }
- for index := range labels {
- label = labels[index]
- if len(label) > 0 {
- x = startX
-
- // 如果图例标记靠右展示
- if align == LegendAlignRight {
- textBox = r.MeasureText(label)
- r.Text(label, x, ty)
- x = startX + textBox.Width() + lineTextGap
- }
-
- // 图标
- iconDraw(r, LegendIconDrawOption{
- Theme: opt.Theme,
- Index: index,
- Style: lines[index],
- Box: chart.Box{
- Left: x,
- Top: ty,
- Right: x + iconWidth,
- Bottom: ty + textHeight,
- },
- })
- x += (iconWidth + lineTextGap)
-
- // 如果图例标记靠左展示
- if align == LegendAlignLeft {
- textBox = r.MeasureText(label)
- r.Text(label, x, ty)
- x += textBox.Width()
- }
-
- // 计算下一个legend的位置
- startX = x + chart.DefaultMinimumTickHorizontalSpacing
- }
+ if opt.Align != PositionRight {
+ renderText()
}
}
+ legendBox := padding.Clone()
+ // 计算展示区域
+ if opt.Orient == OrientVertical {
+ legendBox.Right = legendBox.Left + left + maxTextWidth + legendWidth + textPadding
+ legendBox.Bottom = legendBox.Top + y
+ } else {
+ legendBox.Right = legendBox.Left + x
+ legendBox.Bottom = legendBox.Top + 2*legendDotHeight + top + textPadding
+ }
+ return legendBox, nil
}
diff --git a/legend_test.go b/legend_test.go
index 8f21210..c5d7e50 100644
--- a/legend_test.go
+++ b/legend_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,91 +23,163 @@
package charts
import (
- "bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
)
-func TestNewLegendCustomize(t *testing.T) {
+func TestNewLegendOption(t *testing.T) {
assert := assert.New(t)
- series := GetSeries([]Series{
- {
- Name: "chrome",
+ opt := NewLegendOption([]string{
+ "a",
+ "b",
+ }, PositionRight)
+ assert.Equal(LegendOption{
+ Data: []string{
+ "a",
+ "b",
},
- {
- Name: "edge",
- },
- }, chart.TickPositionBetweenTicks, "")
+ Left: PositionRight,
+ }, opt)
+}
+
+func TestLegendRender(t *testing.T) {
+ assert := assert.New(t)
+
+ newDraw := func() *Draw {
+ d, _ := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ return d
+ }
+ style := chart.Style{
+ FontSize: 10,
+ FontColor: drawing.ColorBlack,
+ }
+ style.Font, _ = chart.GetDefaultFont()
tests := []struct {
- align string
- svg string
+ newDraw func() *Draw
+ newLegend func(*Draw) *legend
+ box chart.Box
+ result string
}{
{
- align: LegendAlignLeft,
- svg: "",
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 214,
+ Bottom: 25,
+ },
},
{
- align: LegendAlignRight,
- svg: "",
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: PositionRight,
+ Align: PositionRight,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 400,
+ Bottom: 25,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: PositionCenter,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 307,
+ Bottom: 25,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: PositionLeft,
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ Orient: OrientVertical,
+ })
+ },
+ result: "",
+ box: chart.Box{
+ Right: 61,
+ Bottom: 80,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newLegend: func(d *Draw) *legend {
+ return NewLegend(d, LegendOption{
+ Top: "10",
+ Left: "10%",
+ Data: []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ },
+ Style: style,
+ Orient: OrientVertical,
+ })
+ },
+ box: chart.Box{
+ Right: 101,
+ Bottom: 80,
+ },
+ result: "",
},
}
for _, tt := range tests {
- r, err := chart.SVG(800, 600)
+ d := tt.newDraw()
+ b, err := tt.newLegend(d).Render()
assert.Nil(err)
- fn := NewLegendCustomize(series, LegendOption{
- Align: tt.align,
- IconDraw: DefaultLegendIconDraw,
- Padding: chart.Box{
- Left: 100,
- Top: 100,
- },
- })
- fn(r, chart.NewBox(11, 47, 784, 373), chart.Style{
- Font: chart.StyleTextDefaults().Font,
- })
- buf := bytes.Buffer{}
- err = r.Save(&buf)
+ assert.Equal(tt.box, b)
+ data, err := d.Bytes()
assert.Nil(err)
- assert.Equal(tt.svg, buf.String())
+ assert.NotEmpty(data)
+ assert.Equal(tt.result, string(data))
}
}
-
-func TestConvertPercent(t *testing.T) {
- assert := assert.New(t)
-
- assert.Equal(-1.0, convertPercent("12"))
-
- assert.Equal(0.12, convertPercent("12%"))
-}
-
-func TestGetLegendLeft(t *testing.T) {
- assert := assert.New(t)
-
- assert.Equal(150, getLegendLeft(500, 200, LegendOption{}))
-
- assert.Equal(0, getLegendLeft(500, 200, LegendOption{
- Left: "left",
- }))
- assert.Equal(100, getLegendLeft(500, 200, LegendOption{
- Left: "20%",
- }))
- assert.Equal(20, getLegendLeft(500, 200, LegendOption{
- Left: "20",
- }))
-
- assert.Equal(300, getLegendLeft(500, 200, LegendOption{
- Right: "right",
- }))
- assert.Equal(200, getLegendLeft(500, 200, LegendOption{
- Right: "20%",
- }))
- assert.Equal(280, getLegendLeft(500, 200, LegendOption{
- Right: "20",
- }))
-
-}
diff --git a/line.go b/line.go
new file mode 100644
index 0000000..e1f3583
--- /dev/null
+++ b/line.go
@@ -0,0 +1,105 @@
+// 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"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+type LineStyle struct {
+ ClassName string
+ StrokeDashArray []float64
+ StrokeColor drawing.Color
+ StrokeWidth float64
+ FillColor drawing.Color
+ DotWidth float64
+ DotColor drawing.Color
+ DotFillColor drawing.Color
+}
+
+func (ls *LineStyle) Style() chart.Style {
+ return chart.Style{
+ ClassName: ls.ClassName,
+ StrokeDashArray: ls.StrokeDashArray,
+ StrokeColor: ls.StrokeColor,
+ StrokeWidth: ls.StrokeWidth,
+ FillColor: ls.FillColor,
+ DotWidth: ls.DotWidth,
+ DotColor: ls.DotColor,
+ }
+}
+
+func (d *Draw) lineFill(points []Point, style LineStyle) {
+ s := style.Style()
+ if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) {
+ return
+ }
+ r := d.Render
+ var x, y int
+ s.GetFillOptions().WriteDrawingOptionsToRenderer(r)
+ for index, point := range points {
+ x = point.X
+ y = point.Y
+ if index == 0 {
+ d.moveTo(x, y)
+ } else {
+ d.lineTo(x, y)
+ }
+ }
+ height := d.Box.Height()
+ d.lineTo(x, height)
+ x0 := points[0].X
+ y0 := points[0].Y
+ d.lineTo(x0, height)
+ d.lineTo(x0, y0)
+ r.Fill()
+}
+
+func (d *Draw) lineDot(points []Point, style LineStyle) {
+ s := style.Style()
+ if !s.ShouldDrawDot() {
+ return
+ }
+ r := d.Render
+ dotWith := s.GetDotWidth()
+
+ s.GetDotOptions().WriteDrawingOptionsToRenderer(r)
+ for _, point := range points {
+ if !style.DotFillColor.IsZero() {
+ r.SetFillColor(style.DotFillColor)
+ }
+ r.SetStrokeColor(s.DotColor)
+ d.circle(dotWith, point.X, point.Y)
+ r.FillStroke()
+ }
+}
+
+func (d *Draw) Line(points []Point, style LineStyle) {
+ if len(points) == 0 {
+ return
+ }
+ d.lineFill(points, style)
+ d.lineStroke(points, style)
+ d.lineDot(points, style)
+}
diff --git a/line_chart.go b/line_chart.go
new file mode 100644
index 0000000..78790d3
--- /dev/null
+++ b/line_chart.go
@@ -0,0 +1,128 @@
+// 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 lineChartOption struct {
+ Theme string
+ SeriesList SeriesList
+ Font *truetype.Font
+}
+
+func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPointRenderOption, error) {
+
+ theme := NewTheme(opt.Theme)
+
+ d, err := NewDraw(DrawOption{
+ Parent: result.d,
+ }, PaddingOption(chart.Box{
+ Top: result.titleBox.Height(),
+ Left: YAxisWidth,
+ }))
+ if err != nil {
+ return nil, err
+ }
+ seriesNames := opt.SeriesList.Names()
+
+ r := d.Render
+ xRange := result.xRange
+ markPointRenderOptions := make([]markPointRenderOption, 0)
+ for i, s := range opt.SeriesList {
+ // 由于series是for range,为同一个数据,因此需要clone
+ // 后续需要使用,如mark point
+ series := s
+ index := series.index
+ if index == 0 {
+ index = i
+ }
+ seriesColor := theme.GetSeriesColor(index)
+
+ yRange := result.getYRange(series.YAxisIndex)
+ points := make([]Point, len(series.Data))
+ // mark line
+ markLineRender(markLineRenderOption{
+ Draw: d,
+ FillColor: seriesColor,
+ FontColor: theme.GetTextColor(),
+ StrokeColor: seriesColor,
+ Font: opt.Font,
+ Series: &series,
+ Range: yRange,
+ })
+
+ for j, item := range series.Data {
+ y := yRange.getRestHeight(item.Value)
+ x := xRange.getWidth(float64(j))
+ points[j] = Point{
+ Y: y,
+ X: x,
+ }
+ if !series.Label.Show {
+ continue
+ }
+ distance := series.Label.Distance
+ if distance == 0 {
+ distance = 5
+ }
+ text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
+ labelStyle := chart.Style{
+ FontColor: theme.GetTextColor(),
+ FontSize: labelFontSize,
+ Font: opt.Font,
+ }
+ if !series.Label.Color.IsZero() {
+ labelStyle.FontColor = series.Label.Color
+ }
+ labelStyle.GetTextOptions().WriteToRenderer(r)
+ textBox := r.MeasureText(text)
+ d.text(text, x-textBox.Width()>>1, y-distance)
+ }
+
+ dotFillColor := drawing.ColorWhite
+ if theme.IsDark() {
+ dotFillColor = seriesColor
+ }
+ d.Line(points, LineStyle{
+ StrokeColor: seriesColor,
+ StrokeWidth: 2,
+ DotColor: seriesColor,
+ DotWidth: 2,
+ DotFillColor: dotFillColor,
+ })
+ // draw mark point
+ markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{
+ Draw: d,
+ FillColor: seriesColor,
+ Font: opt.Font,
+ Points: points,
+ Series: &series,
+ })
+ }
+
+ return markPointRenderOptions, nil
+}
diff --git a/line_chart_test.go b/line_chart_test.go
new file mode 100644
index 0000000..62d0a40
--- /dev/null
+++ b/line_chart_test.go
@@ -0,0 +1,96 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestLineChartRender(t *testing.T) {
+ assert := assert.New(t)
+
+ width := 400
+ height := 300
+ d, err := NewDraw(DrawOption{
+ Width: width,
+ Height: height,
+ })
+ assert.Nil(err)
+
+ result := basicRenderResult{
+ xRange: &Range{
+ Min: 0,
+ Max: 4,
+ divideCount: 4,
+ Size: width,
+ Boundary: true,
+ },
+ yRangeList: []*Range{
+ {
+ divideCount: 6,
+ Max: 100,
+ Min: 0,
+ Size: height,
+ },
+ },
+ d: d,
+ }
+ f, _ := chart.GetDefaultFont()
+ lineChartRender(lineChartOption{
+ Font: f,
+ SeriesList: SeriesList{
+ {
+ Label: SeriesLabel{
+ Show: true,
+ Color: drawing.ColorBlue,
+ },
+ MarkLine: NewMarkLine(
+ SeriesMarkDataTypeAverage,
+ ),
+ Data: []SeriesData{
+ {
+ Value: 20,
+ },
+ {
+ Value: 60,
+ },
+ {
+ Value: 90,
+ },
+ },
+ },
+ NewSeriesFromValues([]float64{
+ 40,
+ 60,
+ 70,
+ }),
+ },
+ }, &result)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/line_test.go b/line_test.go
new file mode 100644
index 0000000..e10b806
--- /dev/null
+++ b/line_test.go
@@ -0,0 +1,165 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestLineStyle(t *testing.T) {
+ assert := assert.New(t)
+
+ ls := LineStyle{
+ ClassName: "test",
+ StrokeDashArray: []float64{
+ 1.0,
+ },
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ FillColor: drawing.ColorBlack.WithAlpha(60),
+ DotWidth: 2,
+ DotColor: drawing.ColorBlack,
+ DotFillColor: drawing.ColorWhite,
+ }
+
+ assert.Equal(chart.Style{
+ ClassName: "test",
+ StrokeDashArray: []float64{
+ 1.0,
+ },
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ FillColor: drawing.ColorBlack.WithAlpha(60),
+ DotWidth: 2,
+ DotColor: drawing.ColorBlack,
+ }, ls.Style())
+}
+
+func TestDrawLineFill(t *testing.T) {
+ assert := assert.New(t)
+
+ ls := LineStyle{
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ FillColor: drawing.ColorBlack.WithAlpha(60),
+ DotWidth: 2,
+ DotColor: drawing.ColorBlack,
+ DotFillColor: drawing.ColorWhite,
+ }
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+ d.lineFill([]Point{
+ {
+ X: 0,
+ Y: 0,
+ },
+ {
+ X: 10,
+ Y: 20,
+ },
+ {
+ X: 50,
+ Y: 60,
+ },
+ }, ls)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestDrawLineDot(t *testing.T) {
+ assert := assert.New(t)
+
+ ls := LineStyle{
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ FillColor: drawing.ColorBlack.WithAlpha(60),
+ DotWidth: 2,
+ DotColor: drawing.ColorBlack,
+ DotFillColor: drawing.ColorWhite,
+ }
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+ d.lineDot([]Point{
+ {
+ X: 0,
+ Y: 0,
+ },
+ {
+ X: 10,
+ Y: 20,
+ },
+ {
+ X: 50,
+ Y: 60,
+ },
+ }, ls)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
+
+func TestDrawLine(t *testing.T) {
+ assert := assert.New(t)
+
+ ls := LineStyle{
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ FillColor: drawing.ColorBlack.WithAlpha(60),
+ DotWidth: 2,
+ DotColor: drawing.ColorBlack,
+ DotFillColor: drawing.ColorWhite,
+ }
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+ d.Line([]Point{
+ {
+ X: 0,
+ Y: 0,
+ },
+ {
+ X: 10,
+ Y: 20,
+ },
+ {
+ X: 50,
+ Y: 60,
+ },
+ }, ls)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/mark_line.go b/mark_line.go
new file mode 100644
index 0000000..464fe71
--- /dev/null
+++ b/mark_line.go
@@ -0,0 +1,92 @@
+// 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"
+)
+
+func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
+ data := make([]SeriesMarkData, len(markLineTypes))
+ for index, t := range markLineTypes {
+ data[index] = SeriesMarkData{
+ Type: t,
+ }
+ }
+ return SeriesMarkLine{
+ Data: data,
+ }
+}
+
+type markLineRenderOption struct {
+ Draw *Draw
+ FillColor drawing.Color
+ FontColor drawing.Color
+ StrokeColor drawing.Color
+ Font *truetype.Font
+ Series *Series
+ Range *Range
+}
+
+func markLineRender(opt markLineRenderOption) {
+ d := opt.Draw
+ s := opt.Series
+ if len(s.MarkLine.Data) == 0 {
+ return
+ }
+ r := d.Render
+ summary := s.Summary()
+ for _, markLine := range s.MarkLine.Data {
+ // 由于mark line会修改style,因此每次重新设置
+ chart.Style{
+ FillColor: opt.FillColor,
+ FontColor: opt.FontColor,
+ FontSize: labelFontSize,
+ StrokeColor: opt.StrokeColor,
+ StrokeWidth: 1,
+ Font: opt.Font,
+ StrokeDashArray: []float64{
+ 4,
+ 2,
+ },
+ }.WriteToRenderer(r)
+ value := float64(0)
+ switch markLine.Type {
+ case SeriesMarkDataTypeMax:
+ value = summary.MaxValue
+ case SeriesMarkDataTypeMin:
+ value = summary.MinValue
+ default:
+ value = summary.AverageValue
+ }
+ y := opt.Range.getRestHeight(value)
+ width := d.Box.Width()
+ text := commafWithDigits(value)
+ textBox := r.MeasureText(text)
+ d.makeLine(0, y, width-2)
+ d.text(text, width, y+textBox.Height()>>1-2)
+ }
+
+}
diff --git a/mark_line_test.go b/mark_line_test.go
new file mode 100644
index 0000000..abb3308
--- /dev/null
+++ b/mark_line_test.go
@@ -0,0 +1,99 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestNewMarkLine(t *testing.T) {
+ assert := assert.New(t)
+
+ markLine := NewMarkLine(
+ SeriesMarkDataTypeMax,
+ SeriesMarkDataTypeMin,
+ SeriesMarkDataTypeAverage,
+ )
+
+ assert.Equal(SeriesMarkLine{
+ Data: []SeriesMarkData{
+ {
+ Type: SeriesMarkDataTypeMax,
+ },
+ {
+ Type: SeriesMarkDataTypeMin,
+ },
+ {
+ Type: SeriesMarkDataTypeAverage,
+ },
+ },
+ }, markLine)
+}
+
+func TestMarkLineRender(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ }, PaddingOption(chart.Box{
+ Left: 20,
+ Right: 20,
+ }))
+ assert.Nil(err)
+ f, _ := chart.GetDefaultFont()
+
+ markLineRender(markLineRenderOption{
+ Draw: d,
+ FillColor: drawing.ColorBlack,
+ FontColor: drawing.ColorBlack,
+ StrokeColor: drawing.ColorBlack,
+ Font: f,
+ Series: &Series{
+ MarkLine: NewMarkLine(
+ SeriesMarkDataTypeMax,
+ SeriesMarkDataTypeMin,
+ SeriesMarkDataTypeAverage,
+ ),
+ Data: NewSeriesDataFromValues([]float64{
+ 1,
+ 3,
+ 5,
+ 7,
+ 9,
+ }),
+ },
+ Range: &Range{
+ Min: 0,
+ Max: 10,
+ Size: 200,
+ },
+ })
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/mark_point.go b/mark_point.go
new file mode 100644
index 0000000..5fd34c4
--- /dev/null
+++ b/mark_point.go
@@ -0,0 +1,89 @@
+// 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"
+)
+
+func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint {
+ data := make([]SeriesMarkData, len(markPointTypes))
+ for index, t := range markPointTypes {
+ data[index] = SeriesMarkData{
+ Type: t,
+ }
+ }
+ return SeriesMarkPoint{
+ Data: data,
+ }
+}
+
+type markPointRenderOption struct {
+ Draw *Draw
+ FillColor drawing.Color
+ Font *truetype.Font
+ Series *Series
+ Points []Point
+}
+
+func markPointRender(opt markPointRenderOption) {
+ d := opt.Draw
+ 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: labelFontSize,
+ StrokeWidth: 1,
+ Font: opt.Font,
+ }.WriteTextOptionsToRenderer(r)
+ for _, markPointData := range s.MarkPoint.Data {
+ p := points[summary.MinIndex]
+ value := summary.MinValue
+ switch markPointData.Type {
+ case SeriesMarkDataTypeMax:
+ 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)
+ }
+}
diff --git a/mark_point_test.go b/mark_point_test.go
new file mode 100644
index 0000000..2cd8fdd
--- /dev/null
+++ b/mark_point_test.go
@@ -0,0 +1,103 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestNewMarkPoint(t *testing.T) {
+ assert := assert.New(t)
+
+ markPoint := NewMarkPoint(
+ SeriesMarkDataTypeMax,
+ SeriesMarkDataTypeMin,
+ SeriesMarkDataTypeAverage,
+ )
+
+ assert.Equal(SeriesMarkPoint{
+ Data: []SeriesMarkData{
+ {
+ Type: SeriesMarkDataTypeMax,
+ },
+ {
+ Type: SeriesMarkDataTypeMin,
+ },
+ {
+ Type: SeriesMarkDataTypeAverage,
+ },
+ },
+ }, markPoint)
+}
+
+func TestMarkPointRender(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ }, PaddingOption(chart.Box{
+ Left: 20,
+ Right: 20,
+ }))
+ assert.Nil(err)
+ f, _ := chart.GetDefaultFont()
+
+ markPointRender(markPointRenderOption{
+ Draw: d,
+ FillColor: drawing.ColorBlack,
+ Font: f,
+ Series: &Series{
+ MarkPoint: NewMarkPoint(
+ SeriesMarkDataTypeMax,
+ SeriesMarkDataTypeMin,
+ ),
+ Data: NewSeriesDataFromValues([]float64{
+ 1,
+ 3,
+ 5,
+ }),
+ },
+ Points: []Point{
+ {
+ X: 1,
+ Y: 50,
+ },
+ {
+ X: 100,
+ Y: 100,
+ },
+ {
+ X: 200,
+ Y: 200,
+ },
+ },
+ })
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/pie_chart.go b/pie_chart.go
new file mode 100644
index 0000000..f581273
--- /dev/null
+++ b/pie_chart.go
@@ -0,0 +1,174 @@
+// 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"
+ "strconv"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+const defaultRadiusPercent = 0.4
+
+func getPieStyle(theme *Theme, index int) chart.Style {
+ seriesColor := theme.GetSeriesColor(index)
+ return chart.Style{
+ StrokeColor: seriesColor,
+ StrokeWidth: 1,
+ FillColor: seriesColor,
+ }
+}
+
+type pieChartOption struct {
+ Theme string
+ Font *truetype.Font
+ SeriesList SeriesList
+}
+
+func getPieRadius(diameter float64, radiusValue string) float64 {
+ var radius float64
+ if len(radiusValue) != 0 {
+ v := convertPercent(radiusValue)
+ if v != -1 {
+ radius = float64(diameter) * v
+ } else {
+ radius, _ = strconv.ParseFloat(radiusValue, 64)
+ }
+ }
+ if radius <= 0 {
+ radius = float64(diameter) * defaultRadiusPercent
+ }
+ return radius
+}
+
+func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
+ d, err := NewDraw(DrawOption{
+ Parent: result.d,
+ }, PaddingOption(chart.Box{
+ Top: result.titleBox.Height(),
+ }))
+ if err != nil {
+ return err
+ }
+
+ values := make([]float64, len(opt.SeriesList))
+ total := float64(0)
+ radiusValue := ""
+ for index, series := range opt.SeriesList {
+ if len(series.Radius) != 0 {
+ radiusValue = series.Radius
+ }
+ value := float64(0)
+ for _, item := range series.Data {
+ value += item.Value
+ }
+ values[index] = value
+ total += value
+ }
+ r := d.Render
+ theme := NewTheme(opt.Theme)
+
+ box := d.Box
+ cx := box.Width() >> 1
+ cy := box.Height() >> 1
+
+ diameter := chart.MinInt(box.Width(), box.Height())
+ radius := getPieRadius(float64(diameter), radiusValue)
+
+ labelLineWidth := 15
+ if radius < 50 {
+ labelLineWidth = 10
+ }
+ labelRadius := radius + float64(labelLineWidth)
+
+ seriesNames := opt.SeriesList.Names()
+
+ if len(values) == 1 {
+ getPieStyle(theme, 0).WriteToRenderer(r)
+ d.moveTo(cx, cy)
+ d.circle(radius, cx, cy)
+ } else {
+ currentValue := float64(0)
+ for index, v := range values {
+
+ pieStyle := getPieStyle(theme, index)
+ pieStyle.WriteToRenderer(r)
+ d.moveTo(cx, cy)
+ start := chart.PercentToRadians(currentValue/total) - math.Pi/2
+ currentValue += v
+ percent := (v / total)
+ delta := chart.PercentToRadians(percent)
+ d.arcTo(cx, cy, radius, radius, start, delta)
+ d.lineTo(cx, cy)
+ r.Close()
+ r.FillStroke()
+
+ series := opt.SeriesList[index]
+ // 是否显示label
+ showLabel := series.Label.Show
+ if !showLabel {
+ continue
+ }
+
+ // label的角度为饼块中间
+ angle := start + delta/2
+ startx := cx + int(radius*math.Cos(angle))
+ starty := cy + int(radius*math.Sin(angle))
+
+ endx := cx + int(labelRadius*math.Cos(angle))
+ endy := cy + int(labelRadius*math.Sin(angle))
+ d.moveTo(startx, starty)
+ d.lineTo(endx, endy)
+ offset := labelLineWidth
+ if endx < cx {
+ offset *= -1
+ }
+ d.moveTo(endx, endy)
+ endx += offset
+ d.lineTo(endx, endy)
+ r.Stroke()
+ textStyle := chart.Style{
+ FontColor: theme.GetTextColor(),
+ FontSize: labelFontSize,
+ Font: opt.Font,
+ }
+ if !series.Label.Color.IsZero() {
+ textStyle.FontColor = series.Label.Color
+ }
+ textStyle.GetTextOptions().WriteToRenderer(r)
+ text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent)
+ textBox := r.MeasureText(text)
+ textMargin := 3
+ x := endx + textMargin
+ y := endy + textBox.Height()>>1 - 1
+ if offset < 0 {
+ textWidth := textBox.Width()
+ x = endx - textWidth - textMargin
+ }
+ d.text(text, x, y)
+ }
+ }
+ return nil
+}
diff --git a/pie_chart_test.go b/pie_chart_test.go
new file mode 100644
index 0000000..92ef6d0
--- /dev/null
+++ b/pie_chart_test.go
@@ -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 (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestGetPieRadius(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal(50.0, getPieRadius(100, "50%"))
+ assert.Equal(30.0, getPieRadius(100, "30"))
+ assert.Equal(40.0, getPieRadius(100, ""))
+}
+
+func TestPieChartRender(t *testing.T) {
+ assert := assert.New(t)
+
+ d, err := NewDraw(DrawOption{
+ Width: 250,
+ Height: 150,
+ })
+ assert.Nil(err)
+
+ f, _ := chart.GetDefaultFont()
+
+ err = pieChartRender(pieChartOption{
+ Font: f,
+ SeriesList: NewPieSeriesList([]float64{
+ 5,
+ 10,
+ }, PieSeriesOption{
+ Names: []string{
+ "a",
+ "b",
+ },
+ Label: SeriesLabel{
+ Show: true,
+ Color: drawing.ColorRed,
+ },
+ Radius: "20%",
+ }),
+ }, &basicRenderResult{
+ d: d,
+ })
+ assert.Nil(err)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal("", string(data))
+}
diff --git a/range.go b/range.go
index 4e00c60..255a51b 100644
--- a/range.go
+++ b/range.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -24,70 +24,86 @@ package charts
import (
"math"
-
- "github.com/wcharczuk/go-chart/v2"
)
type Range struct {
- TickPosition chart.TickPosition
- chart.ContinuousRange
+ divideCount int
+ Min float64
+ Max float64
+ Size int
+ Boundary bool
}
-func wrapRange(r chart.Range, tickPosition chart.TickPosition) chart.Range {
- xr, ok := r.(*chart.ContinuousRange)
- if !ok {
- return r
+func NewRange(min, max float64, divideCount int) Range {
+ r := math.Abs(max - min)
+
+ // 最小单位计算
+ unit := 2
+ if r > 10 {
+ unit = 4
}
- return &Range{
- TickPosition: tickPosition,
- ContinuousRange: *xr,
+ 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 Range{
+ Min: min,
+ Max: max,
+ divideCount: divideCount,
}
}
-// Translate maps a given value into the ContinuousRange space.
-func (r Range) Translate(value float64) int {
- v := r.ContinuousRange.Translate(value)
- if r.TickPosition == chart.TickPositionBetweenTicks {
- v -= int(float64(r.Domain) / (r.GetDelta() * 2))
+func (r Range) Values() []string {
+ offset := (r.Max - r.Min) / float64(r.divideCount)
+ values := make([]string, 0)
+ for i := 0; i <= r.divideCount; i++ {
+ v := r.Min + float64(i)*offset
+ value := commafWithDigits(v)
+ values = append(values, value)
}
- return v
+ return values
}
-type HiddenRange struct {
- chart.ContinuousRange
+func (r *Range) getHeight(value float64) int {
+ v := (value - r.Min) / (r.Max - r.Min)
+ return int(v * float64(r.Size))
}
-func (r HiddenRange) GetDelta() float64 {
- return 0
+func (r *Range) getRestHeight(value float64) int {
+ return r.Size - r.getHeight(value)
}
-// Y轴使用的continuous range
-// min 与max只允许设置一次
-// 如果是计算得出的max,增加20%的值并取整
-type YContinuousRange struct {
- chart.ContinuousRange
+func (r *Range) GetRange(index int) (float64, float64) {
+ unit := float64(r.Size) / float64(r.divideCount)
+ return unit * float64(index), unit * float64(index+1)
+}
+func (r *Range) AutoDivide() []int {
+ return autoDivide(r.Size, r.divideCount)
}
-func (m YContinuousRange) IsZero() bool {
- // 默认返回true,允许修改
- return true
-}
-
-func (m *YContinuousRange) SetMin(min float64) {
- // 如果已修改,则忽略
- if m.Min != -math.MaxFloat64 {
- return
+func (r *Range) getWidth(value float64) int {
+ v := value / (r.Max - r.Min)
+ // 移至居中
+ if r.Boundary &&
+ r.divideCount != 0 {
+ v += 1 / float64(r.divideCount*2)
}
- m.Min = min
-}
-
-func (m *YContinuousRange) SetMax(max float64) {
- // 如果已修改,则忽略
- if m.Max != math.MaxFloat64 {
- return
- }
- // 此处为计算得来的最大值,放大20%
- v := int(max * 1.2)
- // TODO 是否要取整十整百
- m.Max = float64(v)
+ return int(v * float64(r.Size))
}
diff --git a/range_test.go b/range_test.go
index 33937bf..d1aea8f 100644
--- a/range_test.go
+++ b/range_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,55 +23,72 @@
package charts
import (
- "math"
"testing"
"github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
)
func TestRange(t *testing.T) {
assert := assert.New(t)
+ r := NewRange(0, 8, 6)
+ assert.Equal(0.0, r.Min)
+ assert.Equal(12.0, r.Max)
+
+ r = NewRange(0, 12, 6)
+ assert.Equal(0.0, r.Min)
+ 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)
+}
+
+func TestRangeHeightWidth(t *testing.T) {
+ assert := assert.New(t)
+ r := NewRange(0, 8, 6)
+ r.Size = 100
+
+ assert.Equal(33, r.getHeight(4))
+ assert.Equal(67, r.getRestHeight(4))
+
+ assert.Equal(33, r.getWidth(4))
+ r.Boundary = true
+ assert.Equal(41, r.getWidth(4))
+}
+
+func TestRangeGetRange(t *testing.T) {
+ assert := assert.New(t)
+ r := NewRange(0, 8, 6)
+ r.Size = 120
+
+ f1, f2 := r.GetRange(0)
+ assert.Equal(0.0, f1)
+ assert.Equal(20.0, f2)
+
+ f1, f2 = r.GetRange(2)
+ assert.Equal(40.0, f1)
+ assert.Equal(60.0, f2)
+}
+
+func TestRangeAutoDivide(t *testing.T) {
+ assert := assert.New(t)
+
r := Range{
- ContinuousRange: chart.ContinuousRange{
- Min: 0,
- Max: 5,
- Domain: 500,
- },
+ Size: 120,
+ divideCount: 6,
}
- assert.Equal(100, r.Translate(1))
+ assert.Equal([]int{0, 20, 40, 60, 80, 100, 120}, r.AutoDivide())
- r.TickPosition = chart.TickPositionBetweenTicks
- assert.Equal(50, r.Translate(1))
-}
-
-func TestHiddenRange(t *testing.T) {
- assert := assert.New(t)
- r := HiddenRange{}
-
- assert.Equal(float64(0), r.GetDelta())
-}
-
-func TestYContinuousRange(t *testing.T) {
- assert := assert.New(t)
- r := YContinuousRange{}
- r.Min = -math.MaxFloat64
- r.Max = math.MaxFloat64
-
- assert.True(r.IsZero())
-
- r.SetMin(1.0)
- assert.Equal(1.0, r.GetMin())
- // 再次设置无效
- r.SetMin(2.0)
- assert.Equal(1.0, r.GetMin())
-
- r.SetMax(5.0)
- // *1.2
- assert.Equal(6.0, r.GetMax())
- // 再次设置无效
- r.SetMax(10.0)
- assert.Equal(6.0, r.GetMax())
+ r.Size = 130
+ assert.Equal([]int{0, 22, 44, 66, 88, 109, 130}, r.AutoDivide())
}
diff --git a/series.go b/series.go
index f645749..8a9ba73 100644
--- a/series.go
+++ b/series.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,113 +23,207 @@
package charts
import (
+ "math"
+ "strings"
+
+ "github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
)
type SeriesData struct {
+ // The value of series data
Value float64
+ // The style of series data
Style chart.Style
}
-type Series struct {
- Type string
- Name string
- Data []SeriesData
- XValues []float64
- YAxisIndex int
- Style chart.Style
- Label SeriesLabel
+func NewSeriesFromValues(values []float64, chartType ...string) Series {
+ s := Series{
+ Data: NewSeriesDataFromValues(values),
+ }
+ if len(chartType) != 0 {
+ s.Type = chartType[0]
+ }
+ return s
}
-const lineStrokeWidth = 2
-const dotWith = 2
-
-const (
- SeriesBar = "bar"
- SeriesLine = "line"
- SeriesPie = "pie"
-)
-
-func NewSeriesDataListFromFloat(values []float64) []SeriesData {
- dataList := make([]SeriesData, len(values))
+func NewSeriesDataFromValues(values []float64) []SeriesData {
+ data := make([]SeriesData, len(values))
for index, value := range values {
- dataList[index] = SeriesData{
+ data[index] = SeriesData{
Value: value,
}
}
- return dataList
+ return data
}
-func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) []chart.Series {
- arr := make([]chart.Series, len(series))
- barCount := 0
- barIndex := 0
- for _, item := range series {
- if item.Type == SeriesBar {
- barCount++
- }
- }
- for index, item := range series {
- style := chart.Style{
- StrokeWidth: lineStrokeWidth,
- StrokeColor: getSeriesColor(theme, index),
- // TODO 调整为通过dot with color 生成
- DotColor: getSeriesColor(theme, index),
- DotWidth: dotWith,
- FontColor: getAxisColor(theme),
- }
- if !item.Style.StrokeColor.IsZero() {
- style.StrokeColor = item.Style.StrokeColor
- style.DotColor = item.Style.StrokeColor
- }
- pointIndexOffset := 0
- // 如果居中,需要多增加一个点
- if tickPosition == chart.TickPositionBetweenTicks {
- item.Data = append([]SeriesData{
- {
- Value: 0.0,
- },
- }, item.Data...)
- pointIndexOffset = -1
- }
- yValues := make([]float64, len(item.Data))
- barCustomStyles := make([]BarSeriesCustomStyle, 0)
- for i, item := range item.Data {
- yValues[i] = item.Value
- if !item.Style.IsZero() {
- barCustomStyles = append(barCustomStyles, BarSeriesCustomStyle{
- PointIndex: i + pointIndexOffset,
- Index: barIndex,
- Style: item.Style,
- })
- }
- }
- baseSeries := BaseSeries{
- Name: item.Name,
- XValues: item.XValues,
- Style: style,
- YValues: yValues,
- TickPosition: tickPosition,
- YAxis: chart.YAxisSecondary,
- Label: item.Label,
- }
- if item.YAxisIndex != 0 {
- baseSeries.YAxis = chart.YAxisPrimary
- }
- switch item.Type {
- case SeriesBar:
- arr[index] = BarSeries{
- Count: barCount,
- Index: barIndex,
- BaseSeries: baseSeries,
- CustomStyles: barCustomStyles,
- }
- barIndex++
- default:
- arr[index] = LineSeries{
- BaseSeries: baseSeries,
- }
- }
- }
- return arr
+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 drawing.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
+}
+type SeriesList []Series
+
+type PieSeriesOption struct {
+ Radius string
+ Label SeriesLabel
+ Names []string
+}
+
+func NewPieSeriesList(values []float64, opts ...PieSeriesOption) []Series {
+ 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
+ }
}
diff --git a/series_test.go b/series_test.go
index 5016aab..aae83de 100644
--- a/series_test.go
+++ b/series_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -26,10 +26,28 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
)
-func TestNewSeriesDataListFromFloat(t *testing.T) {
+func TestNewSeriesFromValues(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal(Series{
+ Data: []SeriesData{
+ {
+ Value: 1,
+ },
+ {
+ Value: 2,
+ },
+ },
+ Type: ChartTypeBar,
+ }, NewSeriesFromValues([]float64{
+ 1,
+ 2,
+ }, ChartTypeBar))
+}
+
+func TestNewSeriesDataFromValues(t *testing.T) {
assert := assert.New(t)
assert.Equal([]SeriesData{
@@ -39,87 +57,110 @@ func TestNewSeriesDataListFromFloat(t *testing.T) {
{
Value: 2,
},
- }, NewSeriesDataListFromFloat([]float64{
+ }, NewSeriesDataFromValues([]float64{
1,
2,
}))
}
-func TestGetSeries(t *testing.T) {
+func TestNewPieSeriesList(t *testing.T) {
assert := assert.New(t)
- xValues := []float64{
+ assert.Equal([]Series{
+ {
+ Type: ChartTypePie,
+ Name: "a",
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "30%",
+ Data: []SeriesData{
+ {
+ Value: 1,
+ },
+ },
+ },
+ {
+ Type: ChartTypePie,
+ Name: "b",
+ Label: SeriesLabel{
+ Show: true,
+ },
+ Radius: "30%",
+ Data: []SeriesData{
+ {
+ Value: 2,
+ },
+ },
+ },
+ }, NewPieSeriesList([]float64{
1,
2,
- 3,
- 4,
- 5,
- }
-
- barData := NewSeriesDataListFromFloat([]float64{
- 10,
- 20,
- 30,
- 40,
- 50,
- })
- barData[1].Style = chart.Style{
- FillColor: AxisColorDark,
- }
- seriesList := GetSeries([]Series{
- {
- Type: SeriesBar,
- Data: barData,
- XValues: xValues,
- YAxisIndex: 1,
+ }, PieSeriesOption{
+ Radius: "30%",
+ Label: SeriesLabel{
+ Show: true,
},
- {
- Data: NewSeriesDataListFromFloat([]float64{
- 11,
- 21,
- 31,
- 41,
- 51,
- }),
- XValues: xValues,
+ Names: []string{
+ "a",
+ "b",
},
- }, chart.TickPositionBetweenTicks, "")
-
- assert.Equal(seriesList[0].GetYAxis(), chart.YAxisPrimary)
- assert.Equal(seriesList[1].GetYAxis(), chart.YAxisSecondary)
-
- barSeries, ok := seriesList[0].(BarSeries)
- assert.True(ok)
- // 居中前置多插入一个点
- assert.Equal([]float64{
- 0,
- 10,
- 20,
- 30,
- 40,
- 50,
- }, barSeries.YValues)
- assert.Equal(xValues, barSeries.XValues)
- assert.Equal(1, barSeries.Count)
- assert.Equal(0, barSeries.Index)
- assert.Equal([]BarSeriesCustomStyle{
- {
- PointIndex: 1,
- Index: 0,
- Style: barData[1].Style,
- },
- }, barSeries.CustomStyles)
-
- lineSeries, ok := seriesList[1].(LineSeries)
- assert.True(ok)
- // 居中前置多插入一个点
- assert.Equal([]float64{
- 0,
- 11,
- 21,
- 31,
- 41,
- 51,
- }, lineSeries.YValues)
- assert.Equal(xValues, lineSeries.XValues)
+ }))
+}
+
+func TestSeriesSummary(t *testing.T) {
+ assert := assert.New(t)
+
+ s := Series{
+ Data: NewSeriesDataFromValues([]float64{
+ 1,
+ 3,
+ 5,
+ 7,
+ 9,
+ }),
+ }
+ assert.Equal(seriesSummary{
+ MaxIndex: 4,
+ MaxValue: 9,
+ MinIndex: 0,
+ MinValue: 1,
+ AverageValue: 5,
+ }, s.Summary())
+}
+
+func TestGetSeriesNames(t *testing.T) {
+ assert := assert.New(t)
+
+ sl := SeriesList{
+ {
+ Name: "a",
+ },
+ {
+ Name: "b",
+ },
+ }
+ assert.Equal([]string{
+ "a",
+ "b",
+ }, sl.Names())
+}
+
+func TestNewPieLabelFormatter(t *testing.T) {
+ assert := assert.New(t)
+
+ fn := NewPieLabelFormatter([]string{
+ "a",
+ "b",
+ }, "")
+ assert.Equal("a: 35%", fn(0, 1.2, 0.35))
+}
+
+func TestNewValueLabelFormater(t *testing.T) {
+ assert := assert.New(t)
+ fn := NewValueLabelFormater([]string{
+ "a",
+ "b",
+ }, "")
+ assert.Equal("1.2", fn(0, 1.2, 0.35))
}
diff --git a/theme.go b/theme.go
index 63e000a..d5d51cd 100644
--- a/theme.go
+++ b/theme.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,200 +23,177 @@
package charts
import (
- "regexp"
- "strconv"
- "strings"
-
- "github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
-var hiddenColor = drawing.Color{R: 255, G: 255, B: 255, A: 0}
+const ThemeDark = "dark"
+const ThemeLight = "light"
+const ThemeGrafana = "grafana"
-var AxisColorLight = drawing.Color{
- R: 110,
- G: 112,
- B: 121,
- A: 255,
-}
-var AxisColorDark = drawing.Color{
- R: 185,
- G: 184,
- B: 206,
- A: 255,
+type Theme struct {
+ palette *themeColorPalette
}
-var GridColorDark = drawing.Color{
- R: 72,
- G: 71,
- B: 83,
- A: 255,
+type themeColorPalette struct {
+ isDarkMode bool
+ axisStrokeColor drawing.Color
+ axisSplitLineColor drawing.Color
+ backgroundColor drawing.Color
+ textColor drawing.Color
+ seriesColors []drawing.Color
}
-var GridColorLight = drawing.Color{
- R: 224,
- G: 230,
- B: 241,
- A: 255,
-}
+var palettes = map[string]*themeColorPalette{}
-var BackgroundColorDark = drawing.Color{
- R: 16,
- G: 12,
- B: 42,
- A: 255,
-}
-
-var TextColorDark = drawing.Color{
- R: 204,
- G: 204,
- B: 204,
- A: 255,
-}
-
-func getAxisColor(theme string) drawing.Color {
- if theme == ThemeDark {
- return AxisColorDark
+func init() {
+ echartSeriesColors := []drawing.Color{
+ parseColor("#5470c6"),
+ parseColor("#91cc75"),
+ parseColor("#fac858"),
+ parseColor("#ee6666"),
+ parseColor("#73c0de"),
+ parseColor("#3ba272"),
+ parseColor("#fc8452"),
+ parseColor("#9a60b4"),
+ parseColor("#ea7ccc"),
}
- return AxisColorLight
-}
-
-func getGridColor(theme string) drawing.Color {
- if theme == ThemeDark {
- return GridColorDark
+ grafanaSeriesColors := []drawing.Color{
+ parseColor("#7EB26D"),
+ parseColor("#EAB839"),
+ parseColor("#6ED0E0"),
+ parseColor("#EF843C"),
+ parseColor("#E24D42"),
+ parseColor("#1F78C1"),
+ parseColor("#705DA0"),
+ parseColor("#508642"),
}
- return GridColorLight
+ AddTheme(
+ ThemeDark,
+ true,
+ drawing.Color{
+ R: 185,
+ G: 184,
+ B: 206,
+ A: 255,
+ },
+ drawing.Color{
+ R: 72,
+ G: 71,
+ B: 83,
+ A: 255,
+ },
+ drawing.Color{
+ R: 16,
+ G: 12,
+ B: 42,
+ A: 255,
+ },
+ drawing.Color{
+ R: 238,
+ G: 238,
+ B: 238,
+ A: 255,
+ },
+ echartSeriesColors,
+ )
+
+ AddTheme(
+ ThemeLight,
+ false,
+ drawing.Color{
+ R: 110,
+ G: 112,
+ B: 121,
+ A: 255,
+ },
+ drawing.Color{
+ R: 224,
+ G: 230,
+ B: 242,
+ A: 255,
+ },
+ drawing.ColorWhite,
+ drawing.Color{
+ R: 70,
+ G: 70,
+ B: 70,
+ A: 255,
+ },
+ echartSeriesColors,
+ )
+ AddTheme(
+ ThemeGrafana,
+ true,
+ drawing.Color{
+ R: 185,
+ G: 184,
+ B: 206,
+ A: 255,
+ },
+ drawing.Color{
+ R: 68,
+ G: 67,
+ B: 67,
+ A: 255,
+ },
+ drawing.Color{
+ R: 31,
+ G: 29,
+ B: 29,
+ A: 255,
+ },
+ drawing.Color{
+ R: 216,
+ G: 217,
+ B: 218,
+ A: 255,
+ },
+ grafanaSeriesColors,
+ )
}
-var SeriesColorsLight = []drawing.Color{
- {
- R: 84,
- G: 112,
- B: 198,
- A: 255,
- },
- {
- R: 145,
- G: 204,
- B: 117,
- A: 255,
- },
- {
- R: 250,
- G: 200,
- B: 88,
- A: 255,
- },
- {
- R: 238,
- G: 102,
- B: 102,
- A: 255,
- },
- {
- R: 115,
- G: 192,
- B: 222,
- A: 255,
- },
-}
-
-func getBackgroundColor(theme string) drawing.Color {
- if theme == ThemeDark {
- return BackgroundColorDark
+func AddTheme(name string, isDarkMode bool, axisStrokeColor, axisSplitLineColor, backgroundColor, textColor drawing.Color, seriesColors []drawing.Color) {
+ palettes[name] = &themeColorPalette{
+ isDarkMode: isDarkMode,
+ axisStrokeColor: axisStrokeColor,
+ axisSplitLineColor: axisSplitLineColor,
+ backgroundColor: backgroundColor,
+ textColor: textColor,
+ seriesColors: seriesColors,
}
- return chart.DefaultBackgroundColor
}
-func getTextColor(theme string) drawing.Color {
- if theme == ThemeDark {
- return TextColorDark
+func NewTheme(name string) *Theme {
+ p, ok := palettes[name]
+ if !ok {
+ p = palettes[ThemeLight]
}
- return chart.DefaultTextColor
-}
-
-type ThemeColorPalette struct {
- Theme string
-}
-
-type PieThemeColorPalette struct {
- ThemeColorPalette
-}
-
-func (tp PieThemeColorPalette) TextColor() drawing.Color {
- return getTextColor("")
-}
-
-func (tp ThemeColorPalette) BackgroundColor() drawing.Color {
- return getBackgroundColor(tp.Theme)
-}
-
-func (tp ThemeColorPalette) BackgroundStrokeColor() drawing.Color {
- return chart.DefaultBackgroundStrokeColor
-}
-
-func (tp ThemeColorPalette) CanvasColor() drawing.Color {
- if tp.Theme == ThemeDark {
- return BackgroundColorDark
+ return &Theme{
+ palette: p,
}
- return chart.DefaultCanvasColor
}
-func (tp ThemeColorPalette) CanvasStrokeColor() drawing.Color {
- return chart.DefaultCanvasStrokeColor
+func (t *Theme) IsDark() bool {
+ return t.palette.isDarkMode
}
-func (tp ThemeColorPalette) AxisStrokeColor() drawing.Color {
- if tp.Theme == ThemeDark {
- return BackgroundColorDark
- }
- return chart.DefaultAxisColor
+func (t *Theme) GetAxisStrokeColor() drawing.Color {
+ return t.palette.axisStrokeColor
}
-func (tp ThemeColorPalette) TextColor() drawing.Color {
- return getTextColor(tp.Theme)
+func (t *Theme) GetAxisSplitLineColor() drawing.Color {
+ return t.palette.axisSplitLineColor
}
-func (tp ThemeColorPalette) GetSeriesColor(index int) drawing.Color {
- return getSeriesColor(tp.Theme, index)
+func (t *Theme) GetSeriesColor(index int) drawing.Color {
+ colors := t.palette.seriesColors
+ return colors[index%len(colors)]
}
-func getSeriesColor(theme string, index int) drawing.Color {
- return SeriesColorsLight[index%len(SeriesColorsLight)]
+func (t *Theme) GetBackgroundColor() drawing.Color {
+ return t.palette.backgroundColor
}
-func parseColor(color string) drawing.Color {
- c := drawing.Color{}
- if color == "" {
- return c
- }
- if strings.HasPrefix(color, "#") {
- return drawing.ColorFromHex(color[1:])
- }
- reg := regexp.MustCompile(`\((\S+)\)`)
- result := reg.FindAllStringSubmatch(color, 1)
- if len(result) == 0 || len(result[0]) != 2 {
- return c
- }
- arr := strings.Split(result[0][1], ",")
- if len(arr) < 3 {
- return c
- }
- // 设置默认为255
- c.A = 255
- for index, v := range arr {
- value, _ := strconv.Atoi(strings.TrimSpace(v))
- ui8 := uint8(value)
- switch index {
- case 0:
- c.R = ui8
- case 1:
- c.G = ui8
- case 2:
- c.B = ui8
- default:
- c.A = ui8
- }
- }
- return c
+func (t *Theme) GetTextColor() drawing.Color {
+ return t.palette.textColor
}
diff --git a/theme_test.go b/theme_test.go
index a25a2db..bf22afd 100644
--- a/theme_test.go
+++ b/theme_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -26,97 +26,62 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
-func TestThemeColors(t *testing.T) {
+func TestTheme(t *testing.T) {
assert := assert.New(t)
- assert.Equal(AxisColorDark, getAxisColor(ThemeDark))
- assert.Equal(AxisColorLight, getAxisColor(""))
+ darkTheme := NewTheme(ThemeDark)
+ lightTheme := NewTheme(ThemeLight)
- assert.Equal(GridColorDark, getGridColor(ThemeDark))
- assert.Equal(GridColorLight, getGridColor(""))
+ assert.True(darkTheme.IsDark())
+ assert.False(lightTheme.IsDark())
- assert.Equal(BackgroundColorDark, getBackgroundColor(ThemeDark))
- assert.Equal(chart.DefaultBackgroundColor, getBackgroundColor(""))
-
- assert.Equal(TextColorDark, getTextColor(ThemeDark))
- assert.Equal(chart.DefaultTextColor, getTextColor(""))
-}
-
-func TestThemeColorPalette(t *testing.T) {
- assert := assert.New(t)
-
- dark := ThemeColorPalette{
- Theme: ThemeDark,
- }
- assert.Equal(BackgroundColorDark, dark.BackgroundColor())
- assert.Equal(chart.DefaultBackgroundStrokeColor, dark.BackgroundStrokeColor())
- assert.Equal(BackgroundColorDark, dark.CanvasColor())
- assert.Equal(chart.DefaultCanvasStrokeColor, dark.CanvasStrokeColor())
- assert.Equal(BackgroundColorDark, dark.AxisStrokeColor())
- assert.Equal(TextColorDark, dark.TextColor())
- // series 使用统一的color
- assert.Equal(SeriesColorsLight[0], dark.GetSeriesColor(0))
-
- light := ThemeColorPalette{}
- assert.Equal(chart.DefaultBackgroundColor, light.BackgroundColor())
- assert.Equal(chart.DefaultBackgroundStrokeColor, light.BackgroundStrokeColor())
- assert.Equal(chart.DefaultCanvasColor, light.CanvasColor())
- assert.Equal(chart.DefaultCanvasStrokeColor, light.CanvasStrokeColor())
- assert.Equal(chart.DefaultAxisColor, light.AxisStrokeColor())
- assert.Equal(chart.DefaultTextColor, light.TextColor())
- // series 使用统一的color
- assert.Equal(SeriesColorsLight[0], light.GetSeriesColor(0))
-}
-
-func TestPieThemeColorPalette(t *testing.T) {
- assert := assert.New(t)
- p := PieThemeColorPalette{}
-
- // pie无认哪种theme,文本的颜色都一样
- assert.Equal(chart.DefaultTextColor, p.TextColor())
- p.Theme = ThemeDark
- assert.Equal(chart.DefaultTextColor, p.TextColor())
-}
-
-func TestParseColor(t *testing.T) {
- assert := assert.New(t)
-
- c := parseColor("")
- assert.True(c.IsZero())
-
- c = parseColor("#333")
assert.Equal(drawing.Color{
- R: 51,
- G: 51,
- B: 51,
+ R: 185,
+ G: 184,
+ B: 206,
A: 255,
- }, c)
-
- c = parseColor("#313233")
+ }, darkTheme.GetAxisStrokeColor())
assert.Equal(drawing.Color{
- R: 49,
- G: 50,
- B: 51,
+ R: 110,
+ G: 112,
+ B: 121,
A: 255,
- }, c)
+ }, lightTheme.GetAxisStrokeColor())
- c = parseColor("rgb(31,32,33)")
assert.Equal(drawing.Color{
- R: 31,
- G: 32,
- B: 33,
+ R: 72,
+ G: 71,
+ B: 83,
A: 255,
- }, c)
-
- c = parseColor("rgba(50,51,52,250)")
+ }, darkTheme.GetAxisSplitLineColor())
assert.Equal(drawing.Color{
- R: 50,
- G: 51,
- B: 52,
- A: 250,
- }, c)
+ R: 224,
+ G: 230,
+ B: 242,
+ A: 255,
+ }, lightTheme.GetAxisSplitLineColor())
+
+ assert.Equal(drawing.Color{
+ R: 16,
+ G: 12,
+ B: 42,
+ A: 255,
+ }, darkTheme.GetBackgroundColor())
+ assert.Equal(drawing.ColorWhite, lightTheme.GetBackgroundColor())
+
+ assert.Equal(drawing.Color{
+ R: 238,
+ G: 238,
+ B: 238,
+ A: 255,
+ }, darkTheme.GetTextColor())
+ assert.Equal(drawing.Color{
+ R: 70,
+ G: 70,
+ B: 70,
+ A: 255,
+ }, lightTheme.GetTextColor())
}
diff --git a/title.go b/title.go
index 228b2c0..07a2eef 100644
--- a/title.go
+++ b/title.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -29,75 +29,127 @@ import (
"github.com/wcharczuk/go-chart/v2"
)
+type TitleOption struct {
+ // Title text, support \n for new line
+ Text string
+ // Subtitle text, support \n for new line
+ Subtext string
+ // Title style
+ Style chart.Style
+ // Subtitle style
+ SubtextStyle chart.Style
+ // Distance between title component and the left side of the container.
+ // It can be pixel value: 20, percentage value: 20%,
+ // or position value: right, center.
+ Left string
+ // Distance between title component and the top side of the container.
+ // It can be pixel value: 20.
+ Top string
+}
type titleMeasureOption struct {
width int
height int
text string
+ style chart.Style
}
-func NewTitleCustomize(title Title) chart.Renderable {
- return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
- if len(title.Text) == 0 || title.Style.Hidden {
- return
+func splitTitleText(text string) []string {
+ arr := strings.Split(text, "\n")
+ result := make([]string, 0)
+ for _, v := range arr {
+ v = strings.TrimSpace(v)
+ if v == "" {
+ continue
}
- font := title.Font
- if font == nil {
- font, _ = chart.GetDefaultFont()
+ result = append(result, v)
+ }
+ return result
+}
+
+func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) {
+ if len(opt.Text) == 0 {
+ return chart.BoxZero, nil
+ }
+
+ padding := opt.Style.Padding
+ d, err := NewDraw(DrawOption{
+ Parent: p,
+ }, PaddingOption(padding))
+ if err != nil {
+ return chart.BoxZero, err
+ }
+
+ r := d.Render
+
+ measureOptions := make([]titleMeasureOption, 0)
+
+ // 主标题
+ for _, v := range splitTitleText(opt.Text) {
+ measureOptions = append(measureOptions, titleMeasureOption{
+ text: v,
+ style: opt.Style.GetTextOptions(),
+ })
+ }
+ // 副标题
+ for _, v := range splitTitleText(opt.Subtext) {
+ measureOptions = append(measureOptions, titleMeasureOption{
+ text: v,
+ style: opt.SubtextStyle.GetTextOptions(),
+ })
+ }
+
+ textMaxWidth := 0
+ textMaxHeight := 0
+ width := 0
+ for index, item := range measureOptions {
+ item.style.WriteTextOptionsToRenderer(r)
+ textBox := r.MeasureText(item.text)
+
+ w := textBox.Width()
+ h := textBox.Height()
+ if w > textMaxWidth {
+ textMaxWidth = w
}
- r.SetFont(font)
- r.SetFontColor(title.Style.FontColor)
- titleFontSize := title.Style.GetFontSize(chart.DefaultTitleFontSize)
- r.SetFontSize(titleFontSize)
-
- arr := strings.Split(title.Text, "\n")
- textWidth := 0
- textHeight := 0
- measureOptions := make([]titleMeasureOption, len(arr))
- for index, str := range arr {
- textBox := r.MeasureText(str)
-
- w := textBox.Width()
- h := textBox.Height()
- if w > textWidth {
- textWidth = w
- }
- if h > textHeight {
- textHeight = h
- }
- measureOptions[index] = titleMeasureOption{
- text: str,
- width: w,
- height: h,
- }
+ if h > textMaxHeight {
+ textMaxHeight = h
}
-
- titleX := 0
- switch title.Left {
- case "right":
- titleX = cb.Left + cb.Width() - textWidth
- case "center":
- titleX = cb.Left + cb.Width()>>1 - (textWidth >> 1)
- default:
- if strings.HasSuffix(title.Left, "%") {
- value, _ := strconv.Atoi(strings.ReplaceAll(title.Left, "%", ""))
- titleX = cb.Left + cb.Width()*value/100
- } else {
- value, _ := strconv.Atoi(title.Left)
- titleX = cb.Left + value
- }
- }
-
- titleY := cb.Top + title.Style.Padding.GetTop(chart.DefaultTitleTop) + (textHeight >> 1)
- // TOP 暂只支持数值
- if title.Top != "" {
- value, _ := strconv.Atoi(title.Top)
- titleY += value
- }
-
- for _, item := range measureOptions {
- x := titleX + (textWidth-item.width)>>1
- r.Text(item.text, x, titleY)
- titleY += textHeight
+ measureOptions[index].height = h
+ measureOptions[index].width = w
+ }
+ width = textMaxWidth
+ titleX := 0
+ b := d.Box
+ switch opt.Left {
+ case PositionRight:
+ titleX = b.Width() - textMaxWidth
+ case PositionCenter:
+ titleX = b.Width()>>1 - (textMaxWidth >> 1)
+ default:
+ if strings.HasSuffix(opt.Left, "%") {
+ value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
+ titleX = b.Width() * value / 100
+ } else {
+ value, _ := strconv.Atoi(opt.Left)
+ titleX = value
}
}
+ titleY := 0
+ // TODO TOP 暂只支持数值
+ if opt.Top != "" {
+ value, _ := strconv.Atoi(opt.Top)
+ titleY += value
+ }
+ for _, item := range measureOptions {
+ item.style.WriteTextOptionsToRenderer(r)
+ x := titleX + (textMaxWidth-item.width)>>1
+ y := titleY + item.height
+ d.text(item.text, x, y)
+ titleY += item.height
+ }
+ height := titleY + padding.Top + padding.Bottom
+ box := padding.Clone()
+ box.Right = box.Left + titleX + width
+ box.Bottom = box.Top + height
+
+ return box, nil
}
diff --git a/title_test.go b/title_test.go
index 0fe8256..23573c3 100644
--- a/title_test.go
+++ b/title_test.go
@@ -1,6 +1,6 @@
// MIT License
-// Copyright (c) 2021 Tree Xie
+// 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
@@ -23,7 +23,6 @@
package charts
import (
- "bytes"
"testing"
"github.com/stretchr/testify/assert"
@@ -31,55 +30,113 @@ import (
"github.com/wcharczuk/go-chart/v2/drawing"
)
-func TestTitleCustomize(t *testing.T) {
+func TestSplitTitleText(t *testing.T) {
assert := assert.New(t)
+
+ assert.Equal([]string{
+ "a",
+ "b",
+ }, splitTitleText("a\nb"))
+ assert.Equal([]string{
+ "a",
+ }, splitTitleText("a\n "))
+}
+
+func TestDrawTitle(t *testing.T) {
+ assert := assert.New(t)
+
+ newOption := func() *TitleOption {
+ f, _ := chart.GetDefaultFont()
+ return &TitleOption{
+ Text: "title\nHello",
+ Subtext: "subtitle\nWorld!",
+ Style: chart.Style{
+ FontSize: 14,
+ Font: f,
+ FontColor: drawing.ColorBlack,
+ },
+ SubtextStyle: chart.Style{
+ FontSize: 10,
+ Font: f,
+ FontColor: drawing.ColorBlue,
+ },
+ }
+ }
+ newDraw := func() *Draw {
+ d, _ := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ return d
+ }
+
tests := []struct {
- title Title
- svg string
+ newDraw func() *Draw
+ newOption func() *TitleOption
+ result string
+ box chart.Box
}{
- // 单行标题
{
- title: Title{
- Text: "Hello World!",
- Style: chart.Style{
- FontColor: drawing.ColorBlack,
- },
+ newDraw: newDraw,
+ newOption: newOption,
+ result: "",
+ box: chart.Box{
+ Right: 43,
+ Bottom: 58,
},
- svg: "",
},
- // 多行标题,靠右
{
- title: Title{
- Text: "Hello World!\nHello World",
- Style: chart.Style{
- FontColor: drawing.ColorBlack,
- },
- Left: "right",
+ newDraw: newDraw,
+ newOption: func() *TitleOption {
+ opt := newOption()
+ opt.Left = PositionRight
+ opt.Top = "50"
+ return opt
+ },
+ result: "",
+ box: chart.Box{
+ Right: 400,
+ Bottom: 108,
},
- svg: "",
},
- // 标题居中
{
- title: Title{
- Text: "Hello World!",
- Style: chart.Style{
- FontColor: drawing.ColorBlack,
- },
- Left: "center",
+ newDraw: newDraw,
+ newOption: func() *TitleOption {
+ opt := newOption()
+ opt.Left = PositionCenter
+ opt.Top = "10"
+ return opt
+ },
+ result: "",
+ box: chart.Box{
+ Right: 222,
+ Bottom: 68,
+ },
+ },
+ {
+ newDraw: newDraw,
+ newOption: func() *TitleOption {
+ opt := newOption()
+ opt.Left = "10%"
+ opt.Top = "10"
+ return opt
+ },
+ result: "",
+ box: chart.Box{
+ Right: 83,
+ Bottom: 68,
},
- svg: "",
},
}
for _, tt := range tests {
- r, err := chart.SVG(800, 600)
+ d := tt.newDraw()
+ o := tt.newOption()
+ b, err := drawTitle(d, o)
assert.Nil(err)
- fn := NewTitleCustomize(tt.title)
- fn(r, chart.NewBox(50, 50, 600, 400), chart.Style{
- Font: chart.StyleTextDefaults().Font,
- })
- buf := bytes.Buffer{}
- err = r.Save(&buf)
+ assert.Equal(tt.box, b)
+ data, err := d.Bytes()
assert.Nil(err)
- assert.Equal(tt.svg, buf.String())
+ assert.NotEmpty(data)
+ assert.Equal(tt.result, string(data))
}
}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..2adaba8
--- /dev/null
+++ b/util.go
@@ -0,0 +1,170 @@
+// 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 (
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/dustin/go-humanize"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TrueFlag() *bool {
+ t := true
+ return &t
+}
+
+func FalseFlag() *bool {
+ f := false
+ return &f
+}
+
+func ceilFloatToInt(value float64) int {
+ i := int(value)
+ if value == float64(i) {
+ return i
+ }
+ return i + 1
+}
+
+func getDefaultInt(value, defaultValue int) int {
+ if value == 0 {
+ return defaultValue
+ }
+ return value
+}
+
+func autoDivide(max, size int) []int {
+ unit := max / size
+
+ rest := max - unit*size
+ values := make([]int, size+1)
+ value := 0
+ for i := 0; i < size; i++ {
+ values[i] = value
+ if i < rest {
+ value++
+ }
+ value += unit
+ }
+ values[size] = max
+ return values
+}
+
+// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
+func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) {
+ maxWidth := 0
+ maxHeight := 0
+ for _, text := range textList {
+ box := r.MeasureText(text)
+ maxWidth = chart.MaxInt(maxWidth, box.Width())
+ maxHeight = chart.MaxInt(maxHeight, box.Height())
+ }
+ return maxWidth, maxHeight
+}
+
+func reverseStringSlice(stringList []string) {
+ for i, j := 0, len(stringList)-1; i < j; i, j = i+1, j-1 {
+ stringList[i], stringList[j] = stringList[j], stringList[i]
+ }
+}
+
+func reverseIntSlice(intList []int) {
+ for i, j := 0, len(intList)-1; i < j; i, j = i+1, j-1 {
+ intList[i], intList[j] = intList[j], intList[i]
+ }
+}
+
+func convertPercent(value string) float64 {
+ if !strings.HasSuffix(value, "%") {
+ return -1
+ }
+ v, err := strconv.Atoi(strings.ReplaceAll(value, "%", ""))
+ if err != nil {
+ return -1
+ }
+ return float64(v) / 100
+}
+
+func isFalse(flag *bool) bool {
+ if flag != nil && !*flag {
+ return true
+ }
+ return false
+}
+
+func NewFloatPoint(f float64) *float64 {
+ v := f
+ return &v
+}
+func commafWithDigits(value float64) string {
+ decimals := 2
+ m := float64(1000 * 1000)
+ if value >= 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, decimals)
+}
+
+func parseColor(color string) drawing.Color {
+ c := drawing.Color{}
+ if color == "" {
+ return c
+ }
+ if strings.HasPrefix(color, "#") {
+ return drawing.ColorFromHex(color[1:])
+ }
+ reg := regexp.MustCompile(`\((\S+)\)`)
+ result := reg.FindAllStringSubmatch(color, 1)
+ if len(result) == 0 || len(result[0]) != 2 {
+ return c
+ }
+ arr := strings.Split(result[0][1], ",")
+ if len(arr) < 3 {
+ return c
+ }
+ // 设置默认为255
+ c.A = 255
+ for index, v := range arr {
+ value, _ := strconv.Atoi(strings.TrimSpace(v))
+ ui8 := uint8(value)
+ switch index {
+ case 0:
+ c.R = ui8
+ case 1:
+ c.G = ui8
+ case 2:
+ c.B = ui8
+ default:
+ c.A = ui8
+ }
+ }
+ return c
+}
diff --git a/util_test.go b/util_test.go
new file mode 100644
index 0000000..dc5d98e
--- /dev/null
+++ b/util_test.go
@@ -0,0 +1,181 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestGetDefaultInt(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal(1, getDefaultInt(0, 1))
+ assert.Equal(10, getDefaultInt(10, 1))
+}
+
+func TestCeilFloatToInt(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal(1, ceilFloatToInt(0.8))
+ assert.Equal(1, ceilFloatToInt(1.0))
+ assert.Equal(2, ceilFloatToInt(1.2))
+}
+
+func TestCommafWithDigits(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal("1.2", commafWithDigits(1.2))
+ assert.Equal("1.21", commafWithDigits(1.21231))
+
+ assert.Equal("1.20k", commafWithDigits(1200.121))
+ assert.Equal("1.20M", commafWithDigits(1200000.121))
+}
+
+func TestAutoDivide(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal([]int{
+ 0,
+ 86,
+ 172,
+ 258,
+ 344,
+ 430,
+ 515,
+ 600,
+ }, autoDivide(600, 7))
+}
+
+func TestMeasureTextMaxWidthHeight(t *testing.T) {
+ assert := assert.New(t)
+ r, err := chart.SVG(400, 300)
+ assert.Nil(err)
+ style := chart.Style{
+ FontSize: 10,
+ }
+ style.Font, _ = chart.GetDefaultFont()
+ style.WriteToRenderer(r)
+
+ maxWidth, maxHeight := measureTextMaxWidthHeight([]string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }, r)
+ assert.Equal(26, maxWidth)
+ assert.Equal(12, maxHeight)
+}
+
+func TestReverseSlice(t *testing.T) {
+ assert := assert.New(t)
+
+ arr := []string{
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun",
+ }
+ reverseStringSlice(arr)
+ assert.Equal([]string{
+ "Sun",
+ "Sat",
+ "Fri",
+ "Thu",
+ "Wed",
+ "Tue",
+ "Mon",
+ }, arr)
+
+ numbers := []int{
+ 1,
+ 3,
+ 5,
+ 7,
+ 9,
+ }
+ reverseIntSlice(numbers)
+ assert.Equal([]int{
+ 9,
+ 7,
+ 5,
+ 3,
+ 1,
+ }, numbers)
+}
+
+func TestConvertPercent(t *testing.T) {
+ assert := assert.New(t)
+
+ assert.Equal(-1.0, convertPercent("1"))
+ assert.Equal(-1.0, convertPercent("a%"))
+ assert.Equal(0.1, convertPercent("10%"))
+}
+
+func TestParseColor(t *testing.T) {
+ assert := assert.New(t)
+
+ c := parseColor("")
+ assert.True(c.IsZero())
+
+ c = parseColor("#333")
+ assert.Equal(drawing.Color{
+ R: 51,
+ G: 51,
+ B: 51,
+ A: 255,
+ }, c)
+
+ c = parseColor("#313233")
+ assert.Equal(drawing.Color{
+ R: 49,
+ G: 50,
+ B: 51,
+ A: 255,
+ }, c)
+
+ c = parseColor("rgb(31,32,33)")
+ assert.Equal(drawing.Color{
+ R: 31,
+ G: 32,
+ B: 33,
+ A: 255,
+ }, c)
+
+ c = parseColor("rgba(50,51,52,250)")
+ assert.Equal(drawing.Color{
+ R: 50,
+ G: 51,
+ B: 52,
+ A: 250,
+ }, c)
+}
diff --git a/xaxis.go b/xaxis.go
new file mode 100644
index 0000000..1dca7bb
--- /dev/null
+++ b/xaxis.go
@@ -0,0 +1,97 @@
+// 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 XAxisOption struct {
+ // 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 string
+ // 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
+}
+
+func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
+ opt := XAxisOption{
+ Data: data,
+ }
+ if len(boundaryGap) != 0 {
+ opt.BoundaryGap = boundaryGap[0]
+ }
+ return opt
+}
+
+// drawXAxis draws x axis, and returns the height, range of if.
+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: left,
+ Right: right,
+ }),
+ )
+ if err != nil {
+ return 0, nil, err
+ }
+ theme := NewTheme(opt.Theme)
+ data := NewAxisDataListFromStringList(opt.Data)
+ style := AxisOption{
+ BoundaryGap: opt.BoundaryGap,
+ StrokeColor: theme.GetAxisStrokeColor(),
+ FontColor: theme.GetAxisStrokeColor(),
+ StrokeWidth: 1,
+ SplitNumber: opt.SplitNumber,
+ }
+
+ boundary := true
+ max := float64(len(opt.Data))
+ if isFalse(opt.BoundaryGap) {
+ boundary = false
+ max--
+ }
+ axis := NewAxis(dXAxis, data, style)
+ axis.Render()
+ return axis.measure().Height, &Range{
+ divideCount: len(opt.Data),
+ Min: 0,
+ Max: max,
+ Size: dXAxis.Box.Width(),
+ Boundary: boundary,
+ }, nil
+}
diff --git a/xaxis_test.go b/xaxis_test.go
new file mode 100644
index 0000000..267cdb1
--- /dev/null
+++ b/xaxis_test.go
@@ -0,0 +1,108 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewXAxisOption(t *testing.T) {
+ assert := assert.New(t)
+
+ opt := NewXAxisOption([]string{
+ "a",
+ "b",
+ }, FalseFlag())
+
+ assert.Equal(XAxisOption{
+ Data: []string{
+ "a",
+ "b",
+ },
+ BoundaryGap: FalseFlag(),
+ }, opt)
+
+}
+func TestDrawXAxis(t *testing.T) {
+ assert := assert.New(t)
+
+ newDraw := func() *Draw {
+ d, _ := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ return d
+ }
+
+ tests := []struct {
+ newDraw func() *Draw
+ newOption func() *XAxisOption
+ result string
+ }{
+ {
+ newDraw: newDraw,
+ newOption: func() *XAxisOption {
+ return &XAxisOption{
+ BoundaryGap: FalseFlag(),
+ Data: []string{
+ "Mon",
+ "Tue",
+ },
+ }
+ },
+ result: "",
+ },
+ {
+ newDraw: newDraw,
+ newOption: func() *XAxisOption {
+ return &XAxisOption{
+ Data: []string{
+ "01-01",
+ "01-02",
+ "01-03",
+ "01-04",
+ "01-05",
+ "01-06",
+ "01-07",
+ "01-08",
+ "01-09",
+ },
+ SplitNumber: 3,
+ }
+ },
+ result: "",
+ },
+ }
+
+ for _, tt := range tests {
+ d := tt.newDraw()
+ height, _, err := drawXAxis(d, tt.newOption(), 1)
+ assert.Nil(err)
+ assert.Equal(25, height)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal(tt.result, string(data))
+ }
+}
diff --git a/yaxis.go b/yaxis.go
new file mode 100644
index 0000000..99093ec
--- /dev/null
+++ b/yaxis.go
@@ -0,0 +1,102 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "strings"
+
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+type YAxisOption struct {
+ // The minimun value of axis.
+ Min *float64
+ // The maximum value of axis.
+ Max *float64
+ // Hidden y axis
+ Hidden bool
+ // Formatter for y axis text value
+ Formatter string
+ // Color for y axis
+ Color drawing.Color
+}
+
+// TODO 长度是否可以变化
+const YAxisWidth = 40
+
+func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) {
+ theme := NewTheme(opt.Theme)
+ yRange := opt.newYRange(axisIndex)
+ values := yRange.Values()
+ yAxis := opt.YAxisList[axisIndex]
+ formatter := yAxis.Formatter
+ if len(formatter) != 0 {
+ for index, text := range values {
+ values[index] = strings.ReplaceAll(formatter, "{value}", text)
+ }
+ }
+
+ data := NewAxisDataListFromStringList(values)
+ style := AxisOption{
+ Position: PositionLeft,
+ BoundaryGap: FalseFlag(),
+ FontColor: theme.GetAxisStrokeColor(),
+ TickShow: FalseFlag(),
+ StrokeWidth: 1,
+ SplitLineColor: theme.GetAxisSplitLineColor(),
+ SplitLineShow: true,
+ }
+ if !yAxis.Color.IsZero() {
+ style.FontColor = yAxis.Color
+ style.StrokeColor = yAxis.Color
+ }
+ width := NewAxis(p, data, style).measure().Width
+
+ 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: boxWidth,
+ // 减去x轴的高
+ Height: p.Box.Height() - xAxisHeight,
+ },
+ PaddingOption(padding),
+ )
+ if err != nil {
+ return nil, err
+ }
+ NewAxis(dYAxis, data, style).Render()
+ yRange.Size = dYAxis.Box.Height()
+ return &yRange, nil
+}
diff --git a/yaxis_test.go b/yaxis_test.go
new file mode 100644
index 0000000..0bbef7a
--- /dev/null
+++ b/yaxis_test.go
@@ -0,0 +1,119 @@
+// MIT License
+
+// Copyright (c) 2022 Tree Xie
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package charts
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+func TestDrawYAxis(t *testing.T) {
+ assert := assert.New(t)
+ newDraw := func() *Draw {
+ d, _ := NewDraw(DrawOption{
+ Width: 400,
+ Height: 300,
+ })
+ return d
+ }
+
+ tests := []struct {
+ newDraw func() *Draw
+ newOption func() *ChartOption
+ axisIndex int
+ xAxisHeight int
+ result string
+ }{
+ {
+ newDraw: newDraw,
+ newOption: func() *ChartOption {
+ return &ChartOption{
+ YAxisList: []YAxisOption{
+ {
+ Max: NewFloatPoint(20),
+ },
+ },
+ SeriesList: []Series{
+ {
+ Data: []SeriesData{
+ {
+ Value: 1,
+ },
+ {
+ Value: 2,
+ },
+ },
+ },
+ },
+ }
+ },
+ result: "",
+ },
+ {
+ newDraw: newDraw,
+ newOption: func() *ChartOption {
+ return &ChartOption{
+ YAxisList: []YAxisOption{
+ {},
+ {
+ Max: NewFloatPoint(20),
+ Formatter: "{value} C",
+ },
+ },
+ SeriesList: []Series{
+ {
+ YAxisIndex: 1,
+ Data: []SeriesData{
+ {
+ Value: 1,
+ },
+ {
+ Value: 2,
+ },
+ },
+ },
+ },
+ }
+ },
+ axisIndex: 1,
+ result: "",
+ },
+ }
+
+ for _, tt := range tests {
+ d := tt.newDraw()
+ r, err := drawYAxis(d, tt.newOption(), tt.axisIndex, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
+ assert.Nil(err)
+ assert.Equal(&Range{
+ divideCount: 6,
+ Max: 20,
+ Size: 280,
+ }, r)
+
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal(tt.result, string(data))
+ }
+}