diff --git a/README.md b/README.md index 0b36a49..382a2f2 100644 --- a/README.md +++ b/README.md @@ -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` 坐标轴的分割段数,需要注意的是这个分割段数只是个预估值,最后实际显示的段数会在这个基础上根据分割后坐标轴刻度显示的易读程度作调整 diff --git a/axis_test.go b/axis_test.go index cc50864..43779e9 100644 --- a/axis_test.go +++ b/axis_test.go @@ -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()) } diff --git a/charts.go b/charts.go index 3cef373..9216606 100644 --- a/charts.go +++ b/charts.go @@ -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,20 +245,26 @@ 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{ - 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, - }), - } + 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 } diff --git a/echarts.go b/echarts.go index a55da5a..cc4ad2f 100644 --- a/echarts.go +++ b/echarts.go @@ -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 } @@ -180,9 +180,8 @@ type ECharsOptions struct { Theme string `json:"theme"` Padding EChartsPadding `json:"padding"` Title struct { - Text string `json:"text"` - // 暂不支持(go-chart默认title只能居中) - TextAlign string `json:"textAlign"` + Text string `json:"text"` + 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, diff --git a/examples/charts/main.go b/examples/charts/main.go index 4000b44..fde8f5f 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -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"), diff --git a/legend.go b/legend.go index bf968fe..bc9cd39 100644 --- a/legend.go +++ b/legend.go @@ -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, diff --git a/legend_test.go b/legend_test.go index 5bfacf8..66d3e47 100644 --- a/legend_test.go +++ b/legend_test.go @@ -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: "\\nchromeedge", + svg: "\\nchromeedge", }, { align: LegendAlignRight, - svg: "\\nchromeedge", + svg: "\\nchromeedge", }, } 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{ diff --git a/theme.go b/theme.go index 051e099..63e000a 100644 --- a/theme.go +++ b/theme.go @@ -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, diff --git a/title.go b/title.go new file mode 100644 index 0000000..654711c --- /dev/null +++ b/title.go @@ -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 + } + } +}