refactor: support echarts options

This commit is contained in:
vicanso 2021-12-16 23:00:31 +08:00
parent 4d8086a283
commit 8f7587561f
7 changed files with 294 additions and 51 deletions

26
axis.go
View file

@ -102,6 +102,32 @@ func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme strin
}, xValues }, xValues
} }
func GetSecondaryYAxis(theme string) chart.YAxis {
// TODO
if theme == ThemeDark {
return chart.YAxis{}
}
// strokeColor := drawing.Color{
// R: 224,
// G: 230,
// B: 241,
// A: 255,
// }
return chart.YAxis{
ValueFormatter: func(v interface{}) string {
value, ok := v.(float64)
if !ok {
return ""
}
return humanize.Commaf(value)
},
AxisType: chart.YAxisPrimary,
GridMajorStyle: chart.Hidden(),
GridMinorStyle: chart.Hidden(),
Style: chart.Hidden(),
}
}
func GetYAxis(theme string) chart.YAxis { func GetYAxis(theme string) chart.YAxis {
// TODO // TODO
if theme == ThemeDark { if theme == ThemeDark {

View file

@ -35,19 +35,25 @@ const (
ThemeDark = "dark" ThemeDark = "dark"
) )
const (
DefaultChartWidth = 800
DefaultChartHeight = 400
)
type ( type (
Title struct { Title struct {
Text string Text string
Style chart.Style
} }
Legend struct { Legend struct {
Data []string Data []string
} }
Option struct { Options struct {
Width int Width int
Height int Height int
Theme string Theme string
XAxis XAxis XAxis XAxis
// YAxis Axis YAxisList []chart.YAxis
Series []Series Series []Series
Title Title Title Title
Legend Legend Legend Legend
@ -55,27 +61,11 @@ type (
} }
) )
type Chart interface { type Graph interface {
Render(rp chart.RendererProvider, w io.Writer) error Render(rp chart.RendererProvider, w io.Writer) error
} }
type ECharOption struct { func (o *Options) validate() error {
Title struct {
Text string
TextStyle struct {
Color string
FontFamily string
}
}
XAxis struct {
Type string
BoundaryGap *bool
SplitNumber int
Data []string
}
}
func (o *Option) validate() error {
xAxisCount := len(o.XAxis.Data) xAxisCount := len(o.XAxis.Data)
if len(o.Series) == 0 { if len(o.Series) == 0 {
return errors.New("series can not be empty") return errors.New("series can not be empty")
@ -89,28 +79,36 @@ func (o *Option) validate() error {
return nil return nil
} }
func render(c Chart, rp chart.RendererProvider) ([]byte, error) { func render(g Graph, rp chart.RendererProvider) ([]byte, error) {
buf := bytes.Buffer{} buf := bytes.Buffer{}
err := c.Render(rp, &buf) err := g.Render(rp, &buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return buf.Bytes(), nil return buf.Bytes(), nil
} }
func ToPNG(c Chart) ([]byte, error) { func ToPNG(g Graph) ([]byte, error) {
return render(c, chart.PNG) return render(g, chart.PNG)
} }
func ToSVG(c Chart) ([]byte, error) { func ToSVG(g Graph) ([]byte, error) {
return render(c, chart.SVG) return render(g, chart.SVG)
} }
func New(opt Option) (Chart, error) { func New(opt Options) (Graph, error) {
err := opt.validate() err := opt.validate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
tickPosition := opt.TickPosition tickPosition := opt.TickPosition
width := opt.Width
if width <= 0 {
width = DefaultChartWidth
}
height := opt.Height
if height <= 0 {
height = DefaultChartHeight
}
xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme) xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme)
@ -131,33 +129,35 @@ func New(opt Option) (Chart, error) {
Label: item.Name, Label: item.Name,
} }
} }
c := &chart.PieChart{ g := &chart.PieChart{
Title: opt.Title.Text, Title: opt.Title.Text,
Width: opt.Width, TitleStyle: opt.Title.Style,
Height: opt.Height, Width: width,
Values: values, Height: height,
Values: values,
ColorPalette: &ThemeColorPalette{ ColorPalette: &ThemeColorPalette{
Theme: opt.Theme, Theme: opt.Theme,
}, },
} }
return c, nil return g, nil
} }
c := &chart.Chart{ g := &chart.Chart{
Title: opt.Title.Text, Title: opt.Title.Text,
Width: opt.Width, TitleStyle: opt.Title.Style,
Height: opt.Height, Width: width,
XAxis: xAxis, Height: height,
YAxis: GetYAxis(opt.Theme), XAxis: xAxis,
Series: GetSeries(opt.Series, tickPosition, opt.Theme), YAxis: GetYAxis(opt.Theme),
YAxisSecondary: GetSecondaryYAxis(opt.Theme),
Series: GetSeries(opt.Series, tickPosition, opt.Theme),
} }
// 设置secondary的样式 // 设置secondary的样式
c.YAxisSecondary.Style = c.YAxis.Style
if legendSize != 0 { if legendSize != 0 {
c.Elements = []chart.Renderable{ g.Elements = []chart.Renderable{
DefaultLegend(c), DefaultLegend(g),
} }
} }
return c, nil return g, nil
} }

171
echarts.go Normal file
View file

@ -0,0 +1,171 @@
// 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"
"encoding/json"
"regexp"
"strconv"
"github.com/wcharczuk/go-chart/v2"
)
type EChartStyle struct {
Color string `json:"color"`
}
type ECharsSeriesData struct {
Value float64 `json:"value"`
ItemStyle EChartStyle `json:"itemStyle"`
}
type _ECharsSeriesData ECharsSeriesData
func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
if regexp.MustCompile(`^\d+`).Match(data) {
v, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return err
}
es.Value = v
return nil
}
v := _ECharsSeriesData{}
err := json.Unmarshal(data, &v)
if err != nil {
return err
}
es.Value = v.Value
es.ItemStyle = v.ItemStyle
return nil
}
type EChartsYAxis struct {
Data []struct {
Min int `json:"min"`
Max int `json:"max"`
Interval int `json:"interval"`
AxisLabel struct {
Formatter string `json:"formatter"`
} `json:"axisLabel"`
} `json:"data"`
}
func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
if data[0] != '[' {
data = []byte("[" + string(data) + "]")
}
return json.Unmarshal(data, &ey.Data)
}
type ECharsOptions struct {
Title struct {
Text string `json:"text"`
TextStyle struct {
Color string `json:"color"`
FontFamily string `json:"fontFamily"`
} `json:"textStyle"`
} `json:"title"`
XAxis struct {
Type string `json:"type"`
BoundaryGap *bool `json:"boundaryGap"`
SplitNumber int `json:"splitNumber"`
Data []string `json:"data"`
} `json:"xAxis"`
YAxis EChartsYAxis `json:"yAxis"`
Legend struct {
Data []string `json:"data"`
} `json:"legend"`
Series []struct {
Data []ECharsSeriesData `json:"data"`
Type string `json:"type"`
} `json:"series"`
}
func (e *ECharsOptions) ToOptions() Options {
o := Options{}
o.Title = Title{
Text: e.Title.Text,
}
o.XAxis = XAxis{
Type: e.XAxis.Type,
Data: e.XAxis.Data,
SplitNumber: e.XAxis.SplitNumber,
}
o.Legend = Legend{
Data: e.Legend.Data,
}
// TODO 生成yAxis
tickPosition := chart.TickPositionUnset
series := make([]Series, len(e.Series))
for index, item := range e.Series {
// bar默认tick居中
if item.Type == SeriesBar {
tickPosition = chart.TickPositionBetweenTicks
}
data := make([]SeriesData, len(item.Data))
for j, itemData := range item.Data {
sd := SeriesData{
Value: itemData.Value,
}
if itemData.ItemStyle.Color != "" {
c := parseColor(itemData.ItemStyle.Color)
sd.Style.FillColor = c
sd.Style.StrokeColor = c
}
data[j] = sd
}
series[index] = Series{
Data: data,
Type: item.Type,
}
}
o.Series = series
if e.XAxis.BoundaryGap == nil || *e.XAxis.BoundaryGap {
tickPosition = chart.TickPositionBetweenTicks
}
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
}

