diff --git a/axis.go b/axis.go index dc8844a..46292e4 100644 --- a/axis.go +++ b/axis.go @@ -73,6 +73,7 @@ type axisMeasurement struct { Height int } +// NewAxis creates a new axis with data and style options func NewAxis(d *Draw, data AxisDataList, option AxisOption) *axis { return &axis{ d: d, @@ -112,6 +113,7 @@ func (as *AxisOption) Style(f *truetype.Font) chart.Style { } type AxisData struct { + // The text value of axis Text string } type AxisDataList []AxisData diff --git a/bar_chart.go b/bar_chart.go index ac5a0a9..11e9d5a 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -28,17 +28,20 @@ import ( ) type barChartOption struct { + // The series list fo bar chart SeriesList SeriesList - Theme string - Font *truetype.Font + // The theme + Theme string + // The font + Font *truetype.Font } func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointRenderOption, error) { - d, err := NewDraw(DrawOption{ Parent: result.d, }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), + Top: result.titleBox.Height(), + // TODO 后续考虑是否需要根据左侧是否展示y轴再生成对应的left Left: YAxisWidth, })) if err != nil { @@ -118,6 +121,7 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR X: x + barWidth>>1, Y: top, } + // 如果label不需要展示,则返回 if !series.Label.Show { continue } @@ -135,6 +139,7 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5) } + // 生成mark point的参数 markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{ Draw: d, FillColor: seriesColor, diff --git a/chart.go b/chart.go index 1f31252..44bee0c 100644 --- a/chart.go +++ b/chart.go @@ -44,24 +44,45 @@ type Point struct { const labelFontSize = 10 +var defaultChartWidth = 600 +var defaultChartHeight = 400 + type ChartOption struct { - Type string - Font *truetype.Font - Theme string - Title TitleOption - Legend LegendOption - XAxis XAxisOption - YAxisList []YAxisOption - Width int - Height int - Parent *Draw - Padding chart.Box - Box chart.Box - SeriesList SeriesList + // The output type of chart, "svg" or "png", default value is "svg" + Type string + // The font family, which should be installed first + FontFamily string + // The font of chart, the default font is "roboto" + Font *truetype.Font + // The theme of chart, "light" and "dark". + // The default theme is "light" + Theme string + // The title option + Title TitleOption + // The legend option + Legend LegendOption + // The x axis option + XAxis XAxisOption + // The y axis option list + YAxisList []YAxisOption + // The width of chart, default width is 600 + Width int + // The height of chart, default height is 400 + Height int + Parent *Draw + // The padding for chart, default padding is [20, 10, 10, 10] + Padding chart.Box + // The canvas box for chart + Box chart.Box + // The series list + SeriesList SeriesList + // The background color of chart BackgroundColor drawing.Color - Children []ChartOption + // The child charts + Children []ChartOption } +// FillDefault fills the default value for chart option func (o *ChartOption) FillDefault(theme string) { t := NewTheme(theme) // 如果为空,初始化 @@ -83,7 +104,7 @@ func (o *ChartOption) FillDefault(theme string) { } if o.Padding.IsZero() { o.Padding = chart.Box{ - Top: 20, + Top: 10, Right: 10, Bottom: 10, Left: 10, @@ -102,10 +123,7 @@ func (o *ChartOption) FillDefault(theme string) { } if o.Title.Style.Padding.IsZero() { o.Title.Style.Padding = chart.Box{ - Left: 5, - Top: 5, - Right: 5, - Bottom: 5, + Bottom: 10, } } // 副标题 @@ -113,7 +131,7 @@ func (o *ChartOption) FillDefault(theme string) { o.Title.SubtextStyle.FontColor = o.Title.Style.FontColor.WithAlpha(180) } if o.Title.SubtextStyle.FontSize == 0 { - o.Title.SubtextStyle.FontSize = 10 + o.Title.SubtextStyle.FontSize = labelFontSize } if o.Title.SubtextStyle.Font == nil { o.Title.SubtextStyle.Font = o.Font @@ -121,7 +139,7 @@ func (o *ChartOption) FillDefault(theme string) { o.Legend.theme = theme if o.Legend.Style.FontSize == 0 { - o.Legend.Style.FontSize = 10 + o.Legend.Style.FontSize = labelFontSize } if o.Legend.Left == "" { o.Legend.Left = PositionCenter @@ -155,7 +173,18 @@ func (o *ChartOption) getWidth() int { if o.Parent != nil { return o.Parent.Box.Width() } - return 600 + return defaultChartWidth +} + +func SetDefaultWidth(width int) { + if width > 0 { + defaultChartWidth = width + } +} +func SetDefaultHeight(height int) { + if height > 0 { + defaultChartHeight = height + } } func (o *ChartOption) getHeight() int { @@ -166,7 +195,7 @@ func (o *ChartOption) getHeight() int { if o.Parent != nil { return o.Parent.Box.Height() } - return 400 + return defaultChartHeight } func (o *ChartOption) newYRange(axisIndex int) Range { @@ -227,10 +256,18 @@ func (r *basicRenderResult) getYRange(index int) *Range { return r.yRangeList[index] } +// Render renders the chart by option func Render(opt ChartOption) (*Draw, error) { if len(opt.SeriesList) == 0 { return nil, errors.New("series can not be nil") } + if len(opt.FontFamily) != 0 { + f, err := GetFont(opt.FontFamily) + if err != nil { + return nil, err + } + opt.Font = f + } opt.FillDefault(opt.Theme) lineSeries := make([]Series, 0) diff --git a/chart_test.go b/chart_test.go index 06370c8..ee768ec 100644 --- a/chart_test.go +++ b/chart_test.go @@ -30,6 +30,20 @@ import ( "github.com/wcharczuk/go-chart/v2/drawing" ) +func TestChartSetDefaultWidthHeight(t *testing.T) { + assert := assert.New(t) + + width := defaultChartWidth + height := defaultChartHeight + defer SetDefaultWidth(width) + defer SetDefaultHeight(height) + + SetDefaultWidth(60) + assert.Equal(60, defaultChartWidth) + SetDefaultHeight(40) + assert.Equal(40, defaultChartHeight) +} + func TestChartFillDefault(t *testing.T) { assert := assert.New(t) // default value @@ -37,7 +51,7 @@ func TestChartFillDefault(t *testing.T) { opt.FillDefault("") // padding assert.Equal(chart.Box{ - Top: 20, + Top: 10, Right: 10, Bottom: 10, Left: 10, @@ -53,13 +67,6 @@ func TestChartFillDefault(t *testing.T) { }, opt.Title.Style.FontColor) // title font size assert.Equal(float64(14), opt.Title.Style.FontSize) - // title padding - assert.Equal(chart.Box{ - Left: 5, - Top: 5, - Right: 5, - Bottom: 5, - }, opt.Title.Style.Padding) // sub title font color assert.Equal(drawing.Color{ R: 70, @@ -267,5 +274,5 @@ func TestChartRender(t *testing.T) { assert.Nil(err) data, err := d.Bytes() assert.Nil(err) - assert.Equal("\\n2012201320142015201620170153045607590Milk TeaMatcha LatteCheese CocoaWalnut BrownieMilk Tea: 34.03%Matcha Latte: 27.66%Cheese Cocoa: 22.32%Walnut Brownie: 15.96%", string(data)) + assert.Equal("\\n2012201320142015201620170153045607590Milk TeaMatcha LatteCheese CocoaWalnut BrownieMilk Tea: 34.03%Matcha Latte: 27.66%Cheese Cocoa: 22.32%Walnut Brownie: 15.96%", string(data)) } diff --git a/draw.go b/draw.go index 0dbcf83..228f622 100644 --- a/draw.go +++ b/draw.go @@ -46,21 +46,30 @@ const ( ) type Draw struct { + // Render Render chart.Renderer - Box chart.Box - Font *truetype.Font + // The canvas box + Box chart.Box + // The font for draw + Font *truetype.Font + // The parent of draw parent *Draw } type DrawOption struct { - Type string + // Draw type, "svg" or "png", default type is "svg" + Type string + // Parent of draw Parent *Draw - Width int + // The width of draw canvas + Width int + // The height of draw canvas Height int } type Option func(*Draw) error +// PaddingOption sets the padding of draw canvas func PaddingOption(padding chart.Box) Option { return func(d *Draw) error { d.Box.Left += padding.Left @@ -71,6 +80,7 @@ func PaddingOption(padding chart.Box) Option { } } +// BoxOption set the box of draw canvas func BoxOption(box chart.Box) Option { return func(d *Draw) error { if box.IsZero() { @@ -81,6 +91,7 @@ func BoxOption(box chart.Box) Option { } } +// NewDraw returns a new draw canvas func NewDraw(opt DrawOption, opts ...Option) (*Draw, error) { if opt.Parent == nil && (opt.Width <= 0 || opt.Height <= 0) { return nil, errors.New("parent and width/height can not be nil") @@ -122,10 +133,12 @@ func NewDraw(opt DrawOption, opts ...Option) (*Draw, error) { return d, nil } +// Parent returns the parent of draw func (d *Draw) Parent() *Draw { return d.parent } +// Top returns the top parent of draw func (d *Draw) Top() *Draw { if d.parent == nil { return nil @@ -141,6 +154,7 @@ func (d *Draw) Top() *Draw { return t } +// Bytes returns the data of draw canvas func (d *Draw) Bytes() ([]byte, error) { buffer := bytes.Buffer{} err := d.Render.Save(&buffer) diff --git a/examples/demo/main.go b/examples/demo/main.go index 5d58cda..7916b4d 100644 --- a/examples/demo/main.go +++ b/examples/demo/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "net/http" + "strconv" charts "github.com/vicanso/go-charts" "github.com/wcharczuk/go-chart/v2" @@ -65,7 +66,12 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { if req.URL.Path != "/" { return } - theme := req.URL.Query().Get("theme") + query := req.URL.Query() + theme := query.Get("theme") + width, _ := strconv.Atoi(query.Get("width")) + height, _ := strconv.Atoi(query.Get("height")) + charts.SetDefaultWidth(width) + charts.SetDefaultWidth(height) chartOptions := []charts.ChartOption{ // 普通折线图 { diff --git a/font.go b/font.go new file mode 100644 index 0000000..c40b51e --- /dev/null +++ b/font.go @@ -0,0 +1,61 @@ +// MIT License + +// Copyright (c) 2022 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 ( + "errors" + "sync" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2/roboto" +) + +var fonts = sync.Map{} +var ErrFontNotExists = errors.New("font is not exists") + +func init() { + _ = InstallFont("roboto", roboto.Roboto) +} + +// InstallFont installs the font for charts +func InstallFont(fontFamily string, data []byte) error { + font, err := truetype.Parse(data) + if err != nil { + return err + } + fonts.Store(fontFamily, font) + return nil +} + +// GetFont get the font by font family +func GetFont(fontFamily string) (*truetype.Font, error) { + value, ok := fonts.Load(fontFamily) + if !ok { + return nil, ErrFontNotExists + } + f, ok := value.(*truetype.Font) + if !ok { + return nil, ErrFontNotExists + } + return f, nil +} diff --git a/font_test.go b/font_test.go new file mode 100644 index 0000000..9dc731c --- /dev/null +++ b/font_test.go @@ -0,0 +1,42 @@ +// MIT License + +// Copyright (c) 2022 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2/roboto" +) + +func TestInstallFont(t *testing.T) { + assert := assert.New(t) + + fontFamily := "test" + err := InstallFont(fontFamily, roboto.Roboto) + assert.Nil(err) + + font, err := GetFont(fontFamily) + assert.Nil(err) + assert.NotNil(font) +} diff --git a/legend.go b/legend.go index f11b50f..7eb33b3 100644 --- a/legend.go +++ b/legend.go @@ -50,6 +50,7 @@ type LegendOption struct { Orient string } +// NewLegendOption creates a new legend option by legend text list func NewLegendOption(data []string, position ...string) LegendOption { opt := LegendOption{ Data: data, @@ -101,6 +102,8 @@ func (l *legend) Render() (chart.Box, error) { legendDotHeight := 5 textPadding := 5 legendMargin := 10 + // 往下移2倍dot的高度 + y += 2 * legendDotHeight widthCount := 0 maxTextWidth := 0 diff --git a/legend_test.go b/legend_test.go index 0c3a0c9..2dc286f 100644 --- a/legend_test.go +++ b/legend_test.go @@ -23,6 +23,7 @@ package charts import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -81,7 +82,7 @@ func TestLegendRender(t *testing.T) { Style: style, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 214, Bottom: 25, @@ -102,7 +103,7 @@ func TestLegendRender(t *testing.T) { Style: style, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 400, Bottom: 25, @@ -122,7 +123,7 @@ func TestLegendRender(t *testing.T) { Style: style, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 307, Bottom: 25, @@ -143,10 +144,10 @@ func TestLegendRender(t *testing.T) { Orient: OrientVertical, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 61, - Bottom: 70, + Bottom: 80, }, }, { @@ -166,9 +167,9 @@ func TestLegendRender(t *testing.T) { }, box: chart.Box{ Right: 101, - Bottom: 70, + Bottom: 80, }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", }, } @@ -178,6 +179,7 @@ func TestLegendRender(t *testing.T) { assert.Nil(err) assert.Equal(tt.box, b) data, err := d.Bytes() + fmt.Println(string(data)) assert.Nil(err) assert.NotEmpty(data) assert.Equal(tt.result, string(data)) diff --git a/series.go b/series.go index f986c54..e5d9bd8 100644 --- a/series.go +++ b/series.go @@ -32,7 +32,9 @@ import ( ) type SeriesData struct { + // The value of series data Value float64 + // The style of series data Style chart.Style } @@ -57,9 +59,15 @@ func NewSeriesDataFromValues(values []float64) []SeriesData { } type SeriesLabel struct { + // Data label formatter, which supports string template. + // {b}: the name of a data item. + // {c}: the value of a data item. + // {d}: the percent of a data item(pie chart). Formatter string - Color drawing.Color - Show bool + // The color for label + Color drawing.Color + // Show flag for label + Show bool } const ( @@ -69,27 +77,42 @@ const ( ) type SeriesMarkData struct { + // The mark data type, it can be "max", "min", "average". + // The "average" is only for mark line Type string } type SeriesMarkPoint struct { + // The width of symbol, default value is 30 SymbolSize int - Data []SeriesMarkData + // The mark data of series mark point + Data []SeriesMarkData } type SeriesMarkLine struct { + // The mark data of series mark line Data []SeriesMarkData } type Series struct { - index int - Type string - Data []SeriesData + index int + // The type of series, it can be "line", "bar" or "pie". + // Default value is "line" + Type string + // The data list of series + Data []SeriesData + // The Y axis index, it should be 0 or 1. + // Default value is 1 YAxisIndex int - Style chart.Style - Label SeriesLabel - Name string - // Radius of Pie chart, e.g.: 40% - Radius string + // The style for series + Style chart.Style + // The label for series + Label SeriesLabel + // The name of series + Name string + // Radius for Pie chart, e.g.: 40%, default is "40%" + Radius string + // Mark point for series MarkPoint SeriesMarkPoint - MarkLine SeriesMarkLine + // Make line for series + MarkLine SeriesMarkLine } type SeriesList []Series diff --git a/title.go b/title.go index a5ba6b3..07a2eef 100644 --- a/title.go +++ b/title.go @@ -142,7 +142,8 @@ func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) { for _, item := range measureOptions { item.style.WriteTextOptionsToRenderer(r) x := titleX + (textMaxWidth-item.width)>>1 - d.text(item.text, x, titleY) + y := titleY + item.height + d.text(item.text, x, y) titleY += item.height } height := titleY + padding.Top + padding.Bottom diff --git a/title_test.go b/title_test.go index e5f895c..23573c3 100644 --- a/title_test.go +++ b/title_test.go @@ -79,7 +79,7 @@ func TestDrawTitle(t *testing.T) { { newDraw: newDraw, newOption: newOption, - result: "\\ntitleHellosubtitleWorld!", + result: "\\ntitleHellosubtitleWorld!", box: chart.Box{ Right: 43, Bottom: 58, @@ -93,7 +93,7 @@ func TestDrawTitle(t *testing.T) { opt.Top = "50" return opt }, - result: "\\ntitleHellosubtitleWorld!", + result: "\\ntitleHellosubtitleWorld!", box: chart.Box{ Right: 400, Bottom: 108, @@ -107,7 +107,7 @@ func TestDrawTitle(t *testing.T) { opt.Top = "10" return opt }, - result: "\\ntitleHellosubtitleWorld!", + result: "\\ntitleHellosubtitleWorld!", box: chart.Box{ Right: 222, Bottom: 68, @@ -121,7 +121,7 @@ func TestDrawTitle(t *testing.T) { opt.Top = "10" return opt }, - result: "\\ntitleHellosubtitleWorld!", + result: "\\ntitleHellosubtitleWorld!", box: chart.Box{ Right: 83, Bottom: 68,