diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce56fe7..22e77a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,12 +14,12 @@ jobs: strategy: matrix: go: - - '1.22' - - '1.21' - - '1.20' - - '1.19' - '1.18' - '1.17' + - '1.16' + - '1.15' + - '1.14' + - '1.13' steps: - name: Go ${{ matrix.go }} test diff --git a/.gitignore b/.gitignore index 57206ee..2e33342 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,3 @@ *.png *.svg tmp -NotoSansSC.ttf -.vscode \ No newline at end of file diff --git a/README.md b/README.md index 0650395..8183871 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ # go-charts -Clone from https://github.com/vicanso/go-charts - [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) [![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) [中文](./README_zh.md) -`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. The default format is `png` and the default theme is `light`. +`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. `Apache ECharts` is popular among Front-end developers, so `go-charts` supports the option of `Apache ECharts`. Developers can generate charts almost the same as `Apache ECharts`. @@ -17,13 +15,9 @@ Screenshot of common charts, the left part is light theme, the right part is gra go-charts

-

- go-table -

- ## Chart Type -These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`. +These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. ## Example @@ -35,7 +29,7 @@ More examples can be found in the [./examples/](./examples/) directory. package main import ( - charts "git.smarteching.com/zeni/go-charts/v2" + charts "github.com/vicanso/go-charts/v2" ) func main() { @@ -101,7 +95,7 @@ func main() { package main import ( - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func main() { @@ -176,7 +170,7 @@ func main() { package main import ( - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func main() { @@ -233,7 +227,7 @@ func main() { package main import ( - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func main() { @@ -288,7 +282,7 @@ func main() { package main import ( - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func main() { @@ -346,7 +340,7 @@ func main() { package main import ( - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func main() { @@ -380,78 +374,13 @@ func main() { } ``` -### Table - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - header := []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - } - data := [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - } - spans := map[int]int{ - 0: 2, - 1: 1, - // 设置第三列的span - 2: 3, - 3: 2, - 4: 2, - } - p, err := charts.TableRender( - header, - data, - spans, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - ### ECharts Render ```go package main import ( - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index 3f35b97..fed2d61 100644 --- a/README_zh.md +++ b/README_zh.md @@ -3,7 +3,7 @@ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) [![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) -`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。 +`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。 `Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana): @@ -12,13 +12,9 @@ go-charts

-

- go-table -

