diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 22e77a8..ce56fe7 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 2e33342..57206ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,5 @@
*.png
*.svg
tmp
+NotoSansSC.ttf
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index 8183871..0650395 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
# go-charts
+Clone from https://github.com/vicanso/go-charts
+
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[](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`.
+`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`.
`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`.
@@ -15,9 +17,13 @@ Screenshot of common charts, the left part is light theme, the right part is gra
+
+
+
+
## Chart Type
-These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`.
+These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`.
## Example
@@ -29,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() {
@@ -95,7 +101,7 @@ func main() {
package main
import (
- "github.com/vicanso/go-charts/v2"
+ "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@@ -170,7 +176,7 @@ func main() {
package main
import (
- "github.com/vicanso/go-charts/v2"
+ "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@@ -227,7 +233,7 @@ func main() {
package main
import (
- "github.com/vicanso/go-charts/v2"
+ "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@@ -282,7 +288,7 @@ func main() {
package main
import (
- "github.com/vicanso/go-charts/v2"
+ "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@@ -340,7 +346,7 @@ func main() {
package main
import (
- "github.com/vicanso/go-charts/v2"
+ "git.smarteching.com/zeni/go-charts/v2"
)
func main() {
@@ -374,13 +380,78 @@ 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 (
- "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 fed2d61..3f35b97 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -3,7 +3,7 @@
[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[](https://github.com/vicanso/go-charts/actions)
-`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。
+`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。
`Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana):
@@ -12,9 +12,13 @@
+
+
+
\\nMon Tue Wed Thu Fri Sat Sun ",
+ result: "\\nMon Tue Wed Thu Fri Sat Sun ",
},
// 右侧
{
@@ -135,7 +135,7 @@ func TestAxis(t *testing.T) {
}).Render()
return p.Bytes()
},
- result: "\\nMon Tue Wed Thu Fri Sat Sun ",
+ result: "\\nMon Tue Wed Thu Fri Sat Sun ",
},
// 顶部
{
diff --git a/bar_chart.go b/bar_chart.go
index 0ac9f47..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 {
@@ -32,6 +34,7 @@ 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
@@ -43,6 +46,7 @@ func NewBarChart(p *Painter, opt BarChartOption) *barChart {
}
type BarChartOption struct {
+ // The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@@ -57,14 +61,10 @@ type BarChartOption struct {
// The option of title
Title TitleOption
// The legend option
- Legend LegendOption
-}
-
-type barChartLabelRenderOption struct {
- Text string
- Style Style
- X int
- Y int
+ Legend LegendOption
+ BarWidth int
+ // Margin of bar
+ BarMargin int
}
func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@@ -73,6 +73,7 @@ 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(),
})
@@ -89,9 +90,17 @@ 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)) / len(seriesList)
+ 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
+ }
barMaxHeight := seriesPainter.Height()
theme := opt.Theme
seriesNames := seriesList.Names()
@@ -102,7 +111,6 @@ 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]
@@ -110,6 +118,18 @@ 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
@@ -127,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{
// 居中的位置
@@ -148,30 +179,33 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Y: top,
}
// 如果label不需要展示,则返回
- if !series.Label.Show {
+ if labelPainter == nil {
continue
}
- distance := series.Label.Distance
- if distance == 0 {
- distance = 5
+ 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
+ }
+ }
}
- 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,
+ 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,
})
}
@@ -190,10 +224,6 @@ 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 138b3ca..654c320 100644
--- a/bar_chart_test.go
+++ b/bar_chart_test.go
@@ -102,7 +102,77 @@ func TestBarChart(t *testing.T) {
}
return p.Bytes()
},
- result: "\\n240 200 160 120 80 40 0 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 2 4.9 7 23.2 25.6 76.7 135.6 162.2 32.6 20 6.4 3.3 2.6 5.9 9 26.4 28.7 70.7 175.6 182.2 48.7 18.8 6 2.3 ",
+ result: "\\n240 200 160 120 80 40 0 Feb May Aug Nov 2 4.9 7 23.2 25.6 76.7 135.6 162.2 32.6 20 6.4 3.3 2.6 5.9 9 26.4 28.7 70.7 175.6 182.2 48.7 18.8 6 2.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: "\\n240 200 160 120 80 40 0 Feb May Aug Nov 2 4.9 7 23.2 25.6 76.7 135.6 162.2 32.6 20 6.4 3.3 2.6 5.9 9 26.4 28.7 70.7 175.6 182.2 48.7 18.8 6 2.3 ",
},
}
diff --git a/chart_option.go b/chart_option.go
index f2f3b51..d80a383 100644
--- a/chart_option.go
+++ b/chart_option.go
@@ -26,7 +26,6 @@ import (
"sort"
"github.com/golang/freetype/truetype"
- "github.com/wcharczuk/go-chart/v2"
)
type ChartOption struct {
@@ -62,8 +61,24 @@ 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
@@ -108,9 +123,12 @@ func TitleOptionFunc(title TitleOption) OptionFunc {
}
// TitleTextOptionFunc set title text of chart
-func TitleTextOptionFunc(text string) OptionFunc {
+func TitleTextOptionFunc(text string, subtext ...string) OptionFunc {
return func(opt *ChartOption) {
opt.Title.Text = text
+ if len(subtext) != 0 {
+ opt.Title.Subtext = subtext[0]
+ }
}
}
@@ -255,7 +273,10 @@ func (o *ChartOption) fillDefault() {
o.font, _ = GetFont(o.FontFamily)
if o.font == nil {
- o.font, _ = chart.GetDefaultFont()
+ o.font, _ = GetDefaultFont()
+ } else {
+ // 如果指定了字体,则设置主题的字体
+ t.SetFont(o.font)
}
if o.BackgroundColor.IsZero() {
o.BackgroundColor = t.GetBackgroundColor()
@@ -336,3 +357,70 @@ 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 c77bb4f..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) {
@@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) {
assert.Nil(err)
data, err := p.Bytes()
assert.Nil(err)
- assert.Equal("\\nEmail Union Ads Video Ads Direct Search Engine Line 1.44k 1.2k 960 720 480 240 0 Mon Tue Wed Thu Fri Sat Sun ", string(data))
+ assert.Equal("\\nEmail Union Ads Video Ads Direct Search Engine Line 1.44k 1.2k 960 720 480 240 0 Mon Tue Wed Thu Fri Sat Sun ", 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("\\nRainfall Evaporation 240 200 160 120 80 40 0 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 162.2 2 182.2 2.3 41.62 48.07 ", string(data))
+ assert.Equal("\\nRainfall Evaporation 240 200 160 120 80 40 0 Feb May Aug Nov 162.2 2 182.2 2.3 41.62 48.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("\\n2011 2012 World Population World China India USA Indonesia Brazil 0 122.28k 244.56k 366.84k 489.12k 611.4k 733.68k ", string(data))
+ assert.Equal("\\n2011 2012 World Population World China India USA Indonesia Brazil 0 122.28k 244.56k 366.84k 489.12k 611.4k 733.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 Engine Direct Email Union Ads Video Ads Rainfall vs Evaporation Fake Data Search Engine: 33.3% Direct: 23.35% Email: 18.43% Union Ads: 15.37% Video Ads: 9.53% ", string(data))
+ assert.Equal("\\nSearch Engine Direct Email Union Ads Video Ads Rainfall vs Evaporation Fake Data Search 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 Budget Actual Spending Basic Radar Chart Sales Administration Information Technology Customer Support Development Marketing ", string(data))
+ assert.Equal("\\nAllocated Budget Actual Spending Basic Radar Chart Sales Administration Information Technology Customer Support Development Marketing ", 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("\\nShow Click Visit Inquiry Order Funnel Show(100%) Click(80%) Visit(60%) Inquiry(40%) Order(20%) ", string(data))
+ assert.Equal("\\nShow Click Visit Inquiry Order Funnel Show(100%) Click(80%) Visit(60%) Inquiry(40%) Order(20%) ", string(data))
}
diff --git a/charts.go b/charts.go
index cd1ac2b..31df11c 100644
--- a/charts.go
+++ b/charts.go
@@ -24,27 +24,46 @@ 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)
}
@@ -106,14 +125,16 @@ 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
}
- _, err := NewLegendPainter(p, opt.LegendOption).Render()
+ legendResult, err := NewLegendPainter(p, opt.LegendOption).Render()
if err != nil {
return nil, err
}
+ legendHeight = legendResult.Height()
}
// 如果有标题
@@ -127,9 +148,15 @@ 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: titleBox.Height() + 20,
+ Top: top + 20,
}))
}
@@ -159,21 +186,26 @@ 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{
- Min: min,
- Max: max,
+ Painter: p,
+ Min: min,
+ Max: max,
// 高度需要减去x轴的高度
Size: rangeHeight,
// 分隔数量
- DivideCount: defaultAxisDivideCount,
+ DivideCount: divideCount,
})
+ 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 {
@@ -183,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)
@@ -260,6 +301,9 @@ 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))
}
@@ -302,9 +346,8 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
TitleOption: opt.Title,
LegendOption: opt.Legend,
axisReversed: axisReversed,
- }
- if isChild {
- renderOpt.backgroundIsFilled = true
+ // 前置已设置背景色
+ backgroundIsFilled: true,
}
if len(pieSeriesList) != 0 ||
len(radarSeriesList) != 0 ||
@@ -316,6 +359,10 @@ 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 {
@@ -328,9 +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,
+ Theme: opt.theme,
+ Font: opt.font,
+ XAxis: opt.XAxis,
+ BarWidth: opt.BarWidth,
+ BarMargin: opt.BarMargin,
}).render(renderResult, barSeriesList)
return err
})
@@ -342,6 +391,8 @@ 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
@@ -363,9 +414,13 @@ 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,
+ Theme: opt.theme,
+ Font: opt.font,
+ XAxis: opt.XAxis,
+ 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
new file mode 100644
index 0000000..bd581e9
--- /dev/null
+++ b/charts_test.go
@@ -0,0 +1,255 @@
+// 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 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 9c31286..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("\\nRainfall Evaporation Rainfall vs Evaporation Fake Data 240 200 160 120 80 40 0 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 162.2 2 182.2 2.3 41.62 48.07 ", string(data))
+ assert.Equal("\\nRainfall Evaporation Rainfall vs Evaporation Fake Data 240 200 160 120 80 40 0 Feb May Aug Nov 162.2 2 182.2 2.3 41.62 48.07 ", string(data))
}
diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go
new file mode 100644
index 0000000..57ca1e9
--- /dev/null
+++ b/examples/area_line_chart/main.go
@@ -0,0 +1,73 @@
+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 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 0e1d48e..81bc4f2 100644
--- a/examples/charts/main.go
+++ b/examples/charts/main.go
@@ -2,10 +2,11 @@ package main
import (
"bytes"
+ "fmt"
"net/http"
"strconv"
- charts "github.com/vicanso/go-charts/v2"
+ charts "git.smarteching.com/zeni/go-charts/v2"
)
var html = `
@@ -92,6 +93,48 @@ 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)
@@ -219,6 +262,35 @@ 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{
@@ -283,6 +355,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Value: 180,
},
},
+ Label: charts.SeriesLabel{
+ Show: true,
+ Position: charts.PositionBottom,
+ },
},
},
},
@@ -1893,5 +1969,6 @@ 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 bb7cc00..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
}
@@ -25,7 +25,8 @@ func writeFile(buf []byte) error {
func main() {
// 字体文件需要自行下载
- buf, err := ioutil.ReadFile("../NotoSansSC.ttf")
+ // https://github.com/googlefonts/noto-cjk
+ buf, err := ioutil.ReadFile("./NotoSansSC.ttf")
if err != nil {
panic(err)
}
@@ -33,6 +34,8 @@ func main() {
if err != nil {
panic(err)
}
+ font, _ := charts.GetFont("noto")
+ charts.SetDefaultFont(font)
values := [][]float64{
{
@@ -83,8 +86,7 @@ func main() {
}
p, err := charts.LineRender(
values,
- charts.TitleTextOptionFunc("Line"),
- charts.FontFamilyOptionFunc("noto"),
+ charts.TitleTextOptionFunc("测试"),
charts.XAxisDataOptionFunc([]string{
"星期一",
"星期二",
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 8b996b6..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,20 +25,22 @@ func writeFile(buf []byte) error {
func main() {
values := [][]float64{
{
- 18203,
- 23489,
- 29034,
- 104970,
- 131744,
- 630230,
+ 10,
+ 30,
+ 50,
+ 70,
+ 90,
+ 110,
+ 130,
},
{
- 19325,
- 23438,
- 31000,
- 121594,
- 134141,
- 681807,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
},
}
p, err := charts.HorizontalBarRender(
@@ -56,6 +57,7 @@ func main() {
"2012",
}),
charts.YAxisDataOptionFunc([]string{
+ "UN",
"Brazil",
"Indonesia",
"USA",
@@ -63,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 45ff894..baee8a3 100644
--- a/examples/line_chart/main.go
+++ b/examples/line_chart/main.go
@@ -1,11 +1,11 @@
package main
import (
- "io/ioutil"
+ "fmt"
"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, "line-chart.png")
- err = ioutil.WriteFile(file, buf, 0600)
+ err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
@@ -29,7 +29,8 @@ func main() {
120,
132,
101,
- 134,
+ // 134,
+ charts.GetNullValue(),
90,
230,
210,
@@ -89,7 +90,23 @@ func main() {
"Video Ads",
"Direct",
"Search Engine",
- }, charts.PositionCenter),
+ }, "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)
+ }
+ },
)
if err != nil {
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
new file mode 100644
index 0000000..de994eb
--- /dev/null
+++ b/examples/table/main.go
@@ -0,0 +1,178 @@
+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
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 c40b51e..828654e 100644
--- a/font.go
+++ b/font.go
@@ -27,14 +27,18 @@ 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{}
var ErrFontNotExists = errors.New("font is not exists")
+var defaultFontFamily = "defaultFontFamily"
func init() {
- _ = InstallFont("roboto", roboto.Roboto)
+ name := "roboto"
+ _ = InstallFont(name, roboto.Roboto)
+ font, _ := GetFont(name)
+ SetDefaultFont(font)
}
// InstallFont installs the font for charts
@@ -47,6 +51,19 @@ 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 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 7c04bfe..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"
)
@@ -34,6 +31,7 @@ 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 {
@@ -44,6 +42,7 @@ 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
@@ -55,6 +54,7 @@ func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
}
type FunnelChartOption struct {
+ // The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@@ -92,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 66145c7..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.7.2
- 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-20220617043117-41969df76e82 // 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 5f953b0..3e1a48a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,23 +1,17 @@
-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/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=
+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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/grid.go b/grid.go
index 252fe2e..0ebd226 100644
--- a/grid.go
+++ b/grid.go
@@ -28,16 +28,27 @@ type gridPainter struct {
}
type GridPainterOption struct {
- StrokeWidth float64
- StrokeColor Color
- Column int
- Row int
- IgnoreFirstRow bool
- IgnoreLastRow bool
+ // 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
IgnoreFirstColumn bool
- IgnoreLastColumn bool
+ // Ignore last column
+ IgnoreLastColumn bool
}
+// NewGridPainter returns new a grid renderer
func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter {
return &gridPainter{
p: p,
@@ -72,6 +83,7 @@ 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 f6880dc..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) {
@@ -54,6 +54,24 @@ 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 fb23734..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 {
@@ -33,6 +33,7 @@ type horizontalBarChart struct {
}
type HorizontalBarChartOption struct {
+ // The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@@ -47,9 +48,13 @@ type HorizontalBarChartOption struct {
// The option of title
Title TitleOption
// The legend option
- Legend LegendOption
+ Legend LegendOption
+ BarHeight int
+ // Margin of bar
+ BarMargin int
}
+// NewHorizontalBarChart returns a horizontal bar chart renderer
func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart {
if opt.Theme == nil {
opt.Theme = defaultTheme
@@ -78,24 +83,46 @@ 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)) / len(seriesList)
+ 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
+ }
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
@@ -114,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/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go
index 5555df6..e078c4a 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: "\\n2011 2012 World Population World China India USA Indonesia Brazil 0 122.28k 244.56k 366.84k 489.12k 611.4k 733.68k ",
+ result: "\\n2011 2012 World Population World China India USA Indonesia Brazil 0 122.28k 244.56k 366.84k 489.12k 611.4k 733.68k ",
},
}
for _, tt := range tests {
diff --git a/legend.go b/legend.go
index 65db102..035642c 100644
--- a/legend.go
+++ b/legend.go
@@ -36,6 +36,7 @@ const IconRect = "rect"
const IconLineDot = "lineDot"
type LegendOption struct {
+ // The theme
Theme ColorPalette
// Text array of legend
Data []string
@@ -58,8 +59,11 @@ 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,
@@ -70,6 +74,7 @@ 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 {
@@ -81,6 +86,7 @@ func (opt *LegendOption) IsEmpty() bool {
return isEmpty
}
+// NewLegendPainter returns a legend renderer
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
return &legendPainter{
p: p,
@@ -107,9 +113,11 @@ func (l *legendPainter) Render() (Box, error) {
if opt.Left == "" {
opt.Left = PositionCenter
}
- p := l.p.Child(PainterPaddingOption(Box{
- Top: 5,
- }))
+ padding := opt.Padding
+ if padding.IsZero() {
+ padding.Top = 5
+ }
+ p := l.p.Child(PainterPaddingOption(padding))
p.SetTextStyle(Style{
FontSize: opt.FontSize,
FontColor: opt.FontColor,
@@ -131,13 +139,19 @@ 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)
@@ -166,8 +180,13 @@ 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
@@ -189,12 +208,22 @@ 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
@@ -203,7 +232,7 @@ func (l *legendPainter) Render() (Box, error) {
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
- x0 = drawIcon(0, x0)
+ x0 = drawIcon(y0, x0)
}
if opt.Orient == OrientVertical {
y0 += offset
@@ -212,10 +241,11 @@ func (l *legendPainter) Render() (Box, error) {
x0 += offset
y0 = y
}
+ height = y0 - startY + 10
}
return Box{
Right: width,
- Bottom: height,
+ Bottom: height + padding.Bottom + padding.Top,
}, nil
}
diff --git a/line_chart.go b/line_chart.go
index f171813..fb1d16a 100644
--- a/line_chart.go
+++ b/line_chart.go
@@ -23,8 +23,10 @@
package charts
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 {
@@ -32,6 +34,7 @@ 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
@@ -43,6 +46,7 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
}
type LineChartOption struct {
+ // The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@@ -58,8 +62,16 @@ 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) {
@@ -91,25 +103,82 @@ 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: defaultStrokeWidth,
+ StrokeWidth: strokeWidth,
+ }
+ if len(series.Style.StrokeDashArray) > 0 {
+ drawingStyle.StrokeDashArray = series.Style.StrokeDashArray
}
- 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)
@@ -121,7 +190,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
}
drawingStyle.StrokeWidth = 1
seriesPainter.SetDrawingStyle(drawingStyle)
- seriesPainter.Dots(points)
+ if !isFalse(opt.SymbolShow) {
+ seriesPainter.Dots(points)
+ }
markPointPainter.Add(markPointRenderOption{
FillColor: seriesColor,
Font: opt.Font,
diff --git a/line_chart_test.go b/line_chart_test.go
index 856cdf3..e169f90 100644
--- a/line_chart_test.go
+++ b/line_chart_test.go
@@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) {
}
return p.Bytes()
},
- result: "\\nEmail Union Ads Video Ads Direct Search Engine Line 1.44k 1.2k 960 720 480 240 0 Mon Tue Wed Thu Fri Sat Sun ",
+ result: "\\nEmail Union Ads Video Ads Direct Search Engine Line 1.44k 1.2k 960 720 480 240 0 Mon Tue Wed Thu Fri Sat Sun ",
},
{
render: func(p *Painter) ([]byte, error) {
@@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) {
}
return p.Bytes()
},
- result: "\\nEmail Union Ads Video Ads Direct Search Engine Line 1.44k 1.2k 960 720 480 240 0 Mon Tue Wed Thu Fri Sat Sun ",
+ result: "\\nEmail Union Ads Video Ads Direct Search Engine Line 1.44k 1.2k 960 720 480 240 0 Mon Tue Wed Thu Fri Sat Sun ",
},
}
diff --git a/mark_line.go b/mark_line.go
index a0efcfb..bc850bb 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,6 +48,7 @@ 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,
@@ -73,7 +74,7 @@ func (m *markLinePainter) Render() (Box, error) {
}
font := opt.Font
if font == nil {
- font, _ = chart.GetDefaultFont()
+ font, _ = GetDefaultFont()
}
summary := s.Summary()
for _, markLine := range s.MarkLine.Data {
diff --git a/mark_line_test.go b/mark_line_test.go
index 84152ce..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) {
@@ -55,6 +55,7 @@ func TestMarkLine(t *testing.T) {
StrokeColor: drawing.ColorBlack,
Series: series,
Range: NewRange(AxisRangeOption{
+ Painter: p,
Min: 0,
Max: 5,
Size: p.Height(),
@@ -67,7 +68,7 @@ func TestMarkLine(t *testing.T) {
}
return p.Bytes()
},
- result: "\\n3 2 1 ",
+ result: "\\n3 2 1 ",
},
}
for _, tt := range tests {
diff --git a/mark_point.go b/mark_point.go
index 3d43a73..fd8a88b 100644
--- a/mark_point.go
+++ b/mark_point.go
@@ -26,6 +26,7 @@ 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 {
@@ -54,6 +55,7 @@ type markPointRenderOption struct {
Points []Point
}
+// NewMarkPointPainter returns a mark point renderer
func NewMarkPointPainter(p *Painter) *markPointPainter {
return &markPointPainter{
p: p,
@@ -63,7 +65,6 @@ 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 {
@@ -75,15 +76,22 @@ func (m *markPointPainter) Render() (Box, error) {
if symbolSize == 0 {
symbolSize = 30
}
- painter.OverrideDrawingStyle(Style{
- FillColor: opt.FillColor,
- }).OverrideTextStyle(Style{
- FontColor: theme.GetTextColor(),
+ textStyle := Style{
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 {
@@ -95,6 +103,11 @@ 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 1a810cf..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) {
@@ -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 da07007..bee646f 100644
--- a/painter.go
+++ b/painter.go
@@ -28,9 +28,11 @@ 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
+
type Painter struct {
render chart.Renderer
box Box
@@ -38,6 +40,9 @@ type Painter struct {
parent *Painter
style Style
theme ColorPalette
+ // 类型
+ outputType string
+ valueFormatter ValueFormatter
}
type PainterOptions struct {
@@ -54,6 +59,8 @@ type PainterOptions struct {
type PainterOption func(*Painter)
type TicksOption struct {
+ // the first tick
+ First int
Length int
Orient string
Count int
@@ -66,11 +73,17 @@ 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
+ Column int
+ Row int
+ ColumnSpans []int
// 忽略不展示的column
IgnoreColumnLines []int
// 忽略不展示的row
@@ -136,14 +149,14 @@ func PainterWidthHeightOption(width, height int) PainterOption {
}
}
-// NewPainter creates a new painter
+// NewPainter creates a 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 := chart.GetDefaultFont()
+ f, err := GetDefaultFont()
if err != nil {
return nil, err
}
@@ -168,6 +181,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
Bottom: opts.Height,
},
font: font,
+ // 类型
+ outputType: opts.Type,
}
p.setOptions(opt...)
if p.theme == nil {
@@ -183,6 +198,9 @@ 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,
@@ -433,11 +451,18 @@ 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 index == 0 {
+ if y == int(math.MaxInt32) {
+ p.Stroke()
+ shouldMoveTo = true
+ continue
+ }
+ if shouldMoveTo || index == 0 {
p.MoveTo(x, y)
+ shouldMoveTo = false
} else {
p.LineTo(x, y)
}
@@ -467,7 +492,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter {
return p
}
-func (p *Painter) SetBackground(width, height int, color Color) *Painter {
+func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter {
r := p.render
s := chart.Style{
FillColor: color,
@@ -475,12 +500,20 @@ func (p *Painter) SetBackground(width, height int, color Color) *Painter {
// 背景色
p.SetDrawingStyle(s)
defer p.ResetStyle()
- // 设置背景色不使用box,因此不直接使用Painter
- r.MoveTo(0, 0)
- r.LineTo(width, 0)
- r.LineTo(width, height)
- r.LineTo(0, height)
- r.LineTo(0, 0)
+ 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)
+ }
p.FillStroke()
return p
}
@@ -532,7 +565,20 @@ func (p *Painter) Text(body string, x, y int) *Painter {
return p
}
-func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
+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
style.TextWrap = chart.TextWrapWord
@@ -541,11 +587,24 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
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()
- p.Text(line, x0, y0)
lineBox := r.MeasureText(line)
+ switch textAlign {
+ case AlignRight:
+ x0 += width - lineBox.Width()
+ case AlignCenter:
+ x0 += (width - lineBox.Width()) >> 1
+ }
+ p.Text(line, x0, y0)
output.Right = chart.MaxInt(lineBox.Right, output.Right)
output.Bottom += lineBox.Height()
if index < len(lines)-1 {
@@ -561,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
@@ -575,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 {
@@ -611,12 +674,15 @@ 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()
@@ -627,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 != 0 {
+ 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 {
@@ -651,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
}
@@ -690,8 +770,12 @@ func (p *Painter) Grid(opt GridOption) *Painter {
})
}
}
- if opt.Column > 0 {
- values := autoDivide(width, opt.Column)
+ columnCount := sumInt(opt.ColumnSpans)
+ if columnCount == 0 {
+ columnCount = opt.Column
+ }
+ if columnCount > 0 {
+ values := autoDivideSpans(width, columnCount, opt.ColumnSpans)
drawLines(values, opt.IgnoreColumnLines, true)
}
if opt.Row > 0 {
@@ -719,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()
@@ -734,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 8892563..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) {
@@ -143,13 +143,13 @@ func TestPainter(t *testing.T) {
fn: func(p *Painter) {
p.SetStyle(Style{
StrokeWidth: 1,
- StrokeColor: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
- FillColor: drawing.Color{
+ FillColor: 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: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
- FillColor: drawing.Color{
+ FillColor: 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: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
- FillColor: drawing.Color{
+ FillColor: 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: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
- FillColor: drawing.Color{
+ FillColor: 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: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
- FillColor: drawing.Color{
+ FillColor: 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: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
A: 255,
},
- FillColor: drawing.Color{
+ FillColor: 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: drawing.Color{
+ StrokeColor: Color{
R: 84,
G: 112,
B: 198,
@@ -297,7 +297,7 @@ func TestPainter(t *testing.T) {
{
fn: func(p *Painter) {
p.SetDrawingStyle(Style{
- FillColor: drawing.Color{
+ FillColor: Color{
R: 84,
G: 112,
B: 198,
@@ -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{
@@ -351,7 +374,7 @@ func TestPainterTextFit(t *testing.T) {
Type: ChartOutputSVG,
})
assert.Nil(err)
- f, _ := chart.GetDefaultFont()
+ f, _ := GetDefaultFont()
style := Style{
FontSize: 12,
FontColor: chart.ColorBlack,
diff --git a/pie_chart.go b/pie_chart.go
index 972b4c1..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 {
@@ -36,6 +36,7 @@ type pieChart struct {
}
type PieChartOption struct {
+ // The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@@ -51,6 +52,7 @@ 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
@@ -61,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))
@@ -97,90 +189,105 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
seriesNames = seriesList.Names()
}
theme := opt.Theme
- if len(values) == 1 {
+
+ 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 {
seriesPainter.OverrideDrawingStyle(Style{
StrokeWidth: 1,
- StrokeColor: theme.GetSeriesColor(0),
- FillColor: theme.GetSeriesColor(0),
+ StrokeColor: s.color,
+ FillColor: s.color,
})
- 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
- }
-
- // 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)
- }
- prevEndX = endx
- prevEndY = endy
-
- seriesPainter.MoveTo(startx, starty)
- seriesPainter.LineTo(endx, endy)
- offset := labelLineWidth
- if endx < cx {
- offset *= -1
- }
- seriesPainter.MoveTo(endx, endy)
- endx += offset
- seriesPainter.LineTo(endx, endy)
- seriesPainter.Stroke()
-
- textStyle := Style{
- FontColor: theme.GetTextColor(),
- FontSize: labelFontSize,
- Font: opt.Font,
- }
- 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)
+ 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
+ }
+ 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
+ }
+ 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 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 population Germany (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 population France (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 labels Label 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 5b8aa85..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 {
@@ -45,6 +46,7 @@ type RadarIndicator struct {
}
type RadarChartOption struct {
+ // The theme
Theme ColorPalette
// The font size
Font *truetype.Font
@@ -62,6 +64,7 @@ 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
@@ -76,6 +79,7 @@ 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
@@ -197,7 +201,11 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
continue
}
indicator := indicators[j]
- percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min)
+ var percent float64
+ offset := indicator.Max - indicator.Min
+ if offset > 0 {
+ percent = (item.Value - indicator.Min) / offset
+ }
r := percent * radius
p := getPolygonPoint(center, r, angles[j])
linePoints = append(linePoints, p)
@@ -223,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 d5a9ef7..ec64c2d 100644
--- a/range.go
+++ b/range.go
@@ -29,6 +29,7 @@ import (
const defaultAxisDivideCount = 6
type axisRange struct {
+ p *Painter
divideCount int
min float64
max float64
@@ -37,13 +38,20 @@ type axisRange struct {
}
type AxisRangeOption struct {
- Min float64
- Max float64
- Size int
- Boundary bool
+ 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
DivideCount int
}
+// NewRange returns a axis range
func NewRange(opt AxisRangeOption) axisRange {
max := opt.Max
min := opt.Min
@@ -54,7 +62,10 @@ func NewRange(opt AxisRangeOption) axisRange {
r := math.Abs(max - min)
// 最小单位计算
- unit := 2
+ unit := 1
+ if r > 5 {
+ unit = 2
+ }
if r > 10 {
unit = 4
}
@@ -79,7 +90,12 @@ 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,
@@ -88,18 +104,26 @@ 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 := commafWithDigits(v)
+ value := formatter(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))
}
@@ -108,10 +132,13 @@ 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 44c4749..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 {
@@ -36,6 +36,7 @@ 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 {
@@ -44,6 +45,7 @@ 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),
@@ -54,6 +56,7 @@ 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 {
@@ -76,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 (
@@ -117,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
@@ -129,6 +140,9 @@ type Series struct {
type SeriesList []Series
func (sl SeriesList) init() {
+ if len(sl) == 0 {
+ return
+ }
if sl[len(sl)-1].index != 0 {
return
}
@@ -159,6 +173,10 @@ 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
}
@@ -204,13 +222,19 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
}
type seriesSummary struct {
- MaxIndex int
- MaxValue float64
- MinIndex int
- MinValue float64
+ // 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
AverageValue float64
}
+// Summary get summary of series
func (s *Series) Summary() seriesSummary {
minIndex := -1
maxIndex := -1
@@ -237,6 +261,7 @@ 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 {
@@ -245,8 +270,10 @@ 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}"
@@ -254,13 +281,23 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
return NewLabelFormatter(seriesNames, layout)
}
-func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter {
+// 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 {
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
new file mode 100644
index 0000000..af873fc
--- /dev/null
+++ b/series_label.go
@@ -0,0 +1,148 @@
+// 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
new file mode 100644
index 0000000..40d2f91
--- /dev/null
+++ b/series_test.go
@@ -0,0 +1,89 @@
+// 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
new file mode 100644
index 0000000..ee8359c
--- /dev/null
+++ b/start_zh.md
@@ -0,0 +1,254 @@
+# 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
new file mode 100644
index 0000000..3e6f273
--- /dev/null
+++ b/table.go
@@ -0,0 +1,438 @@
+// 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
new file mode 100644
index 0000000..a958c95
--- /dev/null
+++ b/table_test.go
@@ -0,0 +1,140 @@
+// 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: "\\nName Age Address Tag Action 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: 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: "\\nName Age Address Tag Action 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 ",
+ },
+ }
+ 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 26786b9..85016a5 100644
--- a/theme.go
+++ b/theme.go
@@ -24,8 +24,7 @@ package charts
import (
"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/drawing"
)
const ThemeDark = "dark"
@@ -36,12 +35,19 @@ 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 {
@@ -64,12 +70,25 @@ type ThemeOption struct {
SeriesColors []Color
}
-var palettes = map[string]ColorPalette{}
+var palettes = map[string]*themeColorPalette{}
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"),
@@ -220,6 +239,7 @@ func init() {
SetDefaultTheme(ThemeLight)
}
+// SetDefaultTheme sets default theme
func SetDefaultTheme(name string) {
defaultTheme = NewTheme(name)
}
@@ -240,7 +260,8 @@ func NewTheme(name string) ColorPalette {
if !ok {
p = palettes[ThemeLight]
}
- return p
+ clone := *p
+ return &clone
}
func (t *themeColorPalette) IsDark() bool {
@@ -251,23 +272,42 @@ 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
@@ -275,10 +315,18 @@ 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, _ := chart.GetDefaultFont()
+ f, _ := GetDefaultFont()
return f
}
+
+func (t *themeColorPalette) SetFont(f *truetype.Font) {
+ t.font = f
+}
diff --git a/title.go b/title.go
index a805c55..74ab4f9 100644
--- a/title.go
+++ b/title.go
@@ -36,10 +36,6 @@ 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.
@@ -84,6 +80,7 @@ type titlePainter struct {
opt *TitleOption
}
+// NewTitlePainter returns a title renderer
func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter {
return &titlePainter{
p: p,
@@ -96,6 +93,9 @@ 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
new file mode 100644
index 0000000..add8163
--- /dev/null
+++ b/title_test.go
@@ -0,0 +1,93 @@
+// 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: "\\ntitle subTitle ",
+ },
+ {
+ 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: "\\ntitle subTitle ",
+ },
+ {
+ 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: "\\ntitle subTitle ",
+ },
+ }
+ 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 adfa9fd..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 {
@@ -90,6 +90,30 @@ 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
@@ -136,15 +160,25 @@ 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
- m := float64(1000 * 1000)
- if value >= m {
- return humanize.CommafWithDigits(value/m, decimals) + "M"
+ if value >= T_VALUE {
+ return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T"
}
- k := float64(1000)
- if value >= k {
- return humanize.CommafWithDigits(value/k, decimals) + "k"
+ 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"
}
return humanize.CommafWithDigits(value, decimals)
}
@@ -228,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 bfb57cb..61698d7 100644
--- a/xaxis.go
+++ b/xaxis.go
@@ -47,12 +47,19 @@ 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
}
const defaultXAxisHeight = 30
+// NewXAxisOption returns a x axis option
func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
opt := XAxisOption{
Data: data,
@@ -80,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
@@ -89,6 +99,7 @@ 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 265ac59..e58b7a6 100644
--- a/yaxis.go
+++ b/yaxis.go
@@ -47,9 +47,14 @@ 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,
@@ -64,14 +69,18 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption {
return opts
}
-func (opt *YAxisOption) ToAxisOption() AxisOption {
+func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
position := PositionLeft
if opt.Position == PositionRight {
position = PositionRight
}
+ theme := opt.Theme
+ if theme == nil {
+ theme = p.theme
+ }
axisOpt := AxisOption{
Formatter: opt.Formatter,
- Theme: opt.Theme,
+ Theme: theme,
Data: opt.Data,
Position: position,
FontSize: opt.FontSize,
@@ -80,8 +89,9 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
FontColor: opt.FontColor,
BoundaryGap: FalseFlag(),
SplitLineShow: true,
- SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
+ SplitLineColor: theme.GetAxisSplitLineColor(),
Show: opt.Show,
+ Unit: opt.Unit,
}
if !opt.Color.IsZero() {
axisOpt.FontColor = opt.Color
@@ -92,21 +102,26 @@ func (opt *YAxisOption) ToAxisOption() 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())
+ return NewAxisPainter(p, opt.ToAxisOption(p))
}
+// NewRightYAxis returns a right y axis renderer
func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
p = p.Child(PainterPaddingOption(Box{
Bottom: defaultXAxisHeight,
}))
- axisOpt := opt.ToAxisOption()
+ axisOpt := opt.ToAxisOption(p)
axisOpt.Position = PositionRight
axisOpt.SplitLineShow = false
return NewAxisPainter(p, axisOpt)
diff --git a/yaxis_test.go b/yaxis_test.go
new file mode 100644
index 0000000..0f565ac
--- /dev/null
+++ b/yaxis_test.go
@@ -0,0 +1,70 @@
+// 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: "\\na b c d ",
+ },
+ }
+ 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))
+ }
+}