go-charts/echarts.go
2022-01-15 10:43:38 +08:00

403 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
"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 {
return nil
}
if data[0] != '[' {
data = []byte("[" + string(data) + "]")
}
return data
}
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.Name = v.Name
es.Value = v.Value
es.ItemStyle = v.ItemStyle
return nil
}
type EChartsPadding struct {
box chart.Box
}
type Position string
func (lp *Position) 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)(lp)
return json.Unmarshal(data, s)
}
func (ep *EChartsPadding) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
if len(data) == 0 {
return nil
}
arr := make([]int, 0)
err := json.Unmarshal(data, &arr)
if err != nil {
return err
}
if len(arr) == 0 {
return nil
}
switch len(arr) {
case 1:
ep.box = chart.Box{
Left: arr[0],
Top: arr[0],
Bottom: arr[0],
Right: arr[0],
}
case 2:
ep.box = chart.Box{
Top: arr[0],
Bottom: arr[0],
Left: arr[1],
Right: arr[1],
}
default:
result := make([]int, 4)
copy(result, arr)
if len(arr) == 3 {
result[3] = result[1]
}
// 上右下左
ep.box = chart.Box{
Top: result[0],
Right: result[1],
Bottom: result[2],
Left: result[3],
}
}
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"`
} `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 EChartsXAxis struct {
Data []struct {
// Type string `json:"type"`
BoundaryGap *bool `json:"boundaryGap"`
SplitNumber int `json:"splitNumber"`
Data []string `json:"data"`
}
}
func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
if len(data) == 0 {
return nil
}
return json.Unmarshal(data, &ex.Data)
}
type EChartsLabelOption struct {
Show bool `json:"show"`
Distance int `json:"distance"`
}
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,
},
},
Type: seriesType,
Name: item.Name,
Label: label,
}
}
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
}
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{
Style: style,
YAxisIndex: item.YAxisIndex,
Data: data,
Type: item.Type,
Label: item.Label.ToLabel(),
}
}
return series, tickPosition
}
func (e *ECharsOptions) ToOptions() Options {
o := Options{
Theme: e.Theme,
Padding: e.Padding.box,
}
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,
},
}
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
}
}
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
}
o.YAxisOptions = yAxisOptions
}
series, tickPosition := convertEChartsSeries(e)
o.Series = series
if 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
}
func echartsRender(options string, rp chart.RendererProvider) ([]byte, error) {
o, err := ParseECharsOptions(options)
if err != nil {
return nil, err
}
g, err := New(o)
if err != nil {
return nil, err
}
return render(g, rp)
}
func RenderEChartsToPNG(options string) ([]byte, error) {
return echartsRender(options, chart.PNG)
}
func RenderEChartsToSVG(options string) ([]byte, error) {
return echartsRender(options, chart.SVG)
}