feat: support customize title

This commit is contained in:
vicanso 2021-12-29 23:18:41 +08:00
parent 2772798122
commit 06c326bdc3
9 changed files with 170 additions and 44 deletions

View file

@ -56,9 +56,11 @@ func main() {
- `padding:[5, 10, 5, 10]` 分别设置`上右下左`边距
- `title` 图表标题,包括标题内容、高度、颜色等
- `title.text` 标题内容
- `title.left` 标题与容器左侧的距离,可设置为`left`, `right`, `center`, `20%` 以及 `20` 这样的具体数值
- `title.textStyle.color` 标题文字颜色
- `title.textStyle.fontSize` 标题文字字体大小
- `title.textStyle.height` 标题高度
- `title.textStyle.fontFamily` 标题文字的字体系列,需要注意此配置是会影响整个图表的字体
- `xAxis` 直角坐标系grid中的x轴由于go-charts仅支持单一个x轴因此若参数为数组多个x轴只使用第一个配置
- `xAxis.boundaryGap` 坐标轴两边留白策略,仅支持三种设置方式`null`, `true`或者`false``null``true`时则数据点展示在两个刻度中间
- `xAxis.splitNumber` 坐标轴的分割段数,需要注意的是这个分割段数只是个预估值,最后实际显示的段数会在这个基础上根据分割后坐标轴刻度显示的易读程度作调整

View file

@ -168,5 +168,5 @@ func TestGetYAxis(t *testing.T) {
yAxis = GetSecondaryYAxis(ThemeDark, nil)
assert.False(yAxis.GridMajorStyle.Hidden)
assert.False(yAxis.GridMajorStyle.Hidden)
assert.True(yAxis.Style.StrokeColor.IsZero())
assert.True(yAxis.Style.StrokeColor.IsTransparent())
}

View file

@ -30,6 +30,7 @@ import (
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
const (
@ -46,6 +47,8 @@ type (
Title struct {
Text string
Style chart.Style
Font *truetype.Font
Left string
}
Legend struct {
Data []string
@ -156,6 +159,17 @@ 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 {
@ -164,11 +178,9 @@ func newPieChart(opt Options) *chart.PieChart {
Label: item.Name,
}
}
return &chart.PieChart{
p := &chart.PieChart{
Font: opt.Font,
Background: opt.getBackground(),
Title: opt.Title.Text,
TitleStyle: opt.Title.Style,
Width: opt.getWidth(),
Height: opt.getHeight(),
Values: values,
@ -178,6 +190,17 @@ func newPieChart(opt Options) *chart.PieChart {
},
},
}
// pie 图表默认设置为居中
if opt.Title.Left == "" {
opt.Title.Left = "center"
}
titleRender := newTitleRenderable(opt.Title, p.GetFont(), p.GetColorPalette().TextColor())
if titleRender != nil {
p.Elements = []chart.Renderable{
titleRender,
}
}
return p
}
func newChart(opt Options) *chart.Chart {
@ -214,8 +237,6 @@ func newChart(opt Options) *chart.Chart {
ColorPalette: &ThemeColorPalette{
Theme: opt.Theme,
},
Title: opt.Title.Text,
TitleStyle: opt.Title.Style,
Width: opt.getWidth(),
Height: opt.getHeight(),
XAxis: xAxis,
@ -224,10 +245,10 @@ func newChart(opt Options) *chart.Chart {
Series: GetSeries(opt.Series, tickPosition, opt.Theme),
}
// 设置secondary的样式
elements := make([]chart.Renderable, 0)
if legendSize != 0 {
c.Elements = []chart.Renderable{
LegendCustomize(c.Series, LegendOption{
elements = append(elements, NewLegendCustomize(c.Series, LegendOption{
Theme: opt.Theme,
IconDraw: DefaultLegendIconDraw,
Align: opt.Legend.Align,
@ -236,8 +257,14 @@ func newChart(opt Options) *chart.Chart {
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
}

View file

@ -82,9 +82,9 @@ type EChartsPadding struct {
box chart.Box
}
type LegendPostion string
type Position string
func (lp *LegendPostion) UnmarshalJSON(data []byte) error {
func (lp *Position) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
@ -181,8 +181,7 @@ type ECharsOptions struct {
Padding EChartsPadding `json:"padding"`
Title struct {
Text string `json:"text"`
// 暂不支持(go-chart默认title只能居中)
TextAlign string `json:"textAlign"`
Left Position `json:"left"`
TextStyle struct {
Color string `json:"color"`
FontFamily string `json:"fontFamily"`
@ -196,8 +195,8 @@ type ECharsOptions struct {
Data []string `json:"data"`
Align string `json:"align"`
Padding EChartsPadding `json:"padding"`
Left LegendPostion `json:"left"`
Right LegendPostion `json:"right"`
Left Position `json:"left"`
Right Position `json:"right"`
// Top string `json:"top"`
// Bottom string `json:"bottom"`
} `json:"legend"`
@ -282,6 +281,7 @@ func (e *ECharsOptions) ToOptions() Options {
titleTextStyle := e.Title.TextStyle
o.Title = Title{
Text: e.Title.Text,
Left: string(e.Title.Left),
Style: chart.Style{
FontColor: parseColor(titleTextStyle.Color),
FontSize: titleTextStyle.FontSize,

View file

@ -55,8 +55,8 @@ var chartOptions = []map[string]string{
"title": "折线图",
"option": `{
"title": {
"text": "Line",
"textAlign": "left",
"text": "Line\nHello World",
"left": "right",
"textStyle": {
"fontSize": 24,
"height": 40
@ -363,6 +363,9 @@ func render(opts renderOptions) ([]byte, error) {
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
if r.RequestURI != "/" {
return
}
query := r.URL.Query()
opts := renderOptions{
theme: query.Get("theme"),

View file

@ -83,8 +83,8 @@ func covertPercent(value string) float64 {
return float64(v) / 100
}
func getLegendLeft(width, legendBoxWidth int, opt LegendOption) int {
left := (width - legendBoxWidth) / 2
func getLegendLeft(canvasWidth, legendBoxWidth int, opt LegendOption) int {
left := (canvasWidth - legendBoxWidth) / 2
leftValue := opt.Left
if leftValue == "auto" || leftValue == "center" {
leftValue = ""
@ -106,7 +106,7 @@ func getLegendLeft(width, legendBoxWidth int, opt LegendOption) int {
if leftValue != "" {
percent := covertPercent(leftValue)
if percent >= 0 {
return int(float64(width) * percent)
return int(float64(canvasWidth) * percent)
}
v, _ := strconv.Atoi(leftValue)
return v
@ -114,10 +114,10 @@ func getLegendLeft(width, legendBoxWidth int, opt LegendOption) int {
if rightValue != "" {
percent := covertPercent(rightValue)
if percent >= 0 {
return width - legendBoxWidth - int(float64(width)*percent)
return canvasWidth - legendBoxWidth - int(float64(canvasWidth)*percent)
}
v, _ := strconv.Atoi(rightValue)
return width - legendBoxWidth - v
return canvasWidth - legendBoxWidth - v
}
return left
}
@ -127,7 +127,7 @@ func getLegendTop(height, legendBoxHeight int, opt LegendOption) int {
return 0
}
func LegendCustomize(series []chart.Series, opt LegendOption) chart.Renderable {
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),
@ -154,7 +154,6 @@ func LegendCustomize(series []chart.Series, opt LegendOption) chart.Renderable {
}
var textHeight int
var textWidth int
var textBox chart.Box
labelWidth := 0
// 计算文本宽度与高度(取最大值)
@ -163,7 +162,6 @@ func LegendCustomize(series []chart.Series, opt LegendOption) chart.Renderable {
textBox = r.MeasureText(labels[x])
labelWidth += textBox.Width()
textHeight = chart.MaxInt(textBox.Height(), textHeight)
textWidth = chart.MaxInt(textBox.Width(), textWidth)
}
}
@ -175,15 +173,15 @@ func LegendCustomize(series []chart.Series, opt LegendOption) chart.Renderable {
lineTextGap := 5
iconAllWidth := iconWidth * len(labels)
spaceAllWidth := chart.DefaultMinimumTickHorizontalSpacing * (len(labels) - 1)
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
top += opt.Padding.Top
left += (opt.Padding.Left + cb.Left)
top += (opt.Padding.Top + cb.Top)
legendBox := chart.Box{
Left: left,

View file

@ -30,7 +30,7 @@ import (
"github.com/wcharczuk/go-chart/v2"
)
func TestLegendCustomize(t *testing.T) {
func TestNewLegendCustomize(t *testing.T) {
assert := assert.New(t)
series := GetSeries([]Series{
@ -48,18 +48,18 @@ func TestLegendCustomize(t *testing.T) {
}{
{
align: LegendAlignLeft,
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"800\" height=\"600\">\\n<path d=\"M 404 100\nL 532 100\nL 532 110\nL 404 110\nL 404 100\" style=\"stroke-width:0;stroke:rgba(51,51,51,1.0);fill:none\"/><path d=\"M 404 107\nL 429 107\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"416\" cy=\"107\" r=\"5\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"434\" y=\"110\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">chrome</text><path d=\"M 489 107\nL 514 107\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"501\" cy=\"107\" r=\"5\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"519\" y=\"110\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">edge</text></svg>",
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"800\" height=\"600\">\\n<path d=\"M 449 111\nL 582 111\nL 582 121\nL 449 121\nL 449 111\" style=\"stroke-width:0;stroke:rgba(51,51,51,1.0);fill:none\"/><path d=\"M 449 118\nL 474 118\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"461\" cy=\"118\" r=\"5\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"479\" y=\"121\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">chrome</text><path d=\"M 534 118\nL 559 118\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"546\" cy=\"118\" r=\"5\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"564\" y=\"121\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">edge</text></svg>",
},
{
align: LegendAlignRight,
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"800\" height=\"600\">\\n<path d=\"M 404 100\nL 532 100\nL 532 110\nL 404 110\nL 404 100\" style=\"stroke-width:0;stroke:rgba(51,51,51,1.0);fill:none\"/><text x=\"404\" y=\"110\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">chrome</text><path d=\"M 444 107\nL 469 107\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"456\" cy=\"107\" r=\"5\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"494\" y=\"110\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">edge</text><path d=\"M 522 107\nL 547 107\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"534\" cy=\"107\" r=\"5\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/></svg>",
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"800\" height=\"600\">\\n<path d=\"M 449 111\nL 582 111\nL 582 121\nL 449 121\nL 449 111\" style=\"stroke-width:0;stroke:rgba(51,51,51,1.0);fill:none\"/><text x=\"449\" y=\"121\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">chrome</text><path d=\"M 489 118\nL 514 118\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"501\" cy=\"118\" r=\"5\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"539\" y=\"121\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">edge</text><path d=\"M 567 118\nL 592 118\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"579\" cy=\"118\" r=\"5\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/></svg>",
},
}
for _, tt := range tests {
r, err := chart.SVG(800, 600)
assert.Nil(err)
fn := LegendCustomize(series, LegendOption{
fn := NewLegendCustomize(series, LegendOption{
Align: tt.align,
IconDraw: DefaultLegendIconDraw,
Padding: chart.Box{

View file

@ -31,7 +31,7 @@ import (
"github.com/wcharczuk/go-chart/v2/drawing"
)
var hiddenColor = drawing.Color{R: 0, G: 0, B: 0, A: 0}
var hiddenColor = drawing.Color{R: 255, G: 255, B: 255, A: 0}
var AxisColorLight = drawing.Color{
R: 110,

96
title.go Normal file
View file

@ -0,0 +1,96 @@
// 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 (
"strconv"
"strings"
"github.com/wcharczuk/go-chart/v2"
)
type titleMeasureOption struct {
width int
height int
text string
}
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
}
if title.Font != nil {
r.SetFont(title.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,
}
}
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)
for _, item := range measureOptions {
x := titleX + (textWidth-item.width)>>1
r.Text(item.text, x, titleY)
titleY += textHeight
}
}
}