\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 右侧 { @@ -135,7 +135,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 顶部 { diff --git a/bar_chart.go b/bar_chart.go index 043e044..0ac9f47 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,10 +23,8 @@ package charts import ( - "math" - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart/v2" ) type barChart struct { @@ -34,7 +32,6 @@ type barChart struct { opt *BarChartOption } -// NewBarChart returns a bar chart renderer func NewBarChart(p *Painter, opt BarChartOption) *barChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -46,7 +43,6 @@ func NewBarChart(p *Painter, opt BarChartOption) *barChart { } type BarChartOption struct { - // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -61,10 +57,14 @@ type BarChartOption struct { // The option of title Title TitleOption // The legend option - Legend LegendOption - BarWidth int - // Margin of bar - BarMargin int + Legend LegendOption +} + +type barChartLabelRenderOption struct { + Text string + Style Style + X int + Y int } func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -73,7 +73,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B seriesPainter := result.seriesPainter xRange := NewRange(AxisRangeOption{ - Painter: b.p, DivideCount: len(opt.XAxis.Data), Size: seriesPainter.Width(), }) @@ -90,17 +89,9 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B margin = 5 barMargin = 3 } - if opt.BarMargin > 0 { - barMargin = opt.BarMargin - } seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin - barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount - if opt.BarWidth > 0 && opt.BarWidth < barWidth { - barWidth = opt.BarWidth - // 重新计算margin - margin = (width - seriesCount*barWidth - barMargin*(seriesCount-1)) / 2 - } + barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) barMaxHeight := seriesPainter.Height() theme := opt.Theme seriesNames := seriesList.Names() @@ -111,6 +102,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter, markLinePainter, } + labelRenderOptions := make([]barChartLabelRenderOption, 0) for index := range seriesList { series := seriesList[index] yRange := result.axisRanges[series.AxisIndex] @@ -118,18 +110,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B divideValues := xRange.AutoDivide() points := make([]Point, len(series.Data)) - var labelPainter *SeriesLabelPainter - if series.Label.Show { - labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ - P: seriesPainter, - SeriesNames: seriesNames, - Label: series.Label, - Theme: opt.Theme, - Font: opt.Font, - }) - rendererList = append(rendererList, labelPainter) - } - for j, item := range series.Data { if j >= xRange.divideCount { continue @@ -147,25 +127,14 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } top := barMaxHeight - h - if series.RoundRadius <= 0 { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }) - } else { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).RoundedRect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }, series.RoundRadius) - } + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: top, + Left: x, + Right: x + barWidth, + Bottom: barMaxHeight - 1, + }) // 用于生成marker point points[j] = Point{ // 居中的位置 @@ -179,33 +148,30 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Y: top, } // 如果label不需要展示,则返回 - if labelPainter == nil { + if !series.Label.Show { continue } - y := barMaxHeight - h - radians := float64(0) - fontColor := series.Label.Color - if series.Label.Position == PositionBottom { - y = barMaxHeight - radians = -math.Pi / 2 - if fontColor.IsZero() { - if isLightColor(fillColor) { - fontColor = defaultLightFontColor - } else { - fontColor = defaultDarkFontColor - } - } + distance := series.Label.Distance + if distance == 0 { + distance = 5 } - labelPainter.Add(LabelValue{ - Index: index, - Value: item.Value, - X: x + barWidth>>1, - Y: y, - // 旋转 - Radians: radians, - FontColor: fontColor, - Offset: series.Label.Offset, - FontSize: series.Label.FontSize, + text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1) + labelStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + labelStyle.FontColor = series.Label.Color + } + + textBox := seriesPainter.MeasureText(text) + + labelRenderOptions = append(labelRenderOptions, barChartLabelRenderOption{ + Text: text, + Style: labelStyle, + X: x + (barWidth-textBox.Width())>>1, + Y: barMaxHeight - h - distance, }) } @@ -224,6 +190,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Range: yRange, }) } + for _, labelOpt := range labelRenderOptions { + seriesPainter.OverrideTextStyle(labelOpt.Style) + seriesPainter.Text(labelOpt.Text, labelOpt.X, labelOpt.Y) + } // 最大、最小的mark point err := doRender(rendererList...) if err != nil { diff --git a/bar_chart_test.go b/bar_chart_test.go index 654c320..138b3ca 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,77 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", - }, - { - render: func(p *Painter) ([]byte, error) { - seriesList := NewSeriesListDataFromValues([][]float64{ - { - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }, - { - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }, - }) - for index := range seriesList { - seriesList[index].Label.Show = true - seriesList[index].RoundRadius = 5 - } - _, err := NewBarChart(p, BarChartOption{ - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - SeriesList: seriesList, - XAxis: NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - YAxisOptions: NewYAxisOptions([]string{ - "Rainfall", - "Evaporation", - }), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option.go b/chart_option.go index d80a383..f2f3b51 100644 --- a/chart_option.go +++ b/chart_option.go @@ -26,6 +26,7 @@ import ( "sort" "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" ) type ChartOption struct { @@ -61,24 +62,8 @@ type ChartOption struct { RadarIndicators []RadarIndicator // The background color of chart BackgroundColor Color - // The flag for show symbol of line, set this to *false will hide symbol - SymbolShow *bool - // The stroke width of line chart - LineStrokeWidth float64 - // The bar with of bar chart - BarWidth int - // The margin of each bar - BarMargin int - // The bar height of horizontal bar chart - BarHeight int - // Fill the area of line chart - FillArea bool - // background fill (alpha) opacity - Opacity uint8 // The child charts Children []ChartOption - // The value formatter - ValueFormatter ValueFormatter } // OptionFunc option function @@ -123,12 +108,9 @@ func TitleOptionFunc(title TitleOption) OptionFunc { } // TitleTextOptionFunc set title text of chart -func TitleTextOptionFunc(text string, subtext ...string) OptionFunc { +func TitleTextOptionFunc(text string) OptionFunc { return func(opt *ChartOption) { opt.Title.Text = text - if len(subtext) != 0 { - opt.Title.Subtext = subtext[0] - } } } @@ -273,10 +255,7 @@ func (o *ChartOption) fillDefault() { o.font, _ = GetFont(o.FontFamily) if o.font == nil { - o.font, _ = GetDefaultFont() - } else { - // 如果指定了字体,则设置主题的字体 - t.SetFont(o.font) + o.font, _ = chart.GetDefaultFont() } if o.BackgroundColor.IsZero() { o.BackgroundColor = t.GetBackgroundColor() @@ -357,70 +336,3 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { SeriesList: seriesList, }, opts...) } - -// TableRender table chart render -func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) { - opt := TableChartOption{ - Header: header, - Data: data, - } - if len(spanMaps) != 0 { - spanMap := spanMaps[0] - spans := make([]int, len(opt.Header)) - for index := range spans { - v, ok := spanMap[index] - if !ok { - v = 1 - } - spans[index] = v - } - opt.Spans = spans - } - return TableOptionRender(opt) -} - -// TableOptionRender table render with option -func TableOptionRender(opt TableChartOption) (*Painter, error) { - if opt.Type == "" { - opt.Type = ChartOutputPNG - } - if opt.Width <= 0 { - opt.Width = defaultChartWidth - } - if opt.FontFamily != "" { - opt.Font, _ = GetFont(opt.FontFamily) - } - if opt.Font == nil { - opt.Font, _ = GetDefaultFont() - } - - p, err := NewPainter(PainterOptions{ - Type: opt.Type, - Width: opt.Width, - // 仅用于计算表格高度,因此随便设置即可 - Height: 100, - Font: opt.Font, - }) - if err != nil { - return nil, err - } - info, err := NewTableChart(p, opt).render() - if err != nil { - return nil, err - } - - p, err = NewPainter(PainterOptions{ - Type: opt.Type, - Width: info.Width, - Height: info.Height, - Font: opt.Font, - }) - if err != nil { - return nil, err - } - _, err = NewTableChart(p, opt).renderWithInfo(info) - if err != nil { - return nil, err - } - return p, nil -} diff --git a/chart_option_test.go b/chart_option_test.go index c354b26..c77bb4f 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestChartOption(t *testing.T) { @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { @@ -419,7 +419,7 @@ func TestRadarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) + assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) } func TestFunnelRender(t *testing.T) { @@ -447,5 +447,5 @@ func TestFunnelRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) + assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) } diff --git a/charts.go b/charts.go index 31df11c..cd1ac2b 100644 --- a/charts.go +++ b/charts.go @@ -24,46 +24,27 @@ package charts import ( "errors" - "math" "sort" - - "git.smarteching.com/zeni/go-chart/v2" ) const labelFontSize = 10 -const smallLabelFontSize = 8 const defaultDotWidth = 2.0 const defaultStrokeWidth = 2.0 var defaultChartWidth = 600 var defaultChartHeight = 400 -// SetDefaultWidth sets default width of chart func SetDefaultWidth(width int) { if width > 0 { defaultChartWidth = width } } - -// SetDefaultHeight sets default height of chart func SetDefaultHeight(height int) { if height > 0 { defaultChartHeight = height } } -var nullValue = math.MaxFloat64 - -// SetNullValue sets the null value, default is MaxFloat64 -func SetNullValue(v float64) { - nullValue = v -} - -// GetNullValue gets the null value -func GetNullValue() float64 { - return nullValue -} - type Renderer interface { Render() (Box, error) } @@ -125,16 +106,14 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e p = p.Child(PainterPaddingOption(opt.Padding)) } - legendHeight := 0 if len(opt.LegendOption.Data) != 0 { if opt.LegendOption.Theme == nil { opt.LegendOption.Theme = opt.Theme } - legendResult, err := NewLegendPainter(p, opt.LegendOption).Render() + _, err := NewLegendPainter(p, opt.LegendOption).Render() if err != nil { return nil, err } - legendHeight = legendResult.Height() } // 如果有标题 @@ -148,15 +127,9 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if err != nil { return nil, err } - - top := chart.MaxInt(legendHeight, titleBox.Height()) - // 如果是垂直方式,则不计算legend高度 - if opt.LegendOption.Orient == OrientVertical { - top = titleBox.Height() - } p = p.Child(PainterPaddingOption(Box{ // 标题下留白 - Top: top + 20, + Top: titleBox.Height() + 20, })) } @@ -186,26 +159,21 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if len(opt.YAxisOptions) > index { yAxisOption = opt.YAxisOptions[index] } - divideCount := yAxisOption.DivideCount - if divideCount <= 0 { - divideCount = defaultAxisDivideCount - } max, min := opt.SeriesList.GetMaxMin(index) + if yAxisOption.Min != nil { + min = *yAxisOption.Min + } + if yAxisOption.Max != nil { + max = *yAxisOption.Max + } r := NewRange(AxisRangeOption{ - Painter: p, - Min: min, - Max: max, + Min: min, + Max: max, // 高度需要减去x轴的高度 Size: rangeHeight, // 分隔数量 - DivideCount: divideCount, + DivideCount: defaultAxisDivideCount, }) - if yAxisOption.Min != nil && *yAxisOption.Min <= min { - r.min = *yAxisOption.Min - } - if yAxisOption.Max != nil && *yAxisOption.Max >= max { - r.max = *yAxisOption.Max - } result.axisRanges[index] = r if yAxisOption.Theme == nil { @@ -215,16 +183,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e yAxisOption.Data = r.Values() } else { yAxisOption.isCategoryAxis = true - // 由于x轴为value部分,因此计算其label单独处理 - opt.XAxis.Data = NewRange(AxisRangeOption{ - Painter: p, - Min: min, - Max: max, - // 高度需要减去x轴的高度 - Size: rangeHeight, - // 分隔数量 - DivideCount: defaultAxisDivideCount, - }).Values() + opt.XAxis.Data = r.Values() opt.XAxis.isValueAxis = true } reverseStringSlice(yAxisOption.Data) @@ -301,9 +260,6 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { opt.Parent = p } p := opt.Parent - if opt.ValueFormatter != nil { - p.valueFormatter = opt.ValueFormatter - } if !opt.Box.IsZero() { p = p.Child(PainterBoxOption(opt.Box)) } @@ -346,8 +302,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { TitleOption: opt.Title, LegendOption: opt.Legend, axisReversed: axisReversed, - // 前置已设置背景色 - backgroundIsFilled: true, + } + if isChild { + renderOpt.backgroundIsFilled = true } if len(pieSeriesList) != 0 || len(radarSeriesList) != 0 || @@ -359,10 +316,6 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { }, } } - if len(horizontalBarSeriesList) != 0 { - renderOpt.YAxisOptions[0].DivideCount = len(renderOpt.YAxisOptions[0].Data) - renderOpt.YAxisOptions[0].Unit = 1 - } renderResult, err := defaultRender(p, renderOpt) if err != nil { @@ -375,11 +328,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(barSeriesList) != 0 { handler.Add(func() error { _, err := NewBarChart(p, BarChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - BarWidth: opt.BarWidth, - BarMargin: opt.BarMargin, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, }).render(renderResult, barSeriesList) return err }) @@ -391,8 +342,6 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ Theme: opt.theme, Font: opt.font, - BarHeight: opt.BarHeight, - BarMargin: opt.BarMargin, YAxisOptions: opt.YAxisOptions, }).render(renderResult, horizontalBarSeriesList) return err @@ -414,13 +363,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(lineSeriesList) != 0 { handler.Add(func() error { _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - SymbolShow: opt.SymbolShow, - StrokeWidth: opt.LineStrokeWidth, - FillArea: opt.FillArea, - Opacity: opt.Opacity, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, }).render(renderResult, lineSeriesList) return err }) diff --git a/charts_test.go b/charts_test.go deleted file mode 100644 index bd581e9..0000000 --- a/charts_test.go +++ /dev/null @@ -1,255 +0,0 @@ -// 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" - "testing" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func BenchmarkMultiChartPNGRender(b *testing.B) { - for i := 0; i < b.N; i++ { - opt := ChartOption{ - Type: ChartOutputPNG, - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisOptions: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - } - d, err := Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - if len(buf) == 0 { - panic(errors.New("data is nil")) - } - } -} - -func BenchmarkMultiChartSVGRender(b *testing.B) { - for i := 0; i < b.N; i++ { - opt := ChartOption{ - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisOptions: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - } - d, err := Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - if len(buf) == 0 { - panic(errors.New("data is nil")) - } - } -} diff --git a/echarts.go b/echarts.go index aaef1f1..fbe9a36 100644 --- a/echarts.go +++ b/echarts.go @@ -29,7 +29,7 @@ import ( "regexp" "strconv" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart/v2" ) func convertToArray(data []byte) []byte { @@ -344,11 +344,6 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { Data: NewSeriesDataFromValues(dataItem.Value.values), Max: item.Max, Min: item.Min, - Label: SeriesLabel{ - Color: parseColor(item.Label.Color), - Show: item.Label.Show, - Distance: item.Label.Distance, - }, }) } continue diff --git a/echarts_test.go b/echarts_test.go index 2077278..9c31286 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -27,7 +27,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestConvertToArray(t *testing.T) { @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go deleted file mode 100644 index 57ca1e9..0000000 --- a/examples/area_line_chart/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "area-line-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - } - p, err := charts.LineRender( - values, - charts.TitleTextOptionFunc("Line"), - charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - charts.LegendLabelsOptionFunc([]string{ - "Email", - }, "50"), - func(opt *charts.ChartOption) { - opt.Legend.Padding = charts.Box{ - Top: 5, - Bottom: 10, - } - opt.FillArea = true - }, - ) - - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index 91c9f81..c559a76 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -1,10 +1,11 @@ package main import ( + "io/ioutil" "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -15,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "bar-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/charts/main.go b/examples/charts/main.go index 81bc4f2..0e1d48e 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -2,11 +2,10 @@ package main import ( "bytes" - "fmt" "net/http" "strconv" - charts "git.smarteching.com/zeni/go-charts/v2" + charts "github.com/vicanso/go-charts/v2" ) var html = ` @@ -93,48 +92,6 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha bytesList = append(bytesList, buf) } - p, err := charts.TableOptionRender(charts.TableChartOption{ - Type: charts.ChartOutputSVG, - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - }, - }) - if err != nil { - panic(err) - } - buf, err := p.Bytes() - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) w.Header().Set("Content-Type", "text/html") w.Write(data) @@ -262,35 +219,6 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, - { - Title: charts.TitleOption{ - Text: "Line Area", - }, - Legend: charts.NewLegendOption([]string{ - "Email", - }), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }), - }, - FillArea: true, - }, // 柱状图 { Title: charts.TitleOption{ @@ -355,10 +283,6 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { Value: 180, }, }, - Label: charts.SeriesLabel{ - Show: true, - Position: charts.PositionBottom, - }, }, }, }, @@ -1969,6 +1893,5 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { func main() { http.HandleFunc("/", indexHandler) http.HandleFunc("/echarts", echartsHandler) - fmt.Println("http://127.0.0.1:3012/") http.ListenAndServe(":3012", nil) } diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 601f54e..bb7cc00 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "chinese-line-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } @@ -25,8 +25,7 @@ func writeFile(buf []byte) error { func main() { // 字体文件需要自行下载 - // https://github.com/googlefonts/noto-cjk - buf, err := ioutil.ReadFile("./NotoSansSC.ttf") + buf, err := ioutil.ReadFile("../NotoSansSC.ttf") if err != nil { panic(err) } @@ -34,8 +33,6 @@ func main() { if err != nil { panic(err) } - font, _ := charts.GetFont("noto") - charts.SetDefaultFont(font) values := [][]float64{ { @@ -86,7 +83,8 @@ func main() { } p, err := charts.LineRender( values, - charts.TitleTextOptionFunc("测试"), + charts.TitleTextOptionFunc("Line"), + charts.FontFamilyOptionFunc("noto"), charts.XAxisDataOptionFunc([]string{ "星期一", "星期二", diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 653f834..8f21db6 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -1,10 +1,11 @@ package main import ( + "io/ioutil" "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -15,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "funnel-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } @@ -29,8 +30,6 @@ func main() { 60, 40, 20, - 10, - 0, } p, err := charts.FunnelRender( values, @@ -41,8 +40,6 @@ func main() { "Visit", "Inquiry", "Order", - "Pay", - "Cancel", }), ) if err != nil { diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index f5d8497..8b996b6 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -1,10 +1,11 @@ package main import ( + "io/ioutil" "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -15,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "horizontal-bar-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } @@ -25,22 +26,20 @@ func writeFile(buf []byte) error { func main() { values := [][]float64{ { - 10, - 30, - 50, - 70, - 90, - 110, - 130, + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, }, { - 20, - 40, - 60, - 80, - 100, - 120, - 140, + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, }, } p, err := charts.HorizontalBarRender( @@ -57,7 +56,6 @@ func main() { "2012", }), charts.YAxisDataOptionFunc([]string{ - "UN", "Brazil", "Indonesia", "USA", @@ -65,9 +63,6 @@ func main() { "China", "World", }), - func(opt *charts.ChartOption) { - opt.SeriesList[0].RoundRadius = 5 - }, ) if err != nil { panic(err) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index baee8a3..45ff894 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -1,11 +1,11 @@ package main import ( - "fmt" + "io/ioutil" "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "line-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } @@ -29,8 +29,7 @@ func main() { 120, 132, 101, - // 134, - charts.GetNullValue(), + 134, 90, 230, 210, @@ -90,23 +89,7 @@ func main() { "Video Ads", "Direct", "Search Engine", - }, "50"), - func(opt *charts.ChartOption) { - opt.Legend.Padding = charts.Box{ - Top: 5, - Bottom: 10, - } - opt.YAxisOptions = []charts.YAxisOption{ - { - SplitLineShow: charts.FalseFlag(), - }, - } - opt.SymbolShow = charts.FalseFlag() - opt.LineStrokeWidth = 1 - opt.ValueFormatter = func(f float64) string { - return fmt.Sprintf("%.0f", f) - } - }, + }, charts.PositionCenter), ) if err != nil { diff --git a/examples/painter/main.go b/examples/painter/main.go index 1b842b3..3c31ce4 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -1,11 +1,12 @@ package main import ( + "io/ioutil" "os" "path/filepath" - charts "git.smarteching.com/zeni/go-charts/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" + charts "github.com/vicanso/go-charts/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) func writeFile(buf []byte) error { @@ -16,7 +17,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "painter.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 5d70438..3721ed1 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -1,10 +1,11 @@ package main import ( + "io/ioutil" "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -15,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "pie-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index e7053af..51f7409 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -1,10 +1,11 @@ package main import ( + "io/ioutil" "os" "path/filepath" - "git.smarteching.com/zeni/go-charts/v2" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { @@ -15,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "radar-chart.png") - err = os.WriteFile(file, buf, 0600) + err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/table/main.go b/examples/table/main.go deleted file mode 100644 index de994eb..0000000 --- a/examples/table/main.go +++ /dev/null @@ -1,178 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "strconv" - "strings" - - "git.smarteching.com/zeni/go-charts/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func writeFile(buf []byte, filename string) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, filename) - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - // charts.SetDefaultTableSetting(charts.TableDarkThemeSetting) - charts.SetDefaultWidth(810) - header := []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - } - data := [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - } - spans := map[int]int{ - 0: 2, - 1: 1, - // 设置第三列的span - 2: 3, - 3: 2, - 4: 2, - } - p, err := charts.TableRender( - header, - data, - spans, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf, "table.png") - if err != nil { - panic(err) - } - - bgColor := charts.Color{ - R: 16, - G: 22, - B: 30, - A: 255, - } - p, err = charts.TableOptionRender(charts.TableChartOption{ - Header: []string{ - "Name", - "Price", - "Change", - }, - BackgroundColor: bgColor, - HeaderBackgroundColor: bgColor, - RowBackgroundColors: []charts.Color{ - bgColor, - }, - HeaderFontColor: drawing.ColorWhite, - FontColor: drawing.ColorWhite, - Padding: charts.Box{ - Top: 15, - Right: 10, - Bottom: 15, - Left: 10, - }, - Data: [][]string{ - { - "Datadog Inc", - "97.32", - "-7.49%", - }, - { - "Hashicorp Inc", - "28.66", - "-9.25%", - }, - { - "Gitlab Inc", - "51.63", - "+4.32%", - }, - }, - TextAligns: []string{ - "", - charts.AlignRight, - charts.AlignRight, - }, - CellStyle: func(tc charts.TableCell) *charts.Style { - column := tc.Column - if column != 2 { - return nil - } - value, _ := strconv.ParseFloat(strings.Replace(tc.Text, "%", "", 1), 64) - if value == 0 { - return nil - } - style := charts.Style{ - Padding: charts.Box{ - Bottom: 5, - }, - } - if value > 0 { - style.FillColor = charts.Color{ - R: 179, - G: 53, - B: 20, - A: 255, - } - } else if value < 0 { - style.FillColor = charts.Color{ - R: 33, - G: 124, - B: 50, - A: 255, - } - } - return &style - }, - }) - if err != nil { - panic(err) - } - - buf, err = p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf, "table-color.png") - if err != nil { - panic(err) - } -} diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go deleted file mode 100644 index c6c93bf..0000000 --- a/examples/time_line_chart/main.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "crypto/rand" - "fmt" - "math/big" - "os" - "path/filepath" - "time" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "time-line-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - xAxisValue := []string{} - values := []float64{} - now := time.Now() - firstAxis := 0 - for i := 0; i < 300; i++ { - // 设置首个axis为xx:00的时间点 - if firstAxis == 0 && now.Minute() == 0 { - firstAxis = i - } - xAxisValue = append(xAxisValue, now.Format("15:04")) - now = now.Add(time.Minute) - value, _ := rand.Int(rand.Reader, big.NewInt(100)) - values = append(values, float64(value.Int64())) - } - p, err := charts.LineRender( - [][]float64{ - values, - }, - charts.TitleTextOptionFunc("Line"), - charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()), - charts.LegendLabelsOptionFunc([]string{ - "Demo", - }, "50"), - func(opt *charts.ChartOption) { - opt.XAxis.FirstAxis = firstAxis - // 必须要比计算得来的最小值更大(每60分钟) - opt.XAxis.SplitNumber = 60 - opt.Legend.Padding = charts.Box{ - Top: 5, - Bottom: 10, - } - opt.SymbolShow = charts.FalseFlag() - opt.LineStrokeWidth = 1 - opt.ValueFormatter = func(f float64) string { - return fmt.Sprintf("%.0f", f) - } - }, - ) - - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/font.go b/font.go index 828654e..c40b51e 100644 --- a/font.go +++ b/font.go @@ -27,18 +27,14 @@ import ( "sync" "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2/roboto" + "github.com/wcharczuk/go-chart/v2/roboto" ) var fonts = sync.Map{} var ErrFontNotExists = errors.New("font is not exists") -var defaultFontFamily = "defaultFontFamily" func init() { - name := "roboto" - _ = InstallFont(name, roboto.Roboto) - font, _ := GetFont(name) - SetDefaultFont(font) + _ = InstallFont("roboto", roboto.Roboto) } // InstallFont installs the font for charts @@ -51,19 +47,6 @@ func InstallFont(fontFamily string, data []byte) error { return nil } -// GetDefaultFont get default font -func GetDefaultFont() (*truetype.Font, error) { - return GetFont(defaultFontFamily) -} - -// SetDefaultFont set default font -func SetDefaultFont(font *truetype.Font) { - if font == nil { - return - } - fonts.Store(defaultFontFamily, font) -} - // GetFont get the font by font family func GetFont(fontFamily string) (*truetype.Font, error) { value, ok := fonts.Load(fontFamily) diff --git a/font_test.go b/font_test.go index e0c56b2..9dc731c 100644 --- a/font_test.go +++ b/font_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/roboto" + "github.com/wcharczuk/go-chart/v2/roboto" ) func TestInstallFont(t *testing.T) { diff --git a/funnel_chart.go b/funnel_chart.go index d4a8bdd..7c04bfe 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -23,6 +23,9 @@ package charts import ( + "fmt" + + "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" ) @@ -31,7 +34,6 @@ type funnelChart struct { opt *FunnelChartOption } -// NewFunnelSeriesList returns a series list for funnel func NewFunnelSeriesList(values []float64) SeriesList { seriesList := make(SeriesList, len(values)) for index, value := range values { @@ -42,7 +44,6 @@ func NewFunnelSeriesList(values []float64) SeriesList { return seriesList } -// NewFunnelChart returns a funnel chart renderer func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -54,7 +55,6 @@ func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { } type FunnelChartOption struct { - // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -92,23 +92,13 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) y := 0 widthList := make([]int, len(seriesList)) textList := make([]string, len(seriesList)) - seriesNames := seriesList.Names() - offset := max - min for index, item := range seriesList { value := item.Data[0].Value - // 最大最小值一致则为100% - widthPercent := 100.0 - if offset != 0 { - widthPercent = (value - min) / offset - } + widthPercent := (value - min) / (max - min) w := int(widthPercent * float64(width)) widthList[index] = w - // 如果最大值为0,则占比100% - percent := 1.0 - if max != 0 { - percent = value / max - } - textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) + p := humanize.CommafWithDigits(value/max*100, 2) + "%" + textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) } for index, w := range widthList { diff --git a/go.mod b/go.mod index 76a47b6..66145c7 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ -module git.smarteching.com/zeni/go-charts/v2 +module github.com/vicanso/go-charts/v2 -go 1.24.1 +go 1.17 require ( - git.smarteching.com/zeni/go-chart/v2 v2.1.4 - github.com/dustin/go-humanize v1.0.1 + github.com/dustin/go-humanize v1.0.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.7.2 + github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.21.0 // indirect + golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3e1a48a..5f953b0 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,23 @@ -git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= -git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= +golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/grid.go b/grid.go index 0ebd226..252fe2e 100644 --- a/grid.go +++ b/grid.go @@ -28,27 +28,16 @@ type gridPainter struct { } type GridPainterOption struct { - // The stroke width - StrokeWidth float64 - // The stroke color - StrokeColor Color - // The spans of column - ColumnSpans []int - // The column of grid - Column int - // The row of grid - Row int - // Ignore first row - IgnoreFirstRow bool - // Ignore last row - IgnoreLastRow bool - // Ignore first column + StrokeWidth float64 + StrokeColor Color + Column int + Row int + IgnoreFirstRow bool + IgnoreLastRow bool IgnoreFirstColumn bool - // Ignore last column - IgnoreLastColumn bool + IgnoreLastColumn bool } -// NewGridPainter returns new a grid renderer func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter { return &gridPainter{ p: p, @@ -83,7 +72,6 @@ func (g *gridPainter) Render() (Box, error) { }) g.p.Grid(GridOption{ Column: opt.Column, - ColumnSpans: opt.ColumnSpans, Row: opt.Row, IgnoreColumnLines: ignoreColumnLines, IgnoreRowLines: ignoreRowLines, diff --git a/grid_test.go b/grid_test.go index fa9c3a6..f6880dc 100644 --- a/grid_test.go +++ b/grid_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestGrid(t *testing.T) { @@ -54,24 +54,6 @@ func TestGrid(t *testing.T) { }, result: "\\n", }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewGridPainter(p, GridPainterOption{ - StrokeColor: drawing.ColorBlack, - ColumnSpans: []int{ - 2, - 5, - 3, - }, - Row: 6, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n", - }, } for _, tt := range tests { p, err := NewPainter(PainterOptions{ diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index ed091c9..fb23734 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart/v2" ) type horizontalBarChart struct { @@ -33,7 +33,6 @@ type horizontalBarChart struct { } type HorizontalBarChartOption struct { - // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -48,13 +47,9 @@ type HorizontalBarChartOption struct { // The option of title Title TitleOption // The legend option - Legend LegendOption - BarHeight int - // Margin of bar - BarMargin int + Legend LegendOption } -// NewHorizontalBarChart returns a horizontal bar chart renderer func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -83,46 +78,24 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri margin = 5 barMargin = 3 } - if opt.BarMargin > 0 { - barMargin = opt.BarMargin - } seriesCount := len(seriesList) // 总的高度-两个margin-(总数-1)的barMargin - barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount - if opt.BarHeight > 0 && opt.BarHeight < barHeight { - barHeight = opt.BarHeight - margin = (height - seriesCount*barHeight - barMargin*(seriesCount-1)) / 2 - } + barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) theme := opt.Theme max, min := seriesList.GetMaxMin(0) xRange := NewRange(AxisRangeOption{ - Painter: p, Min: min, Max: max, DivideCount: defaultAxisDivideCount, Size: seriesPainter.Width(), }) - seriesNames := seriesList.Names() - rendererList := []Renderer{} for index := range seriesList { series := seriesList[index] seriesColor := theme.GetSeriesColor(series.index) divideValues := yRange.AutoDivide() - - var labelPainter *SeriesLabelPainter - if series.Label.Show { - labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ - P: seriesPainter, - SeriesNames: seriesNames, - Label: series.Label, - Theme: opt.Theme, - Font: opt.Font, - }) - rendererList = append(rendererList, labelPainter) - } for j, item := range series.Data { if j >= yRange.divideCount { continue @@ -141,57 +114,16 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri fillColor = item.Style.FillColor } right := w - if series.RoundRadius <= 0 { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }) - } else { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).RoundedRect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }, series.RoundRadius) - } - - // 如果label不需要展示,则返回 - if labelPainter == nil { - continue - } - labelValue := LabelValue{ - Orient: OrientHorizontal, - Index: index, - Value: item.Value, - X: right, - Y: y + barHeight>>1, - Offset: series.Label.Offset, - FontColor: series.Label.Color, - FontSize: series.Label.FontSize, - } - if series.Label.Position == PositionLeft { - labelValue.X = 0 - if labelValue.FontColor.IsZero() { - if isLightColor(fillColor) { - labelValue.FontColor = defaultLightFontColor - } else { - labelValue.FontColor = defaultDarkFontColor - } - } - } - labelPainter.Add(labelValue) + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }) } } - err := doRender(rendererList...) - if err != nil { - return BoxZero, err - } return p.box, nil } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index e078c4a..5555df6 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/legend.go b/legend.go index 035642c..65db102 100644 --- a/legend.go +++ b/legend.go @@ -36,7 +36,6 @@ const IconRect = "rect" const IconLineDot = "lineDot" type LegendOption struct { - // The theme Theme ColorPalette // Text array of legend Data []string @@ -59,11 +58,8 @@ type LegendOption struct { FontColor Color // The flag for show legend, set this to *false will hide legend Show *bool - // The padding of legend - Padding Box } -// NewLegendOption returns a legend option func NewLegendOption(labels []string, left ...string) LegendOption { opt := LegendOption{ Data: labels, @@ -74,7 +70,6 @@ func NewLegendOption(labels []string, left ...string) LegendOption { return opt } -// IsEmpty checks legend is empty func (opt *LegendOption) IsEmpty() bool { isEmpty := true for _, v := range opt.Data { @@ -86,7 +81,6 @@ func (opt *LegendOption) IsEmpty() bool { return isEmpty } -// NewLegendPainter returns a legend renderer func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { return &legendPainter{ p: p, @@ -113,11 +107,9 @@ func (l *legendPainter) Render() (Box, error) { if opt.Left == "" { opt.Left = PositionCenter } - padding := opt.Padding - if padding.IsZero() { - padding.Top = 5 - } - p := l.p.Child(PainterPaddingOption(padding)) + p := l.p.Child(PainterPaddingOption(Box{ + Top: 5, + })) p.SetTextStyle(Style{ FontSize: opt.FontSize, FontColor: opt.FontColor, @@ -139,19 +131,13 @@ func (l *legendPainter) Render() (Box, error) { textOffset := 2 legendWidth := 30 legendHeight := 20 - itemMaxHeight := 0 for _, item := range measureList { - if item.Height() > itemMaxHeight { - itemMaxHeight = item.Height() - } if opt.Orient == OrientVertical { height += item.Height() } else { width += item.Width() } } - // 增加padding - itemMaxHeight += 10 if opt.Orient == OrientVertical { width = maxTextWidth + textOffset + legendWidth height = offset * len(opt.Data) @@ -180,13 +166,8 @@ func (l *legendPainter) Render() (Box, error) { } top, _ := strconv.Atoi(opt.Top) - if left < 0 { - left = 0 - } - x := int(left) y := int(top) + 10 - startY := y x0 := x y0 := y @@ -208,22 +189,12 @@ func (l *legendPainter) Render() (Box, error) { } return left + legendWidth } - lastIndex := len(opt.Data) - 1 for index, text := range opt.Data { color := theme.GetSeriesColor(index) p.SetDrawingStyle(Style{ FillColor: color, StrokeColor: color, }) - itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth - if lastIndex == index { - itemWidth = x0 + measureList[index].Width() + legendWidth - } - if itemWidth > p.Width() { - x0 = 0 - y += itemMaxHeight - y0 = y - } if opt.Align != AlignRight { x0 = drawIcon(y0, x0) x0 += textOffset @@ -232,7 +203,7 @@ func (l *legendPainter) Render() (Box, error) { x0 += measureList[index].Width() if opt.Align == AlignRight { x0 += textOffset - x0 = drawIcon(y0, x0) + x0 = drawIcon(0, x0) } if opt.Orient == OrientVertical { y0 += offset @@ -241,11 +212,10 @@ func (l *legendPainter) Render() (Box, error) { x0 += offset y0 = y } - height = y0 - startY + 10 } return Box{ Right: width, - Bottom: height + padding.Bottom + padding.Top, + Bottom: height, }, nil } diff --git a/line_chart.go b/line_chart.go index fb1d16a..f171813 100644 --- a/line_chart.go +++ b/line_chart.go @@ -23,10 +23,8 @@ package charts import ( - "math" - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2/drawing" ) type lineChart struct { @@ -34,7 +32,6 @@ type lineChart struct { opt *LineChartOption } -// NewLineChart returns a line chart render func NewLineChart(p *Painter, opt LineChartOption) *lineChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -46,7 +43,6 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart { } type LineChartOption struct { - // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -62,16 +58,8 @@ type LineChartOption struct { Title TitleOption // The legend option Legend LegendOption - // The flag for show symbol of line, set this to *false will hide symbol - SymbolShow *bool - // The stroke width of line - StrokeWidth float64 - // Fill the area of line - FillArea bool // background is filled backgroundIsFilled bool - // background fill (alpha) opacity - Opacity uint8 } func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -103,82 +91,25 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( markPointPainter, markLinePainter, } - strokeWidth := opt.StrokeWidth - if strokeWidth == 0 { - strokeWidth = defaultStrokeWidth - } - seriesNames := seriesList.Names() for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) drawingStyle := Style{ StrokeColor: seriesColor, - StrokeWidth: strokeWidth, - } - if len(series.Style.StrokeDashArray) > 0 { - drawingStyle.StrokeDashArray = series.Style.StrokeDashArray + StrokeWidth: defaultStrokeWidth, } + seriesPainter.SetDrawingStyle(drawingStyle) yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) - var labelPainter *SeriesLabelPainter - if series.Label.Show { - labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ - P: seriesPainter, - SeriesNames: seriesNames, - Label: series.Label, - Theme: opt.Theme, - Font: opt.Font, - }) - rendererList = append(rendererList, labelPainter) - } for i, item := range series.Data { h := yRange.getRestHeight(item.Value) - if item.Value == nullValue { - h = int(math.MaxInt32) - } p := Point{ X: xValues[i], Y: h, } points = append(points, p) - - // 如果label不需要展示,则返回 - if labelPainter == nil { - continue - } - labelPainter.Add(LabelValue{ - Index: index, - Value: item.Value, - X: p.X, - Y: p.Y, - // 字体大小 - FontSize: series.Label.FontSize, - }) } - // 如果需要填充区域 - if opt.FillArea { - areaPoints := make([]Point, len(points)) - copy(areaPoints, points) - bottomY := yRange.getRestHeight(yRange.min) - var opacity uint8 = 200 - if opt.Opacity != 0 { - opacity = opt.Opacity - } - areaPoints = append(areaPoints, Point{ - X: areaPoints[len(areaPoints)-1].X, - Y: bottomY, - }, Point{ - X: areaPoints[0].X, - Y: bottomY, - }, areaPoints[0]) - seriesPainter.SetDrawingStyle(Style{ - FillColor: seriesColor.WithAlpha(opacity), - }) - seriesPainter.FillArea(areaPoints) - } - seriesPainter.SetDrawingStyle(drawingStyle) - // 画线 seriesPainter.LineStroke(points) @@ -190,9 +121,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } drawingStyle.StrokeWidth = 1 seriesPainter.SetDrawingStyle(drawingStyle) - if !isFalse(opt.SymbolShow) { - seriesPainter.Dots(points) - } + seriesPainter.Dots(points) markPointPainter.Add(markPointRenderOption{ FillColor: seriesColor, Font: opt.Font, diff --git a/line_chart_test.go b/line_chart_test.go index e169f90..856cdf3 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } diff --git a/mark_line.go b/mark_line.go index bc850bb..a0efcfb 100644 --- a/mark_line.go +++ b/mark_line.go @@ -24,9 +24,9 @@ package charts import ( "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" ) -// NewMarkLine returns a series mark line func NewMarkLine(markLineTypes ...string) SeriesMarkLine { data := make([]SeriesMarkData, len(markLineTypes)) for index, t := range markLineTypes { @@ -48,7 +48,6 @@ func (m *markLinePainter) Add(opt markLineRenderOption) { m.options = append(m.options, opt) } -// NewMarkLinePainter returns a mark line renderer func NewMarkLinePainter(p *Painter) *markLinePainter { return &markLinePainter{ p: p, @@ -74,7 +73,7 @@ func (m *markLinePainter) Render() (Box, error) { } font := opt.Font if font == nil { - font, _ = GetDefaultFont() + font, _ = chart.GetDefaultFont() } summary := s.Summary() for _, markLine := range s.MarkLine.Data { diff --git a/mark_line_test.go b/mark_line_test.go index 0448cda..84152ce 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestMarkLine(t *testing.T) { @@ -55,7 +55,6 @@ func TestMarkLine(t *testing.T) { StrokeColor: drawing.ColorBlack, Series: series, Range: NewRange(AxisRangeOption{ - Painter: p, Min: 0, Max: 5, Size: p.Height(), @@ -68,7 +67,7 @@ func TestMarkLine(t *testing.T) { } return p.Bytes() }, - result: "\\n321", + result: "\\n321", }, } for _, tt := range tests { diff --git a/mark_point.go b/mark_point.go index fd8a88b..3d43a73 100644 --- a/mark_point.go +++ b/mark_point.go @@ -26,7 +26,6 @@ import ( "github.com/golang/freetype/truetype" ) -// NewMarkPoint returns a series mark point func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { data := make([]SeriesMarkData, len(markPointTypes)) for index, t := range markPointTypes { @@ -55,7 +54,6 @@ type markPointRenderOption struct { Points []Point } -// NewMarkPointPainter returns a mark point renderer func NewMarkPointPainter(p *Painter) *markPointPainter { return &markPointPainter{ p: p, @@ -65,6 +63,7 @@ func NewMarkPointPainter(p *Painter) *markPointPainter { func (m *markPointPainter) Render() (Box, error) { painter := m.p + theme := m.p.theme for _, opt := range m.options { s := opt.Series if len(s.MarkPoint.Data) == 0 { @@ -76,22 +75,15 @@ func (m *markPointPainter) Render() (Box, error) { if symbolSize == 0 { symbolSize = 30 } - textStyle := Style{ + painter.OverrideDrawingStyle(Style{ + FillColor: opt.FillColor, + }).OverrideTextStyle(Style{ + FontColor: theme.GetTextColor(), FontSize: labelFontSize, StrokeWidth: 1, Font: opt.Font, - } - if isLightColor(opt.FillColor) { - textStyle.FontColor = defaultLightFontColor - } else { - textStyle.FontColor = defaultDarkFontColor - } - painter.OverrideDrawingStyle(Style{ - FillColor: opt.FillColor, - }).OverrideTextStyle(textStyle) + }) for _, markPointData := range s.MarkPoint.Data { - textStyle.FontSize = labelFontSize - painter.OverrideTextStyle(textStyle) p := points[summary.MinIndex] value := summary.MinValue switch markPointData.Type { @@ -103,11 +95,6 @@ func (m *markPointPainter) Render() (Box, error) { painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize) text := commafWithDigits(value) textBox := painter.MeasureText(text) - if textBox.Width() > symbolSize { - textStyle.FontSize = smallLabelFontSize - painter.OverrideTextStyle(textStyle) - textBox = painter.MeasureText(text) - } painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) } } diff --git a/mark_point_test.go b/mark_point_test.go index 298345b..1a810cf 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestMarkPoint(t *testing.T) { @@ -69,7 +69,7 @@ func TestMarkPoint(t *testing.T) { } return p.Bytes() }, - result: "\\n3", + result: "\\n3", }, } diff --git a/painter.go b/painter.go index bee646f..da07007 100644 --- a/painter.go +++ b/painter.go @@ -28,11 +28,9 @@ import ( "math" "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart/v2" ) -type ValueFormatter func(float64) string - type Painter struct { render chart.Renderer box Box @@ -40,9 +38,6 @@ type Painter struct { parent *Painter style Style theme ColorPalette - // 类型 - outputType string - valueFormatter ValueFormatter } type PainterOptions struct { @@ -59,8 +54,6 @@ type PainterOptions struct { type PainterOption func(*Painter) type TicksOption struct { - // the first tick - First int Length int Orient string Count int @@ -73,17 +66,11 @@ type MultiTextOption struct { Unit int Position string Align string - // The text rotation of label - TextRotation float64 - Offset Box - // The first text index - First int } type GridOption struct { - Column int - Row int - ColumnSpans []int + Column int + Row int // 忽略不展示的column IgnoreColumnLines []int // 忽略不展示的row @@ -149,14 +136,14 @@ func PainterWidthHeightOption(width, height int) PainterOption { } } -// NewPainter creates a painter +// NewPainter creates a new painter func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { if opts.Width <= 0 || opts.Height <= 0 { return nil, errors.New("width/height can not be nil") } font := opts.Font if font == nil { - f, err := GetDefaultFont() + f, err := chart.GetDefaultFont() if err != nil { return nil, err } @@ -181,8 +168,6 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { Bottom: opts.Height, }, font: font, - // 类型 - outputType: opts.Type, } p.setOptions(opt...) if p.theme == nil { @@ -198,9 +183,6 @@ func (p *Painter) setOptions(opts ...PainterOption) { func (p *Painter) Child(opt ...PainterOption) *Painter { child := &Painter{ - // 格式化 - valueFormatter: p.valueFormatter, - // render render: p.render, box: p.box.Clone(), font: p.font, @@ -451,18 +433,11 @@ func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) { } func (p *Painter) LineStroke(points []Point) *Painter { - shouldMoveTo := false for index, point := range points { x := point.X y := point.Y - if y == int(math.MaxInt32) { - p.Stroke() - shouldMoveTo = true - continue - } - if shouldMoveTo || index == 0 { + if index == 0 { p.MoveTo(x, y) - shouldMoveTo = false } else { p.LineTo(x, y) } @@ -492,7 +467,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter { return p } -func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter { +func (p *Painter) SetBackground(width, height int, color Color) *Painter { r := p.render s := chart.Style{ FillColor: color, @@ -500,20 +475,12 @@ func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) // 背景色 p.SetDrawingStyle(s) defer p.ResetStyle() - if len(inside) != 0 && inside[0] { - p.MoveTo(0, 0) - p.LineTo(width, 0) - p.LineTo(width, height) - p.LineTo(0, height) - p.LineTo(0, 0) - } else { - // 设置背景色不使用box,因此不直接使用Painter - r.MoveTo(0, 0) - r.LineTo(width, 0) - r.LineTo(width, height) - r.LineTo(0, height) - r.LineTo(0, 0) - } + // 设置背景色不使用box,因此不直接使用Painter + r.MoveTo(0, 0) + r.LineTo(width, 0) + r.LineTo(width, height) + r.LineTo(0, height) + r.LineTo(0, 0) p.FillStroke() return p } @@ -565,20 +532,7 @@ func (p *Painter) Text(body string, x, y int) *Painter { return p } -func (p *Painter) TextRotation(body string, x, y int, radians float64) { - p.render.SetTextRotation(radians) - p.render.Text(body, x+p.box.Left, y+p.box.Top) - p.render.ClearTextRotation() -} - -func (p *Painter) SetTextRotation(radians float64) { - p.render.SetTextRotation(radians) -} -func (p *Painter) ClearTextRotation() { - p.render.ClearTextRotation() -} - -func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box { +func (p *Painter) TextFit(body string, x, y, width int) chart.Box { style := p.style textWarp := style.TextWrap style.TextWrap = chart.TextWrapWord @@ -587,24 +541,11 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch p.SetTextStyle(style) var output chart.Box - textAlign := "" - if len(textAligns) != 0 { - textAlign = textAligns[0] - } for index, line := range lines { - if line == "" { - continue - } x0 := x y0 := y + output.Height() - lineBox := r.MeasureText(line) - switch textAlign { - case AlignRight: - x0 += width - lineBox.Width() - case AlignCenter: - x0 += (width - lineBox.Width()) >> 1 - } p.Text(line, x0, y0) + lineBox := r.MeasureText(line) output.Right = chart.MaxInt(lineBox.Right, output.Right) output.Bottom += lineBox.Height() if index < len(lines)-1 { @@ -620,7 +561,6 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { return p } count := opt.Count - first := opt.First width := p.Width() height := p.Height() unit := 1 @@ -635,10 +575,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { values = autoDivide(width, count) } for index, value := range values { - if index < first { - continue - } - if (index-first)%unit != 0 { + if index%unit != 0 { continue } if isVertical { @@ -674,15 +611,12 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true - showIndex := opt.Unit / 2 if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- - // 非居中 - showIndex = 0 } width := p.Width() height := p.Height() @@ -693,19 +627,10 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } - isTextRotation := opt.TextRotation != 0 - offset := opt.Offset for index, text := range opt.TextList { - if index < opt.First { + if opt.Unit != 0 && index%opt.Unit != 0 { continue } - if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { - continue - } - if isTextRotation { - p.ClearTextRotation() - p.SetTextRotation(opt.TextRotation) - } box := p.MeasureText(text) start := values[index] if positionCenter { @@ -726,13 +651,8 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { x = start - box.Width()>>1 } - x += offset.Left - y += offset.Top p.Text(text, x, y) } - if isTextRotation { - p.ClearTextRotation() - } return p } @@ -770,12 +690,8 @@ func (p *Painter) Grid(opt GridOption) *Painter { }) } } - columnCount := sumInt(opt.ColumnSpans) - if columnCount == 0 { - columnCount = opt.Column - } - if columnCount > 0 { - values := autoDivideSpans(width, columnCount, opt.ColumnSpans) + if opt.Column > 0 { + values := autoDivide(width, opt.Column) drawLines(values, opt.IgnoreColumnLines, true) } if opt.Row > 0 { @@ -803,48 +719,6 @@ func (p *Painter) Rect(box Box) *Painter { return p } -func (p *Painter) RoundedRect(box Box, radius int) *Painter { - r := (box.Right - box.Left) / 2 - if radius > r { - radius = r - } - rx := float64(radius) - ry := float64(radius) - p.MoveTo(box.Left+radius, box.Top) - p.LineTo(box.Right-radius, box.Top) - - cx := box.Right - radius - cy := box.Top + radius - // right top - p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2) - - p.LineTo(box.Right, box.Bottom-radius) - - // right bottom - cx = box.Right - radius - cy = box.Bottom - radius - p.ArcTo(cx, cy, rx, ry, 0.0, math.Pi/2) - - p.LineTo(box.Left+radius, box.Bottom) - - // left bottom - cx = box.Left + radius - cy = box.Bottom - radius - p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2) - - p.LineTo(box.Left, box.Top+radius) - - // left top - cx = box.Left + radius - cy = box.Top + radius - p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2) - - p.Close() - p.FillStroke() - p.Fill() - return p -} - func (p *Painter) LegendLineDot(box Box) *Painter { width := box.Width() height := box.Height() @@ -860,7 +734,3 @@ func (p *Painter) LegendLineDot(box Box) *Painter { p.FillStroke() return p } - -func (p *Painter) GetRenderer() chart.Renderer { - return p.render -} diff --git a/painter_test.go b/painter_test.go index 07c4113..8892563 100644 --- a/painter_test.go +++ b/painter_test.go @@ -28,8 +28,8 @@ import ( "github.com/golang/freetype/truetype" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestPainterOption(t *testing.T) { @@ -143,13 +143,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -165,13 +165,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -187,13 +187,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -209,13 +209,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -231,13 +231,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -253,13 +253,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -279,7 +279,7 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: Color{ + StrokeColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -297,7 +297,7 @@ func TestPainter(t *testing.T) { { fn: func(p *Painter) { p.SetDrawingStyle(Style{ - FillColor: Color{ + FillColor: drawing.Color{ R: 84, G: 112, B: 198, @@ -343,29 +343,6 @@ func TestPainter(t *testing.T) { } } -func TestRoundedRect(t *testing.T) { - assert := assert.New(t) - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - Type: ChartOutputSVG, - }) - assert.Nil(err) - p.OverrideDrawingStyle(Style{ - FillColor: drawing.ColorWhite, - StrokeWidth: 1, - StrokeColor: drawing.ColorWhite, - }).RoundedRect(Box{ - Left: 10, - Right: 30, - Bottom: 150, - Top: 10, - }, 5) - buf, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(buf)) -} - func TestPainterTextFit(t *testing.T) { assert := assert.New(t) p, err := NewPainter(PainterOptions{ @@ -374,7 +351,7 @@ func TestPainterTextFit(t *testing.T) { Type: ChartOutputSVG, }) assert.Nil(err) - f, _ := GetDefaultFont() + f, _ := chart.GetDefaultFont() style := Style{ FontSize: 12, FontColor: chart.ColorBlack, diff --git a/pie_chart.go b/pie_chart.go index 5c04ed8..972b4c1 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -27,7 +27,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart/v2" ) type pieChart struct { @@ -36,7 +36,6 @@ type pieChart struct { } type PieChartOption struct { - // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -52,7 +51,6 @@ type PieChartOption struct { backgroundIsFilled bool } -// NewPieChart returns a pie chart renderer func NewPieChart(p *Painter, opt PieChartOption) *pieChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -63,96 +61,6 @@ func NewPieChart(p *Painter, opt PieChartOption) *pieChart { } } -type sector struct { - value float64 - percent float64 - cx int - cy int - rx float64 - ry float64 - start float64 - delta float64 - offset int - quadrant int - lineStartX int - lineStartY int - lineBranchX int - lineBranchY int - lineEndX int - lineEndY int - showLabel bool - label string - series Series - color Color -} - -func NewSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector { - s := sector{} - s.value = value - s.percent = value / totalValue - s.cx = cx - s.cy = cy - s.rx = radius - s.ry = radius - p := (currentValue + value/2) / totalValue - if p < 0.25 { - s.quadrant = 1 - } else if p < 0.5 { - s.quadrant = 4 - } else if p < 0.75 { - s.quadrant = 3 - } else { - s.quadrant = 2 - } - s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2 - s.delta = chart.PercentToRadians(value / totalValue) - angle := s.start + s.delta/2 - s.lineStartX = cx + int(radius*math.Cos(angle)) - s.lineStartY = cy + int(radius*math.Sin(angle)) - s.lineBranchX = cx + int(labelRadius*math.Cos(angle)) - s.lineBranchY = cy + int(labelRadius*math.Sin(angle)) - s.offset = labelLineLength - if s.lineBranchX <= cx { - s.offset *= -1 - } - s.lineEndX = s.lineBranchX + s.offset - s.lineEndY = s.lineBranchY - s.series = series - s.color = color - s.showLabel = series.Label.Show - s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent) - return s -} - -func (s *sector) calculateY(prevY int) int { - for i := 0; i <= s.cy; i++ { - if s.quadrant <= 2 { - if (prevY - s.lineBranchY) > labelFontSize+5 { - break - } - s.lineBranchY -= 1 - } else { - if (s.lineBranchY - prevY) > labelFontSize+5 { - break - } - s.lineBranchY += 1 - } - } - s.lineEndY = s.lineBranchY - return s.lineBranchY -} - -func (s *sector) calculateTextXY(textBox Box) (x int, y int) { - textMargin := 3 - x = s.lineEndX + textMargin - y = s.lineEndY + textBox.Height()>>1 - 1 - if s.offset < 0 { - textWidth := textBox.Width() - x = s.lineEndX - textWidth - textMargin - } - return -} - func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { opt := p.opt values := make([]float64, len(seriesList)) @@ -189,105 +97,90 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B seriesNames = seriesList.Names() } theme := opt.Theme - - currentValue := float64(0) - - var quadrant1, quadrant2, quadrant3, quadrant4 []sector - for index, v := range values { - series := seriesList[index] - color := theme.GetSeriesColor(index) - if index == len(values)-1 { - if color == theme.GetSeriesColor(0) { - color = theme.GetSeriesColor(1) - } - } - s := NewSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color) - switch quadrant := s.quadrant; quadrant { - case 1: - quadrant1 = append([]sector{s}, quadrant1...) - case 2: - quadrant2 = append(quadrant2, s) - case 3: - quadrant3 = append([]sector{s}, quadrant3...) - case 4: - quadrant4 = append(quadrant4, s) - } - currentValue += v - } - sectors := append(quadrant1, quadrant4...) - sectors = append(sectors, quadrant3...) - sectors = append(sectors, quadrant2...) - - currentQuadrant := 0 - prevY := 0 - maxY := 0 - minY := 0 - for _, s := range sectors { + if len(values) == 1 { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, - StrokeColor: s.color, - FillColor: s.color, + StrokeColor: theme.GetSeriesColor(0), + FillColor: theme.GetSeriesColor(0), }) - seriesPainter.MoveTo(s.cx, s.cy) - seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke() - if !s.showLabel { - continue - } - if currentQuadrant != s.quadrant { - if s.quadrant == 1 { - minY = cy * 2 - maxY = 0 - prevY = cy * 2 + seriesPainter.MoveTo(cx, cy). + Circle(radius, cx, cy) + } else { + currentValue := float64(0) + prevEndX := 0 + prevEndY := 0 + for index, v := range values { + seriesPainter.OverrideDrawingStyle(Style{ + StrokeWidth: 1, + StrokeColor: theme.GetSeriesColor(index), + FillColor: theme.GetSeriesColor(index), + }) + seriesPainter.MoveTo(cx, cy) + start := chart.PercentToRadians(currentValue/total) - math.Pi/2 + currentValue += v + percent := (v / total) + delta := chart.PercentToRadians(percent) + seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). + LineTo(cx, cy). + Close(). + FillStroke() + + series := seriesList[index] + // 是否显示label + showLabel := series.Label.Show + if !showLabel { + continue } - if s.quadrant == 2 { - if currentQuadrant != 3 { - prevY = s.lineEndY - } else { - prevY = minY - } + + // label的角度为饼块中间 + angle := start + delta/2 + startx := cx + int(radius*math.Cos(angle)) + starty := cy + int(radius*math.Sin(angle)) + + endx := cx + int(labelRadius*math.Cos(angle)) + endy := cy + int(labelRadius*math.Sin(angle)) + // 计算是否有重叠,如果有则调整y坐标位置 + if index != 0 && + math.Abs(float64(endx-prevEndX)) < labelFontSize && + math.Abs(float64(endy-prevEndY)) < labelFontSize { + endy -= (labelFontSize << 1) } - if s.quadrant == 3 { - if currentQuadrant != 4 { - prevY = s.lineEndY - } else { - minY = cy * 2 - maxY = 0 - prevY = 0 - } + prevEndX = endx + prevEndY = endy + + seriesPainter.MoveTo(startx, starty) + seriesPainter.LineTo(endx, endy) + offset := labelLineWidth + if endx < cx { + offset *= -1 } - if s.quadrant == 4 { - if currentQuadrant != 1 { - prevY = s.lineEndY - } else { - prevY = maxY - } + seriesPainter.MoveTo(endx, endy) + endx += offset + seriesPainter.LineTo(endx, endy) + seriesPainter.Stroke() + + textStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, } - currentQuadrant = s.quadrant + if !series.Label.Color.IsZero() { + textStyle.FontColor = series.Label.Color + } + seriesPainter.OverrideTextStyle(textStyle) + text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) + textBox := seriesPainter.MeasureText(text) + textMargin := 3 + x := endx + textMargin + y := endy + textBox.Height()>>1 - 1 + if offset < 0 { + textWidth := textBox.Width() + x = endx - textWidth - textMargin + } + seriesPainter.Text(text, x, y) } - prevY = s.calculateY(prevY) - if prevY > maxY { - maxY = prevY - } - if prevY < minY { - minY = prevY - } - seriesPainter.MoveTo(s.lineStartX, s.lineStartY) - seriesPainter.LineTo(s.lineBranchX, s.lineBranchY) - seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY) - seriesPainter.LineTo(s.lineEndX, s.lineEndY) - seriesPainter.Stroke() - textStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !s.series.Label.Color.IsZero() { - textStyle.FontColor = s.series.Label.Color - } - seriesPainter.OverrideTextStyle(textStyle) - x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label)) - seriesPainter.Text(s.label, x, y) } + return p.p.box, nil } diff --git a/pie_chart_test.go b/pie_chart_test.go index 3795d32..c373a7e 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -23,7 +23,6 @@ package charts import ( - "strconv" "testing" "github.com/stretchr/testify/assert" @@ -99,435 +98,3 @@ func TestPieChart(t *testing.T) { assert.Equal(tt.result, string(data)) } } - -func TestPieChartWithLabelsValuesSortedDescending(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 84358845, - 68070697, - 58850717, - 48059777, - 36753736, - 19051562, - 17947406, - 11754004, - 10827529, - 10521556, - 10467366, - 10394055, - 9597085, - 9104772, - 6447710, - 5932654, - 5563970, - 5428792, - 5194336, - 3850894, - 2857279, - 2116792, - 1883008, - 1373101, - 920701, - 660809, - 542051, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - Formatter: "{b} ({c} ≅ {d})", - }, - Radius: "200", - }), - Title: TitleOption{ - Text: "European Union member states by population", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: []string{ - "Germany", - "France", - "Italy", - "Spain", - "Poland", - "Romania", - "Netherlands", - "Belgium", - "Czech Republic", - "Sweden", - "Portugal", - "Greece", - "Hungary", - "Austria", - "Bulgaria", - "Denmark", - "Finland", - "Slovakia", - "Ireland", - "Croatia", - "Lithuania", - "Slovenia", - "Latvia", - "Estonia", - "Cyprus", - "Luxembourg", - "Malta", - }, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nEuropean Union member states by populationGermany (84358845 ≅ 18.8%)France (68070697 ≅ 15.17%)Italy (58850717 ≅ 13.12%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1000, - Height: 800, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartWithLabelsValuesUnsorted(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 9104772, - 11754004, - 6447710, - 3850894, - 920701, - 10827529, - 5932654, - 1373101, - 5563970, - 68070697, - 84358845, - 10394055, - 9597085, - 5194336, - 58850717, - 1883008, - 2857279, - 660809, - 542051, - 17947406, - 36753736, - 10467366, - 19051562, - 5428792, - 2116792, - 48059777, - 10521556, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - Formatter: "{b} ({c} ≅ {d})", - }, - Radius: "200", - }), - Title: TitleOption{ - Text: "European Union member states by population", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: []string{ - "Austria", - "Belgium", - "Bulgaria", - "Croatia", - "Cyprus", - "Czech Republic", - "Denmark", - "Estonia", - "Finland", - "France", - "Germany", - "Greece", - "Hungary", - "Ireland", - "Italy", - "Latvia", - "Lithuania", - "Luxembourg", - "Malta", - "Netherlands", - "Poland", - "Portugal", - "Romania", - "Slovakia", - "Slovenia", - "Spain", - "Sweden", - }, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Finland (5563970 ≅ 1.24%)Estonia (1373101 ≅ 0.3%)Denmark (5932654 ≅ 1.32%)Czech Republic (10827529 ≅ 2.41%)Cyprus (920701 ≅ 0.2%)Croatia (3850894 ≅ 0.85%)Bulgaria (6447710 ≅ 1.43%)Belgium (11754004 ≅ 2.62%)Austria (9104772 ≅ 2.02%)Germany (84358845 ≅ 18.8%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Poland (36753736 ≅ 8.19%)Netherlands (17947406 ≅ 4%)Malta (542051 ≅ 0.12%)Luxembourg (660809 ≅ 0.14%)Lithuania (2857279 ≅ 0.63%)Latvia (1883008 ≅ 0.41%)Italy (58850717 ≅ 13.12%)Ireland (5194336 ≅ 1.15%)Portugal (10467366 ≅ 2.33%)Romania (19051562 ≅ 4.24%)Slovakia (5428792 ≅ 1.21%)Slovenia (2116792 ≅ 0.47%)Spain (48059777 ≅ 10.71%)Sweden (10521556 ≅ 2.34%)", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1000, - Height: 800, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartWith100Labels(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - var values []float64 - var labels []string - for i := 1; i <= 100; i++ { - values = append(values, float64(1)) - labels = append(labels, "Label "+strconv.Itoa(i)) - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "200", - }), - Title: TitleOption{ - Text: "Test with 100 labels", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: labels, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nTest with 100 labelsLabel 25: 1%Label 24: 1%Label 23: 1%Label 22: 1%Label 21: 1%Label 20: 1%Label 19: 1%Label 18: 1%Label 17: 1%Label 16: 1%Label 15: 1%Label 14: 1%Label 13: 1%Label 12: 1%Label 11: 1%Label 10: 1%Label 9: 1%Label 8: 1%Label 7: 1%Label 6: 1%Label 5: 1%Label 4: 1%Label 3: 1%Label 2: 1%Label 1: 1%Label 26: 1%Label 27: 1%Label 28: 1%Label 29: 1%Label 30: 1%Label 31: 1%Label 32: 1%Label 33: 1%Label 34: 1%Label 35: 1%Label 36: 1%Label 37: 1%Label 38: 1%Label 39: 1%Label 40: 1%Label 41: 1%Label 42: 1%Label 43: 1%Label 44: 1%Label 45: 1%Label 46: 1%Label 47: 1%Label 48: 1%Label 49: 1%Label 50: 1%Label 75: 1%Label 74: 1%Label 73: 1%Label 72: 1%Label 71: 1%Label 70: 1%Label 69: 1%Label 68: 1%Label 67: 1%Label 66: 1%Label 65: 1%Label 64: 1%Label 63: 1%Label 62: 1%Label 61: 1%Label 60: 1%Label 59: 1%Label 58: 1%Label 57: 1%Label 56: 1%Label 55: 1%Label 54: 1%Label 53: 1%Label 52: 1%Label 51: 1%Label 76: 1%Label 77: 1%Label 78: 1%Label 79: 1%Label 80: 1%Label 81: 1%Label 82: 1%Label 83: 1%Label 84: 1%Label 85: 1%Label 86: 1%Label 87: 1%Label 88: 1%Label 89: 1%Label 90: 1%Label 91: 1%Label 92: 1%Label 93: 1%Label 94: 1%Label 95: 1%Label 96: 1%Label 97: 1%Label 98: 1%Label 99: 1%Label 100: 1%", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1000, - Height: 900, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartFixLabelPos72586(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 397594, - 185596, - 149086, - 144258, - 120194, - 117514, - 99412, - 91135, - 87282, - 76790, - 72586, - 58818, - 58270, - 56306, - 55486, - 54792, - 53746, - 51460, - 41242, - 39476, - 37414, - 36644, - 33784, - 32788, - 32566, - 29608, - 29558, - 29384, - 28166, - 26998, - 26948, - 26054, - 25804, - 25730, - 24438, - 23782, - 22896, - 21404, - 428978, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - Formatter: "{b} ({c} ≅ {d})", - }, - Radius: "150", - }), - Title: TitleOption{ - Text: "Fix label K (72586)", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: []string{ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P", - "Q", - "R", - "S", - "T", - "U", - "V", - "W", - "X", - "Y", - "Z", - "AA", - "AB", - "AC", - "AD", - "AE", - "AF", - "AG", - "AH", - "AI", - "AJ", - "AK", - "AL", - "AM", - }, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nFix label K (72586)C (149086 ≅ 5.04%)B (185596 ≅ 6.28%)A (397594 ≅ 13.45%)D (144258 ≅ 4.88%)E (120194 ≅ 4.06%)F (117514 ≅ 3.97%)G (99412 ≅ 3.36%)H (91135 ≅ 3.08%)I (87282 ≅ 2.95%)J (76790 ≅ 2.59%)Z (29608 ≅ 1%)Y (32566 ≅ 1.1%)X (32788 ≅ 1.1%)W (33784 ≅ 1.14%)V (36644 ≅ 1.24%)U (37414 ≅ 1.26%)T (39476 ≅ 1.33%)S (41242 ≅ 1.39%)R (51460 ≅ 1.74%)Q (53746 ≅ 1.81%)P (54792 ≅ 1.85%)O (55486 ≅ 1.87%)N (56306 ≅ 1.9%)M (58270 ≅ 1.97%)L (58818 ≅ 1.99%)K (72586 ≅ 2.45%)AA (29558 ≅ 1%)AB (29384 ≅ 0.99%)AC (28166 ≅ 0.95%)AD (26998 ≅ 0.91%)AE (26948 ≅ 0.91%)AF (26054 ≅ 0.88%)AG (25804 ≅ 0.87%)AH (25730 ≅ 0.87%)AI (24438 ≅ 0.82%)AJ (23782 ≅ 0.8%)AK (22896 ≅ 0.77%)AL (21404 ≅ 0.72%)AM (428978 ≅ 14.52%)", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1150, - Height: 550, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/radar_chart.go b/radar_chart.go index cf18135..5b8aa85 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -25,10 +25,9 @@ package charts import ( "errors" - "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) type radarChart struct { @@ -46,7 +45,6 @@ type RadarIndicator struct { } type RadarChartOption struct { - // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -64,7 +62,6 @@ type RadarChartOption struct { backgroundIsFilled bool } -// NewRadarIndicators returns a radar indicator list func NewRadarIndicators(names []string, values []float64) []RadarIndicator { if len(names) != len(values) { return nil @@ -79,7 +76,6 @@ func NewRadarIndicators(names []string, values []float64) []RadarIndicator { return indicators } -// NewRadarChart returns a radar chart renderer func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -201,11 +197,7 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) continue } indicator := indicators[j] - var percent float64 - offset := indicator.Max - indicator.Min - if offset > 0 { - percent = (item.Value - indicator.Min) / offset - } + percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min) r := percent * radius p := getPolygonPoint(center, r, angles[j]) linePoints = append(linePoints, p) @@ -231,15 +223,9 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) StrokeColor: color, FillColor: dotFillColor, }) - for index, point := range linePoints { + for _, point := range linePoints { seriesPainter.Circle(dotWith, point.X, point.Y) seriesPainter.FillStroke() - if series.Label.Show && index < len(series.Data) { - value := humanize.FtoaWithDigits(series.Data[index].Value, 2) - b := seriesPainter.MeasureText(value) - seriesPainter.Text(value, point.X-b.Width()/2, point.Y) - } - } } diff --git a/range.go b/range.go index ec64c2d..d5a9ef7 100644 --- a/range.go +++ b/range.go @@ -29,7 +29,6 @@ import ( const defaultAxisDivideCount = 6 type axisRange struct { - p *Painter divideCount int min float64 max float64 @@ -38,20 +37,13 @@ type axisRange struct { } type AxisRangeOption struct { - Painter *Painter - // The min value of axis - Min float64 - // The max value of axis - Max float64 - // The size of axis - Size int - // Boundary gap - Boundary bool - // The count of divide + Min float64 + Max float64 + Size int + Boundary bool DivideCount int } -// NewRange returns a axis range func NewRange(opt AxisRangeOption) axisRange { max := opt.Max min := opt.Min @@ -62,10 +54,7 @@ func NewRange(opt AxisRangeOption) axisRange { r := math.Abs(max - min) // 最小单位计算 - unit := 1 - if r > 5 { - unit = 2 - } + unit := 2 if r > 10 { unit = 4 } @@ -90,12 +79,7 @@ func NewRange(opt AxisRangeOption) axisRange { } } max = min + float64(unit*divideCount) - expectMax := opt.Max * 2 - if max > expectMax { - max = float64(ceilFloatToInt(expectMax)) - } return axisRange{ - p: opt.Painter, divideCount: divideCount, min: min, max: max, @@ -104,26 +88,18 @@ func NewRange(opt AxisRangeOption) axisRange { } } -// Values returns values of range func (r axisRange) Values() []string { offset := (r.max - r.min) / float64(r.divideCount) values := make([]string, 0) - formatter := commafWithDigits - if r.p != nil && r.p.valueFormatter != nil { - formatter = r.p.valueFormatter - } for i := 0; i <= r.divideCount; i++ { v := r.min + float64(i)*offset - value := formatter(v) + value := commafWithDigits(v) values = append(values, value) } return values } func (r *axisRange) getHeight(value float64) int { - if r.max <= r.min { - return 0 - } v := (value - r.min) / (r.max - r.min) return int(v * float64(r.size)) } @@ -132,13 +108,10 @@ func (r *axisRange) getRestHeight(value float64) int { return r.size - r.getHeight(value) } -// GetRange returns a range of index func (r *axisRange) GetRange(index int) (float64, float64) { unit := float64(r.size) / float64(r.divideCount) return unit * float64(index), unit * float64(index+1) } - -// AutoDivide divides the axis func (r *axisRange) AutoDivide() []int { return autoDivide(r.size, r.divideCount) } diff --git a/series.go b/series.go index da50e64..44c4749 100644 --- a/series.go +++ b/series.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/dustin/go-humanize" - "git.smarteching.com/zeni/go-chart/v2" + "github.com/wcharczuk/go-chart/v2" ) type SeriesData struct { @@ -36,7 +36,6 @@ type SeriesData struct { Style Style } -// NewSeriesListDataFromValues returns a series list func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList { seriesList := make(SeriesList, len(values)) for index, value := range values { @@ -45,7 +44,6 @@ func NewSeriesListDataFromValues(values [][]float64, chartType ...string) Series return seriesList } -// NewSeriesFromValues returns a series func NewSeriesFromValues(values []float64, chartType ...string) Series { s := Series{ Data: NewSeriesDataFromValues(values), @@ -56,7 +54,6 @@ func NewSeriesFromValues(values []float64, chartType ...string) Series { return s } -// NewSeriesDataFromValues return a series data func NewSeriesDataFromValues(values []float64) []SeriesData { data := make([]SeriesData, len(values)) for index, value := range values { @@ -79,12 +76,6 @@ type SeriesLabel struct { Show bool // Distance to the host graphic element. Distance int - // The position of label - Position string - // The offset of label's position - Offset Box - // The font size of label - FontSize float64 } const ( @@ -126,8 +117,6 @@ type Series struct { Name string // Radius for Pie chart, e.g.: 40%, default is "40%" Radius string - // Round for bar chart - RoundRadius int // Mark point for series MarkPoint SeriesMarkPoint // Make line for series @@ -140,9 +129,6 @@ type Series struct { type SeriesList []Series func (sl SeriesList) init() { - if len(sl) == 0 { - return - } if sl[len(sl)-1].index != 0 { return } @@ -173,10 +159,6 @@ func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) { continue } for _, item := range series.Data { - // 如果为空值,忽略 - if item.Value == nullValue { - continue - } if item.Value > max { max = item.Value } @@ -222,19 +204,13 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { } type seriesSummary struct { - // The index of max value - MaxIndex int - // The max value - MaxValue float64 - // The index of min value - MinIndex int - // The min value - MinValue float64 - // THe average value + MaxIndex int + MaxValue float64 + MinIndex int + MinValue float64 AverageValue float64 } -// Summary get summary of series func (s *Series) Summary() seriesSummary { minIndex := -1 maxIndex := -1 @@ -261,7 +237,6 @@ func (s *Series) Summary() seriesSummary { } } -// Names returns the names of series list func (sl SeriesList) Names() []string { names := make([]string, len(sl)) for index, s := range sl { @@ -270,10 +245,8 @@ func (sl SeriesList) Names() []string { return names } -// LabelFormatter label formatter type LabelFormatter func(index int, value float64, percent float64) string -// NewPieLabelFormatter returns a pie label formatter func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { layout = "{b}: {d}" @@ -281,23 +254,13 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { return NewLabelFormatter(seriesNames, layout) } -// NewFunnelLabelFormatter returns a funner label formatter -func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{b}({d})" - } - return NewLabelFormatter(seriesNames, layout) -} - -// NewValueLabelFormatter returns a value formatter -func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { +func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { layout = "{c}" } return NewLabelFormatter(seriesNames, layout) } -// NewLabelFormatter returns a label formaatter func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { return func(index int, value, percent float64) string { // 如果无percent的则设置为<0 diff --git a/series_label.go b/series_label.go deleted file mode 100644 index af873fc..0000000 --- a/series_label.go +++ /dev/null @@ -1,148 +0,0 @@ -// 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 ( - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" -) - -type labelRenderValue struct { - Text string - Style Style - X int - Y int - // 旋转 - Radians float64 -} - -type LabelValue struct { - Index int - Value float64 - X int - Y int - // 旋转 - Radians float64 - // 字体颜色 - FontColor Color - // 字体大小 - FontSize float64 - Orient string - Offset Box -} - -type SeriesLabelPainter struct { - p *Painter - seriesNames []string - label *SeriesLabel - theme ColorPalette - font *truetype.Font - values []labelRenderValue -} - -type SeriesLabelPainterParams struct { - P *Painter - SeriesNames []string - Label SeriesLabel - Theme ColorPalette - Font *truetype.Font -} - -func NewSeriesLabelPainter(params SeriesLabelPainterParams) *SeriesLabelPainter { - return &SeriesLabelPainter{ - p: params.P, - seriesNames: params.SeriesNames, - label: ¶ms.Label, - theme: params.Theme, - font: params.Font, - values: make([]labelRenderValue, 0), - } -} - -func (o *SeriesLabelPainter) Add(value LabelValue) { - label := o.label - distance := label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormatter(o.seriesNames, label.Formatter)(value.Index, value.Value, -1) - labelStyle := Style{ - FontColor: o.theme.GetTextColor(), - FontSize: labelFontSize, - Font: o.font, - } - if value.FontSize != 0 { - labelStyle.FontSize = value.FontSize - } - if !value.FontColor.IsZero() { - label.Color = value.FontColor - } - if !label.Color.IsZero() { - labelStyle.FontColor = label.Color - } - p := o.p - p.OverrideDrawingStyle(labelStyle) - rotated := value.Radians != 0 - if rotated { - p.SetTextRotation(value.Radians) - } - textBox := p.MeasureText(text) - renderValue := labelRenderValue{ - Text: text, - Style: labelStyle, - X: value.X, - Y: value.Y, - Radians: value.Radians, - } - if value.Orient != OrientHorizontal { - renderValue.X -= textBox.Width() >> 1 - renderValue.Y -= distance - } else { - renderValue.X += distance - renderValue.Y += textBox.Height() >> 1 - renderValue.Y -= 2 - } - if rotated { - renderValue.X = value.X + textBox.Width()>>1 - 1 - p.ClearTextRotation() - } else { - if textBox.Width()%2 != 0 { - renderValue.X++ - } - } - renderValue.X += value.Offset.Left - renderValue.Y += value.Offset.Top - o.values = append(o.values, renderValue) -} - -func (o *SeriesLabelPainter) Render() (Box, error) { - for _, item := range o.values { - o.p.OverrideTextStyle(item.Style) - if item.Radians != 0 { - o.p.TextRotation(item.Text, item.X, item.Y, item.Radians) - } else { - o.p.Text(item.Text, item.X, item.Y) - } - } - return chart.BoxZero, nil -} diff --git a/series_test.go b/series_test.go deleted file mode 100644 index 40d2f91..0000000 --- a/series_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// 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" -) - -func TestNewSeriesListDataFromValues(t *testing.T) { - assert := assert.New(t) - - assert.Equal(SeriesList{ - { - Type: ChartTypeBar, - Data: []SeriesData{ - { - Value: 1.0, - }, - }, - }, - }, NewSeriesListDataFromValues([][]float64{ - { - 1, - }, - }, ChartTypeBar)) -} - -func TestSeriesLists(t *testing.T) { - assert := assert.New(t) - seriesList := NewSeriesListDataFromValues([][]float64{ - { - 1, - 2, - }, - { - 10, - }, - }, ChartTypeBar) - - assert.Equal(2, len(seriesList.Filter(ChartTypeBar))) - assert.Equal(0, len(seriesList.Filter(ChartTypeLine))) - - max, min := seriesList.GetMaxMin(0) - assert.Equal(float64(10), max) - assert.Equal(float64(1), min) - - assert.Equal(seriesSummary{ - MaxIndex: 1, - MaxValue: 2, - MinIndex: 0, - MinValue: 1, - AverageValue: 1.5, - }, seriesList[0].Summary()) -} - -func TestFormatter(t *testing.T) { - assert := assert.New(t) - - assert.Equal("a: 12%", NewPieLabelFormatter([]string{ - "a", - "b", - }, "")(0, 10, 0.12)) - - assert.Equal("10", NewValueLabelFormatter([]string{ - "a", - "b", - }, "")(0, 10, 0.12)) -} diff --git a/start_zh.md b/start_zh.md deleted file mode 100644 index ee8359c..0000000 --- a/start_zh.md +++ /dev/null @@ -1,254 +0,0 @@ -# go-charts - -`go-charts`主要分为了下几个模块: - -- `标题`:图表的标题,包括主副标题,位置为图表的顶部 -- `图例`:图表的图例列表,用于标识每个图例对应的颜色与名称信息,默认为图表的顶部,可自定义位置 -- `X轴`:图表的x轴,用于折线图、柱状图中,表示每个点对应的时间,位置图表的底部 -- `Y轴`:图表的y轴,用于折线图、柱状图中,最多可使用两组y轴(一左一右),默认位置图表的左侧 -- `内容`: 图表的内容,折线图、柱状图、饼图等,在图表的中间区域 - -## 标题 - -### 常用设置 - -标题一般仅需要设置主副标题即可,其它的属性均会设置默认值,常用的方式是使用`TitleTextOptionFunc`设置,其中副标题为可选值,方式如下: - -```go - charts.TitleTextOptionFunc("Text", "Subtext"), -``` - -### 个性化设置 - -```go -func(opt *charts.ChartOption) { - opt.Title = charts.TitleOption{ - // 主标题 - Text: "Text", - // 副标题 - Subtext: "Subtext", - // 标题左侧位置,可设置为"center","right",数值("20")或百份比("20%") - Left: charts.PositionRight, - // 标题顶部位置,只可调为数值 - Top: "20", - // 主标题文字大小 - FontSize: 14, - // 副标题文字大小 - SubtextFontSize: 12, - // 主标题字体颜色 - FontColor: charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, - // 副标题字体影响 - SubtextFontColor: charts.Color{ - R: 200, - G: 200, - B: 200, - A: 255, - }, - } -}, -``` - -### 部分属性个性化设置 - -```go -charts.TitleTextOptionFunc("Text", "Subtext"), -func(opt *charts.ChartOption) { - // 修改top的值 - opt.Title.Top = "20" -}, -``` - -## 图例 - -### 常用设置 - -图例组件与图表中的数据一一对应,常用仅设置其名称及左侧的值即可(可选),方式如下: - - -```go -charts.LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", -}, "50"), -``` - -### 个性化设置 - -```go -func(opt *charts.ChartOption) { - opt.Legend = charts.LegendOption{ - // 图例名称 - Data: []string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, - // 图例左侧位置,可设置为"center","right",数值("20")或百份比("20%") - // 如果示例有多行,只影响第一行,而且对于多行的示例,设置"center", "right"无效 - Left: "50", - // 图例顶部位置,只可调为数值 - Top: "10", - // 图例图标的位置,默认为左侧,只允许左或右 - Align: charts.AlignRight, - // 图例排列方式,默认为水平,只允许水平或垂直 - Orient: charts.OrientVertical, - // 图标类型,提供"rect"与"lineDot"两种类型 - Icon: charts.IconRect, - // 字体大小 - FontSize: 14, - // 字体颜色 - FontColor: charts.Color{ - R: 150, - G: 150, - B: 150, - A: 255, - }, - // 是否展示,如果不需要展示则设置 - // Show: charts.FalseFlag(), - // 图例区域的padding值 - Padding: charts.Box{ - Top: 10, - Left: 10, - }, - } -}, -``` - -### 部分属性个性化设置 - -```go -charts.LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", -}, "50"), -func(opt *charts.ChartOption) { - opt.Legend.Top = "10" -}, -``` - -## X轴 - -### 常用设置 - -图表中X轴的展示,常用的设置方式是指定数组即可: - -```go -charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", -}), -``` - -### 个性化设置 - -```go -func(opt *charts.ChartOption) { - opt.XAxis = charts.XAxisOption{ - // X轴内容 - Data: []string{ - "01", - "02", - "03", - "04", - "05", - "06", - "07", - "08", - "09", - }, - // 如果数据点不居中,则设置为false - BoundaryGap: charts.FalseFlag(), - // 字体大小 - FontSize: 14, - // 是否展示,如果不需要展示则设置 - // Show: charts.FalseFlag(), - // 会根据文本内容以及此值选择适合的分块大小,一般不需要设置 - // SplitNumber: 3, - // 线条颜色 - StrokeColor: charts.Color{ - R: 200, - G: 200, - B: 200, - A: 255, - }, - // 文字颜色 - FontColor: charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, - } -}, -``` - -### 部分属性个性化设置 - -```go -charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", -}), -func(opt *charts.ChartOption) { - opt.XAxis.FontColor = charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, -}, -``` - -## Y轴 - -图表中的y轴展示的相关数据会根据图表中的数据自动生成适合的值,如果需要自定义,则可自定义以下部分数据: - -```go -func(opt *charts.ChartOption) { - opt.YAxisOptions = []charts.YAxisOption{ - { - // 字体大小 - FontSize: 16, - // 字体颜色 - FontColor: charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, - // 内容,{value}会替换为对应的值 - Formatter: "{value} ml", - // Y轴颜色,如果设置此值,会覆盖font color - Color: charts.Color{ - R: 255, - G: 0, - B: 0, - A: 255, - }, - }, - } -}, -``` diff --git a/table.go b/table.go deleted file mode 100644 index 3e6f273..0000000 --- a/table.go +++ /dev/null @@ -1,438 +0,0 @@ -// 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" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -type tableChart struct { - p *Painter - opt *TableChartOption -} - -// NewTableChart returns a table chart render -func NewTableChart(p *Painter, opt TableChartOption) *tableChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &tableChart{ - p: p, - opt: &opt, - } -} - -type TableCell struct { - // Text the text of table cell - Text string - // Style the current style of table cell - Style Style - // Row the row index of table cell - Row int - // Column the column index of table cell - Column int -} - -type TableChartOption struct { - // The output type - Type string - // The width of table - Width int - // The theme - Theme ColorPalette - // The padding of table cell - Padding Box - // The header data of table - Header []string - // The data of table - Data [][]string - // The span list of table column - Spans []int - // The text align list of table cell - TextAligns []string - // The font size of table - FontSize float64 - // The font family, which should be installed first - FontFamily string - Font *truetype.Font - // The font color of table - FontColor Color - // The background color of header - HeaderBackgroundColor Color - // The header font color - HeaderFontColor Color - // The background color of row - RowBackgroundColors []Color - // The background color - BackgroundColor Color - // CellTextStyle customize text style of table cell - CellTextStyle func(TableCell) *Style - // CellStyle customize drawing style of table cell - CellStyle func(TableCell) *Style -} - -type TableSetting struct { - // The color of header - HeaderColor Color - // The color of heder text - HeaderFontColor Color - // The color of table text - FontColor Color - // The color list of row - RowColors []Color - // The padding of cell - Padding Box -} - -var TableLightThemeSetting = TableSetting{ - HeaderColor: Color{ - R: 240, - G: 240, - B: 240, - A: 255, - }, - HeaderFontColor: Color{ - R: 98, - G: 105, - B: 118, - A: 255, - }, - FontColor: Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - RowColors: []Color{ - drawing.ColorWhite, - { - R: 247, - G: 247, - B: 247, - A: 255, - }, - }, - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, -} - -var TableDarkThemeSetting = TableSetting{ - HeaderColor: Color{ - R: 38, - G: 38, - B: 42, - A: 255, - }, - HeaderFontColor: Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - FontColor: Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - RowColors: []Color{ - { - R: 24, - G: 24, - B: 28, - A: 255, - }, - { - R: 38, - G: 38, - B: 42, - A: 255, - }, - }, - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, -} - -var tableDefaultSetting = TableLightThemeSetting - -// SetDefaultTableSetting sets the default setting for table -func SetDefaultTableSetting(setting TableSetting) { - tableDefaultSetting = setting -} - -type renderInfo struct { - Width int - Height int - HeaderHeight int - RowHeights []int - ColumnWidths []int -} - -func (t *tableChart) render() (*renderInfo, error) { - info := renderInfo{ - RowHeights: make([]int, 0), - } - p := t.p - opt := t.opt - if len(opt.Header) == 0 { - return nil, errors.New("header can not be nil") - } - theme := opt.Theme - if theme == nil { - theme = p.theme - } - fontSize := opt.FontSize - if fontSize == 0 { - fontSize = 12 - } - fontColor := opt.FontColor - if fontColor.IsZero() { - fontColor = tableDefaultSetting.FontColor - } - font := opt.Font - if font == nil { - font = theme.GetFont() - } - headerFontColor := opt.HeaderFontColor - if opt.HeaderFontColor.IsZero() { - headerFontColor = tableDefaultSetting.HeaderFontColor - } - - spans := opt.Spans - if len(spans) != len(opt.Header) { - newSpans := make([]int, len(opt.Header)) - for index := range opt.Header { - if index >= len(spans) { - newSpans[index] = 1 - } else { - newSpans[index] = spans[index] - } - } - spans = newSpans - } - - sum := sumInt(spans) - values := autoDivideSpans(p.Width(), sum, spans) - columnWidths := make([]int, 0) - for index, v := range values { - if index == len(values)-1 { - break - } - columnWidths = append(columnWidths, values[index+1]-v) - } - info.ColumnWidths = columnWidths - - height := 0 - textStyle := Style{ - FontSize: fontSize, - FontColor: headerFontColor, - FillColor: headerFontColor, - Font: font, - } - - headerHeight := 0 - padding := opt.Padding - if padding.IsZero() { - padding = tableDefaultSetting.Padding - } - getCellTextStyle := opt.CellTextStyle - if getCellTextStyle == nil { - getCellTextStyle = func(_ TableCell) *Style { - return nil - } - } - // textAligns := opt.TextAligns - getTextAlign := func(index int) string { - if len(opt.TextAligns) <= index { - return "" - } - return opt.TextAligns[index] - } - - // 表格单元的处理 - renderTableCells := func( - currentStyle Style, - rowIndex int, - textList []string, - currentHeight int, - cellPadding Box, - ) int { - cellMaxHeight := 0 - paddingHeight := cellPadding.Top + cellPadding.Bottom - paddingWidth := cellPadding.Left + cellPadding.Right - for index, text := range textList { - cellStyle := getCellTextStyle(TableCell{ - Text: text, - Row: rowIndex, - Column: index, - Style: currentStyle, - }) - if cellStyle == nil { - cellStyle = ¤tStyle - } - p.SetStyle(*cellStyle) - x := values[index] - y := currentHeight + cellPadding.Top - width := values[index+1] - x - x += cellPadding.Left - width -= paddingWidth - box := p.TextFit(text, x, y+int(fontSize), width, getTextAlign(index)) - // 计算最高的高度 - if box.Height()+paddingHeight > cellMaxHeight { - cellMaxHeight = box.Height() + paddingHeight - } - } - return cellMaxHeight - } - - // 表头的处理 - headerHeight = renderTableCells(textStyle, 0, opt.Header, height, padding) - height += headerHeight - info.HeaderHeight = headerHeight - - // 表格内容的处理 - textStyle.FontColor = fontColor - textStyle.FillColor = fontColor - for index, textList := range opt.Data { - cellHeight := renderTableCells(textStyle, index+1, textList, height, padding) - info.RowHeights = append(info.RowHeights, cellHeight) - height += cellHeight - } - - info.Width = p.Width() - info.Height = height - return &info, nil -} - -func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { - p := t.p - opt := t.opt - if !opt.BackgroundColor.IsZero() { - p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) - } - headerBGColor := opt.HeaderBackgroundColor - if headerBGColor.IsZero() { - headerBGColor = tableDefaultSetting.HeaderColor - } - - // 如果设置表头背景色 - p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) - currentHeight := info.HeaderHeight - rowColors := opt.RowBackgroundColors - if rowColors == nil { - rowColors = tableDefaultSetting.RowColors - } - for index, h := range info.RowHeights { - color := rowColors[index%len(rowColors)] - child := p.Child(PainterPaddingOption(Box{ - Top: currentHeight, - })) - child.SetBackground(p.Width(), h, color, true) - currentHeight += h - } - // 根据是否有设置表格样式调整背景色 - getCellStyle := opt.CellStyle - if getCellStyle != nil { - arr := [][]string{ - opt.Header, - } - arr = append(arr, opt.Data...) - top := 0 - heights := []int{ - info.HeaderHeight, - } - heights = append(heights, info.RowHeights...) - // 循环所有表格单元,生成背景色 - for i, textList := range arr { - left := 0 - for j, v := range textList { - style := getCellStyle(TableCell{ - Text: v, - Row: i, - Column: j, - }) - if style != nil && !style.FillColor.IsZero() { - padding := style.Padding - child := p.Child(PainterPaddingOption(Box{ - Top: top + padding.Top, - Left: left + padding.Left, - })) - w := info.ColumnWidths[j] - padding.Left - padding.Top - h := heights[i] - padding.Top - padding.Bottom - child.SetBackground(w, h, style.FillColor, true) - } - left += info.ColumnWidths[j] - } - top += heights[i] - } - } - _, err := t.render() - if err != nil { - return BoxZero, err - } - - return Box{ - Right: info.Width, - Bottom: info.Height, - }, nil -} - -func (t *tableChart) Render() (Box, error) { - p := t.p - opt := t.opt - if !opt.BackgroundColor.IsZero() { - p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) - } - if opt.Font == nil && opt.FontFamily != "" { - opt.Font, _ = GetFont(opt.FontFamily) - } - - r := p.render - fn := chart.PNG - if p.outputType == ChartOutputSVG { - fn = chart.SVG - } - newRender, err := fn(p.Width(), 100) - if err != nil { - return BoxZero, err - } - p.render = newRender - info, err := t.render() - if err != nil { - return BoxZero, err - } - p.render = r - return t.renderWithInfo(info) -} diff --git a/table_test.go b/table_test.go deleted file mode 100644 index a958c95..0000000 --- a/table_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// 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" -) - -func TestTableChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTableChart(p, TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - }, - Spans: []int{ - 1, - 1, - 2, - 1, - // span和header不匹配,最后自动设置为1 - // 1, - }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nNameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTableChart(p, TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nNameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/theme.go b/theme.go index 85016a5..26786b9 100644 --- a/theme.go +++ b/theme.go @@ -24,7 +24,8 @@ package charts import ( "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) const ThemeDark = "dark" @@ -35,19 +36,12 @@ const ThemeAnt = "ant" type ColorPalette interface { IsDark() bool GetAxisStrokeColor() Color - SetAxisStrokeColor(Color) GetAxisSplitLineColor() Color - SetAxisSplitLineColor(Color) GetSeriesColor(int) Color - SetSeriesColor([]Color) GetBackgroundColor() Color - SetBackgroundColor(Color) GetTextColor() Color - SetTextColor(Color) GetFontSize() float64 - SetFontSize(float64) GetFont() *truetype.Font - SetFont(*truetype.Font) } type themeColorPalette struct { @@ -70,25 +64,12 @@ type ThemeOption struct { SeriesColors []Color } -var palettes = map[string]*themeColorPalette{} +var palettes = map[string]ColorPalette{} const defaultFontSize = 12.0 var defaultTheme ColorPalette -var defaultLightFontColor = drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, -} -var defaultDarkFontColor = drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, -} - func init() { echartSeriesColors := []Color{ parseColor("#5470c6"), @@ -239,7 +220,6 @@ func init() { SetDefaultTheme(ThemeLight) } -// SetDefaultTheme sets default theme func SetDefaultTheme(name string) { defaultTheme = NewTheme(name) } @@ -260,8 +240,7 @@ func NewTheme(name string) ColorPalette { if !ok { p = palettes[ThemeLight] } - clone := *p - return &clone + return p } func (t *themeColorPalette) IsDark() bool { @@ -272,42 +251,23 @@ func (t *themeColorPalette) GetAxisStrokeColor() Color { return t.axisStrokeColor } -func (t *themeColorPalette) SetAxisStrokeColor(c Color) { - t.axisStrokeColor = c -} - func (t *themeColorPalette) GetAxisSplitLineColor() Color { return t.axisSplitLineColor } -func (t *themeColorPalette) SetAxisSplitLineColor(c Color) { - t.axisSplitLineColor = c -} - func (t *themeColorPalette) GetSeriesColor(index int) Color { colors := t.seriesColors return colors[index%len(colors)] } -func (t *themeColorPalette) SetSeriesColor(colors []Color) { - t.seriesColors = colors -} func (t *themeColorPalette) GetBackgroundColor() Color { return t.backgroundColor } -func (t *themeColorPalette) SetBackgroundColor(c Color) { - t.backgroundColor = c -} - func (t *themeColorPalette) GetTextColor() Color { return t.textColor } -func (t *themeColorPalette) SetTextColor(c Color) { - t.textColor = c -} - func (t *themeColorPalette) GetFontSize() float64 { if t.fontSize != 0 { return t.fontSize @@ -315,18 +275,10 @@ func (t *themeColorPalette) GetFontSize() float64 { return defaultFontSize } -func (t *themeColorPalette) SetFontSize(fontSize float64) { - t.fontSize = fontSize -} - func (t *themeColorPalette) GetFont() *truetype.Font { if t.font != nil { return t.font } - f, _ := GetDefaultFont() + f, _ := chart.GetDefaultFont() return f } - -func (t *themeColorPalette) SetFont(f *truetype.Font) { - t.font = f -} diff --git a/title.go b/title.go index 74ab4f9..a805c55 100644 --- a/title.go +++ b/title.go @@ -36,6 +36,10 @@ type TitleOption struct { Text string // Subtitle text, support \n for new line Subtext string + // // Title style + // Style Style + // // Subtitle style + // SubtextStyle Style // Distance between title component and the left side of the container. // It can be pixel value: 20, percentage value: 20%, // or position value: right, center. @@ -80,7 +84,6 @@ type titlePainter struct { opt *TitleOption } -// NewTitlePainter returns a title renderer func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter { return &titlePainter{ p: p, @@ -93,9 +96,6 @@ func (t *titlePainter) Render() (Box, error) { p := t.p theme := opt.Theme - if theme == nil { - theme = p.theme - } if opt.Text == "" && opt.Subtext == "" { return BoxZero, nil } diff --git a/title_test.go b/title_test.go deleted file mode 100644 index add8163..0000000 --- a/title_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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" -) - -func TestTitleRenderer(t *testing.T) { - assert := assert.New(t) - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTitlePainter(p, TitleOption{ - Text: "title", - Subtext: "subTitle", - Left: "20", - Top: "20", - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\ntitlesubTitle", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTitlePainter(p, TitleOption{ - Text: "title", - Subtext: "subTitle", - Left: "20%", - Top: "20", - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\ntitlesubTitle", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTitlePainter(p, TitleOption{ - Text: "title", - Subtext: "subTitle", - Left: PositionRight, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\ntitlesubTitle", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/util.go b/util.go index 87ff31c..adfa9fd 100644 --- a/util.go +++ b/util.go @@ -29,8 +29,8 @@ import ( "strings" "github.com/dustin/go-humanize" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TrueFlag() *bool { @@ -90,30 +90,6 @@ func autoDivide(max, size int) []int { return values } -func autoDivideSpans(max, size int, spans []int) []int { - values := autoDivide(max, size) - // 重新合并 - if len(spans) != 0 { - newValues := make([]int, len(spans)+1) - newValues[0] = 0 - end := 0 - for index, v := range spans { - end += v - newValues[index+1] = values[end] - } - values = newValues - } - return values -} - -func sumInt(values []int) int { - sum := 0 - for _, v := range values { - sum += v - } - return sum -} - // measureTextMaxWidthHeight returns maxWidth and maxHeight of text list func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) { maxWidth := 0 @@ -160,25 +136,15 @@ func NewFloatPoint(f float64) *float64 { v := f return &v } - -const K_VALUE = float64(1000) -const M_VALUE = K_VALUE * K_VALUE -const G_VALUE = M_VALUE * K_VALUE -const T_VALUE = G_VALUE * K_VALUE - func commafWithDigits(value float64) string { decimals := 2 - if value >= T_VALUE { - return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T" + m := float64(1000 * 1000) + if value >= m { + return humanize.CommafWithDigits(value/m, decimals) + "M" } - if value >= G_VALUE { - return humanize.CommafWithDigits(value/G_VALUE, decimals) + "G" - } - if value >= M_VALUE { - return humanize.CommafWithDigits(value/M_VALUE, decimals) + "M" - } - if value >= K_VALUE { - return humanize.CommafWithDigits(value/K_VALUE, decimals) + "k" + k := float64(1000) + if value >= k { + return humanize.CommafWithDigits(value/k, decimals) + "k" } return humanize.CommafWithDigits(value, decimals) } @@ -262,10 +228,3 @@ func getPolygonPoints(center Point, radius float64, sides int) []Point { } return points } - -func isLightColor(c Color) bool { - r := float64(c.R) * float64(c.R) * 0.299 - g := float64(c.G) * float64(c.G) * 0.587 - b := float64(c.B) * float64(c.B) * 0.114 - return math.Sqrt(r+g+b) > 127.5 -} diff --git a/util_test.go b/util_test.go index 5770776..7c2ab2f 100644 --- a/util_test.go +++ b/util_test.go @@ -26,8 +26,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestGetDefaultInt(t *testing.T) { @@ -189,35 +189,3 @@ func TestParseColor(t *testing.T) { A: 250, }, c) } - -func TestIsLightColor(t *testing.T) { - assert := assert.New(t) - - assert.True(isLightColor(drawing.Color{ - R: 255, - G: 255, - B: 255, - })) - assert.True(isLightColor(drawing.Color{ - R: 145, - G: 204, - B: 117, - })) - - assert.False(isLightColor(drawing.Color{ - R: 88, - G: 112, - B: 198, - })) - - assert.False(isLightColor(drawing.Color{ - R: 0, - G: 0, - B: 0, - })) - assert.False(isLightColor(drawing.Color{ - R: 16, - G: 12, - B: 42, - })) -} diff --git a/xaxis.go b/xaxis.go index 61698d7..bfb57cb 100644 --- a/xaxis.go +++ b/xaxis.go @@ -47,19 +47,12 @@ type XAxisOption struct { // The line color of axis StrokeColor Color // The color of label - FontColor Color - // The text rotation of label - TextRotation float64 - // The first axis - FirstAxis int - // The offset of label - LabelOffset Box + FontColor Color isValueAxis bool } const defaultXAxisHeight = 30 -// NewXAxisOption returns a x axis option func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { opt := XAxisOption{ Data: data, @@ -87,9 +80,6 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { FontColor: opt.FontColor, Show: opt.Show, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), - TextRotation: opt.TextRotation, - LabelOffset: opt.LabelOffset, - FirstAxis: opt.FirstAxis, } if opt.isValueAxis { axisOpt.SplitLineShow = true @@ -99,7 +89,6 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { return axisOpt } -// NewBottomXAxis returns a bottom x axis renderer func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { return NewAxisPainter(p, opt.ToAxisOption()) } diff --git a/yaxis.go b/yaxis.go index e58b7a6..265ac59 100644 --- a/yaxis.go +++ b/yaxis.go @@ -47,14 +47,9 @@ type YAxisOption struct { Color Color // The flag for show axis, set this to *false will hide axis Show *bool - DivideCount int - Unit int isCategoryAxis bool - // The flag for show axis split line, set this to true will show axis split line - SplitLineShow *bool } -// NewYAxisOptions returns a y axis option func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { arr := [][]string{ data, @@ -69,18 +64,14 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { return opts } -func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { +func (opt *YAxisOption) ToAxisOption() AxisOption { position := PositionLeft if opt.Position == PositionRight { position = PositionRight } - theme := opt.Theme - if theme == nil { - theme = p.theme - } axisOpt := AxisOption{ Formatter: opt.Formatter, - Theme: theme, + Theme: opt.Theme, Data: opt.Data, Position: position, FontSize: opt.FontSize, @@ -89,9 +80,8 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { FontColor: opt.FontColor, BoundaryGap: FalseFlag(), SplitLineShow: true, - SplitLineColor: theme.GetAxisSplitLineColor(), + SplitLineColor: opt.Theme.GetAxisSplitLineColor(), Show: opt.Show, - Unit: opt.Unit, } if !opt.Color.IsZero() { axisOpt.FontColor = opt.Color @@ -102,26 +92,21 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { axisOpt.StrokeWidth = 1 axisOpt.SplitLineShow = false } - if opt.SplitLineShow != nil { - axisOpt.SplitLineShow = *opt.SplitLineShow - } return axisOpt } -// NewLeftYAxis returns a left y axis renderer func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - return NewAxisPainter(p, opt.ToAxisOption(p)) + return NewAxisPainter(p, opt.ToAxisOption()) } -// NewRightYAxis returns a right y axis renderer func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - axisOpt := opt.ToAxisOption(p) + axisOpt := opt.ToAxisOption() axisOpt.Position = PositionRight axisOpt.SplitLineShow = false return NewAxisPainter(p, axisOpt) diff --git a/yaxis_test.go b/yaxis_test.go deleted file mode 100644 index 0f565ac..0000000 --- a/yaxis_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// 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" -) - -func TestRightYAxis(t *testing.T) { - assert := assert.New(t) - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - opt := NewYAxisOptions([]string{ - "a", - "b", - "c", - "d", - })[0] - _, err := NewRightYAxis(p, opt).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nabcd", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme), PainterPaddingOption(Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - })) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -}