refactor: support dark theme

This commit is contained in:
vicanso 2021-12-18 07:59:37 +08:00
parent 8f7587561f
commit ead48fef8e
7 changed files with 568 additions and 95 deletions

95
axis.go
View file

@ -25,7 +25,6 @@ package charts
import (
"github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
type (
@ -41,6 +40,10 @@ type (
}
)
type YAxisOption struct {
Formater chart.ValueFormatter
}
const axisStrokeWidth = 1
func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme string) (chart.XAxis, []float64) {
@ -85,24 +88,29 @@ func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme strin
})
}
}
// TODO
if theme == ThemeDark {
return chart.XAxis{
Ticks: ticks,
}, xValues
}
return chart.XAxis{
Ticks: ticks,
TickPosition: tickPosition,
Style: chart.Style{
FontColor: AxisColorLight,
StrokeColor: AxisColorLight,
FontColor: getAxisColor(theme),
StrokeColor: getAxisColor(theme),
StrokeWidth: axisStrokeWidth,
},
}, xValues
}
func GetSecondaryYAxis(theme string) chart.YAxis {
func defaultFloatFormater(v interface{}) string {
value, ok := v.(float64)
if !ok {
return ""
}
if value >= 10 {
return humanize.CommafWithDigits(value, 0)
}
return humanize.CommafWithDigits(value, 2)
}
func GetSecondaryYAxis(theme string, option *YAxisOption) chart.YAxis {
// TODO
if theme == ThemeDark {
return chart.YAxis{}
@ -113,41 +121,46 @@ func GetSecondaryYAxis(theme string) chart.YAxis {
// 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(),
formater := defaultFloatFormater
if option != nil {
if option.Formater != nil {
formater = option.Formater
}
}
hidden := chart.Hidden()
return chart.YAxis{
ValueFormatter: formater,
AxisType: chart.YAxisPrimary,
GridMajorStyle: hidden,
GridMinorStyle: hidden,
Style: chart.Style{
FontColor: getAxisColor(theme),
// alpha 0隐藏
StrokeColor: hiddenColor,
StrokeWidth: axisStrokeWidth,
},
}
// c := chart.Hidden()
// return chart.YAxis{
// ValueFormatter: defaultFloatFormater,
// AxisType: chart.YAxisPrimary,
// GridMajorStyle: c,
// GridMinorStyle: c,
// Style: c,
// }
}
func GetYAxis(theme string) chart.YAxis {
// TODO
if theme == ThemeDark {
return chart.YAxis{}
}
strokeColor := drawing.Color{
R: 224,
G: 230,
B: 241,
A: 255,
func GetYAxis(theme string, option *YAxisOption) chart.YAxis {
strokeColor := getGridColor(theme)
formater := defaultFloatFormater
if option != nil {
if option.Formater != nil {
formater = option.Formater
}
}
return chart.YAxis{
ValueFormatter: func(v interface{}) string {
value, ok := v.(float64)
if !ok {
return ""
}
return humanize.Commaf(value)
},
AxisType: chart.YAxisSecondary,
ValueFormatter: formater,
AxisType: chart.YAxisSecondary,
GridMajorStyle: chart.Style{
StrokeColor: strokeColor,
StrokeWidth: axisStrokeWidth,
@ -157,7 +170,7 @@ func GetYAxis(theme string) chart.YAxis {
StrokeWidth: axisStrokeWidth,
},
Style: chart.Style{
FontColor: AxisColorLight,
FontColor: getAxisColor(theme),
// alpha 0隐藏
StrokeColor: hiddenColor,
StrokeWidth: axisStrokeWidth,

View file

@ -53,7 +53,7 @@ type (
Height int
Theme string
XAxis XAxis
YAxisList []chart.YAxis
YAxisOptions []*YAxisOption
Series []Series
Title Title
Legend Legend
@ -72,7 +72,7 @@ func (o *Options) validate() error {
}
for _, item := range o.Series {
if len(item.Data) != xAxisCount {
if item.Type != SeriesPie && len(item.Data) != xAxisCount {
return errors.New("series and xAxis is not matched")
}
}
@ -109,18 +109,6 @@ func New(opt Options) (Graph, error) {
if height <= 0 {
height = DefaultChartHeight
}
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]
}
}
if opt.Series[0].Type == SeriesPie {
values := make(chart.Values, len(opt.Series))
for index, item := range opt.Series {
@ -142,21 +130,49 @@ func New(opt Options) (Graph, error) {
return g, nil
}
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 yAxisOption *YAxisOption
if len(opt.YAxisOptions) != 0 {
yAxisOption = opt.YAxisOptions[0]
}
var secondaryYAxisOption *YAxisOption
if len(opt.YAxisOptions) > 1 {
secondaryYAxisOption = opt.YAxisOptions[1]
}
g := &chart.Chart{
ColorPalette: &ThemeColorPalette{
Theme: opt.Theme,
},
Title: opt.Title.Text,
TitleStyle: opt.Title.Style,
Width: width,
Height: height,
XAxis: xAxis,
YAxis: GetYAxis(opt.Theme),
YAxisSecondary: GetSecondaryYAxis(opt.Theme),
YAxis: GetYAxis(opt.Theme, yAxisOption),
YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption),
Series: GetSeries(opt.Series, tickPosition, opt.Theme),
}
// 设置secondary的样式
if legendSize != 0 {
g.Elements = []chart.Renderable{
DefaultLegend(g),
LegendCustomize(g, LegendOption{
Theme: opt.Theme,
TextPosition: LegendTextPositionRight,
IconDraw: DefaultLegendIconDraw,
}),
}
}
return g, nil

View file

@ -27,6 +27,7 @@ import (
"encoding/json"
"regexp"
"strconv"
"strings"
"github.com/wcharczuk/go-chart/v2"
)
@ -36,6 +37,7 @@ type EChartStyle struct {
}
type ECharsSeriesData struct {
Value float64 `json:"value"`
Name string `json:"name"`
ItemStyle EChartStyle `json:"itemStyle"`
}
type _ECharsSeriesData ECharsSeriesData
@ -58,6 +60,7 @@ func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
es.Name = v.Name
es.Value = v.Value
es.ItemStyle = v.ItemStyle
return nil
@ -86,6 +89,7 @@ func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error {
}
type ECharsOptions struct {
Theme string `json:"theme"`
Title struct {
Text string `json:"text"`
TextStyle struct {
@ -104,30 +108,34 @@ type ECharsOptions struct {
Data []string `json:"data"`
} `json:"legend"`
Series []struct {
Data []ECharsSeriesData `json:"data"`
Type string `json:"type"`
Data []ECharsSeriesData `json:"data"`
Type string `json:"type"`
YAxisIndex int `json:"yAxisIndex"`
} `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
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))
for index, item := range e.Series[0].Data {
series[index] = Series{
Data: []SeriesData{
{
Value: item.Value,
},
},
Type: seriesType,
Name: item.Name,
}
}
return series, tickPosition
}
series := make([]Series, len(e.Series))
for index, item := range e.Series {
// bar默认tick居中
@ -146,11 +154,54 @@ func (e *ECharsOptions) ToOptions() Options {
}
data[j] = sd
}
yAxisType := chart.YAxisPrimary
if item.YAxisIndex != 0 {
yAxisType = chart.YAxisSecondary
}
series[index] = Series{
Data: data,
Type: item.Type,
YAxis: yAxisType,
Data: data,
Type: item.Type,
}
}
return series, tickPosition
}
func (e *ECharsOptions) ToOptions() Options {
o := Options{
Theme: e.Theme,
}
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,
}
if len(e.YAxis.Data) != 0 {
yAxisOptions := make([]*YAxisOption, len(e.YAxis.Data))
for index, item := range e.YAxis.Data {
opt := &YAxisOption{}
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 e.XAxis.BoundaryGap == nil || *e.XAxis.BoundaryGap {

337
examples/main.go Normal file
View file

@ -0,0 +1,337 @@
package main
import (
"bytes"
"net/http"
charts "github.com/vicanso/echarts"
)
var html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link type="text/css" rel="styleSheet" href="https://unpkg.com/normalize.css@8.0.1/normalize.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
h1 {
text-align: center;
}
pre {
width: 800px;
margin: auto auto 20px auto;
max-height: 300px;
overflow: auto;
display: block;
}
svg{
margin: auto auto 50px auto;
display: block;
}
</style>
<title>go-echarts</title>
</head>
<body>
{{body}}
</body>
</html>
`
var chartOptions = []map[string]string{
{
"title": "折线图",
"option": `{
"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": {
"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",
"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": 12,
"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"
}
]
}
]
}`,
},
}
func render(theme string) ([]byte, error) {
data := bytes.Buffer{}
for _, m := range chartOptions {
if m["title"] != "折柱混合" {
continue
}
chartHTML := []byte(`<div>
<h1>{{title}}</h1>
<pre>{{option}}</pre>
{{svg}}
</div>`)
o, err := charts.ParseECharsOptions(m["option"])
o.Theme = 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) {
buf, err := render(r.URL.Query().Get("theme"))
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)
}

View file

@ -65,22 +65,15 @@ func DefaultLegendIconDraw(r chart.Renderer, opt LegendIconDrawOption) {
r.MoveTo(opt.Box.Left, ly)
r.LineTo(opt.Box.Right, ly)
r.Stroke()
r.SetFillColor(chart.ColorWhite)
r.SetFillColor(getBackgroundColor(opt.Theme))
r.Circle(5, (opt.Box.Left+opt.Box.Right)/2, ly)
r.FillStroke()
}
func DefaultLegend(c *chart.Chart) chart.Renderable {
return LegendCustomize(c, LegendOption{
TextPosition: LegendTextPositionRight,
IconDraw: DefaultLegendIconDraw,
})
}
func LegendCustomize(c *chart.Chart, opt LegendOption) chart.Renderable {
return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
legendDefaults := chart.Style{
FontColor: chart.DefaultTextColor,
FontColor: getTextColor(opt.Theme),
FontSize: 8.0,
StrokeColor: chart.DefaultAxisColor,
}

View file

@ -36,6 +36,7 @@ type Series struct {
Name string
Data []SeriesData
XValues []float64
YAxis chart.YAxisType
}
const lineStrokeWidth = 2
@ -102,6 +103,7 @@ func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) [
Style: style,
YValues: yValues,
TickPosition: tickPosition,
YAxis: item.YAxis,
}
// TODO 判断类型
switch item.Type {

View file

@ -37,6 +37,50 @@ var AxisColorLight = drawing.Color{
B: 121,
A: 255,
}
var AxisColorDark = drawing.Color{
R: 185,
G: 184,
B: 206,
A: 255,
}
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
}
return AxisColorLight
}
func getGridColor(theme string) drawing.Color {
if theme == ThemeDark {
return drawing.Color{
R: 72,
G: 71,
B: 83,
A: 255,
}
}
return drawing.Color{
R: 224,
G: 230,
B: 241,
A: 255,
}
}
var SeriesColorsLight = []drawing.Color{
{
@ -71,12 +115,26 @@ var SeriesColorsLight = []drawing.Color{
},
}
func getBackgroundColor(theme string) drawing.Color {
if theme == ThemeDark {
return BackgroundColorDark
}
return chart.DefaultBackgroundColor
}
func getTextColor(theme string) drawing.Color {
if theme == ThemeDark {
return TextColorDark
}
return chart.DefaultTextColor
}
type ThemeColorPalette struct {
Theme string
}
func (tp ThemeColorPalette) BackgroundColor() drawing.Color {
return chart.DefaultBackgroundColor
return getBackgroundColor(tp.Theme)
}
func (tp ThemeColorPalette) BackgroundStrokeColor() drawing.Color {
@ -84,6 +142,9 @@ func (tp ThemeColorPalette) BackgroundStrokeColor() drawing.Color {
}
func (tp ThemeColorPalette) CanvasColor() drawing.Color {
if tp.Theme == ThemeDark {
return BackgroundColorDark
}
return chart.DefaultCanvasColor
}
@ -92,11 +153,14 @@ func (tp ThemeColorPalette) CanvasStrokeColor() drawing.Color {
}
func (tp ThemeColorPalette) AxisStrokeColor() drawing.Color {
if tp.Theme == ThemeDark {
return BackgroundColorDark
}
return chart.DefaultAxisColor
}
func (tp ThemeColorPalette) TextColor() drawing.Color {
return chart.DefaultTextColor
return getTextColor(tp.Theme)
}
func (tp ThemeColorPalette) GetSeriesColor(index int) drawing.Color {
@ -104,9 +168,6 @@ func (tp ThemeColorPalette) GetSeriesColor(index int) drawing.Color {
}
func getSeriesColor(theme string, index int) drawing.Color {
// TODO
if theme == ThemeDark {
}
return SeriesColorsLight[index%len(SeriesColorsLight)]
}
@ -117,6 +178,6 @@ func parseColor(color string) drawing.Color {
if strings.HasPrefix(color, "#") {
return drawing.ColorFromHex(color[1:])
}
// TODO
// TODO rgba
return drawing.Color{}
}