diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61449a3..ce56fe7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,13 +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/README.md b/README.md index 1e4ea8b..0650395 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 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) @@ -33,7 +35,7 @@ More examples can be found in the [./examples/](./examples/) directory. package main import ( - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -99,7 +101,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -174,7 +176,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -231,7 +233,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -286,7 +288,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -344,7 +346,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -384,7 +386,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -449,7 +451,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index c31cf77..3f35b97 100644 --- a/README_zh.md +++ b/README_zh.md @@ -32,7 +32,7 @@ package main import ( - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -98,7 +98,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -173,7 +173,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -230,7 +230,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -285,7 +285,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -343,7 +343,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -383,7 +383,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -447,7 +447,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { diff --git a/alias.go b/alias.go index a96f50b..edf0dec 100644 --- a/alias.go +++ b/alias.go @@ -23,8 +23,8 @@ package charts import ( - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type Box = chart.Box diff --git a/axis.go b/axis.go index 578813c..55fa219 100644 --- a/axis.go +++ b/axis.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type axisPainter struct { @@ -63,6 +63,8 @@ type AxisOption struct { StrokeWidth float64 // The length of the axis tick TickLength int + // The first axis + FirstAxis int // The margin value of label LabelMargin int // The font size of label @@ -75,7 +77,11 @@ type AxisOption struct { SplitLineShow bool // The color of split line SplitLineColor Color - Unit int + // The text rotation of label + TextRotation float64 + // The offset of label + LabelOffset Box + Unit int } func (a *axisPainter) Render() (Box, error) { @@ -153,7 +159,15 @@ func (a *axisPainter) Render() (Box, error) { } top.SetDrawingStyle(style).OverrideTextStyle(style) + isTextRotation := opt.TextRotation != 0 + + if isTextRotation { + top.SetTextRotation(opt.TextRotation) + } textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) + if isTextRotation { + top.ClearTextRotation() + } // 增加30px来计算文本展示区域 textFillWidth := float64(textMaxWidth + 20) @@ -243,6 +257,7 @@ func (a *axisPainter) Render() (Box, error) { Length: tickLength, Unit: unit, Orient: orient, + First: opt.FirstAxis, }) p.LineStroke([]Point{ { @@ -261,11 +276,14 @@ func (a *axisPainter) Render() (Box, error) { Top: labelPaddingTop, Right: labelPaddingRight, })).MultiText(MultiTextOption{ - Align: textAlign, - TextList: data, - Orient: orient, - Unit: unit, - Position: labelPosition, + First: opt.FirstAxis, + Align: textAlign, + TextList: data, + Orient: orient, + Unit: unit, + Position: labelPosition, + TextRotation: opt.TextRotation, + Offset: opt.LabelOffset, }) // 显示辅助线 if opt.SplitLineShow { diff --git a/axis_test.go b/axis_test.go index d0cff41..85e18ca 100644 --- a/axis_test.go +++ b/axis_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestAxis(t *testing.T) { diff --git a/bar_chart.go b/bar_chart.go index 19c1664..043e044 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,8 +23,10 @@ package charts import ( + "math" + "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type barChart struct { @@ -61,6 +63,8 @@ type BarChartOption struct { // The legend option Legend LegendOption BarWidth int + // Margin of bar + BarMargin int } func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -86,6 +90,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 @@ -140,14 +147,25 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } top := barMaxHeight - h - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }) + 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) + } // 用于生成marker point points[j] = Point{ // 居中的位置 @@ -164,11 +182,30 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if labelPainter == nil { 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 + } + } + } labelPainter.Add(LabelValue{ Index: index, Value: item.Value, X: x + barWidth>>1, - Y: barMaxHeight - h, + Y: y, + // 旋转 + Radians: radians, + FontColor: fontColor, + Offset: series.Label.Offset, + FontSize: series.Label.FontSize, }) } diff --git a/bar_chart_test.go b/bar_chart_test.go index e1522d6..654c320 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -104,6 +104,76 @@ func TestBarChart(t *testing.T) { }, 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", + }, } for _, tt := range tests { diff --git a/chart_option.go b/chart_option.go index ee6851f..d80a383 100644 --- a/chart_option.go +++ b/chart_option.go @@ -67,10 +67,14 @@ type ChartOption struct { 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 diff --git a/chart_option_test.go b/chart_option_test.go index 6f331b3..c354b26 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestChartOption(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("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/charts.go b/charts.go index d6745d3..31df11c 100644 --- a/charts.go +++ b/charts.go @@ -27,7 +27,7 @@ import ( "math" "sort" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) const labelFontSize = 10 @@ -215,7 +215,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e yAxisOption.Data = r.Values() } else { yAxisOption.isCategoryAxis = true - opt.XAxis.Data = r.Values() + // 由于x轴为value部分,因此计算其label单独处理 + opt.XAxis.Data = NewRange(AxisRangeOption{ + Painter: p, + Min: min, + Max: max, + // 高度需要减去x轴的高度 + Size: rangeHeight, + // 分隔数量 + DivideCount: defaultAxisDivideCount, + }).Values() opt.XAxis.isValueAxis = true } reverseStringSlice(yAxisOption.Data) @@ -366,10 +375,11 @@ 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, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + BarWidth: opt.BarWidth, + BarMargin: opt.BarMargin, }).render(renderResult, barSeriesList) return err }) @@ -382,6 +392,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { Theme: opt.theme, Font: opt.font, BarHeight: opt.BarHeight, + BarMargin: opt.BarMargin, YAxisOptions: opt.YAxisOptions, }).render(renderResult, horizontalBarSeriesList) return err @@ -409,6 +420,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { SymbolShow: opt.SymbolShow, StrokeWidth: opt.LineStrokeWidth, FillArea: opt.FillArea, + Opacity: opt.Opacity, }).render(renderResult, lineSeriesList) return err }) diff --git a/charts_test.go b/charts_test.go index da75ee5..bd581e9 100644 --- a/charts_test.go +++ b/charts_test.go @@ -26,7 +26,7 @@ import ( "errors" "testing" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) func BenchmarkMultiChartPNGRender(b *testing.B) { diff --git a/echarts.go b/echarts.go index fbe9a36..aaef1f1 100644 --- a/echarts.go +++ b/echarts.go @@ -29,7 +29,7 @@ import ( "regexp" "strconv" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) func convertToArray(data []byte) []byte { @@ -344,6 +344,11 @@ 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 5c2dbad..2077278 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -27,7 +27,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/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 Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go index 7a84df0..57ca1e9 100644 --- a/examples/area_line_chart/main.go +++ b/examples/area_line_chart/main.go @@ -1,11 +1,10 @@ package main import ( - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "area-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index c559a76..91c9f81 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -1,11 +1,10 @@ package main import ( - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "bar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/charts/main.go b/examples/charts/main.go index c3bb486..81bc4f2 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -6,7 +6,7 @@ import ( "net/http" "strconv" - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) var html = ` @@ -355,6 +355,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { Value: 180, }, }, + Label: charts.SeriesLabel{ + Show: true, + Position: charts.PositionBottom, + }, }, }, }, diff --git a/examples/chinese/main.go b/examples/chinese/main.go index d77216a..601f54e 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/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 = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 8f21db6..653f834 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -1,11 +1,10 @@ package main import ( - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "funnel-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } @@ -30,6 +29,8 @@ func main() { 60, 40, 20, + 10, + 0, } p, err := charts.FunnelRender( values, @@ -40,6 +41,8 @@ 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 a0f5bda..f5d8497 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -1,11 +1,10 @@ package main import ( - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "horizontal-bar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } @@ -26,22 +25,22 @@ func writeFile(buf []byte) error { func main() { values := [][]float64{ { - 8203, - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, + 10, + 30, + 50, + 70, + 90, + 110, + 130, }, { - 9325, - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, + 20, + 40, + 60, + 80, + 100, + 120, + 140, }, } p, err := charts.HorizontalBarRender( @@ -66,6 +65,9 @@ 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 c1478a6..baee8a3 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -2,11 +2,10 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -17,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } @@ -97,6 +96,11 @@ func main() { Top: 5, Bottom: 10, } + opt.YAxisOptions = []charts.YAxisOption{ + { + SplitLineShow: charts.FalseFlag(), + }, + } opt.SymbolShow = charts.FalseFlag() opt.LineStrokeWidth = 1 opt.ValueFormatter = func(f float64) string { diff --git a/examples/painter/main.go b/examples/painter/main.go index 3c31ce4..1b842b3 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -1,12 +1,11 @@ package main import ( - "io/ioutil" "os" "path/filepath" - charts "github.com/vicanso/go-charts/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + charts "git.smarteching.com/zeni/go-charts/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func writeFile(buf []byte) error { @@ -17,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "painter.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 3721ed1..5d70438 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -1,11 +1,10 @@ package main import ( - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "pie-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index 51f7409..e7053af 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -1,11 +1,10 @@ package main import ( - "io/ioutil" "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "radar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/table/main.go b/examples/table/main.go index 2701ec1..de994eb 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -1,14 +1,13 @@ package main import ( - "io/ioutil" "os" "path/filepath" "strconv" "strings" - "github.com/vicanso/go-charts/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-charts/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func writeFile(buf []byte, filename string) error { @@ -19,7 +18,7 @@ func writeFile(buf []byte, filename string) error { } file := filepath.Join(tmpPath, filename) - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go new file mode 100644 index 0000000..c6c93bf --- /dev/null +++ b/examples/time_line_chart/main.go @@ -0,0 +1,81 @@ +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 dae5141..828654e 100644 --- a/font.go +++ b/font.go @@ -27,7 +27,7 @@ import ( "sync" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/roboto" + "git.smarteching.com/zeni/go-chart/v2/roboto" ) var fonts = sync.Map{} diff --git a/font_test.go b/font_test.go index 9dc731c..e0c56b2 100644 --- a/font_test.go +++ b/font_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/roboto" + "git.smarteching.com/zeni/go-chart/v2/roboto" ) func TestInstallFont(t *testing.T) { diff --git a/funnel_chart.go b/funnel_chart.go index 719853a..d4a8bdd 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -23,9 +23,6 @@ package charts import ( - "fmt" - - "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" ) @@ -95,13 +92,23 @@ 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 - widthPercent := (value - min) / (max - min) + // 最大最小值一致则为100% + widthPercent := 100.0 + if offset != 0 { + widthPercent = (value - min) / offset + } w := int(widthPercent * float64(width)) widthList[index] = w - p := humanize.CommafWithDigits(value/max*100, 2) + "%" - textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) + // 如果最大值为0,则占比100% + percent := 1.0 + if max != 0 { + percent = value / max + } + textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) } for index, w := range widthList { diff --git a/go.mod b/go.mod index de0bb9c..76a47b6 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ -module github.com/vicanso/go-charts/v2 +module git.smarteching.com/zeni/go-charts/v2 -go 1.17 +go 1.24.1 require ( - github.com/dustin/go-humanize v1.0.0 + git.smarteching.com/zeni/go-chart/v2 v2.1.4 + github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.0 - github.com/wcharczuk/go-chart/v2 v2.1.0 + github.com/stretchr/testify v1.10.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.0.0-20220902085622-e7cb96979f69 // indirect + golang.org/x/image v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e0b1547..3e1a48a 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,18 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.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.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -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-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/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= +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= 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid_test.go b/grid_test.go index 3110a2b..fa9c3a6 100644 --- a/grid_test.go +++ b/grid_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestGrid(t *testing.T) { diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 58c6e19..ed091c9 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type horizontalBarChart struct { @@ -50,6 +50,8 @@ type HorizontalBarChartOption struct { // The legend option Legend LegendOption BarHeight int + // Margin of bar + BarMargin int } // NewHorizontalBarChart returns a horizontal bar chart renderer @@ -81,6 +83,9 @@ 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 @@ -99,11 +104,25 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri 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 @@ -122,16 +141,57 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri fillColor = item.Style.FillColor } right := w - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }) + 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) } } + err := doRender(rendererList...) + if err != nil { + return BoxZero, err + } return p.box, nil } diff --git a/line_chart.go b/line_chart.go index 26f94a4..fb1d16a 100644 --- a/line_chart.go +++ b/line_chart.go @@ -26,7 +26,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type lineChart struct { @@ -70,6 +70,8 @@ type LineChartOption struct { FillArea bool // background is filled backgroundIsFilled bool + // background fill (alpha) opacity + Opacity uint8 } func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -113,6 +115,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( StrokeColor: seriesColor, StrokeWidth: strokeWidth, } + if len(series.Style.StrokeDashArray) > 0 { + drawingStyle.StrokeDashArray = series.Style.StrokeDashArray + } yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) @@ -147,6 +152,8 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Value: item.Value, X: p.X, Y: p.Y, + // 字体大小 + FontSize: series.Label.FontSize, }) } // 如果需要填充区域 @@ -154,6 +161,10 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( 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, @@ -162,7 +173,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Y: bottomY, }, areaPoints[0]) seriesPainter.SetDrawingStyle(Style{ - FillColor: seriesColor.WithAlpha(200), + FillColor: seriesColor.WithAlpha(opacity), }) seriesPainter.FillArea(areaPoints) } diff --git a/mark_line_test.go b/mark_line_test.go index 00d19ef..0448cda 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestMarkLine(t *testing.T) { diff --git a/mark_point.go b/mark_point.go index f6c93f3..fd8a88b 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" ) // NewMarkPoint returns a series mark point @@ -78,16 +77,15 @@ func (m *markPointPainter) Render() (Box, error) { symbolSize = 30 } textStyle := Style{ - FontColor: drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, 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) diff --git a/mark_point_test.go b/mark_point_test.go index ffa01a7..298345b 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestMarkPoint(t *testing.T) { diff --git a/painter.go b/painter.go index 97ad205..bee646f 100644 --- a/painter.go +++ b/painter.go @@ -28,7 +28,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type ValueFormatter func(float64) string @@ -59,6 +59,8 @@ type PainterOptions struct { type PainterOption func(*Painter) type TicksOption struct { + // the first tick + First int Length int Orient string Count int @@ -71,6 +73,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 { @@ -558,6 +565,19 @@ 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 { style := p.style textWarp := style.TextWrap @@ -600,6 +620,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { return p } count := opt.Count + first := opt.First width := p.Width() height := p.Height() unit := 1 @@ -614,7 +635,10 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { values = autoDivide(width, count) } for index, value := range values { - if index%unit != 0 { + if index < first { + continue + } + if (index-first)%unit != 0 { continue } if isVertical { @@ -669,10 +693,19 @@ 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 opt.Unit != 0 && index%opt.Unit != showIndex { + if index < opt.First { 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 { @@ -693,8 +726,13 @@ 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 } @@ -765,6 +803,48 @@ 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() @@ -780,3 +860,7 @@ 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 2392d5b..07c4113 100644 --- a/painter_test.go +++ b/painter_test.go @@ -28,8 +28,8 @@ import ( "github.com/golang/freetype/truetype" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestPainterOption(t *testing.T) { @@ -343,6 +343,29 @@ 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{ diff --git a/pie_chart.go b/pie_chart.go index b4714ac..5c04ed8 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -27,7 +27,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type pieChart struct { @@ -63,6 +63,96 @@ 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)) @@ -101,98 +191,103 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B theme := opt.Theme currentValue := float64(0) - prevPoints := make([]Point, 0) - isOverride := func(x, y int) bool { - for _, p := range prevPoints { - if math.Abs(float64(p.Y-y)) > labelFontSize { - continue - } - // label可能较多内容,不好计算横向占用空间 - // 因此x的位置需要中间位置两侧,否则认为override - if (p.X <= cx && x <= cx) || - (p.X > cx && x > cx) { - return true + 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) } } - return false + 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...) - for index, v := range values { + currentQuadrant := 0 + prevY := 0 + maxY := 0 + minY := 0 + for _, s := range sectors { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(index), - FillColor: theme.GetSeriesColor(index), + StrokeColor: s.color, + FillColor: s.color, }) - 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 { + 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 } - - // 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坐标位置 - // 最多只尝试5次 - for i := 0; i < 5; i++ { - if !isOverride(endx, endy) { - break + if currentQuadrant != s.quadrant { + if s.quadrant == 1 { + minY = cy * 2 + maxY = 0 + prevY = cy * 2 } - endy -= (labelFontSize << 1) + if s.quadrant == 2 { + if currentQuadrant != 3 { + prevY = s.lineEndY + } else { + prevY = minY + } + } + if s.quadrant == 3 { + if currentQuadrant != 4 { + prevY = s.lineEndY + } else { + minY = cy * 2 + maxY = 0 + prevY = 0 + } + } + if s.quadrant == 4 { + if currentQuadrant != 1 { + prevY = s.lineEndY + } else { + prevY = maxY + } + } + currentQuadrant = s.quadrant } - prevPoints = append(prevPoints, Point{ - X: endx, - Y: endy, - }) - - seriesPainter.MoveTo(startx, starty) - seriesPainter.LineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 + prevY = s.calculateY(prevY) + if prevY > maxY { + maxY = prevY } - seriesPainter.MoveTo(endx, endy) - endx += offset - seriesPainter.LineTo(endx, endy) + 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 !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color + if !s.series.Label.Color.IsZero() { + textStyle.FontColor = s.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) + 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 c373a7e..3795d32 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -23,6 +23,7 @@ package charts import ( + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -98,3 +99,435 @@ 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 429850d..cf18135 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -25,9 +25,10 @@ package charts import ( "errors" + "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type radarChart struct { @@ -230,9 +231,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) StrokeColor: color, FillColor: dotFillColor, }) - for _, point := range linePoints { + for index, 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 51d3332..ec64c2d 100644 --- a/range.go +++ b/range.go @@ -121,6 +121,9 @@ func (r axisRange) Values() []string { } 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)) } diff --git a/series.go b/series.go index 7bd6834..da50e64 100644 --- a/series.go +++ b/series.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type SeriesData struct { @@ -79,6 +79,12 @@ 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 ( @@ -120,6 +126,8 @@ 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 @@ -273,6 +281,14 @@ 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 { if len(layout) == 0 { diff --git a/series_label.go b/series_label.go index 57bd1bf..af873fc 100644 --- a/series_label.go +++ b/series_label.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type labelRenderValue struct { @@ -32,6 +32,8 @@ type labelRenderValue struct { Style Style X int Y int + // 旋转 + Radians float64 } type LabelValue struct { @@ -39,6 +41,14 @@ type LabelValue struct { Value float64 X int Y int + // 旋转 + Radians float64 + // 字体颜色 + FontColor Color + // 字体大小 + FontSize float64 + Orient string + Offset Box } type SeriesLabelPainter struct { @@ -81,27 +91,58 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { 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 } - o.p.OverrideDrawingStyle(labelStyle) - textBox := o.p.MeasureText(text) + 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 - textBox.Width()>>1, - Y: value.Y - distance, + Text: text, + Style: labelStyle, + X: value.X, + Y: value.Y, + Radians: value.Radians, } - if textBox.Width()%2 != 0 { - renderValue.X++ + 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) - o.p.Text(item.Text, item.X, item.Y) + 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/table.go b/table.go index 86ef569..3e6f273 100644 --- a/table.go +++ b/table.go @@ -26,8 +26,8 @@ import ( "errors" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type tableChart struct { diff --git a/theme.go b/theme.go index 17706ad..85016a5 100644 --- a/theme.go +++ b/theme.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) const ThemeDark = "dark" @@ -76,6 +76,19 @@ 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"), diff --git a/util.go b/util.go index f8a451e..87ff31c 100644 --- a/util.go +++ b/util.go @@ -29,8 +29,8 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TrueFlag() *bool { @@ -262,3 +262,10 @@ 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 7c2ab2f..5770776 100644 --- a/util_test.go +++ b/util_test.go @@ -26,8 +26,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestGetDefaultInt(t *testing.T) { @@ -189,3 +189,35 @@ 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 00636a5..61698d7 100644 --- a/xaxis.go +++ b/xaxis.go @@ -47,7 +47,13 @@ type XAxisOption struct { // The line color of axis StrokeColor Color // The color of label - FontColor Color + FontColor Color + // The text rotation of label + TextRotation float64 + // The first axis + FirstAxis int + // The offset of label + LabelOffset Box isValueAxis bool } @@ -81,6 +87,9 @@ 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 diff --git a/yaxis.go b/yaxis.go index bece2cc..e58b7a6 100644 --- a/yaxis.go +++ b/yaxis.go @@ -50,6 +50,8 @@ type YAxisOption struct { 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 @@ -100,6 +102,9 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { axisOpt.StrokeWidth = 1 axisOpt.SplitLineShow = false } + if opt.SplitLineShow != nil { + axisOpt.SplitLineShow = *opt.SplitLineShow + } return axisOpt }