13
go.mod Normal file
View file

@ -0,0 +1,13 @@
module github.com/vicanso/echarts
go 1.17
require (
github.com/dustin/go-humanize v1.0.0
github.com/wcharczuk/go-chart/v2 v2.1.0
)
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
)

9
go.sum Normal file
View file

@ -0,0 +1,9 @@
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View file

@ -30,6 +30,7 @@ type SeriesData struct {
Value float64 Value float64
Style chart.Style Style chart.Style
} }
type Series struct { type Series struct {
Type string Type string
Name string Name string
@ -46,6 +47,15 @@ const (
SeriesPie = "pie" SeriesPie = "pie"
) )
func NewSeriesDataListFromFloat(values []float64) []SeriesData {
dataList := make([]SeriesData, len(values))
for index, value := range values {
dataList[index] = SeriesData{
Value: value,
}
}
return dataList
}
func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) []chart.Series { func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) []chart.Series {
arr := make([]chart.Series, len(series)) arr := make([]chart.Series, len(series))
barCount := 0 barCount := 0
@ -60,11 +70,11 @@ func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) [
style := chart.Style{ style := chart.Style{
StrokeWidth: lineStrokeWidth, StrokeWidth: lineStrokeWidth,
StrokeColor: getSeriesColor(theme, index), StrokeColor: getSeriesColor(theme, index),
// FillColor: getSeriesColor(theme, index),
// TODO 调整为通过dot with color 生成 // TODO 调整为通过dot with color 生成
DotColor: getSeriesColor(theme, index), DotColor: getSeriesColor(theme, index),
DotWidth: dotWith, DotWidth: dotWith,
} }
pointIndexOffset := 0
// 如果居中,需要多增加一个点 // 如果居中,需要多增加一个点
if tickPosition == chart.TickPositionBetweenTicks { if tickPosition == chart.TickPositionBetweenTicks {
item.Data = append([]SeriesData{ item.Data = append([]SeriesData{
@ -72,6 +82,7 @@ func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) [
Value: 0.0, Value: 0.0,
}, },
}, item.Data...) }, item.Data...)
pointIndexOffset = -1
} }
yValues := make([]float64, len(item.Data)) yValues := make([]float64, len(item.Data))
barCustomStyles := make([]BarSeriesCustomStyle, 0) barCustomStyles := make([]BarSeriesCustomStyle, 0)
@ -79,7 +90,7 @@ func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) [
yValues[i] = item.Value yValues[i] = item.Value
if !item.Style.IsZero() { if !item.Style.IsZero() {
barCustomStyles = append(barCustomStyles, BarSeriesCustomStyle{ barCustomStyles = append(barCustomStyles, BarSeriesCustomStyle{
PointIndex: i, PointIndex: i + pointIndexOffset,
Index: barIndex, Index: barIndex,
Style: item.Style, Style: item.Style,
}) })

View file

@ -23,6 +23,8 @@
package charts package charts
import ( import (
"strings"
"github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "github.com/wcharczuk/go-chart/v2/drawing"
) )
@ -107,3 +109,14 @@ func getSeriesColor(theme string, index int) drawing.Color {
} }
return SeriesColorsLight[index%len(SeriesColorsLight)] return SeriesColorsLight[index%len(SeriesColorsLight)]
} }
func parseColor(color string) drawing.Color {
if color == "" {
return drawing.Color{}
}
if strings.HasPrefix(color, "#") {
return drawing.ColorFromHex(color[1:])
}
// TODO
return drawing.Color{}
}