commit
635e440e85
64 changed files with 5261 additions and 6446 deletions
404
README.md
404
README.md
|
|
@ -21,53 +21,45 @@ These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The example is for `golang option` and `echarts option`, more examples can be found in the `./examples/` directory.
|
More examples can be found in the [./examples/](./examples/) directory.
|
||||||
|
|
||||||
|
|
||||||
|
### Line Chart
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
charts "github.com/vicanso/go-charts/v2"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
charts "github.com/vicanso/go-charts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeFile(file string, buf []byte) error {
|
func main() {
|
||||||
tmpPath := "./tmp"
|
values := [][]float64{
|
||||||
err := os.MkdirAll(tmpPath, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file = filepath.Join(tmpPath, file)
|
|
||||||
err = os.WriteFile(file, buf, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func chartsRender() ([]byte, error) {
|
|
||||||
d, err := charts.LineRender([][]float64{
|
|
||||||
{
|
{
|
||||||
150,
|
120,
|
||||||
|
132,
|
||||||
|
101,
|
||||||
|
134,
|
||||||
|
90,
|
||||||
230,
|
230,
|
||||||
224,
|
210,
|
||||||
218,
|
|
||||||
135,
|
|
||||||
147,
|
|
||||||
260,
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
},
|
},
|
||||||
// output type
|
{
|
||||||
charts.PNGTypeOption(),
|
// snip...
|
||||||
// title
|
},
|
||||||
charts.TitleOptionFunc(charts.TitleOption{
|
{
|
||||||
Text: "Line",
|
// snip...
|
||||||
}),
|
},
|
||||||
// x axis
|
{
|
||||||
charts.XAxisOptionFunc(charts.NewXAxisOption([]string{
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.LineRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Line"),
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
"Mon",
|
"Mon",
|
||||||
"Tue",
|
"Tue",
|
||||||
"Wed",
|
"Wed",
|
||||||
|
|
@ -75,16 +67,324 @@ func chartsRender() ([]byte, error) {
|
||||||
"Fri",
|
"Fri",
|
||||||
"Sat",
|
"Sat",
|
||||||
"Sun",
|
"Sun",
|
||||||
})),
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
"Direct",
|
||||||
|
"Search Engine",
|
||||||
|
}, charts.PositionCenter),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bar Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.BarRender(
|
||||||
|
values,
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Rainfall",
|
||||||
|
"Evaporation",
|
||||||
|
}, charts.PositionRight),
|
||||||
|
charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage),
|
||||||
|
charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax,
|
||||||
|
charts.SeriesMarkDataTypeMin),
|
||||||
|
// custom option func
|
||||||
|
func(opt *charts.ChartOption) {
|
||||||
|
opt.SeriesList[1].MarkPoint = charts.NewMarkPoint(
|
||||||
|
charts.SeriesMarkDataTypeMax,
|
||||||
|
charts.SeriesMarkDataTypeMin,
|
||||||
|
)
|
||||||
|
opt.SeriesList[1].MarkLine = charts.NewMarkLine(
|
||||||
|
charts.SeriesMarkDataTypeAverage,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
panic(err)
|
||||||
}
|
}
|
||||||
return d.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func echartsRender() ([]byte, error) {
|
buf, err := p.Bytes()
|
||||||
return charts.RenderEChartsToPNG(`{
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Horizontal Bar Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
18203,
|
||||||
|
23489,
|
||||||
|
29034,
|
||||||
|
104970,
|
||||||
|
131744,
|
||||||
|
630230,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.HorizontalBarRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("World Population"),
|
||||||
|
charts.PaddingOptionFunc(charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 40,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"2011",
|
||||||
|
"2012",
|
||||||
|
}),
|
||||||
|
charts.YAxisDataOptionFunc([]string{
|
||||||
|
"Brazil",
|
||||||
|
"Indonesia",
|
||||||
|
"USA",
|
||||||
|
"India",
|
||||||
|
"China",
|
||||||
|
"World",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pie Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := []float64{
|
||||||
|
1048,
|
||||||
|
735,
|
||||||
|
580,
|
||||||
|
484,
|
||||||
|
300,
|
||||||
|
}
|
||||||
|
p, err := charts.PieRender(
|
||||||
|
values,
|
||||||
|
charts.TitleOptionFunc(charts.TitleOption{
|
||||||
|
Text: "Rainfall vs Evaporation",
|
||||||
|
Subtext: "Fake Data",
|
||||||
|
Left: charts.PositionCenter,
|
||||||
|
}),
|
||||||
|
charts.PaddingOptionFunc(charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 20,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}),
|
||||||
|
charts.LegendOptionFunc(charts.LegendOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Data: []string{
|
||||||
|
"Search Engine",
|
||||||
|
"Direct",
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
},
|
||||||
|
Left: charts.PositionLeft,
|
||||||
|
}),
|
||||||
|
charts.PieSeriesShowLabel(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Radar Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
4200,
|
||||||
|
3000,
|
||||||
|
20000,
|
||||||
|
35000,
|
||||||
|
50000,
|
||||||
|
18000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.RadarRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Basic Radar Chart"),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Allocated Budget",
|
||||||
|
"Actual Spending",
|
||||||
|
}),
|
||||||
|
charts.RadarIndicatorOptionFunc([]string{
|
||||||
|
"Sales",
|
||||||
|
"Administration",
|
||||||
|
"Information Technology",
|
||||||
|
"Customer Support",
|
||||||
|
"Development",
|
||||||
|
"Marketing",
|
||||||
|
}, []float64{
|
||||||
|
6500,
|
||||||
|
16000,
|
||||||
|
30000,
|
||||||
|
38000,
|
||||||
|
52000,
|
||||||
|
25000,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Funnel Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := []float64{
|
||||||
|
100,
|
||||||
|
80,
|
||||||
|
60,
|
||||||
|
40,
|
||||||
|
20,
|
||||||
|
}
|
||||||
|
p, err := charts.FunnelRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Funnel"),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Show",
|
||||||
|
"Click",
|
||||||
|
"Visit",
|
||||||
|
"Inquiry",
|
||||||
|
"Order",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
buf, err := charts.RenderEChartsToPNG(`{
|
||||||
"title": {
|
"title": {
|
||||||
"text": "Line"
|
"text": "Line"
|
||||||
},
|
},
|
||||||
|
|
@ -97,25 +397,7 @@ func echartsRender() ([]byte, error) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`)
|
}`)
|
||||||
}
|
// snip...
|
||||||
|
|
||||||
type Render func() ([]byte, error)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
m := map[string]Render{
|
|
||||||
"charts-line.png": chartsRender,
|
|
||||||
"echarts-line.png": echartsRender,
|
|
||||||
}
|
|
||||||
for name, fn := range m {
|
|
||||||
buf, err := fn()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = writeFile(name, buf)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
407
README_zh.md
407
README_zh.md
|
|
@ -21,53 +21,44 @@
|
||||||
|
|
||||||
|
|
||||||
下面的示例为`go-charts`两种方式的参数配置:golang的参数配置、echarts的JSON配置,输出相同的折线图。
|
下面的示例为`go-charts`两种方式的参数配置:golang的参数配置、echarts的JSON配置,输出相同的折线图。
|
||||||
更多的示例参考:`./examples/`目录
|
更多的示例参考:[./examples/](./examples/)目录
|
||||||
|
|
||||||
|
### Line Chart
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
charts "github.com/vicanso/go-charts/v2"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
charts "github.com/vicanso/go-charts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeFile(file string, buf []byte) error {
|
func main() {
|
||||||
tmpPath := "./tmp"
|
values := [][]float64{
|
||||||
err := os.MkdirAll(tmpPath, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file = filepath.Join(tmpPath, file)
|
|
||||||
err = os.WriteFile(file, buf, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func chartsRender() ([]byte, error) {
|
|
||||||
d, err := charts.LineRender([][]float64{
|
|
||||||
{
|
{
|
||||||
150,
|
120,
|
||||||
|
132,
|
||||||
|
101,
|
||||||
|
134,
|
||||||
|
90,
|
||||||
230,
|
230,
|
||||||
224,
|
210,
|
||||||
218,
|
|
||||||
135,
|
|
||||||
147,
|
|
||||||
260,
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
},
|
},
|
||||||
// output type
|
{
|
||||||
charts.PNGTypeOption(),
|
// snip...
|
||||||
// title
|
},
|
||||||
charts.TitleOptionFunc(charts.TitleOption{
|
{
|
||||||
Text: "Line",
|
// snip...
|
||||||
}),
|
},
|
||||||
// x axis
|
{
|
||||||
charts.XAxisOptionFunc(charts.NewXAxisOption([]string{
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.LineRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Line"),
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
"Mon",
|
"Mon",
|
||||||
"Tue",
|
"Tue",
|
||||||
"Wed",
|
"Wed",
|
||||||
|
|
@ -75,16 +66,324 @@ func chartsRender() ([]byte, error) {
|
||||||
"Fri",
|
"Fri",
|
||||||
"Sat",
|
"Sat",
|
||||||
"Sun",
|
"Sun",
|
||||||
})),
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
"Direct",
|
||||||
|
"Search Engine",
|
||||||
|
}, charts.PositionCenter),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bar Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.BarRender(
|
||||||
|
values,
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Rainfall",
|
||||||
|
"Evaporation",
|
||||||
|
}, charts.PositionRight),
|
||||||
|
charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage),
|
||||||
|
charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax,
|
||||||
|
charts.SeriesMarkDataTypeMin),
|
||||||
|
// custom option func
|
||||||
|
func(opt *charts.ChartOption) {
|
||||||
|
opt.SeriesList[1].MarkPoint = charts.NewMarkPoint(
|
||||||
|
charts.SeriesMarkDataTypeMax,
|
||||||
|
charts.SeriesMarkDataTypeMin,
|
||||||
|
)
|
||||||
|
opt.SeriesList[1].MarkLine = charts.NewMarkLine(
|
||||||
|
charts.SeriesMarkDataTypeAverage,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
panic(err)
|
||||||
}
|
}
|
||||||
return d.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func echartsRender() ([]byte, error) {
|
buf, err := p.Bytes()
|
||||||
return charts.RenderEChartsToPNG(`{
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Horizontal Bar Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
18203,
|
||||||
|
23489,
|
||||||
|
29034,
|
||||||
|
104970,
|
||||||
|
131744,
|
||||||
|
630230,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.HorizontalBarRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("World Population"),
|
||||||
|
charts.PaddingOptionFunc(charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 40,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"2011",
|
||||||
|
"2012",
|
||||||
|
}),
|
||||||
|
charts.YAxisDataOptionFunc([]string{
|
||||||
|
"Brazil",
|
||||||
|
"Indonesia",
|
||||||
|
"USA",
|
||||||
|
"India",
|
||||||
|
"China",
|
||||||
|
"World",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pie Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := []float64{
|
||||||
|
1048,
|
||||||
|
735,
|
||||||
|
580,
|
||||||
|
484,
|
||||||
|
300,
|
||||||
|
}
|
||||||
|
p, err := charts.PieRender(
|
||||||
|
values,
|
||||||
|
charts.TitleOptionFunc(charts.TitleOption{
|
||||||
|
Text: "Rainfall vs Evaporation",
|
||||||
|
Subtext: "Fake Data",
|
||||||
|
Left: charts.PositionCenter,
|
||||||
|
}),
|
||||||
|
charts.PaddingOptionFunc(charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 20,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}),
|
||||||
|
charts.LegendOptionFunc(charts.LegendOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Data: []string{
|
||||||
|
"Search Engine",
|
||||||
|
"Direct",
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
},
|
||||||
|
Left: charts.PositionLeft,
|
||||||
|
}),
|
||||||
|
charts.PieSeriesShowLabel(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Radar Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
4200,
|
||||||
|
3000,
|
||||||
|
20000,
|
||||||
|
35000,
|
||||||
|
50000,
|
||||||
|
18000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// snip...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.RadarRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Basic Radar Chart"),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Allocated Budget",
|
||||||
|
"Actual Spending",
|
||||||
|
}),
|
||||||
|
charts.RadarIndicatorOptionFunc([]string{
|
||||||
|
"Sales",
|
||||||
|
"Administration",
|
||||||
|
"Information Technology",
|
||||||
|
"Customer Support",
|
||||||
|
"Development",
|
||||||
|
"Marketing",
|
||||||
|
}, []float64{
|
||||||
|
6500,
|
||||||
|
16000,
|
||||||
|
30000,
|
||||||
|
38000,
|
||||||
|
52000,
|
||||||
|
25000,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Funnel Chart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := []float64{
|
||||||
|
100,
|
||||||
|
80,
|
||||||
|
60,
|
||||||
|
40,
|
||||||
|
20,
|
||||||
|
}
|
||||||
|
p, err := charts.FunnelRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Funnel"),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Show",
|
||||||
|
"Click",
|
||||||
|
"Visit",
|
||||||
|
"Inquiry",
|
||||||
|
"Order",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
buf, err := charts.RenderEChartsToPNG(`{
|
||||||
"title": {
|
"title": {
|
||||||
"text": "Line"
|
"text": "Line"
|
||||||
},
|
},
|
||||||
|
|
@ -97,25 +396,7 @@ func echartsRender() ([]byte, error) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`)
|
}`)
|
||||||
}
|
// snip...
|
||||||
|
|
||||||
type Render func() ([]byte, error)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
m := map[string]Render{
|
|
||||||
"charts-line.png": chartsRender,
|
|
||||||
"echarts-line.png": echartsRender,
|
|
||||||
}
|
|
||||||
for name, fn := range m {
|
|
||||||
buf, err := fn()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = writeFile(name, buf)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -222,4 +503,6 @@ BenchmarkMultiChartSVGRender-8 367 3356325 ns/op
|
||||||
|
|
||||||
字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。
|
字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。
|
||||||
|
|
||||||
示例见 [examples/basic/chinese.go](examples/basic/chinese.go)
|
|
||||||
|
示例见 [examples/chinese/main.go](examples/chinese/main.go)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,32 +27,47 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BarStyle struct {
|
type Box = chart.Box
|
||||||
ClassName string
|
type Style = chart.Style
|
||||||
StrokeDashArray []float64
|
type Color = drawing.Color
|
||||||
FillColor drawing.Color
|
|
||||||
|
var BoxZero = chart.BoxZero
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs *BarStyle) Style() chart.Style {
|
const (
|
||||||
return chart.Style{
|
ChartTypeLine = "line"
|
||||||
ClassName: bs.ClassName,
|
ChartTypeBar = "bar"
|
||||||
StrokeDashArray: bs.StrokeDashArray,
|
ChartTypePie = "pie"
|
||||||
StrokeColor: bs.FillColor,
|
ChartTypeRadar = "radar"
|
||||||
StrokeWidth: 1,
|
ChartTypeFunnel = "funnel"
|
||||||
FillColor: bs.FillColor,
|
// horizontal bar
|
||||||
}
|
ChartTypeHorizontalBar = "horizontalBar"
|
||||||
}
|
)
|
||||||
|
|
||||||
// Bar renders bar for chart
|
const (
|
||||||
func (d *Draw) Bar(b chart.Box, style BarStyle) {
|
ChartOutputSVG = "svg"
|
||||||
s := style.Style()
|
ChartOutputPNG = "png"
|
||||||
|
)
|
||||||
|
|
||||||
r := d.Render
|
const (
|
||||||
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
PositionLeft = "left"
|
||||||
d.moveTo(b.Left, b.Top)
|
PositionRight = "right"
|
||||||
d.lineTo(b.Right, b.Top)
|
PositionCenter = "center"
|
||||||
d.lineTo(b.Right, b.Bottom)
|
PositionTop = "top"
|
||||||
d.lineTo(b.Left, b.Bottom)
|
PositionBottom = "bottom"
|
||||||
d.lineTo(b.Left, b.Top)
|
)
|
||||||
d.Render.FillStroke()
|
|
||||||
}
|
const (
|
||||||
|
AlignLeft = "left"
|
||||||
|
AlignRight = "right"
|
||||||
|
AlignCenter = "center"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrientHorizontal = "horizontal"
|
||||||
|
OrientVertical = "vertical"
|
||||||
|
)
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 332 KiB |
578
axis.go
578
axis.go
|
|
@ -23,14 +23,31 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type axisPainter struct {
|
||||||
|
p *Painter
|
||||||
|
opt *AxisOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAxisPainter(p *Painter, opt AxisOption) *axisPainter {
|
||||||
|
return &axisPainter{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type AxisOption struct {
|
type AxisOption struct {
|
||||||
|
// The theme of chart
|
||||||
|
Theme ColorPalette
|
||||||
|
// Formatter for y axis text value
|
||||||
|
Formatter string
|
||||||
|
// The label of axis
|
||||||
|
Data []string
|
||||||
// The boundary gap on both sides of a coordinate axis.
|
// The boundary gap on both sides of a coordinate axis.
|
||||||
// Nil or *true means the center part of two axis ticks
|
// Nil or *true means the center part of two axis ticks
|
||||||
BoundaryGap *bool
|
BoundaryGap *bool
|
||||||
|
|
@ -40,15 +57,12 @@ type AxisOption struct {
|
||||||
Position string
|
Position string
|
||||||
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
||||||
SplitNumber int
|
SplitNumber int
|
||||||
ClassName string
|
|
||||||
// The line color of axis
|
// The line color of axis
|
||||||
StrokeColor drawing.Color
|
StrokeColor Color
|
||||||
// The line width
|
// The line width
|
||||||
StrokeWidth float64
|
StrokeWidth float64
|
||||||
// The length of the axis tick
|
// The length of the axis tick
|
||||||
TickLength int
|
TickLength int
|
||||||
// The flag for show axis tick, set this to *false will hide axis tick
|
|
||||||
TickShow *bool
|
|
||||||
// The margin value of label
|
// The margin value of label
|
||||||
LabelMargin int
|
LabelMargin int
|
||||||
// The font size of label
|
// The font size of label
|
||||||
|
|
@ -56,413 +70,231 @@ type AxisOption struct {
|
||||||
// The font of label
|
// The font of label
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
// The color of label
|
// The color of label
|
||||||
FontColor drawing.Color
|
FontColor Color
|
||||||
// The flag for show axis split line, set this to true will show axis split line
|
// The flag for show axis split line, set this to true will show axis split line
|
||||||
SplitLineShow bool
|
SplitLineShow bool
|
||||||
// The color of split line
|
// The color of split line
|
||||||
SplitLineColor drawing.Color
|
SplitLineColor Color
|
||||||
}
|
}
|
||||||
|
|
||||||
type axis struct {
|
func (a *axisPainter) Render() (Box, error) {
|
||||||
d *Draw
|
opt := a.opt
|
||||||
data *AxisDataList
|
top := a.p
|
||||||
option *AxisOption
|
theme := opt.Theme
|
||||||
}
|
if opt.Show != nil && !*opt.Show {
|
||||||
type axisMeasurement struct {
|
return BoxZero, nil
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAxis creates a new axis with data and style options
|
|
||||||
func NewAxis(d *Draw, data AxisDataList, option AxisOption) *axis {
|
|
||||||
return &axis{
|
|
||||||
d: d,
|
|
||||||
data: &data,
|
|
||||||
option: &option,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
strokeWidth := opt.StrokeWidth
|
||||||
|
if strokeWidth == 0 {
|
||||||
// GetLabelMargin returns the label margin value
|
strokeWidth = 1
|
||||||
func (as *AxisOption) GetLabelMargin() int {
|
|
||||||
return getDefaultInt(as.LabelMargin, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTickLength returns the tick length value
|
|
||||||
func (as *AxisOption) GetTickLength() int {
|
|
||||||
return getDefaultInt(as.TickLength, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style returns the style of axis
|
|
||||||
func (as *AxisOption) Style(f *truetype.Font) chart.Style {
|
|
||||||
s := chart.Style{
|
|
||||||
ClassName: as.ClassName,
|
|
||||||
StrokeColor: as.StrokeColor,
|
|
||||||
StrokeWidth: as.StrokeWidth,
|
|
||||||
FontSize: as.FontSize,
|
|
||||||
FontColor: as.FontColor,
|
|
||||||
Font: as.Font,
|
|
||||||
}
|
}
|
||||||
if s.FontSize == 0 {
|
|
||||||
s.FontSize = chart.DefaultFontSize
|
font := opt.Font
|
||||||
|
if font == nil {
|
||||||
|
font = a.p.font
|
||||||
}
|
}
|
||||||
if s.Font == nil {
|
if font == nil {
|
||||||
s.Font = f
|
font = theme.GetFont()
|
||||||
}
|
}
|
||||||
return s
|
fontColor := opt.FontColor
|
||||||
}
|
if fontColor.IsZero() {
|
||||||
|
fontColor = theme.GetTextColor()
|
||||||
type AxisData struct {
|
}
|
||||||
// The text value of axis
|
fontSize := opt.FontSize
|
||||||
Text string
|
if fontSize == 0 {
|
||||||
}
|
fontSize = theme.GetFontSize()
|
||||||
type AxisDataList []AxisData
|
}
|
||||||
|
strokeColor := opt.StrokeColor
|
||||||
// TextList returns the text list of axis data
|
if strokeColor.IsZero() {
|
||||||
func (l AxisDataList) TextList() []string {
|
strokeColor = theme.GetAxisStrokeColor()
|
||||||
textList := make([]string, len(l))
|
|
||||||
for index, item := range l {
|
|
||||||
textList[index] = item.Text
|
|
||||||
}
|
}
|
||||||
return textList
|
|
||||||
}
|
|
||||||
|
|
||||||
type axisRenderOption struct {
|
data := opt.Data
|
||||||
textMaxWith int
|
formatter := opt.Formatter
|
||||||
textMaxHeight int
|
if len(formatter) != 0 {
|
||||||
boundaryGap bool
|
for index, text := range data {
|
||||||
unitCount int
|
data[index] = strings.ReplaceAll(formatter, "{value}", text)
|
||||||
modValue int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAxisDataListFromStringList creates a new axis data list from string list
|
|
||||||
func NewAxisDataListFromStringList(textList []string) AxisDataList {
|
|
||||||
list := make(AxisDataList, len(textList))
|
|
||||||
for index, text := range textList {
|
|
||||||
list[index] = AxisData{
|
|
||||||
Text: text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return list
|
dataCount := len(data)
|
||||||
}
|
tickCount := dataCount
|
||||||
|
|
||||||
func (a *axis) axisLabel(renderOpt *axisRenderOption) {
|
boundaryGap := true
|
||||||
option := a.option
|
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
|
||||||
data := *a.data
|
boundaryGap = false
|
||||||
d := a.d
|
|
||||||
if option.FontColor.IsZero() || len(data) == 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
r := d.Render
|
isVertical := opt.Position == PositionLeft ||
|
||||||
|
opt.Position == PositionRight
|
||||||
|
|
||||||
s := option.Style(d.Font)
|
labelPosition := ""
|
||||||
s.GetTextOptions().WriteTextOptionsToRenderer(r)
|
|
||||||
|
|
||||||
width := d.Box.Width()
|
|
||||||
height := d.Box.Height()
|
|
||||||
textList := data.TextList()
|
|
||||||
count := len(textList)
|
|
||||||
|
|
||||||
boundaryGap := renderOpt.boundaryGap
|
|
||||||
if !boundaryGap {
|
if !boundaryGap {
|
||||||
count--
|
tickCount--
|
||||||
|
labelPosition = PositionLeft
|
||||||
|
}
|
||||||
|
if isVertical && boundaryGap {
|
||||||
|
labelPosition = PositionCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
unitCount := renderOpt.unitCount
|
// 如果小于0,则表示不处理
|
||||||
modValue := renderOpt.modValue
|
tickLength := getDefaultInt(opt.TickLength, 5)
|
||||||
labelMargin := option.GetLabelMargin()
|
labelMargin := getDefaultInt(opt.LabelMargin, 5)
|
||||||
|
|
||||||
// 轴线
|
style := Style{
|
||||||
labelHeight := labelMargin + renderOpt.textMaxHeight
|
StrokeColor: strokeColor,
|
||||||
labelWidth := labelMargin + renderOpt.textMaxWith
|
StrokeWidth: strokeWidth,
|
||||||
|
Font: font,
|
||||||
|
FontColor: fontColor,
|
||||||
|
FontSize: fontSize,
|
||||||
|
}
|
||||||
|
top.SetDrawingStyle(style).OverrideTextStyle(style)
|
||||||
|
|
||||||
// 坐标轴文本
|
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data)
|
||||||
position := option.Position
|
textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth))
|
||||||
switch position {
|
unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber)))
|
||||||
|
|
||||||
|
width := 0
|
||||||
|
height := 0
|
||||||
|
// 垂直
|
||||||
|
if isVertical {
|
||||||
|
width = textMaxWidth + tickLength<<1
|
||||||
|
height = top.Height()
|
||||||
|
} else {
|
||||||
|
width = top.Width()
|
||||||
|
height = tickLength<<1 + textMaxHeight
|
||||||
|
}
|
||||||
|
padding := Box{}
|
||||||
|
switch opt.Position {
|
||||||
|
case PositionTop:
|
||||||
|
padding.Top = top.Height() - height
|
||||||
case PositionLeft:
|
case PositionLeft:
|
||||||
fallthrough
|
padding.Right = top.Width() - width
|
||||||
case PositionRight:
|
case PositionRight:
|
||||||
values := autoDivide(height, count)
|
padding.Left = top.Width() - width
|
||||||
textList := data.TextList()
|
|
||||||
// 由下往上
|
|
||||||
reverseIntSlice(values)
|
|
||||||
for index, text := range textList {
|
|
||||||
y := values[index] - 2
|
|
||||||
b := r.MeasureText(text)
|
|
||||||
if boundaryGap {
|
|
||||||
height := y - values[index+1]
|
|
||||||
y -= (height - b.Height()) >> 1
|
|
||||||
} else {
|
|
||||||
y += b.Height() >> 1
|
|
||||||
}
|
|
||||||
// 左右位置的x不一样
|
|
||||||
x := width - renderOpt.textMaxWith
|
|
||||||
if position == PositionLeft {
|
|
||||||
x = labelWidth - b.Width() - 1
|
|
||||||
}
|
|
||||||
d.text(text, x, y)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// 定位bottom,重新计算y0的定位
|
padding.Top = top.Height() - defaultXAxisHeight
|
||||||
y0 := height - labelMargin
|
|
||||||
if position == PositionTop {
|
|
||||||
y0 = labelHeight - labelMargin
|
|
||||||
}
|
}
|
||||||
values := autoDivide(width, count)
|
|
||||||
for index, text := range data.TextList() {
|
|
||||||
if unitCount != 0 && index%unitCount != modValue {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
x := values[index]
|
|
||||||
leftOffset := 0
|
|
||||||
b := r.MeasureText(text)
|
|
||||||
if boundaryGap {
|
|
||||||
width := values[index+1] - x
|
|
||||||
leftOffset = (width - b.Width()) >> 1
|
|
||||||
} else {
|
|
||||||
// 左移文本长度
|
|
||||||
leftOffset = -b.Width() >> 1
|
|
||||||
}
|
|
||||||
d.text(text, x+leftOffset, y0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *axis) axisLine(renderOpt *axisRenderOption) {
|
p := top.Child(PainterPaddingOption(padding))
|
||||||
d := a.d
|
|
||||||
r := d.Render
|
|
||||||
option := a.option
|
|
||||||
s := option.Style(d.Font)
|
|
||||||
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
|
|
||||||
x0 := 0
|
x0 := 0
|
||||||
y0 := 0
|
y0 := 0
|
||||||
x1 := 0
|
x1 := 0
|
||||||
y1 := 0
|
y1 := 0
|
||||||
width := d.Box.Width()
|
ticksPaddingTop := 0
|
||||||
height := d.Box.Height()
|
ticksPaddingLeft := 0
|
||||||
labelMargin := option.GetLabelMargin()
|
labelPaddingTop := 0
|
||||||
|
labelPaddingLeft := 0
|
||||||
|
labelPaddingRight := 0
|
||||||
|
orient := ""
|
||||||
|
textAlign := ""
|
||||||
|
|
||||||
// 轴线
|
switch opt.Position {
|
||||||
labelHeight := labelMargin + renderOpt.textMaxHeight
|
|
||||||
labelWidth := labelMargin + renderOpt.textMaxWith
|
|
||||||
tickLength := option.GetTickLength()
|
|
||||||
switch option.Position {
|
|
||||||
case PositionLeft:
|
|
||||||
x0 = tickLength + labelWidth
|
|
||||||
x1 = x0
|
|
||||||
y0 = 0
|
|
||||||
y1 = height
|
|
||||||
case PositionRight:
|
|
||||||
x0 = width - labelWidth
|
|
||||||
x1 = x0
|
|
||||||
y0 = 0
|
|
||||||
y1 = height
|
|
||||||
case PositionTop:
|
case PositionTop:
|
||||||
x0 = 0
|
labelPaddingTop = labelMargin
|
||||||
x1 = width
|
x1 = p.Width()
|
||||||
y0 = labelHeight
|
y0 = labelMargin + int(opt.FontSize)
|
||||||
|
ticksPaddingTop = int(opt.FontSize)
|
||||||
y1 = y0
|
y1 = y0
|
||||||
// bottom
|
orient = OrientHorizontal
|
||||||
default:
|
|
||||||
x0 = 0
|
|
||||||
x1 = width
|
|
||||||
y0 = height - tickLength - labelHeight
|
|
||||||
y1 = y0
|
|
||||||
}
|
|
||||||
|
|
||||||
d.moveTo(x0, y0)
|
|
||||||
d.lineTo(x1, y1)
|
|
||||||
r.FillStroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *axis) axisTick(renderOpt *axisRenderOption) {
|
|
||||||
d := a.d
|
|
||||||
r := d.Render
|
|
||||||
|
|
||||||
option := a.option
|
|
||||||
s := option.Style(d.Font)
|
|
||||||
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
|
|
||||||
width := d.Box.Width()
|
|
||||||
height := d.Box.Height()
|
|
||||||
data := *a.data
|
|
||||||
tickCount := len(data)
|
|
||||||
if tickCount == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !renderOpt.boundaryGap {
|
|
||||||
tickCount--
|
|
||||||
}
|
|
||||||
labelMargin := option.GetLabelMargin()
|
|
||||||
tickShow := true
|
|
||||||
if isFalse(option.TickShow) {
|
|
||||||
tickShow = false
|
|
||||||
}
|
|
||||||
unitCount := renderOpt.unitCount
|
|
||||||
|
|
||||||
tickLengthValue := option.GetTickLength()
|
|
||||||
labelHeight := labelMargin + renderOpt.textMaxHeight
|
|
||||||
labelWidth := labelMargin + renderOpt.textMaxWith
|
|
||||||
position := option.Position
|
|
||||||
switch position {
|
|
||||||
case PositionLeft:
|
case PositionLeft:
|
||||||
fallthrough
|
x0 = p.Width()
|
||||||
case PositionRight:
|
|
||||||
values := autoDivide(height, tickCount)
|
|
||||||
// 左右仅是x0的位置不一样
|
|
||||||
x0 := width - labelWidth
|
|
||||||
if option.Position == PositionLeft {
|
|
||||||
x0 = labelWidth
|
|
||||||
}
|
|
||||||
if tickShow {
|
|
||||||
for _, v := range values {
|
|
||||||
x := x0
|
|
||||||
y := v
|
|
||||||
d.moveTo(x, y)
|
|
||||||
d.lineTo(x+tickLengthValue, y)
|
|
||||||
r.Stroke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 辅助线
|
|
||||||
if option.SplitLineShow && !option.SplitLineColor.IsZero() {
|
|
||||||
r.SetStrokeColor(option.SplitLineColor)
|
|
||||||
splitLineWidth := width - labelWidth - tickLengthValue
|
|
||||||
x0 = labelWidth + tickLengthValue
|
|
||||||
if position == PositionRight {
|
|
||||||
x0 = 0
|
|
||||||
splitLineWidth = width - labelWidth - 1
|
|
||||||
}
|
|
||||||
for _, v := range values[0 : len(values)-1] {
|
|
||||||
x := x0
|
|
||||||
y := v
|
|
||||||
d.moveTo(x, y)
|
|
||||||
d.lineTo(x+splitLineWidth, y)
|
|
||||||
r.Stroke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
values := autoDivide(width, tickCount)
|
|
||||||
// 上下仅是y0的位置不一样
|
|
||||||
y0 := height - labelHeight
|
|
||||||
if position == PositionTop {
|
|
||||||
y0 = labelHeight
|
|
||||||
}
|
|
||||||
if tickShow {
|
|
||||||
for index, v := range values {
|
|
||||||
if index%unitCount != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
x := v
|
|
||||||
y := y0
|
|
||||||
d.moveTo(x, y-tickLengthValue)
|
|
||||||
d.lineTo(x, y)
|
|
||||||
r.Stroke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 辅助线
|
|
||||||
if option.SplitLineShow && !option.SplitLineColor.IsZero() {
|
|
||||||
r.SetStrokeColor(option.SplitLineColor)
|
|
||||||
y0 = 0
|
y0 = 0
|
||||||
splitLineHeight := height - labelHeight - tickLengthValue
|
x1 = p.Width()
|
||||||
if position == PositionTop {
|
y1 = p.Height()
|
||||||
y0 = labelHeight
|
orient = OrientVertical
|
||||||
splitLineHeight = height - labelHeight
|
textAlign = AlignRight
|
||||||
|
ticksPaddingLeft = textMaxWidth + tickLength
|
||||||
|
labelPaddingRight = width - textMaxWidth
|
||||||
|
case PositionRight:
|
||||||
|
orient = OrientVertical
|
||||||
|
y1 = p.Height()
|
||||||
|
labelPaddingLeft = width - textMaxWidth
|
||||||
|
default:
|
||||||
|
labelPaddingTop = height
|
||||||
|
x1 = p.Width()
|
||||||
|
orient = OrientHorizontal
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, v := range values {
|
if strokeWidth > 0 {
|
||||||
if index%unitCount != 0 {
|
p.Child(PainterPaddingOption(Box{
|
||||||
|
Top: ticksPaddingTop,
|
||||||
|
Left: ticksPaddingLeft,
|
||||||
|
})).Ticks(TicksOption{
|
||||||
|
Count: tickCount,
|
||||||
|
Length: tickLength,
|
||||||
|
Unit: unit,
|
||||||
|
Orient: orient,
|
||||||
|
})
|
||||||
|
p.LineStroke([]Point{
|
||||||
|
{
|
||||||
|
X: x0,
|
||||||
|
Y: y0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: x1,
|
||||||
|
Y: y1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Child(PainterPaddingOption(Box{
|
||||||
|
Left: labelPaddingLeft,
|
||||||
|
Top: labelPaddingTop,
|
||||||
|
Right: labelPaddingRight,
|
||||||
|
})).MultiText(MultiTextOption{
|
||||||
|
Align: textAlign,
|
||||||
|
TextList: data,
|
||||||
|
Orient: orient,
|
||||||
|
Unit: unit,
|
||||||
|
Position: labelPosition,
|
||||||
|
})
|
||||||
|
// 显示辅助线
|
||||||
|
if opt.SplitLineShow {
|
||||||
|
style.StrokeColor = opt.SplitLineColor
|
||||||
|
top.OverrideDrawingStyle(style)
|
||||||
|
if isVertical {
|
||||||
|
x0 := p.Width()
|
||||||
|
x1 := top.Width()
|
||||||
|
if opt.Position == PositionRight {
|
||||||
|
x0 = 0
|
||||||
|
x1 = top.Width() - p.Width()
|
||||||
|
}
|
||||||
|
for _, y := range autoDivide(height, tickCount) {
|
||||||
|
top.LineStroke([]Point{
|
||||||
|
{
|
||||||
|
X: x0,
|
||||||
|
Y: y,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: x1,
|
||||||
|
Y: y,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
y0 := p.Height() - defaultXAxisHeight
|
||||||
|
y1 := top.Height() - defaultXAxisHeight
|
||||||
|
for index, x := range autoDivide(width, tickCount) {
|
||||||
|
if index == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
x := v
|
top.LineStroke([]Point{
|
||||||
y := y0
|
{
|
||||||
|
X: x,
|
||||||
|
Y: y0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: x,
|
||||||
|
Y: y1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.moveTo(x, y)
|
return Box{
|
||||||
d.lineTo(x, y0+splitLineHeight)
|
Bottom: height,
|
||||||
r.Stroke()
|
Right: width,
|
||||||
}
|
}, nil
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *axis) measureTextMaxWidthHeight() (int, int) {
|
|
||||||
d := a.d
|
|
||||||
r := d.Render
|
|
||||||
s := a.option.Style(d.Font)
|
|
||||||
data := a.data
|
|
||||||
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
s.GetTextOptions().WriteTextOptionsToRenderer(r)
|
|
||||||
return measureTextMaxWidthHeight(data.TextList(), r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// measure returns the measurement of axis.
|
|
||||||
// Width will be textMaxWidth + labelMargin + tickLength for position left or right.
|
|
||||||
// Height will be textMaxHeight + labelMargin + tickLength for position top or bottom.
|
|
||||||
func (a *axis) measure() axisMeasurement {
|
|
||||||
option := a.option
|
|
||||||
value := option.GetLabelMargin() + option.GetTickLength()
|
|
||||||
textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
|
|
||||||
info := axisMeasurement{}
|
|
||||||
if option.Position == PositionLeft ||
|
|
||||||
option.Position == PositionRight {
|
|
||||||
info.Width = textMaxWidth + value
|
|
||||||
} else {
|
|
||||||
info.Height = textMaxHeight + value
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render renders the axis for chart
|
|
||||||
func (a *axis) Render() {
|
|
||||||
option := a.option
|
|
||||||
if isFalse(option.Show) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
|
|
||||||
opt := &axisRenderOption{
|
|
||||||
textMaxWith: textMaxWidth,
|
|
||||||
textMaxHeight: textMaxHeight,
|
|
||||||
boundaryGap: true,
|
|
||||||
}
|
|
||||||
if isFalse(option.BoundaryGap) {
|
|
||||||
opt.boundaryGap = false
|
|
||||||
}
|
|
||||||
|
|
||||||
unitCount := chart.MaxInt(option.SplitNumber, 1)
|
|
||||||
width := a.d.Box.Width()
|
|
||||||
textList := a.data.TextList()
|
|
||||||
count := len(textList)
|
|
||||||
|
|
||||||
position := option.Position
|
|
||||||
switch position {
|
|
||||||
case PositionLeft:
|
|
||||||
fallthrough
|
|
||||||
case PositionRight:
|
|
||||||
default:
|
|
||||||
maxCount := width / (opt.textMaxWith + 10)
|
|
||||||
// 可以显示所有
|
|
||||||
if maxCount >= count {
|
|
||||||
unitCount = 1
|
|
||||||
} else if maxCount < count/unitCount {
|
|
||||||
unitCount = int(math.Ceil(float64(count) / float64(maxCount)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boundaryGap := opt.boundaryGap
|
|
||||||
modValue := 0
|
|
||||||
if boundaryGap && unitCount > 1 {
|
|
||||||
// 如果是居中,unit count需要设置为奇数
|
|
||||||
if unitCount%2 == 0 {
|
|
||||||
unitCount++
|
|
||||||
}
|
|
||||||
modValue = unitCount / 2
|
|
||||||
}
|
|
||||||
opt.modValue = modValue
|
|
||||||
opt.unitCount = unitCount
|
|
||||||
|
|
||||||
// 坐标轴线
|
|
||||||
a.axisLine(opt)
|
|
||||||
a.axisTick(opt)
|
|
||||||
// 坐标文本
|
|
||||||
a.axisLabel(opt)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
259
axis_test.go
259
axis_test.go
|
|
@ -1,259 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAxisOption(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
as := AxisOption{}
|
|
||||||
|
|
||||||
assert.Equal(8, as.GetLabelMargin())
|
|
||||||
as.LabelMargin = 10
|
|
||||||
assert.Equal(10, as.GetLabelMargin())
|
|
||||||
|
|
||||||
assert.Equal(5, as.GetTickLength())
|
|
||||||
as.TickLength = 6
|
|
||||||
assert.Equal(6, as.GetTickLength())
|
|
||||||
|
|
||||||
f := &truetype.Font{}
|
|
||||||
style := as.Style(f)
|
|
||||||
assert.Equal(float64(10), style.FontSize)
|
|
||||||
assert.Equal(f, style.Font)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAxisDataList(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
textList := []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}
|
|
||||||
data := NewAxisDataListFromStringList(textList)
|
|
||||||
assert.Equal(textList, data.TextList())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAxis(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
axisData := NewAxisDataListFromStringList([]string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
"Thu",
|
|
||||||
"Fri",
|
|
||||||
"Sat",
|
|
||||||
"Sun",
|
|
||||||
})
|
|
||||||
getDefaultOption := func() AxisOption {
|
|
||||||
return AxisOption{
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FontColor: drawing.ColorBlack,
|
|
||||||
Show: TrueFlag(),
|
|
||||||
TickShow: TrueFlag(),
|
|
||||||
SplitLineShow: true,
|
|
||||||
SplitLineColor: drawing.ColorBlack.WithAlpha(60),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
newOption func() AxisOption
|
|
||||||
newData func() AxisDataList
|
|
||||||
result string
|
|
||||||
}{
|
|
||||||
// 文本按起始位置展示
|
|
||||||
// axis位于bottom
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.BoundaryGap = FalseFlag()
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 270\nL 395 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 270\nL 5 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 70 270\nL 70 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 135 270\nL 135 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 200 270\nL 200 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 265 270\nL 265 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 330 270\nL 330 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 395 270\nL 395 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 5\nL 5 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 70 5\nL 70 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 135 5\nL 135 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 200 5\nL 200 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 265 5\nL 265 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 330 5\nL 330 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 395 5\nL 395 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"-8\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"59\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"122\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"189\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"257\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"320\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"384\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本居中展示
|
|
||||||
// axis位于bottom
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 270\nL 395 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 270\nL 5 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 61 270\nL 61 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 117 270\nL 117 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 173 270\nL 173 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 229 270\nL 229 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 285 270\nL 285 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 340 270\nL 340 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 395 270\nL 395 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 5\nL 5 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 61 5\nL 61 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 117 5\nL 117 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 173 5\nL 173 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 229 5\nL 229 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 285 5\nL 285 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 340 5\nL 340 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 395 5\nL 395 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"20\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"78\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"132\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"190\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"249\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"303\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"356\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本按起始位置展示
|
|
||||||
// axis位于top
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionTop
|
|
||||||
opt.BoundaryGap = FalseFlag()
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 25\nL 395 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 20\nL 5 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 70 20\nL 70 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 135 20\nL 135 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 200 20\nL 200 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 265 20\nL 265 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 330 20\nL 330 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 395 20\nL 395 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 25\nL 5 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 70 25\nL 70 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 135 25\nL 135 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 200 25\nL 200 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 265 25\nL 265 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 330 25\nL 330 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 395 25\nL 395 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"-8\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"59\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"122\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"189\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"257\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"320\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"384\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本居中展示
|
|
||||||
// axis位于top
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionTop
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 25\nL 395 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 20\nL 5 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 61 20\nL 61 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 117 20\nL 117 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 173 20\nL 173 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 229 20\nL 229 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 285 20\nL 285 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 340 20\nL 340 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 395 20\nL 395 25\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 25\nL 5 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 61 25\nL 61 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 117 25\nL 117 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 173 25\nL 173 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 229 25\nL 229 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 285 25\nL 285 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 340 25\nL 340 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 395 25\nL 395 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"20\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"78\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"132\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"190\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"249\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"303\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"356\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本按起始位置展示
|
|
||||||
// axis位于left
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionLeft
|
|
||||||
opt.BoundaryGap = FalseFlag()
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 54\nL 44 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 103\nL 44 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 151\nL 44 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 199\nL 44 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 247\nL 44 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 44 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 54\nL 395 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 103\nL 395 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 151\nL 395 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 199\nL 395 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 247\nL 395 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"299\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"203\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"107\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"9\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本居中展示
|
|
||||||
// axis位于left
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionLeft
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 47\nL 44 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 89\nL 44 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 131\nL 44 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 172\nL 44 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 213\nL 44 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 254\nL 44 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 44 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 47\nL 395 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 89\nL 395 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 131\nL 395 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 172\nL 395 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 213\nL 395 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 254\nL 395 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"239\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"198\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"31\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本按起始位置展示
|
|
||||||
// axis位于right
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionRight
|
|
||||||
opt.BoundaryGap = FalseFlag()
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 361 5\nL 361 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 5\nL 366 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 54\nL 366 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 103\nL 366 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 151\nL 366 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 199\nL 366 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 247\nL 366 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 295\nL 366 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 5\nL 360 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 54\nL 360 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 103\nL 360 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 151\nL 360 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 199\nL 360 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 247\nL 360 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"369\" y=\"299\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"369\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"369\" y=\"203\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"369\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"369\" y=\"107\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"369\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"369\" y=\"9\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// 文本居中展示
|
|
||||||
// axis位于right
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionRight
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 361 5\nL 361 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 5\nL 366 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 47\nL 366 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 89\nL 366 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 131\nL 366 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 172\nL 366 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 213\nL 366 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 254\nL 366 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 361 295\nL 366 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 5\nL 360 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 47\nL 360 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 89\nL 360 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 131\nL 360 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 172\nL 360 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 213\nL 360 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 5 254\nL 360 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"369\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"369\" y=\"239\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"369\" y=\"198\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"369\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"369\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"369\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"369\" y=\"31\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
|
|
||||||
},
|
|
||||||
// text较多,仅展示部分
|
|
||||||
{
|
|
||||||
newOption: func() AxisOption {
|
|
||||||
opt := getDefaultOption()
|
|
||||||
opt.Position = PositionBottom
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
newData: func() AxisDataList {
|
|
||||||
return NewAxisDataListFromStringList([]string{
|
|
||||||
"01-01",
|
|
||||||
"01-02",
|
|
||||||
"01-03",
|
|
||||||
"01-04",
|
|
||||||
"01-05",
|
|
||||||
"01-06",
|
|
||||||
"01-07",
|
|
||||||
"01-08",
|
|
||||||
"01-09",
|
|
||||||
"01-10",
|
|
||||||
"01-11",
|
|
||||||
"01-12",
|
|
||||||
"01-13",
|
|
||||||
"01-14",
|
|
||||||
"01-15",
|
|
||||||
"01-16",
|
|
||||||
"01-17",
|
|
||||||
"01-18",
|
|
||||||
"01-19",
|
|
||||||
"01-20",
|
|
||||||
"01-21",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 270\nL 395 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 270\nL 5 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 62 270\nL 62 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 119 270\nL 119 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 176 270\nL 176 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 233 270\nL 233 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 287 270\nL 287 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 341 270\nL 341 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 395 270\nL 395 275\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 5 5\nL 5 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 62 5\nL 62 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 119 5\nL 119 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 176 5\nL 176 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 233 5\nL 233 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 287 5\nL 287 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 341 5\nL 341 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 395 5\nL 395 270\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"16\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-02</text><text x=\"73\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-05</text><text x=\"130\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-08</text><text x=\"187\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-11</text><text x=\"243\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-14</text><text x=\"297\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-17</text><text x=\"351\" y=\"287\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-20</text></svg>",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Left: 5,
|
|
||||||
Top: 5,
|
|
||||||
Right: 5,
|
|
||||||
Bottom: 5,
|
|
||||||
}))
|
|
||||||
assert.Nil(err)
|
|
||||||
style := tt.newOption()
|
|
||||||
data := axisData
|
|
||||||
if tt.newData != nil {
|
|
||||||
data = tt.newData()
|
|
||||||
}
|
|
||||||
NewAxis(d, data, style).Render()
|
|
||||||
|
|
||||||
result, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(tt.result, string(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeasureAxis(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
data := NewAxisDataListFromStringList([]string{
|
|
||||||
"Mon",
|
|
||||||
"Sun",
|
|
||||||
})
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
width := NewAxis(d, data, AxisOption{
|
|
||||||
FontSize: 12,
|
|
||||||
Font: f,
|
|
||||||
Position: PositionLeft,
|
|
||||||
}).measure().Width
|
|
||||||
assert.Equal(44, width)
|
|
||||||
|
|
||||||
height := NewAxis(d, data, AxisOption{
|
|
||||||
FontSize: 12,
|
|
||||||
Font: f,
|
|
||||||
Position: PositionTop,
|
|
||||||
}).measure().Height
|
|
||||||
assert.Equal(28, height)
|
|
||||||
}
|
|
||||||
172
bar_chart.go
172
bar_chart.go
|
|
@ -27,27 +27,48 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type barChartOption struct {
|
type barChart struct {
|
||||||
// The series list fo bar chart
|
p *Painter
|
||||||
SeriesList SeriesList
|
opt *BarChartOption
|
||||||
// The theme
|
|
||||||
Theme string
|
|
||||||
// The font
|
|
||||||
Font *truetype.Font
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointRenderOption, error) {
|
func NewBarChart(p *Painter, opt BarChartOption) *barChart {
|
||||||
d, err := NewDraw(DrawOption{
|
if opt.Theme == nil {
|
||||||
Parent: result.d,
|
opt.Theme = defaultTheme
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Top: result.titleBox.Height(),
|
|
||||||
// TODO 后续考虑是否需要根据左侧是否展示y轴再生成对应的left
|
|
||||||
Left: YAxisWidth,
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
xRange := result.xRange
|
return &barChart{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BarChartOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The x axis option
|
||||||
|
XAxis XAxisOption
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The y axis option
|
||||||
|
YAxisOptions []YAxisOption
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
p := b.p
|
||||||
|
opt := b.opt
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
|
|
||||||
|
xRange := NewRange(AxisRangeOption{
|
||||||
|
DivideCount: len(opt.XAxis.Data),
|
||||||
|
Size: seriesPainter.Width(),
|
||||||
|
})
|
||||||
x0, x1 := xRange.GetRange(0)
|
x0, x1 := xRange.GetRange(0)
|
||||||
width := int(x1 - x0)
|
width := int(x1 - x0)
|
||||||
// 每一块之间的margin
|
// 每一块之间的margin
|
||||||
|
|
@ -61,50 +82,34 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
|
||||||
margin = 5
|
margin = 5
|
||||||
barMargin = 3
|
barMargin = 3
|
||||||
}
|
}
|
||||||
|
seriesCount := len(seriesList)
|
||||||
seriesCount := len(opt.SeriesList)
|
|
||||||
// 总的宽度-两个margin-(总数-1)的barMargin
|
// 总的宽度-两个margin-(总数-1)的barMargin
|
||||||
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList)
|
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList)
|
||||||
|
barMaxHeight := seriesPainter.Height()
|
||||||
|
theme := opt.Theme
|
||||||
|
seriesNames := seriesList.Names()
|
||||||
|
|
||||||
barMaxHeight := result.getYRange(0).Size
|
markPointPainter := NewMarkPointPainter(seriesPainter)
|
||||||
theme := NewTheme(opt.Theme)
|
markLinePainter := NewMarkLinePainter(seriesPainter)
|
||||||
|
rendererList := []Renderer{
|
||||||
seriesNames := opt.SeriesList.Names()
|
markPointPainter,
|
||||||
|
markLinePainter,
|
||||||
r := d.Render
|
|
||||||
|
|
||||||
markPointRenderOptions := make([]markPointRenderOption, 0)
|
|
||||||
|
|
||||||
for i, s := range opt.SeriesList {
|
|
||||||
// 由于series是for range,为同一个数据,因此需要clone
|
|
||||||
// 后续需要使用,如mark point
|
|
||||||
series := s
|
|
||||||
yRange := result.getYRange(series.YAxisIndex)
|
|
||||||
points := make([]Point, len(series.Data))
|
|
||||||
index := series.index
|
|
||||||
if index == 0 {
|
|
||||||
index = i
|
|
||||||
}
|
}
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
for index := range seriesList {
|
||||||
// mark line
|
series := seriesList[index]
|
||||||
markLineRender(markLineRenderOption{
|
yRange := result.axisRanges[series.AxisIndex]
|
||||||
Draw: d,
|
seriesColor := theme.GetSeriesColor(series.index)
|
||||||
FillColor: seriesColor,
|
|
||||||
FontColor: theme.GetTextColor(),
|
|
||||||
StrokeColor: seriesColor,
|
|
||||||
Font: opt.Font,
|
|
||||||
Series: &series,
|
|
||||||
Range: yRange,
|
|
||||||
})
|
|
||||||
divideValues := xRange.AutoDivide()
|
divideValues := xRange.AutoDivide()
|
||||||
|
points := make([]Point, len(series.Data))
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
if j >= xRange.divideCount {
|
if j >= xRange.divideCount {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
x := divideValues[j]
|
x := divideValues[j]
|
||||||
x += margin
|
x += margin
|
||||||
if i != 0 {
|
if index != 0 {
|
||||||
x += i * (barWidth + barMargin)
|
x += index * (barWidth + barMargin)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := int(yRange.getHeight(item.Value))
|
h := int(yRange.getHeight(item.Value))
|
||||||
|
|
@ -113,13 +118,14 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
|
||||||
fillColor = item.Style.FillColor
|
fillColor = item.Style.FillColor
|
||||||
}
|
}
|
||||||
top := barMaxHeight - h
|
top := barMaxHeight - h
|
||||||
d.Bar(chart.Box{
|
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
FillColor: fillColor,
|
||||||
|
}).Rect(chart.Box{
|
||||||
Top: top,
|
Top: top,
|
||||||
Left: x,
|
Left: x,
|
||||||
Right: x + barWidth,
|
Right: x + barWidth,
|
||||||
Bottom: barMaxHeight - 1,
|
Bottom: barMaxHeight - 1,
|
||||||
}, BarStyle{
|
|
||||||
FillColor: fillColor,
|
|
||||||
})
|
})
|
||||||
// 用于生成marker point
|
// 用于生成marker point
|
||||||
points[j] = Point{
|
points[j] = Point{
|
||||||
|
|
@ -127,6 +133,12 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
|
||||||
X: x + barWidth>>1,
|
X: x + barWidth>>1,
|
||||||
Y: top,
|
Y: top,
|
||||||
}
|
}
|
||||||
|
// 用于生成marker point
|
||||||
|
points[j] = Point{
|
||||||
|
// 居中的位置
|
||||||
|
X: x + barWidth>>1,
|
||||||
|
Y: top,
|
||||||
|
}
|
||||||
// 如果label不需要展示,则返回
|
// 如果label不需要展示,则返回
|
||||||
if !series.Label.Show {
|
if !series.Label.Show {
|
||||||
continue
|
continue
|
||||||
|
|
@ -135,8 +147,8 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
|
||||||
if distance == 0 {
|
if distance == 0 {
|
||||||
distance = 5
|
distance = 5
|
||||||
}
|
}
|
||||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1)
|
||||||
labelStyle := chart.Style{
|
labelStyle := Style{
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: labelFontSize,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
|
|
@ -144,20 +156,50 @@ func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointR
|
||||||
if !series.Label.Color.IsZero() {
|
if !series.Label.Color.IsZero() {
|
||||||
labelStyle.FontColor = series.Label.Color
|
labelStyle.FontColor = series.Label.Color
|
||||||
}
|
}
|
||||||
labelStyle.GetTextOptions().WriteToRenderer(r)
|
seriesPainter.OverrideTextStyle(labelStyle)
|
||||||
textBox := r.MeasureText(text)
|
textBox := seriesPainter.MeasureText(text)
|
||||||
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance)
|
seriesPainter.Text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成mark point的参数
|
markPointPainter.Add(markPointRenderOption{
|
||||||
markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{
|
|
||||||
Draw: d,
|
|
||||||
FillColor: seriesColor,
|
FillColor: seriesColor,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
|
Series: series,
|
||||||
Points: points,
|
Points: points,
|
||||||
Series: &series,
|
})
|
||||||
|
markLinePainter.Add(markLineRenderOption{
|
||||||
|
FillColor: seriesColor,
|
||||||
|
FontColor: opt.Theme.GetTextColor(),
|
||||||
|
StrokeColor: seriesColor,
|
||||||
|
Font: opt.Font,
|
||||||
|
Series: series,
|
||||||
|
Range: yRange,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 最大、最小的mark point
|
||||||
|
err := doRender(rendererList...)
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
|
||||||
return markPointRenderOptions, nil
|
return p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *barChart) Render() (Box, error) {
|
||||||
|
p := b.p
|
||||||
|
opt := b.opt
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
YAxisOptions: opt.YAxisOptions,
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeLine)
|
||||||
|
return b.render(renderResult, seriesList)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBarChartRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
width := 400
|
|
||||||
height := 300
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
result := basicRenderResult{
|
|
||||||
xRange: &Range{
|
|
||||||
Min: 0,
|
|
||||||
Max: 4,
|
|
||||||
divideCount: 4,
|
|
||||||
Size: width,
|
|
||||||
Boundary: true,
|
|
||||||
},
|
|
||||||
yRangeList: []*Range{
|
|
||||||
{
|
|
||||||
divideCount: 6,
|
|
||||||
Max: 100,
|
|
||||||
Min: 0,
|
|
||||||
Size: height,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
d: d,
|
|
||||||
}
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
|
|
||||||
markPointOptions, err := barChartRender(barChartOption{
|
|
||||||
Font: f,
|
|
||||||
SeriesList: SeriesList{
|
|
||||||
{
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
Color: drawing.ColorBlue,
|
|
||||||
},
|
|
||||||
MarkLine: NewMarkLine(
|
|
||||||
SeriesMarkDataTypeMin,
|
|
||||||
),
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 60,
|
|
||||||
Style: chart.Style{
|
|
||||||
FillColor: drawing.ColorRed,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 90,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NewSeriesFromValues([]float64{
|
|
||||||
80,
|
|
||||||
30,
|
|
||||||
70,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(2, len(markPointOptions))
|
|
||||||
assert.Equal([]Point{
|
|
||||||
{
|
|
||||||
X: 28,
|
|
||||||
Y: 240,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 128,
|
|
||||||
Y: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 228,
|
|
||||||
Y: 30,
|
|
||||||
},
|
|
||||||
}, markPointOptions[0].Points)
|
|
||||||
assert.Equal([]Point{
|
|
||||||
{
|
|
||||||
X: 70,
|
|
||||||
Y: 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 170,
|
|
||||||
Y: 210,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 270,
|
|
||||||
Y: 90,
|
|
||||||
},
|
|
||||||
}, markPointOptions[1].Points)
|
|
||||||
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"40\" cy=\"240\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 45 240\nL 382 240\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 382 235\nL 398 240\nL 382 245\nL 387 240\nL 382 235\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"400\" y=\"244\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text><path d=\"M 50 240\nL 87 240\nL 87 299\nL 50 299\nL 50 240\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"61\" y=\"235\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text><path d=\"M 150 120\nL 187 120\nL 187 299\nL 150 299\nL 150 120\" style=\"stroke-width:1;stroke:rgba(255,0,0,1.0);fill:rgba(255,0,0,1.0)\"/><text x=\"161\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">60</text><path d=\"M 250 30\nL 287 30\nL 287 299\nL 250 299\nL 250 30\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"261\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">90</text><path d=\"M 92 60\nL 129 60\nL 129 299\nL 92 299\nL 92 60\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"M 192 210\nL 229 210\nL 229 299\nL 192 299\nL 192 210\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"M 292 90\nL 329 90\nL 329 299\nL 292 299\nL 292 90\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
78
bar_test.go
78
bar_test.go
|
|
@ -1,78 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBarStyle(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
bs := BarStyle{
|
|
||||||
ClassName: "test",
|
|
||||||
StrokeDashArray: []float64{
|
|
||||||
1.0,
|
|
||||||
},
|
|
||||||
FillColor: drawing.ColorBlack,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(chart.Style{
|
|
||||||
ClassName: "test",
|
|
||||||
StrokeDashArray: []float64{
|
|
||||||
1.0,
|
|
||||||
},
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: drawing.ColorBlack,
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
}, bs.Style())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrawBar(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Left: 10,
|
|
||||||
Top: 20,
|
|
||||||
Right: 30,
|
|
||||||
Bottom: 40,
|
|
||||||
}))
|
|
||||||
assert.Nil(err)
|
|
||||||
d.Bar(chart.Box{
|
|
||||||
Left: 0,
|
|
||||||
Top: 0,
|
|
||||||
Right: 20,
|
|
||||||
Bottom: 200,
|
|
||||||
}, BarStyle{
|
|
||||||
FillColor: drawing.ColorBlack,
|
|
||||||
})
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 10 20\nL 30 20\nL 30 220\nL 10 220\nL 10 20\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
502
chart.go
502
chart.go
|
|
@ -1,502 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChartTypeLine = "line"
|
|
||||||
ChartTypeBar = "bar"
|
|
||||||
ChartTypePie = "pie"
|
|
||||||
ChartTypeRadar = "radar"
|
|
||||||
ChartTypeFunnel = "funnel"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChartOutputSVG = "svg"
|
|
||||||
ChartOutputPNG = "png"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Point struct {
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelFontSize = 10
|
|
||||||
const defaultDotWidth = 2.0
|
|
||||||
const defaultStrokeWidth = 2.0
|
|
||||||
|
|
||||||
var defaultChartWidth = 600
|
|
||||||
var defaultChartHeight = 400
|
|
||||||
|
|
||||||
type ChartOption struct {
|
|
||||||
// The output type of chart, "svg" or "png", default value is "svg"
|
|
||||||
Type string
|
|
||||||
// The font family, which should be installed first
|
|
||||||
FontFamily string
|
|
||||||
// The font of chart, the default font is "roboto"
|
|
||||||
Font *truetype.Font
|
|
||||||
// The theme of chart, "light" and "dark".
|
|
||||||
// The default theme is "light"
|
|
||||||
Theme string
|
|
||||||
// The title option
|
|
||||||
Title TitleOption
|
|
||||||
// The legend option
|
|
||||||
Legend LegendOption
|
|
||||||
// The x axis option
|
|
||||||
XAxis XAxisOption
|
|
||||||
// The y axis option list
|
|
||||||
YAxisList []YAxisOption
|
|
||||||
// The width of chart, default width is 600
|
|
||||||
Width int
|
|
||||||
// The height of chart, default height is 400
|
|
||||||
Height int
|
|
||||||
Parent *Draw
|
|
||||||
// The padding for chart, default padding is [20, 10, 10, 10]
|
|
||||||
Padding chart.Box
|
|
||||||
// The canvas box for chart
|
|
||||||
Box chart.Box
|
|
||||||
// The series list
|
|
||||||
SeriesList SeriesList
|
|
||||||
// The radar indicator list
|
|
||||||
RadarIndicators []RadarIndicator
|
|
||||||
// The background color of chart
|
|
||||||
BackgroundColor drawing.Color
|
|
||||||
// The child charts
|
|
||||||
Children []ChartOption
|
|
||||||
}
|
|
||||||
|
|
||||||
// FillDefault fills the default value for chart option
|
|
||||||
func (o *ChartOption) FillDefault(theme string) {
|
|
||||||
t := NewTheme(theme)
|
|
||||||
// 如果为空,初始化
|
|
||||||
yAxisCount := 1
|
|
||||||
for _, series := range o.SeriesList {
|
|
||||||
if series.YAxisIndex >= yAxisCount {
|
|
||||||
yAxisCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yAxisList := make([]YAxisOption, yAxisCount)
|
|
||||||
copy(yAxisList, o.YAxisList)
|
|
||||||
o.YAxisList = yAxisList
|
|
||||||
|
|
||||||
if o.Font == nil {
|
|
||||||
o.Font, _ = chart.GetDefaultFont()
|
|
||||||
}
|
|
||||||
if o.BackgroundColor.IsZero() {
|
|
||||||
o.BackgroundColor = t.GetBackgroundColor()
|
|
||||||
}
|
|
||||||
if o.Padding.IsZero() {
|
|
||||||
o.Padding = chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
Right: 10,
|
|
||||||
Bottom: 10,
|
|
||||||
Left: 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标题的默认值
|
|
||||||
if o.Title.Style.FontColor.IsZero() {
|
|
||||||
o.Title.Style.FontColor = t.GetTextColor()
|
|
||||||
}
|
|
||||||
if o.Title.Style.FontSize == 0 {
|
|
||||||
o.Title.Style.FontSize = 14
|
|
||||||
}
|
|
||||||
if o.Title.Style.Font == nil {
|
|
||||||
o.Title.Style.Font = o.Font
|
|
||||||
}
|
|
||||||
if o.Title.Style.Padding.IsZero() {
|
|
||||||
o.Title.Style.Padding = chart.Box{
|
|
||||||
Bottom: 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 副标题
|
|
||||||
if o.Title.SubtextStyle.FontColor.IsZero() {
|
|
||||||
o.Title.SubtextStyle.FontColor = o.Title.Style.FontColor.WithAlpha(180)
|
|
||||||
}
|
|
||||||
if o.Title.SubtextStyle.FontSize == 0 {
|
|
||||||
o.Title.SubtextStyle.FontSize = labelFontSize
|
|
||||||
}
|
|
||||||
if o.Title.SubtextStyle.Font == nil {
|
|
||||||
o.Title.SubtextStyle.Font = o.Font
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Legend.theme = theme
|
|
||||||
if o.Legend.Style.FontSize == 0 {
|
|
||||||
o.Legend.Style.FontSize = labelFontSize
|
|
||||||
}
|
|
||||||
if o.Legend.Left == "" {
|
|
||||||
o.Legend.Left = PositionCenter
|
|
||||||
}
|
|
||||||
// legend与series name的关联
|
|
||||||
if len(o.Legend.Data) == 0 {
|
|
||||||
o.Legend.Data = o.SeriesList.Names()
|
|
||||||
} else {
|
|
||||||
seriesCount := len(o.SeriesList)
|
|
||||||
for index, name := range o.Legend.Data {
|
|
||||||
if index < seriesCount &&
|
|
||||||
len(o.SeriesList[index].Name) == 0 {
|
|
||||||
o.SeriesList[index].Name = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nameIndexDict := map[string]int{}
|
|
||||||
for index, name := range o.Legend.Data {
|
|
||||||
nameIndexDict[name] = index
|
|
||||||
}
|
|
||||||
// 保证series的顺序与legend一致
|
|
||||||
sort.Slice(o.SeriesList, func(i, j int) bool {
|
|
||||||
return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 如果无legend数据,则隐藏
|
|
||||||
if len(strings.Join(o.Legend.Data, "")) == 0 {
|
|
||||||
o.Legend.Show = FalseFlag()
|
|
||||||
}
|
|
||||||
if o.Legend.Style.Font == nil {
|
|
||||||
o.Legend.Style.Font = o.Font
|
|
||||||
}
|
|
||||||
if o.Legend.Style.FontColor.IsZero() {
|
|
||||||
o.Legend.Style.FontColor = t.GetTextColor()
|
|
||||||
}
|
|
||||||
if o.XAxis.Theme == "" {
|
|
||||||
o.XAxis.Theme = theme
|
|
||||||
}
|
|
||||||
o.XAxis.Font = o.Font
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *ChartOption) getWidth() int {
|
|
||||||
if o.Width != 0 {
|
|
||||||
return o.Width
|
|
||||||
}
|
|
||||||
if o.Parent != nil {
|
|
||||||
return o.Parent.Box.Width()
|
|
||||||
}
|
|
||||||
return defaultChartWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetDefaultWidth(width int) {
|
|
||||||
if width > 0 {
|
|
||||||
defaultChartWidth = width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func SetDefaultHeight(height int) {
|
|
||||||
if height > 0 {
|
|
||||||
defaultChartHeight = height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *ChartOption) getHeight() int {
|
|
||||||
|
|
||||||
if o.Height != 0 {
|
|
||||||
return o.Height
|
|
||||||
}
|
|
||||||
if o.Parent != nil {
|
|
||||||
return o.Parent.Box.Height()
|
|
||||||
}
|
|
||||||
return defaultChartHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *ChartOption) newYRange(axisIndex int) Range {
|
|
||||||
min := math.MaxFloat64
|
|
||||||
max := -math.MaxFloat64
|
|
||||||
if axisIndex >= len(o.YAxisList) {
|
|
||||||
axisIndex = 0
|
|
||||||
}
|
|
||||||
yAxis := o.YAxisList[axisIndex]
|
|
||||||
|
|
||||||
for _, series := range o.SeriesList {
|
|
||||||
if series.YAxisIndex != axisIndex {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, item := range series.Data {
|
|
||||||
if item.Value > max {
|
|
||||||
max = item.Value
|
|
||||||
}
|
|
||||||
if item.Value < min {
|
|
||||||
min = item.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
min = min * 0.9
|
|
||||||
max = max * 1.1
|
|
||||||
if yAxis.Min != nil {
|
|
||||||
min = *yAxis.Min
|
|
||||||
}
|
|
||||||
if yAxis.Max != nil {
|
|
||||||
max = *yAxis.Max
|
|
||||||
}
|
|
||||||
divideCount := 6
|
|
||||||
// y轴分设置默认划分为6块
|
|
||||||
r := NewRange(min, max, divideCount)
|
|
||||||
|
|
||||||
// 由于NewRange会重新计算min max
|
|
||||||
if yAxis.Min != nil {
|
|
||||||
r.Min = min
|
|
||||||
}
|
|
||||||
if yAxis.Max != nil {
|
|
||||||
r.Max = max
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type basicRenderResult struct {
|
|
||||||
xRange *Range
|
|
||||||
yRangeList []*Range
|
|
||||||
d *Draw
|
|
||||||
titleBox chart.Box
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *basicRenderResult) getYRange(index int) *Range {
|
|
||||||
if index >= len(r.yRangeList) {
|
|
||||||
index = 0
|
|
||||||
}
|
|
||||||
return r.yRangeList[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render renders the chart by option
|
|
||||||
func Render(opt ChartOption, optFuncs ...OptionFunc) (*Draw, error) {
|
|
||||||
for _, optFunc := range optFuncs {
|
|
||||||
optFunc(&opt)
|
|
||||||
}
|
|
||||||
if len(opt.SeriesList) == 0 {
|
|
||||||
return nil, errors.New("series can not be nil")
|
|
||||||
}
|
|
||||||
if len(opt.FontFamily) != 0 {
|
|
||||||
f, err := GetFont(opt.FontFamily)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
opt.Font = f
|
|
||||||
}
|
|
||||||
opt.FillDefault(opt.Theme)
|
|
||||||
|
|
||||||
lineSeries := make([]Series, 0)
|
|
||||||
barSeries := make([]Series, 0)
|
|
||||||
isPieChart := false
|
|
||||||
isRadarChart := false
|
|
||||||
isFunnelChart := false
|
|
||||||
for index := range opt.SeriesList {
|
|
||||||
opt.SeriesList[index].index = index
|
|
||||||
item := opt.SeriesList[index]
|
|
||||||
switch item.Type {
|
|
||||||
case ChartTypePie:
|
|
||||||
isPieChart = true
|
|
||||||
case ChartTypeRadar:
|
|
||||||
isRadarChart = true
|
|
||||||
case ChartTypeFunnel:
|
|
||||||
isFunnelChart = true
|
|
||||||
case ChartTypeBar:
|
|
||||||
barSeries = append(barSeries, item)
|
|
||||||
default:
|
|
||||||
lineSeries = append(lineSeries, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果指定了pie,则以pie的形式处理,pie不支持多类型图表
|
|
||||||
// pie不需要axis
|
|
||||||
// radar 同样处理
|
|
||||||
if isPieChart ||
|
|
||||||
isRadarChart ||
|
|
||||||
isFunnelChart {
|
|
||||||
opt.XAxis.Hidden = true
|
|
||||||
for index := range opt.YAxisList {
|
|
||||||
opt.YAxisList[index].Hidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result, err := chartBasicRender(&opt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
markPointRenderOptions := make([]markPointRenderOption, 0)
|
|
||||||
fns := []func() error{
|
|
||||||
// pie render
|
|
||||||
func() error {
|
|
||||||
if !isPieChart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return pieChartRender(pieChartOption{
|
|
||||||
SeriesList: opt.SeriesList,
|
|
||||||
Theme: opt.Theme,
|
|
||||||
Font: opt.Font,
|
|
||||||
}, result)
|
|
||||||
},
|
|
||||||
// radar render
|
|
||||||
func() error {
|
|
||||||
if !isRadarChart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return radarChartRender(radarChartOption{
|
|
||||||
SeriesList: opt.SeriesList,
|
|
||||||
Theme: opt.Theme,
|
|
||||||
Font: opt.Font,
|
|
||||||
Indicators: opt.RadarIndicators,
|
|
||||||
}, result)
|
|
||||||
},
|
|
||||||
// funnel render
|
|
||||||
func() error {
|
|
||||||
if !isFunnelChart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return funnelChartRender(funnelChartOption{
|
|
||||||
SeriesList: opt.SeriesList,
|
|
||||||
Theme: opt.Theme,
|
|
||||||
Font: opt.Font,
|
|
||||||
}, result)
|
|
||||||
},
|
|
||||||
// bar render
|
|
||||||
func() error {
|
|
||||||
// 如果无bar类型的series
|
|
||||||
if len(barSeries) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
options, err := barChartRender(barChartOption{
|
|
||||||
SeriesList: barSeries,
|
|
||||||
Theme: opt.Theme,
|
|
||||||
Font: opt.Font,
|
|
||||||
}, result)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
markPointRenderOptions = append(markPointRenderOptions, options...)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
// line render
|
|
||||||
func() error {
|
|
||||||
// 如果无line类型的series
|
|
||||||
if len(lineSeries) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
options, err := lineChartRender(lineChartOption{
|
|
||||||
Theme: opt.Theme,
|
|
||||||
SeriesList: lineSeries,
|
|
||||||
Font: opt.Font,
|
|
||||||
}, result)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
markPointRenderOptions = append(markPointRenderOptions, options...)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
// legend需要在顶层,因此此处render
|
|
||||||
func() error {
|
|
||||||
_, err := NewLegend(result.d, opt.Legend).Render()
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
// mark point最后render
|
|
||||||
func() error {
|
|
||||||
// mark point render不会出错
|
|
||||||
for _, opt := range markPointRenderOptions {
|
|
||||||
markPointRender(opt)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fn := range fns {
|
|
||||||
err = fn()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, child := range opt.Children {
|
|
||||||
child.Parent = result.d
|
|
||||||
if len(child.Theme) == 0 {
|
|
||||||
child.Theme = opt.Theme
|
|
||||||
}
|
|
||||||
_, err = Render(child)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
|
||||||
d, err := NewDraw(
|
|
||||||
DrawOption{
|
|
||||||
Type: opt.Type,
|
|
||||||
Parent: opt.Parent,
|
|
||||||
Width: opt.getWidth(),
|
|
||||||
Height: opt.getHeight(),
|
|
||||||
},
|
|
||||||
BoxOption(opt.Box),
|
|
||||||
PaddingOption(opt.Padding),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(opt.YAxisList) > 2 {
|
|
||||||
return nil, errors.New("y axis should not be gt 2")
|
|
||||||
}
|
|
||||||
if opt.Parent == nil {
|
|
||||||
d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
titleBox, err := drawTitle(d, &opt.Title)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
xAxisHeight := 0
|
|
||||||
var xRange *Range
|
|
||||||
|
|
||||||
if !opt.XAxis.Hidden {
|
|
||||||
// xAxis
|
|
||||||
xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis, len(opt.YAxisList))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yRangeList := make([]*Range, len(opt.YAxisList))
|
|
||||||
|
|
||||||
for index, yAxis := range opt.YAxisList {
|
|
||||||
var yRange *Range
|
|
||||||
if !yAxis.Hidden {
|
|
||||||
yRange, err = drawYAxis(d, opt, index, xAxisHeight, chart.Box{
|
|
||||||
Top: titleBox.Height(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
yRangeList[index] = yRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &basicRenderResult{
|
|
||||||
xRange: xRange,
|
|
||||||
yRangeList: yRangeList,
|
|
||||||
d: d,
|
|
||||||
titleBox: titleBox,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
206
chart_option.go
206
chart_option.go
|
|
@ -23,10 +23,49 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChartOption struct {
|
||||||
|
theme ColorPalette
|
||||||
|
font *truetype.Font
|
||||||
|
// The output type of chart, "svg" or "png", default value is "svg"
|
||||||
|
Type string
|
||||||
|
// The font family, which should be installed first
|
||||||
|
FontFamily string
|
||||||
|
// The theme of chart, "light" and "dark".
|
||||||
|
// The default theme is "light"
|
||||||
|
Theme string
|
||||||
|
// The title option
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
// The x axis option
|
||||||
|
XAxis XAxisOption
|
||||||
|
// The y axis option list
|
||||||
|
YAxisOptions []YAxisOption
|
||||||
|
// The width of chart, default width is 600
|
||||||
|
Width int
|
||||||
|
// The height of chart, default height is 400
|
||||||
|
Height int
|
||||||
|
Parent *Painter
|
||||||
|
// The padding for chart, default padding is [20, 10, 10, 10]
|
||||||
|
Padding Box
|
||||||
|
// The canvas box for chart
|
||||||
|
Box Box
|
||||||
|
// The series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The radar indicator list
|
||||||
|
RadarIndicators []RadarIndicator
|
||||||
|
// The background color of chart
|
||||||
|
BackgroundColor Color
|
||||||
|
// The child charts
|
||||||
|
Children []ChartOption
|
||||||
|
}
|
||||||
|
|
||||||
// OptionFunc option function
|
// OptionFunc option function
|
||||||
type OptionFunc func(opt *ChartOption)
|
type OptionFunc func(opt *ChartOption)
|
||||||
|
|
||||||
|
|
@ -63,6 +102,13 @@ func TitleOptionFunc(title TitleOption) OptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TitleTextOptionFunc set title text of chart
|
||||||
|
func TitleTextOptionFunc(text string) OptionFunc {
|
||||||
|
return func(opt *ChartOption) {
|
||||||
|
opt.Title.Text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LegendOptionFunc set legend of chart
|
// LegendOptionFunc set legend of chart
|
||||||
func LegendOptionFunc(legend LegendOption) OptionFunc {
|
func LegendOptionFunc(legend LegendOption) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
|
|
@ -70,6 +116,13 @@ func LegendOptionFunc(legend LegendOption) OptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LegendLabelsOptionFunc set legend labels of chart
|
||||||
|
func LegendLabelsOptionFunc(labels []string, left ...string) OptionFunc {
|
||||||
|
return func(opt *ChartOption) {
|
||||||
|
opt.Legend = NewLegendOption(labels, left...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// XAxisOptionFunc set x axis of chart
|
// XAxisOptionFunc set x axis of chart
|
||||||
func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc {
|
func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
|
|
@ -77,10 +130,24 @@ func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XAxisDataOptionFunc set x axis data of chart
|
||||||
|
func XAxisDataOptionFunc(data []string, boundaryGap ...*bool) OptionFunc {
|
||||||
|
return func(opt *ChartOption) {
|
||||||
|
opt.XAxis = NewXAxisOption(data, boundaryGap...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// YAxisOptionFunc set y axis of chart, support two y axis
|
// YAxisOptionFunc set y axis of chart, support two y axis
|
||||||
func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc {
|
func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
opt.YAxisList = yAxisOption
|
opt.YAxisOptions = yAxisOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAxisDataOptionFunc set y axis data of chart
|
||||||
|
func YAxisDataOptionFunc(data []string) OptionFunc {
|
||||||
|
return func(opt *ChartOption) {
|
||||||
|
opt.YAxisOptions = NewYAxisOptions(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,19 +166,28 @@ func HeightOptionFunc(height int) OptionFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaddingOptionFunc set padding of chart
|
// PaddingOptionFunc set padding of chart
|
||||||
func PaddingOptionFunc(padding chart.Box) OptionFunc {
|
func PaddingOptionFunc(padding Box) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
opt.Padding = padding
|
opt.Padding = padding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoxOptionFunc set box of chart
|
// BoxOptionFunc set box of chart
|
||||||
func BoxOptionFunc(box chart.Box) OptionFunc {
|
func BoxOptionFunc(box Box) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
opt.Box = box
|
opt.Box = box
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PieSeriesShowLabel set pie series show label
|
||||||
|
func PieSeriesShowLabel() OptionFunc {
|
||||||
|
return func(opt *ChartOption) {
|
||||||
|
for index := range opt.SeriesList {
|
||||||
|
opt.SeriesList[index].Label.Show = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ChildOptionFunc add child chart
|
// ChildOptionFunc add child chart
|
||||||
func ChildOptionFunc(child ...ChartOption) OptionFunc {
|
func ChildOptionFunc(child ...ChartOption) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
|
|
@ -123,61 +199,143 @@ func ChildOptionFunc(child ...ChartOption) OptionFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RadarIndicatorOptionFunc set radar indicator of chart
|
// RadarIndicatorOptionFunc set radar indicator of chart
|
||||||
func RadarIndicatorOptionFunc(radarIndicator ...RadarIndicator) OptionFunc {
|
func RadarIndicatorOptionFunc(names []string, values []float64) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
opt.RadarIndicators = radarIndicator
|
if len(names) != len(values) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
indicators := make([]RadarIndicator, len(names))
|
||||||
|
for index, name := range names {
|
||||||
|
indicators[index] = RadarIndicator{
|
||||||
|
Name: name,
|
||||||
|
Max: values[index],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opt.RadarIndicators = indicators
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackgroundColorOptionFunc set background color of chart
|
// BackgroundColorOptionFunc set background color of chart
|
||||||
func BackgroundColorOptionFunc(color drawing.Color) OptionFunc {
|
func BackgroundColorOptionFunc(color Color) OptionFunc {
|
||||||
return func(opt *ChartOption) {
|
return func(opt *ChartOption) {
|
||||||
opt.BackgroundColor = color
|
opt.BackgroundColor = color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LineRender line chart render
|
// MarkLineOptionFunc set mark line for series of chart
|
||||||
func LineRender(values [][]float64, opts ...OptionFunc) (*Draw, error) {
|
func MarkLineOptionFunc(seriesIndex int, markLineTypes ...string) OptionFunc {
|
||||||
seriesList := make(SeriesList, len(values))
|
return func(opt *ChartOption) {
|
||||||
for index, value := range values {
|
if len(opt.SeriesList) <= seriesIndex {
|
||||||
seriesList[index] = NewSeriesFromValues(value, ChartTypeLine)
|
return
|
||||||
}
|
}
|
||||||
|
opt.SeriesList[seriesIndex].MarkLine = NewMarkLine(markLineTypes...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkPointOptionFunc set mark point for series of chart
|
||||||
|
func MarkPointOptionFunc(seriesIndex int, markPointTypes ...string) OptionFunc {
|
||||||
|
return func(opt *ChartOption) {
|
||||||
|
if len(opt.SeriesList) <= seriesIndex {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opt.SeriesList[seriesIndex].MarkPoint = NewMarkPoint(markPointTypes...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ChartOption) fillDefault() {
|
||||||
|
t := NewTheme(o.Theme)
|
||||||
|
o.theme = t
|
||||||
|
// 如果为空,初始化
|
||||||
|
axisCount := 1
|
||||||
|
for _, series := range o.SeriesList {
|
||||||
|
if series.AxisIndex >= axisCount {
|
||||||
|
axisCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.Width = getDefaultInt(o.Width, defaultChartWidth)
|
||||||
|
o.Height = getDefaultInt(o.Height, defaultChartHeight)
|
||||||
|
yAxisOptions := make([]YAxisOption, axisCount)
|
||||||
|
copy(yAxisOptions, o.YAxisOptions)
|
||||||
|
o.YAxisOptions = yAxisOptions
|
||||||
|
o.font, _ = GetFont(o.FontFamily)
|
||||||
|
|
||||||
|
if o.font == nil {
|
||||||
|
o.font, _ = chart.GetDefaultFont()
|
||||||
|
}
|
||||||
|
if o.BackgroundColor.IsZero() {
|
||||||
|
o.BackgroundColor = t.GetBackgroundColor()
|
||||||
|
}
|
||||||
|
if o.Padding.IsZero() {
|
||||||
|
o.Padding = Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 20,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// legend与series name的关联
|
||||||
|
if len(o.Legend.Data) == 0 {
|
||||||
|
o.Legend.Data = o.SeriesList.Names()
|
||||||
|
} else {
|
||||||
|
seriesCount := len(o.SeriesList)
|
||||||
|
for index, name := range o.Legend.Data {
|
||||||
|
if index < seriesCount &&
|
||||||
|
len(o.SeriesList[index].Name) == 0 {
|
||||||
|
o.SeriesList[index].Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nameIndexDict := map[string]int{}
|
||||||
|
for index, name := range o.Legend.Data {
|
||||||
|
nameIndexDict[name] = index
|
||||||
|
}
|
||||||
|
// 保证series的顺序与legend一致
|
||||||
|
sort.Slice(o.SeriesList, func(i, j int) bool {
|
||||||
|
return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineRender line chart render
|
||||||
|
func LineRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
|
||||||
|
seriesList := NewSeriesListDataFromValues(values, ChartTypeLine)
|
||||||
return Render(ChartOption{
|
return Render(ChartOption{
|
||||||
SeriesList: seriesList,
|
SeriesList: seriesList,
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BarRender bar chart render
|
// BarRender bar chart render
|
||||||
func BarRender(values [][]float64, opts ...OptionFunc) (*Draw, error) {
|
func BarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
|
||||||
seriesList := make(SeriesList, len(values))
|
seriesList := NewSeriesListDataFromValues(values, ChartTypeBar)
|
||||||
for index, value := range values {
|
return Render(ChartOption{
|
||||||
seriesList[index] = NewSeriesFromValues(value, ChartTypeBar)
|
SeriesList: seriesList,
|
||||||
}
|
}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalBarRender horizontal bar chart render
|
||||||
|
func HorizontalBarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
|
||||||
|
seriesList := NewSeriesListDataFromValues(values, ChartTypeHorizontalBar)
|
||||||
return Render(ChartOption{
|
return Render(ChartOption{
|
||||||
SeriesList: seriesList,
|
SeriesList: seriesList,
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PieRender pie chart render
|
// PieRender pie chart render
|
||||||
func PieRender(values []float64, opts ...OptionFunc) (*Draw, error) {
|
func PieRender(values []float64, opts ...OptionFunc) (*Painter, error) {
|
||||||
return Render(ChartOption{
|
return Render(ChartOption{
|
||||||
SeriesList: NewPieSeriesList(values),
|
SeriesList: NewPieSeriesList(values),
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RadarRender radar chart render
|
// RadarRender radar chart render
|
||||||
func RadarRender(values [][]float64, opts ...OptionFunc) (*Draw, error) {
|
func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
|
||||||
seriesList := make(SeriesList, len(values))
|
seriesList := NewSeriesListDataFromValues(values, ChartTypeRadar)
|
||||||
for index, value := range values {
|
|
||||||
seriesList[index] = NewSeriesFromValues(value, ChartTypeRadar)
|
|
||||||
}
|
|
||||||
return Render(ChartOption{
|
return Render(ChartOption{
|
||||||
SeriesList: seriesList,
|
SeriesList: seriesList,
|
||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FunnelRender funnel chart render
|
// FunnelRender funnel chart render
|
||||||
func FunnelRender(values []float64, opts ...OptionFunc) (*Draw, error) {
|
func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) {
|
||||||
seriesList := make(SeriesList, len(values))
|
seriesList := make(SeriesList, len(values))
|
||||||
for index, value := range values {
|
for index, value := range values {
|
||||||
seriesList[index] = NewSeriesFromValues([]float64{
|
seriesList[index] = NewSeriesFromValues([]float64{
|
||||||
|
|
|
||||||
|
|
@ -1,238 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOptionFunc(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
fns := []OptionFunc{
|
|
||||||
TypeOptionFunc(ChartOutputPNG),
|
|
||||||
FontFamilyOptionFunc("fontFamily"),
|
|
||||||
ThemeOptionFunc("black"),
|
|
||||||
TitleOptionFunc(TitleOption{
|
|
||||||
Text: "title",
|
|
||||||
}),
|
|
||||||
LegendOptionFunc(LegendOption{
|
|
||||||
Data: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
XAxisOptionFunc(NewXAxisOption([]string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
})),
|
|
||||||
YAxisOptionFunc(YAxisOption{
|
|
||||||
Min: NewFloatPoint(0),
|
|
||||||
Max: NewFloatPoint(100),
|
|
||||||
}),
|
|
||||||
WidthOptionFunc(400),
|
|
||||||
HeightOptionFunc(300),
|
|
||||||
PaddingOptionFunc(chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
}),
|
|
||||||
BoxOptionFunc(chart.Box{
|
|
||||||
Left: 0,
|
|
||||||
Right: 300,
|
|
||||||
}),
|
|
||||||
ChildOptionFunc(ChartOption{}),
|
|
||||||
RadarIndicatorOptionFunc(RadarIndicator{
|
|
||||||
Min: 0,
|
|
||||||
Max: 10,
|
|
||||||
}),
|
|
||||||
BackgroundColorOptionFunc(drawing.ColorBlack),
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := ChartOption{}
|
|
||||||
for _, fn := range fns {
|
|
||||||
fn(&opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal("png", opt.Type)
|
|
||||||
assert.Equal("fontFamily", opt.FontFamily)
|
|
||||||
assert.Equal("black", opt.Theme)
|
|
||||||
assert.Equal(TitleOption{
|
|
||||||
Text: "title",
|
|
||||||
}, opt.Title)
|
|
||||||
assert.Equal(LegendOption{
|
|
||||||
Data: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
},
|
|
||||||
}, opt.Legend)
|
|
||||||
assert.Equal(NewXAxisOption([]string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
}), opt.XAxis)
|
|
||||||
assert.Equal([]YAxisOption{
|
|
||||||
{
|
|
||||||
Min: NewFloatPoint(0),
|
|
||||||
Max: NewFloatPoint(100),
|
|
||||||
},
|
|
||||||
}, opt.YAxisList)
|
|
||||||
assert.Equal(400, opt.Width)
|
|
||||||
assert.Equal(300, opt.Height)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
}, opt.Padding)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Left: 0,
|
|
||||||
Right: 300,
|
|
||||||
}, opt.Box)
|
|
||||||
assert.Equal(1, len(opt.Children))
|
|
||||||
assert.Equal([]RadarIndicator{
|
|
||||||
{
|
|
||||||
Min: 0,
|
|
||||||
Max: 10,
|
|
||||||
},
|
|
||||||
}, opt.RadarIndicators)
|
|
||||||
assert.Equal(drawing.ColorBlack, opt.BackgroundColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLineRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := LineRender([][]float64{
|
|
||||||
{
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
1,
|
|
||||||
5,
|
|
||||||
2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
XAxisOptionFunc(NewXAxisOption([]string{
|
|
||||||
"01",
|
|
||||||
"02",
|
|
||||||
"03",
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 50 365\nL 590 365\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 50 365\nL 50 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 230 365\nL 230 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 410 365\nL 410 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 590 365\nL 590 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"132\" y=\"382\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01</text><text x=\"312\" y=\"382\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">02</text><text x=\"492\" y=\"382\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">03</text><path d=\"M 50 10\nL 50 365\" style=\"stroke-width:1;stroke:none;fill:none\"/><path d=\"M 50 10\nL 590 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 70\nL 590 70\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 129\nL 590 129\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 188\nL 590 188\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 247\nL 590 247\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 306\nL 590 306\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"36\" y=\"369\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0</text><text x=\"36\" y=\"310\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">2</text><text x=\"36\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">4</text><text x=\"36\" y=\"192\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6</text><text x=\"36\" y=\"133\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">8</text><text x=\"29\" y=\"74\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10</text><text x=\"29\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">12</text><path d=\"M 140 336\nL 320 306\nL 499 277\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"140\" cy=\"336\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"320\" cy=\"306\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"499\" cy=\"277\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"M 140 336\nL 320 218\nL 499 306\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:none\"/><circle cx=\"140\" cy=\"336\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"320\" cy=\"218\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"499\" cy=\"306\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBarRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := BarRender([][]float64{
|
|
||||||
{
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
1,
|
|
||||||
5,
|
|
||||||
2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
XAxisOptionFunc(NewXAxisOption([]string{
|
|
||||||
"01",
|
|
||||||
"02",
|
|
||||||
"03",
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 50 365\nL 590 365\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 50 365\nL 50 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 230 365\nL 230 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 410 365\nL 410 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 590 365\nL 590 370\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"132\" y=\"382\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01</text><text x=\"312\" y=\"382\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">02</text><text x=\"492\" y=\"382\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">03</text><path d=\"M 50 10\nL 50 365\" style=\"stroke-width:1;stroke:none;fill:none\"/><path d=\"M 50 10\nL 590 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 70\nL 590 70\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 129\nL 590 129\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 188\nL 590 188\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 247\nL 590 247\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 306\nL 590 306\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"36\" y=\"369\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0</text><text x=\"36\" y=\"310\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">2</text><text x=\"36\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">4</text><text x=\"36\" y=\"192\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6</text><text x=\"36\" y=\"133\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">8</text><text x=\"29\" y=\"74\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10</text><text x=\"29\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">12</text><path d=\"M 60 336\nL 137 336\nL 137 364\nL 60 364\nL 60 336\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 240 306\nL 317 306\nL 317 364\nL 240 364\nL 240 306\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 420 277\nL 497 277\nL 497 364\nL 420 364\nL 420 277\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 142 336\nL 219 336\nL 219 364\nL 142 364\nL 142 336\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"M 322 218\nL 399 218\nL 399 364\nL 322 364\nL 322 218\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"M 502 306\nL 579 306\nL 579 364\nL 502 364\nL 502 306\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPieRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := PieRender([]float64{
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
5,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 300 200\nL 300 48\nA 152 152 40.00 0 1 397 84\nL 300 200\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 300 200\nL 397 84\nA 152 152 120.00 0 1 351 342\nL 300 200\nZ\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"M 300 200\nL 351 342\nA 152 152 200.00 1 1 300 48\nL 300 200\nZ\" style=\"stroke-width:1;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRadarRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
d, err := RadarRender([][]float64{
|
|
||||||
{
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
1,
|
|
||||||
5,
|
|
||||||
2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
RadarIndicatorOptionFunc([]RadarIndicator{
|
|
||||||
{
|
|
||||||
Name: "A",
|
|
||||||
Min: 0,
|
|
||||||
Max: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "B",
|
|
||||||
Min: 0,
|
|
||||||
Max: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "C",
|
|
||||||
Min: 0,
|
|
||||||
Max: 10,
|
|
||||||
},
|
|
||||||
}...),
|
|
||||||
)
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 300 170\nL 325 214\nL 275 215\nL 300 170\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 140\nL 351 229\nL 249 230\nL 300 140\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 110\nL 377 244\nL 223 245\nL 300 110\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 80\nL 403 259\nL 197 260\nL 300 80\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 50\nL 429 274\nL 171 275\nL 300 50\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 200\nL 300 50\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 200\nL 429 274\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 300 200\nL 171 275\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"296\" y=\"43\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">A</text><text x=\"434\" y=\"279\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">B</text><text x=\"157\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">C</text><path d=\"M 300 185\nL 325 214\nL 262 222\nL 300 185\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><path d=\"M 300 185\nL 325 214\nL 262 222\nL 300 185\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,0.1)\"/><circle cx=\"300\" cy=\"185\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"325\" cy=\"214\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"262\" cy=\"222\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"M 300 185\nL 364 237\nL 275 215\nL 300 185\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:none\"/><path d=\"M 300 185\nL 364 237\nL 275 215\nL 300 185\" style=\"stroke-width:0;stroke:none;fill:rgba(145,204,117,0.1)\"/><circle cx=\"300\" cy=\"185\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"364\" cy=\"237\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"275\" cy=\"215\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFunnelRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := FunnelRender([]float64{
|
|
||||||
5,
|
|
||||||
3,
|
|
||||||
1,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><path d=\"M 10 10\nL 590 10\nL 474 135\nL 126 135\nL 10 10\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,1.0)\"/><text x=\"280\" y=\"72\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">(100%)</text><path d=\"M 126 137\nL 474 137\nL 358 262\nL 242 262\nL 126 137\" style=\"stroke-width:0;stroke:none;fill:rgba(145,204,117,1.0)\"/><text x=\"284\" y=\"199\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">(60%)</text><path d=\"M 242 264\nL 358 264\nL 300 389\nL 300 389\nL 242 264\" style=\"stroke-width:0;stroke:none;fill:rgba(250,200,88,1.0)\"/><text x=\"284\" y=\"326\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">(20%)</text></svg>", string(data))
|
|
||||||
}
|
|
||||||
567
chart_test.go
567
chart_test.go
File diff suppressed because one or more lines are too long
418
charts.go
Normal file
418
charts.go
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
// 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"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const labelFontSize = 10
|
||||||
|
const defaultDotWidth = 2.0
|
||||||
|
const defaultStrokeWidth = 2.0
|
||||||
|
|
||||||
|
var defaultChartWidth = 600
|
||||||
|
var defaultChartHeight = 400
|
||||||
|
|
||||||
|
func SetDefaultWidth(width int) {
|
||||||
|
if width > 0 {
|
||||||
|
defaultChartWidth = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func SetDefaultHeight(height int) {
|
||||||
|
if height > 0 {
|
||||||
|
defaultChartHeight = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderer interface {
|
||||||
|
Render() (Box, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderHandler struct {
|
||||||
|
list []func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rh *renderHandler) Add(fn func() error) {
|
||||||
|
list := rh.list
|
||||||
|
if len(list) == 0 {
|
||||||
|
list = make([]func() error, 0)
|
||||||
|
}
|
||||||
|
rh.list = append(list, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rh *renderHandler) Do() error {
|
||||||
|
for _, fn := range rh.list {
|
||||||
|
err := fn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultRenderOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
Padding Box
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The y axis option
|
||||||
|
YAxisOptions []YAxisOption
|
||||||
|
// The x axis option
|
||||||
|
XAxis XAxisOption
|
||||||
|
// The title option
|
||||||
|
TitleOption TitleOption
|
||||||
|
// The legend option
|
||||||
|
LegendOption LegendOption
|
||||||
|
// background is filled
|
||||||
|
backgroundIsFilled bool
|
||||||
|
// x y axis is reversed
|
||||||
|
axisReversed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultRenderResult struct {
|
||||||
|
axisRanges map[int]axisRange
|
||||||
|
// 图例区域
|
||||||
|
seriesPainter *Painter
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) {
|
||||||
|
seriesList := opt.SeriesList
|
||||||
|
seriesList.init()
|
||||||
|
if !opt.backgroundIsFilled {
|
||||||
|
p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opt.Padding.IsZero() {
|
||||||
|
p = p.Child(PainterPaddingOption(opt.Padding))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opt.LegendOption.Data) != 0 {
|
||||||
|
if opt.LegendOption.Theme == nil {
|
||||||
|
opt.LegendOption.Theme = opt.Theme
|
||||||
|
}
|
||||||
|
_, err := NewLegendPainter(p, opt.LegendOption).Render()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有标题
|
||||||
|
if opt.TitleOption.Text != "" {
|
||||||
|
if opt.TitleOption.Theme == nil {
|
||||||
|
opt.TitleOption.Theme = opt.Theme
|
||||||
|
}
|
||||||
|
titlePainter := NewTitlePainter(p, opt.TitleOption)
|
||||||
|
|
||||||
|
titleBox, err := titlePainter.Render()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p = p.Child(PainterPaddingOption(Box{
|
||||||
|
// 标题下留白
|
||||||
|
Top: titleBox.Height() + 20,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := defaultRenderResult{
|
||||||
|
axisRanges: make(map[int]axisRange),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图表对应的轴有哪些
|
||||||
|
axisIndexList := make([]int, 0)
|
||||||
|
for _, series := range opt.SeriesList {
|
||||||
|
if containsInt(axisIndexList, series.AxisIndex) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
axisIndexList = append(axisIndexList, series.AxisIndex)
|
||||||
|
}
|
||||||
|
// 高度需要减去x轴的高度
|
||||||
|
rangeHeight := p.Height() - defaultXAxisHeight
|
||||||
|
rangeWidthLeft := 0
|
||||||
|
rangeWidthRight := 0
|
||||||
|
|
||||||
|
// 倒序
|
||||||
|
sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList)))
|
||||||
|
|
||||||
|
// 计算对应的axis range
|
||||||
|
for _, index := range axisIndexList {
|
||||||
|
yAxisOption := YAxisOption{}
|
||||||
|
if len(opt.YAxisOptions) > index {
|
||||||
|
yAxisOption = opt.YAxisOptions[index]
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
// 高度需要减去x轴的高度
|
||||||
|
Size: rangeHeight,
|
||||||
|
// 分隔数量
|
||||||
|
DivideCount: defaultAxisDivideCount,
|
||||||
|
})
|
||||||
|
result.axisRanges[index] = r
|
||||||
|
|
||||||
|
if yAxisOption.Theme == nil {
|
||||||
|
yAxisOption.Theme = opt.Theme
|
||||||
|
}
|
||||||
|
if !opt.axisReversed {
|
||||||
|
yAxisOption.Data = r.Values()
|
||||||
|
} else {
|
||||||
|
yAxisOption.isCategoryAxis = true
|
||||||
|
opt.XAxis.Data = r.Values()
|
||||||
|
opt.XAxis.isValueAxis = true
|
||||||
|
}
|
||||||
|
reverseStringSlice(yAxisOption.Data)
|
||||||
|
// TODO生成其它位置既yAxis
|
||||||
|
var yAxis *axisPainter
|
||||||
|
child := p.Child(PainterPaddingOption(Box{
|
||||||
|
Left: rangeWidthLeft,
|
||||||
|
Right: rangeWidthRight,
|
||||||
|
}))
|
||||||
|
if index == 0 {
|
||||||
|
yAxis = NewLeftYAxis(child, yAxisOption)
|
||||||
|
} else {
|
||||||
|
yAxis = NewRightYAxis(child, yAxisOption)
|
||||||
|
}
|
||||||
|
yAxisBox, err := yAxis.Render()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if index == 0 {
|
||||||
|
rangeWidthLeft += yAxisBox.Width()
|
||||||
|
} else {
|
||||||
|
rangeWidthRight += yAxisBox.Width()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.XAxis.Theme == nil {
|
||||||
|
opt.XAxis.Theme = opt.Theme
|
||||||
|
}
|
||||||
|
xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
|
||||||
|
Left: rangeWidthLeft,
|
||||||
|
Right: rangeWidthRight,
|
||||||
|
})), opt.XAxis)
|
||||||
|
_, err := xAxis.Render()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.seriesPainter = p.Child(PainterPaddingOption(Box{
|
||||||
|
Bottom: defaultXAxisHeight,
|
||||||
|
Left: rangeWidthLeft,
|
||||||
|
Right: rangeWidthRight,
|
||||||
|
}))
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRender(renderers ...Renderer) error {
|
||||||
|
for _, r := range renderers {
|
||||||
|
_, err := r.Render()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
|
||||||
|
for _, fn := range opts {
|
||||||
|
fn(&opt)
|
||||||
|
}
|
||||||
|
opt.fillDefault()
|
||||||
|
|
||||||
|
isChild := true
|
||||||
|
if opt.Parent == nil {
|
||||||
|
isChild = false
|
||||||
|
p, err := NewPainter(PainterOptions{
|
||||||
|
Type: opt.Type,
|
||||||
|
Width: opt.Width,
|
||||||
|
Height: opt.Height,
|
||||||
|
Font: opt.font,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opt.Parent = p
|
||||||
|
}
|
||||||
|
p := opt.Parent
|
||||||
|
if !opt.Box.IsZero() {
|
||||||
|
p = p.Child(PainterBoxOption(opt.Box))
|
||||||
|
}
|
||||||
|
if !isChild {
|
||||||
|
p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor)
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList
|
||||||
|
seriesList.init()
|
||||||
|
|
||||||
|
seriesCount := len(seriesList)
|
||||||
|
|
||||||
|
// line chart
|
||||||
|
lineSeriesList := seriesList.Filter(ChartTypeLine)
|
||||||
|
barSeriesList := seriesList.Filter(ChartTypeBar)
|
||||||
|
horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar)
|
||||||
|
pieSeriesList := seriesList.Filter(ChartTypePie)
|
||||||
|
radarSeriesList := seriesList.Filter(ChartTypeRadar)
|
||||||
|
funnelSeriesList := seriesList.Filter(ChartTypeFunnel)
|
||||||
|
|
||||||
|
if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Horizontal bar can not mix other charts")
|
||||||
|
}
|
||||||
|
if len(pieSeriesList) != 0 && len(pieSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Pie can not mix other charts")
|
||||||
|
}
|
||||||
|
if len(radarSeriesList) != 0 && len(radarSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Radar can not mix other charts")
|
||||||
|
}
|
||||||
|
if len(funnelSeriesList) != 0 && len(funnelSeriesList) != seriesCount {
|
||||||
|
return nil, errors.New("Funnel can not mix other charts")
|
||||||
|
}
|
||||||
|
|
||||||
|
axisReversed := len(horizontalBarSeriesList) != 0
|
||||||
|
renderOpt := defaultRenderOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
YAxisOptions: opt.YAxisOptions,
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
axisReversed: axisReversed,
|
||||||
|
}
|
||||||
|
if isChild {
|
||||||
|
renderOpt.backgroundIsFilled = true
|
||||||
|
}
|
||||||
|
if len(pieSeriesList) != 0 ||
|
||||||
|
len(radarSeriesList) != 0 ||
|
||||||
|
len(funnelSeriesList) != 0 {
|
||||||
|
renderOpt.XAxis.Show = FalseFlag()
|
||||||
|
renderOpt.YAxisOptions = []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResult, err := defaultRender(p, renderOpt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := renderHandler{}
|
||||||
|
|
||||||
|
// bar chart
|
||||||
|
if len(barSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewBarChart(p, BarChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
}).render(renderResult, barSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// horizontal bar chart
|
||||||
|
if len(horizontalBarSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewHorizontalBarChart(p, HorizontalBarChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
YAxisOptions: opt.YAxisOptions,
|
||||||
|
}).render(renderResult, horizontalBarSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// pie chart
|
||||||
|
if len(pieSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewPieChart(p, PieChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
}).render(renderResult, pieSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// line chart
|
||||||
|
if len(lineSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewLineChart(p, LineChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
}).render(renderResult, lineSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// radar chart
|
||||||
|
if len(radarSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewRadarChart(p, RadarChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
// 相应值
|
||||||
|
RadarIndicators: opt.RadarIndicators,
|
||||||
|
}).render(renderResult, radarSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// funnel chart
|
||||||
|
if len(funnelSeriesList) != 0 {
|
||||||
|
handler.Add(func() error {
|
||||||
|
_, err := NewFunnelChart(p, FunnelChartOption{
|
||||||
|
Theme: opt.theme,
|
||||||
|
Font: opt.font,
|
||||||
|
}).render(renderResult, funnelSeriesList)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.Do()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, item := range opt.Children {
|
||||||
|
item.Parent = p
|
||||||
|
if item.Theme == "" {
|
||||||
|
item.Theme = opt.Theme
|
||||||
|
}
|
||||||
|
if item.FontFamily == "" {
|
||||||
|
item.FontFamily = opt.FontFamily
|
||||||
|
}
|
||||||
|
_, err = Render(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
372
draw.go
372
draw.go
|
|
@ -1,372 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
PositionLeft = "left"
|
|
||||||
PositionRight = "right"
|
|
||||||
PositionCenter = "center"
|
|
||||||
PositionTop = "top"
|
|
||||||
PositionBottom = "bottom"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
OrientHorizontal = "horizontal"
|
|
||||||
OrientVertical = "vertical"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Draw struct {
|
|
||||||
// Render
|
|
||||||
Render chart.Renderer
|
|
||||||
// The canvas box
|
|
||||||
Box chart.Box
|
|
||||||
// The font for draw
|
|
||||||
Font *truetype.Font
|
|
||||||
// The parent of draw
|
|
||||||
parent *Draw
|
|
||||||
}
|
|
||||||
|
|
||||||
type DrawOption struct {
|
|
||||||
// Draw type, "svg" or "png", default type is "svg"
|
|
||||||
Type string
|
|
||||||
// Parent of draw
|
|
||||||
Parent *Draw
|
|
||||||
// The width of draw canvas
|
|
||||||
Width int
|
|
||||||
// The height of draw canvas
|
|
||||||
Height int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option func(*Draw) error
|
|
||||||
|
|
||||||
// PaddingOption sets the padding of draw canvas
|
|
||||||
func PaddingOption(padding chart.Box) Option {
|
|
||||||
return func(d *Draw) error {
|
|
||||||
d.Box.Left += padding.Left
|
|
||||||
d.Box.Top += padding.Top
|
|
||||||
d.Box.Right -= padding.Right
|
|
||||||
d.Box.Bottom -= padding.Bottom
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BoxOption set the box of draw canvas
|
|
||||||
func BoxOption(box chart.Box) Option {
|
|
||||||
return func(d *Draw) error {
|
|
||||||
if box.IsZero() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
d.Box = box
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDraw returns a new draw canvas
|
|
||||||
func NewDraw(opt DrawOption, opts ...Option) (*Draw, error) {
|
|
||||||
if opt.Parent == nil && (opt.Width <= 0 || opt.Height <= 0) {
|
|
||||||
return nil, errors.New("parent and width/height can not be nil")
|
|
||||||
}
|
|
||||||
font, _ := chart.GetDefaultFont()
|
|
||||||
d := &Draw{
|
|
||||||
Font: font,
|
|
||||||
}
|
|
||||||
width := opt.Width
|
|
||||||
height := opt.Height
|
|
||||||
if opt.Parent != nil {
|
|
||||||
d.parent = opt.Parent
|
|
||||||
d.Render = d.parent.Render
|
|
||||||
d.Box = opt.Parent.Box.Clone()
|
|
||||||
}
|
|
||||||
if width != 0 && height != 0 {
|
|
||||||
d.Box.Right = width + d.Box.Left
|
|
||||||
d.Box.Bottom = height + d.Box.Top
|
|
||||||
}
|
|
||||||
// 创建render
|
|
||||||
if d.parent == nil {
|
|
||||||
fn := chart.SVG
|
|
||||||
if opt.Type == ChartOutputPNG {
|
|
||||||
fn = chart.PNG
|
|
||||||
}
|
|
||||||
r, err := fn(d.Box.Right, d.Box.Bottom)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
d.Render = r
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range opts {
|
|
||||||
err := o(d)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent returns the parent of draw
|
|
||||||
func (d *Draw) Parent() *Draw {
|
|
||||||
return d.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top returns the top parent of draw
|
|
||||||
func (d *Draw) Top() *Draw {
|
|
||||||
if d.parent == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t := d.parent
|
|
||||||
// 限制最多查询次数,避免嵌套引用
|
|
||||||
for i := 50; i > 0; i-- {
|
|
||||||
if t.parent == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
t = t.parent
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the data of draw canvas
|
|
||||||
func (d *Draw) Bytes() ([]byte, error) {
|
|
||||||
buffer := bytes.Buffer{}
|
|
||||||
err := d.Render.Save(&buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buffer.Bytes(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) moveTo(x, y int) {
|
|
||||||
d.Render.MoveTo(x+d.Box.Left, y+d.Box.Top)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) arcTo(cx, cy int, rx, ry, startAngle, delta float64) {
|
|
||||||
d.Render.ArcTo(cx+d.Box.Left, cy+d.Box.Top, rx, ry, startAngle, delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) lineTo(x, y int) {
|
|
||||||
d.Render.LineTo(x+d.Box.Left, y+d.Box.Top)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) pin(x, y, width int) {
|
|
||||||
r := float64(width) / 2
|
|
||||||
y -= width / 4
|
|
||||||
angle := chart.DegreesToRadians(15)
|
|
||||||
|
|
||||||
startAngle := math.Pi/2 + angle
|
|
||||||
delta := 2*math.Pi - 2*angle
|
|
||||||
d.arcTo(x, y, r, r, startAngle, delta)
|
|
||||||
d.lineTo(x, y)
|
|
||||||
d.Render.Close()
|
|
||||||
d.Render.FillStroke()
|
|
||||||
|
|
||||||
startX := x - int(r)
|
|
||||||
startY := y
|
|
||||||
endX := x + int(r)
|
|
||||||
endY := y
|
|
||||||
d.moveTo(startX, startY)
|
|
||||||
|
|
||||||
left := d.Box.Left
|
|
||||||
top := d.Box.Top
|
|
||||||
cx := x
|
|
||||||
cy := y + int(r*2.5)
|
|
||||||
d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
|
|
||||||
d.Render.Close()
|
|
||||||
d.Render.Fill()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) arrowLeft(x, y, width, height int) {
|
|
||||||
d.arrow(x, y, width, height, PositionLeft)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) arrowRight(x, y, width, height int) {
|
|
||||||
d.arrow(x, y, width, height, PositionRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) arrowTop(x, y, width, height int) {
|
|
||||||
d.arrow(x, y, width, height, PositionTop)
|
|
||||||
}
|
|
||||||
func (d *Draw) arrowBottom(x, y, width, height int) {
|
|
||||||
d.arrow(x, y, width, height, PositionBottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) arrow(x, y, width, height int, direction string) {
|
|
||||||
halfWidth := width >> 1
|
|
||||||
halfHeight := height >> 1
|
|
||||||
if direction == PositionTop || direction == PositionBottom {
|
|
||||||
x0 := x - halfWidth
|
|
||||||
x1 := x0 + width
|
|
||||||
dy := -height / 3
|
|
||||||
y0 := y
|
|
||||||
y1 := y0 - height
|
|
||||||
if direction == PositionBottom {
|
|
||||||
y0 = y - height
|
|
||||||
y1 = y
|
|
||||||
dy = 2 * dy
|
|
||||||
}
|
|
||||||
d.moveTo(x0, y0)
|
|
||||||
d.lineTo(x0+halfWidth, y1)
|
|
||||||
d.lineTo(x1, y0)
|
|
||||||
d.lineTo(x0+halfWidth, y+dy)
|
|
||||||
d.lineTo(x0, y0)
|
|
||||||
} else {
|
|
||||||
x0 := x + width
|
|
||||||
x1 := x0 - width
|
|
||||||
y0 := y - halfHeight
|
|
||||||
dx := -width / 3
|
|
||||||
if direction == PositionRight {
|
|
||||||
x0 = x - width
|
|
||||||
dx = -dx
|
|
||||||
x1 = x0 + width
|
|
||||||
}
|
|
||||||
d.moveTo(x0, y0)
|
|
||||||
d.lineTo(x1, y0+halfHeight)
|
|
||||||
d.lineTo(x0, y0+height)
|
|
||||||
d.lineTo(x0+dx, y0+halfHeight)
|
|
||||||
d.lineTo(x0, y0)
|
|
||||||
}
|
|
||||||
d.Render.FillStroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) makeLine(x, y, width int) {
|
|
||||||
arrowWidth := 16
|
|
||||||
arrowHeight := 10
|
|
||||||
endX := x + width
|
|
||||||
d.circle(3, x, y)
|
|
||||||
d.Render.Fill()
|
|
||||||
d.moveTo(x+5, y)
|
|
||||||
d.lineTo(endX-arrowWidth, y)
|
|
||||||
d.Render.Stroke()
|
|
||||||
d.Render.SetStrokeDashArray([]float64{})
|
|
||||||
d.arrowRight(endX, y, arrowWidth, arrowHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) circle(radius float64, x, y int) {
|
|
||||||
d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) text(body string, x, y int) {
|
|
||||||
d.Render.Text(body, x+d.Box.Left, y+d.Box.Top)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) textFit(body string, x, y, width int, style chart.Style) chart.Box {
|
|
||||||
style.TextWrap = chart.TextWrapWord
|
|
||||||
r := d.Render
|
|
||||||
lines := chart.Text.WrapFit(r, body, width, style)
|
|
||||||
style.WriteTextOptionsToRenderer(r)
|
|
||||||
var output chart.Box
|
|
||||||
|
|
||||||
for index, line := range lines {
|
|
||||||
x0 := x
|
|
||||||
y0 := y + output.Height()
|
|
||||||
d.text(line, x0, y0)
|
|
||||||
lineBox := r.MeasureText(line)
|
|
||||||
output.Right = chart.MaxInt(lineBox.Right, output.Right)
|
|
||||||
output.Bottom += lineBox.Height()
|
|
||||||
if index < len(lines)-1 {
|
|
||||||
output.Bottom += +style.GetTextLineSpacing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) measureTextFit(body string, x, y, width int, style chart.Style) chart.Box {
|
|
||||||
style.TextWrap = chart.TextWrapWord
|
|
||||||
r := d.Render
|
|
||||||
lines := chart.Text.WrapFit(r, body, width, style)
|
|
||||||
style.WriteTextOptionsToRenderer(r)
|
|
||||||
return chart.Text.MeasureLines(r, lines, style)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) lineStroke(points []Point, style LineStyle) {
|
|
||||||
s := style.Style()
|
|
||||||
if !s.ShouldDrawStroke() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r := d.Render
|
|
||||||
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
for index, point := range points {
|
|
||||||
x := point.X
|
|
||||||
y := point.Y
|
|
||||||
if index == 0 {
|
|
||||||
d.moveTo(x, y)
|
|
||||||
} else {
|
|
||||||
d.lineTo(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.Stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) setBackground(width, height int, color drawing.Color) {
|
|
||||||
r := d.Render
|
|
||||||
s := chart.Style{
|
|
||||||
FillColor: color,
|
|
||||||
}
|
|
||||||
s.WriteToRenderer(r)
|
|
||||||
r.MoveTo(0, 0)
|
|
||||||
r.LineTo(width, 0)
|
|
||||||
r.LineTo(width, height)
|
|
||||||
r.LineTo(0, height)
|
|
||||||
r.LineTo(0, 0)
|
|
||||||
r.FillStroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) polygon(center Point, radius float64, sides int) {
|
|
||||||
points := getPolygonPoints(center, radius, sides)
|
|
||||||
for i, p := range points {
|
|
||||||
if i == 0 {
|
|
||||||
d.moveTo(p.X, p.Y)
|
|
||||||
} else {
|
|
||||||
d.lineTo(p.X, p.Y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.lineTo(points[0].X, points[0].Y)
|
|
||||||
d.Render.Stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) fill(points []Point, s chart.Style) {
|
|
||||||
if !s.ShouldDrawFill() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r := d.Render
|
|
||||||
var x, y int
|
|
||||||
s.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
for index, point := range points {
|
|
||||||
x = point.X
|
|
||||||
y = point.Y
|
|
||||||
if index == 0 {
|
|
||||||
d.moveTo(x, y)
|
|
||||||
} else {
|
|
||||||
d.lineTo(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.Fill()
|
|
||||||
}
|
|
||||||
34
echarts.go
34
echarts.go
|
|
@ -130,6 +130,7 @@ type EChartsXAxisData struct {
|
||||||
BoundaryGap *bool `json:"boundaryGap"`
|
BoundaryGap *bool `json:"boundaryGap"`
|
||||||
SplitNumber int `json:"splitNumber"`
|
SplitNumber int `json:"splitNumber"`
|
||||||
Data []string `json:"data"`
|
Data []string `json:"data"`
|
||||||
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
type EChartsXAxis struct {
|
type EChartsXAxis struct {
|
||||||
Data []EChartsXAxisData
|
Data []EChartsXAxisData
|
||||||
|
|
@ -155,6 +156,7 @@ type EChartsYAxisData struct {
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
} `json:"lineStyle"`
|
} `json:"lineStyle"`
|
||||||
} `json:"axisLine"`
|
} `json:"axisLine"`
|
||||||
|
Data []string `json:"data"`
|
||||||
}
|
}
|
||||||
type EChartsYAxis struct {
|
type EChartsYAxis struct {
|
||||||
Data []EChartsYAxisData `json:"data"`
|
Data []EChartsYAxisData `json:"data"`
|
||||||
|
|
@ -356,7 +358,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
|
||||||
seriesList = append(seriesList, Series{
|
seriesList = append(seriesList, Series{
|
||||||
Type: item.Type,
|
Type: item.Type,
|
||||||
Data: data,
|
Data: data,
|
||||||
YAxisIndex: item.YAxisIndex,
|
AxisIndex: item.YAxisIndex,
|
||||||
Style: item.ItemStyle.ToStyle(),
|
Style: item.ItemStyle.ToStyle(),
|
||||||
Label: SeriesLabel{
|
Label: SeriesLabel{
|
||||||
Color: parseColor(item.Label.Color),
|
Color: parseColor(item.Label.Color),
|
||||||
|
|
@ -419,6 +421,9 @@ func (eo *EChartsOption) ToOption() ChartOption {
|
||||||
if len(fontFamily) == 0 {
|
if len(fontFamily) == 0 {
|
||||||
fontFamily = eo.Title.TextStyle.FontFamily
|
fontFamily = eo.Title.TextStyle.FontFamily
|
||||||
}
|
}
|
||||||
|
titleTextStyle := eo.Title.TextStyle.ToStyle()
|
||||||
|
titleSubtextStyle := eo.Title.SubtextStyle.ToStyle()
|
||||||
|
legendTextStyle := eo.Legend.TextStyle.ToStyle()
|
||||||
o := ChartOption{
|
o := ChartOption{
|
||||||
Type: eo.Type,
|
Type: eo.Type,
|
||||||
FontFamily: fontFamily,
|
FontFamily: fontFamily,
|
||||||
|
|
@ -426,14 +431,17 @@ func (eo *EChartsOption) ToOption() ChartOption {
|
||||||
Title: TitleOption{
|
Title: TitleOption{
|
||||||
Text: eo.Title.Text,
|
Text: eo.Title.Text,
|
||||||
Subtext: eo.Title.Subtext,
|
Subtext: eo.Title.Subtext,
|
||||||
Style: eo.Title.TextStyle.ToStyle(),
|
FontColor: titleTextStyle.FontColor,
|
||||||
SubtextStyle: eo.Title.SubtextStyle.ToStyle(),
|
FontSize: titleTextStyle.FontSize,
|
||||||
|
SubtextFontSize: titleSubtextStyle.FontSize,
|
||||||
|
SubtextFontColor: titleSubtextStyle.FontColor,
|
||||||
Left: string(eo.Title.Left),
|
Left: string(eo.Title.Left),
|
||||||
Top: string(eo.Title.Top),
|
Top: string(eo.Title.Top),
|
||||||
},
|
},
|
||||||
Legend: LegendOption{
|
Legend: LegendOption{
|
||||||
Show: eo.Legend.Show,
|
Show: eo.Legend.Show,
|
||||||
Style: eo.Legend.TextStyle.ToStyle(),
|
FontSize: legendTextStyle.FontSize,
|
||||||
|
FontColor: legendTextStyle.FontColor,
|
||||||
Data: eo.Legend.Data,
|
Data: eo.Legend.Data,
|
||||||
Left: string(eo.Legend.Left),
|
Left: string(eo.Legend.Left),
|
||||||
Top: string(eo.Legend.Top),
|
Top: string(eo.Legend.Top),
|
||||||
|
|
@ -447,6 +455,21 @@ func (eo *EChartsOption) ToOption() ChartOption {
|
||||||
Box: eo.Box,
|
Box: eo.Box,
|
||||||
SeriesList: eo.Series.ToSeriesList(),
|
SeriesList: eo.Series.ToSeriesList(),
|
||||||
}
|
}
|
||||||
|
isHorizontalChart := false
|
||||||
|
for _, item := range eo.XAxis.Data {
|
||||||
|
if item.Type == "value" {
|
||||||
|
isHorizontalChart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isHorizontalChart {
|
||||||
|
for index := range o.SeriesList {
|
||||||
|
series := o.SeriesList[index]
|
||||||
|
if series.Type == ChartTypeBar {
|
||||||
|
o.SeriesList[index].Type = ChartTypeHorizontalBar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(eo.XAxis.Data) != 0 {
|
if len(eo.XAxis.Data) != 0 {
|
||||||
xAxisData := eo.XAxis.Data[0]
|
xAxisData := eo.XAxis.Data[0]
|
||||||
o.XAxis = XAxisOption{
|
o.XAxis = XAxisOption{
|
||||||
|
|
@ -462,9 +485,10 @@ func (eo *EChartsOption) ToOption() ChartOption {
|
||||||
Max: item.Max,
|
Max: item.Max,
|
||||||
Formatter: item.AxisLabel.Formatter,
|
Formatter: item.AxisLabel.Formatter,
|
||||||
Color: parseColor(item.AxisLine.LineStyle.Color),
|
Color: parseColor(item.AxisLine.LineStyle.Color),
|
||||||
|
Data: item.Data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
o.YAxisList = yAxisOptions
|
o.YAxisOptions = yAxisOptions
|
||||||
|
|
||||||
if len(eo.Children) != 0 {
|
if len(eo.Children) != 0 {
|
||||||
o.Children = make([]ChartOption, len(eo.Children))
|
o.Children = make([]ChartOption, len(eo.Children))
|
||||||
|
|
|
||||||
592
echarts_test.go
592
echarts_test.go
|
|
@ -1,592 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEChartsPosition(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
var p EChartsPosition
|
|
||||||
err := p.UnmarshalJSON([]byte("12"))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("12", string(p))
|
|
||||||
|
|
||||||
err = p.UnmarshalJSON([]byte(`"12%"`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("12%", string(p))
|
|
||||||
}
|
|
||||||
func TestEChartStyle(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
s := EChartStyle{
|
|
||||||
Color: "#aaa",
|
|
||||||
}
|
|
||||||
r := drawing.Color{
|
|
||||||
R: 170,
|
|
||||||
G: 170,
|
|
||||||
B: 170,
|
|
||||||
A: 255,
|
|
||||||
}
|
|
||||||
assert.Equal(chart.Style{
|
|
||||||
FillColor: r,
|
|
||||||
FontColor: r,
|
|
||||||
StrokeColor: r,
|
|
||||||
}, s.ToStyle())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsXAxis(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
ex := EChartsXAxis{}
|
|
||||||
err := ex.UnmarshalJSON([]byte(`{
|
|
||||||
"boundaryGap": false,
|
|
||||||
"splitNumber": 5,
|
|
||||||
"data": [
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
"Thu",
|
|
||||||
"Fri",
|
|
||||||
"Sat",
|
|
||||||
"Sun"
|
|
||||||
]
|
|
||||||
}`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsXAxis{
|
|
||||||
Data: []EChartsXAxisData{
|
|
||||||
{
|
|
||||||
BoundaryGap: FalseFlag(),
|
|
||||||
SplitNumber: 5,
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
"Thu",
|
|
||||||
"Fri",
|
|
||||||
"Sat",
|
|
||||||
"Sun",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsYAxis(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
ey := EChartsYAxis{}
|
|
||||||
|
|
||||||
err := ey.UnmarshalJSON([]byte(`{
|
|
||||||
"min": 1,
|
|
||||||
"max": 10,
|
|
||||||
"axisLabel": {
|
|
||||||
"formatter": "ab"
|
|
||||||
}
|
|
||||||
}`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsYAxis{
|
|
||||||
Data: []EChartsYAxisData{
|
|
||||||
{
|
|
||||||
Min: NewFloatPoint(1),
|
|
||||||
Max: NewFloatPoint(10),
|
|
||||||
AxisLabel: EChartsAxisLabel{
|
|
||||||
Formatter: "ab",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, ey)
|
|
||||||
|
|
||||||
ey = EChartsYAxis{}
|
|
||||||
err = ey.UnmarshalJSON([]byte(`[
|
|
||||||
{
|
|
||||||
"min": 1,
|
|
||||||
"max": 10,
|
|
||||||
"axisLabel": {
|
|
||||||
"formatter": "ab"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"min": 2,
|
|
||||||
"max": 20,
|
|
||||||
"axisLabel": {
|
|
||||||
"formatter": "cd"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsYAxis{
|
|
||||||
Data: []EChartsYAxisData{
|
|
||||||
{
|
|
||||||
Min: NewFloatPoint(1),
|
|
||||||
Max: NewFloatPoint(10),
|
|
||||||
AxisLabel: EChartsAxisLabel{
|
|
||||||
Formatter: "ab",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Min: NewFloatPoint(2),
|
|
||||||
Max: NewFloatPoint(20),
|
|
||||||
AxisLabel: EChartsAxisLabel{
|
|
||||||
Formatter: "cd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, ey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsPadding(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
ep := EChartsPadding{}
|
|
||||||
|
|
||||||
err := ep.UnmarshalJSON([]byte(`10`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsPadding{
|
|
||||||
Box: chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
Right: 10,
|
|
||||||
Bottom: 10,
|
|
||||||
Left: 10,
|
|
||||||
},
|
|
||||||
}, ep)
|
|
||||||
|
|
||||||
ep = EChartsPadding{}
|
|
||||||
err = ep.UnmarshalJSON([]byte(`[10, 20]`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsPadding{
|
|
||||||
Box: chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
Right: 20,
|
|
||||||
Bottom: 10,
|
|
||||||
Left: 20,
|
|
||||||
},
|
|
||||||
}, ep)
|
|
||||||
|
|
||||||
ep = EChartsPadding{}
|
|
||||||
err = ep.UnmarshalJSON([]byte(`[10, 20, 30]`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsPadding{
|
|
||||||
Box: chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
Right: 20,
|
|
||||||
Bottom: 30,
|
|
||||||
Left: 20,
|
|
||||||
},
|
|
||||||
}, ep)
|
|
||||||
|
|
||||||
ep = EChartsPadding{}
|
|
||||||
err = ep.UnmarshalJSON([]byte(`[10, 20, 30, 40]`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsPadding{
|
|
||||||
Box: chart.Box{
|
|
||||||
Top: 10,
|
|
||||||
Right: 20,
|
|
||||||
Bottom: 30,
|
|
||||||
Left: 40,
|
|
||||||
},
|
|
||||||
}, ep)
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestEChartsLegend(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
el := EChartsLegend{}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(`{
|
|
||||||
"data": ["a", "b", "c"],
|
|
||||||
"align": "right",
|
|
||||||
"padding": [10],
|
|
||||||
"left": "20%",
|
|
||||||
"top": 10
|
|
||||||
}`), &el)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsLegend{
|
|
||||||
Data: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
"c",
|
|
||||||
},
|
|
||||||
Align: "right",
|
|
||||||
Padding: EChartsPadding{
|
|
||||||
Box: chart.Box{
|
|
||||||
Left: 10,
|
|
||||||
Top: 10,
|
|
||||||
Right: 10,
|
|
||||||
Bottom: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Left: EChartsPosition("20%"),
|
|
||||||
Top: EChartsPosition("10"),
|
|
||||||
}, el)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsSeriesData(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
esd := EChartsSeriesData{}
|
|
||||||
err := esd.UnmarshalJSON([]byte(`123`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsSeriesData{
|
|
||||||
Value: NewEChartsSeriesDataValue(123),
|
|
||||||
}, esd)
|
|
||||||
|
|
||||||
esd = EChartsSeriesData{}
|
|
||||||
err = esd.UnmarshalJSON([]byte(`2.1`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsSeriesData{
|
|
||||||
Value: NewEChartsSeriesDataValue(2.1),
|
|
||||||
}, esd)
|
|
||||||
|
|
||||||
esd = EChartsSeriesData{}
|
|
||||||
err = esd.UnmarshalJSON([]byte(`{
|
|
||||||
"value": 123.12,
|
|
||||||
"name": "test",
|
|
||||||
"itemStyle": {
|
|
||||||
"color": "#aaa"
|
|
||||||
}
|
|
||||||
}`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(EChartsSeriesData{
|
|
||||||
Value: NewEChartsSeriesDataValue(123.12),
|
|
||||||
Name: "test",
|
|
||||||
ItemStyle: EChartStyle{
|
|
||||||
Color: "#aaa",
|
|
||||||
},
|
|
||||||
}, esd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsSeries(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
esList := make([]EChartsSeries, 0)
|
|
||||||
err := json.Unmarshal([]byte(`[
|
|
||||||
{
|
|
||||||
"name": "Email",
|
|
||||||
"data": [
|
|
||||||
120,
|
|
||||||
132
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Union Ads",
|
|
||||||
"type": "bar",
|
|
||||||
"data": [
|
|
||||||
220,
|
|
||||||
182
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]`), &esList)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal([]EChartsSeries{
|
|
||||||
{
|
|
||||||
Name: "Email",
|
|
||||||
Data: []EChartsSeriesData{
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(120),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(132),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Union Ads",
|
|
||||||
Type: "bar",
|
|
||||||
Data: []EChartsSeriesData{
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(220),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(182),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, esList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsMarkData(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
emd := EChartsMarkData{}
|
|
||||||
err := emd.UnmarshalJSON([]byte(`{"type": "average"}`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("average", emd.Type)
|
|
||||||
|
|
||||||
emd = EChartsMarkData{}
|
|
||||||
err = emd.UnmarshalJSON([]byte(`[{}, {"type": "average"}]`))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("average", emd.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsMarkPoint(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
p := EChartsMarkPoint{}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(`{
|
|
||||||
"symbolSize": 30,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"type": "max"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "min"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`), &p)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(SeriesMarkPoint{
|
|
||||||
SymbolSize: 30,
|
|
||||||
Data: []SeriesMarkData{
|
|
||||||
{
|
|
||||||
Type: "max",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "min",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, p.ToSeriesMarkPoint())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsMarkLine(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
l := EChartsMarkLine{}
|
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(`{
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"type": "max"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "min"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`), &l)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(SeriesMarkLine{
|
|
||||||
Data: []SeriesMarkData{
|
|
||||||
{
|
|
||||||
Type: "max",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: "min",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, l.ToSeriesMarkLine())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsTextStyle(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
s := EChartsTextStyle{
|
|
||||||
Color: "#aaa",
|
|
||||||
FontFamily: "test",
|
|
||||||
FontSize: 14,
|
|
||||||
}
|
|
||||||
assert.Equal(chart.Style{
|
|
||||||
FontColor: drawing.Color{
|
|
||||||
R: 170,
|
|
||||||
G: 170,
|
|
||||||
B: 170,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
FontSize: 14,
|
|
||||||
}, s.ToStyle())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEChartsSeriesList(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
// pie
|
|
||||||
es := EChartsSeriesList{
|
|
||||||
{
|
|
||||||
Type: ChartTypePie,
|
|
||||||
Radius: "30%",
|
|
||||||
Data: []EChartsSeriesData{
|
|
||||||
{
|
|
||||||
Name: "1",
|
|
||||||
Value: EChartsSeriesDataValue{
|
|
||||||
values: []float64{
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2",
|
|
||||||
Value: EChartsSeriesDataValue{
|
|
||||||
values: []float64{
|
|
||||||
2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(SeriesList{
|
|
||||||
{
|
|
||||||
Type: ChartTypePie,
|
|
||||||
Name: "1",
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
},
|
|
||||||
Radius: "30%",
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: ChartTypePie,
|
|
||||||
Name: "2",
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
},
|
|
||||||
Radius: "30%",
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, es.ToSeriesList())
|
|
||||||
|
|
||||||
es = EChartsSeriesList{
|
|
||||||
{
|
|
||||||
Type: ChartTypeBar,
|
|
||||||
Data: []EChartsSeriesData{
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(1),
|
|
||||||
ItemStyle: EChartStyle{
|
|
||||||
Color: "#aaa",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(2),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
YAxisIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Data: []EChartsSeriesData{
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(3),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: NewEChartsSeriesDataValue(4),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ItemStyle: EChartStyle{
|
|
||||||
Color: "#ccc",
|
|
||||||
},
|
|
||||||
Label: EChartsLabelOption{
|
|
||||||
Color: "#ddd",
|
|
||||||
Show: true,
|
|
||||||
Distance: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(SeriesList{
|
|
||||||
{
|
|
||||||
Type: ChartTypeBar,
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
Style: chart.Style{
|
|
||||||
FontColor: drawing.Color{
|
|
||||||
R: 170,
|
|
||||||
G: 170,
|
|
||||||
B: 170,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
StrokeColor: drawing.Color{
|
|
||||||
R: 170,
|
|
||||||
G: 170,
|
|
||||||
B: 170,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
FillColor: drawing.Color{
|
|
||||||
R: 170,
|
|
||||||
G: 170,
|
|
||||||
B: 170,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
YAxisIndex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Style: chart.Style{
|
|
||||||
FontColor: drawing.Color{
|
|
||||||
R: 204,
|
|
||||||
G: 204,
|
|
||||||
B: 204,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
StrokeColor: drawing.Color{
|
|
||||||
R: 204,
|
|
||||||
G: 204,
|
|
||||||
B: 204,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
FillColor: drawing.Color{
|
|
||||||
R: 204,
|
|
||||||
G: 204,
|
|
||||||
B: 204,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Color: drawing.Color{
|
|
||||||
R: 221,
|
|
||||||
G: 221,
|
|
||||||
B: 221,
|
|
||||||
A: 255,
|
|
||||||
},
|
|
||||||
Show: true,
|
|
||||||
Distance: 5,
|
|
||||||
},
|
|
||||||
MarkPoint: SeriesMarkPoint{},
|
|
||||||
MarkLine: SeriesMarkLine{},
|
|
||||||
},
|
|
||||||
}, es.ToSeriesList())
|
|
||||||
|
|
||||||
}
|
|
||||||
103
examples/bar_chart/main.go
Normal file
103
examples/bar_chart/main.go
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "bar-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.BarRender(
|
||||||
|
values,
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Rainfall",
|
||||||
|
"Evaporation",
|
||||||
|
}, charts.PositionRight),
|
||||||
|
charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage),
|
||||||
|
charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax,
|
||||||
|
charts.SeriesMarkDataTypeMin),
|
||||||
|
// custom option func
|
||||||
|
func(opt *charts.ChartOption) {
|
||||||
|
opt.SeriesList[1].MarkPoint = charts.NewMarkPoint(
|
||||||
|
charts.SeriesMarkDataTypeMax,
|
||||||
|
charts.SeriesMarkDataTypeMin,
|
||||||
|
)
|
||||||
|
opt.SeriesList[1].MarkLine = charts.NewMarkLine(
|
||||||
|
charts.SeriesMarkDataTypeAverage,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
charts "github.com/vicanso/go-charts"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeFile(file string, buf []byte) error {
|
|
||||||
tmpPath := "./tmp"
|
|
||||||
err := os.MkdirAll(tmpPath, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file = filepath.Join(tmpPath, file)
|
|
||||||
err = ioutil.WriteFile(file, buf, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func chartsRender() ([]byte, error) {
|
|
||||||
d, err := charts.LineRender([][]float64{
|
|
||||||
{
|
|
||||||
150,
|
|
||||||
230,
|
|
||||||
224,
|
|
||||||
218,
|
|
||||||
135,
|
|
||||||
147,
|
|
||||||
260,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// output type
|
|
||||||
charts.PNGTypeOption(),
|
|
||||||
// title
|
|
||||||
charts.TitleOptionFunc(charts.TitleOption{
|
|
||||||
Text: "Line",
|
|
||||||
}),
|
|
||||||
// x axis
|
|
||||||
charts.XAxisOptionFunc(charts.NewXAxisOption([]string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
"Thu",
|
|
||||||
"Fri",
|
|
||||||
"Sat",
|
|
||||||
"Sun",
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func echartsRender() ([]byte, error) {
|
|
||||||
return charts.RenderEChartsToPNG(`{
|
|
||||||
"title": {
|
|
||||||
"text": "Line"
|
|
||||||
},
|
|
||||||
"xAxis": {
|
|
||||||
"data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
||||||
},
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"data": [150, 230, 224, 218, 135, 147, 260]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Render func() ([]byte, error)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
m := map[string]Render{
|
|
||||||
"charts-line.png": chartsRender,
|
|
||||||
"echarts-line.png": echartsRender,
|
|
||||||
}
|
|
||||||
for name, fn := range m {
|
|
||||||
buf, err := fn()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = writeFile(name, buf)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,9 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
charts "github.com/vicanso/go-charts"
|
charts "github.com/vicanso/go-charts/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var html = `<!DOCTYPE html>
|
var html = `<!DOCTYPE html>
|
||||||
|
|
@ -75,6 +73,7 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha
|
||||||
bytesList := make([][]byte, 0)
|
bytesList := make([][]byte, 0)
|
||||||
for _, opt := range chartOptions {
|
for _, opt := range chartOptions {
|
||||||
opt.Theme = theme
|
opt.Theme = theme
|
||||||
|
opt.Type = charts.ChartOutputSVG
|
||||||
d, err := charts.Render(opt)
|
d, err := charts.Render(opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -100,7 +99,6 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha
|
||||||
|
|
||||||
func indexHandler(w http.ResponseWriter, req *http.Request) {
|
func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
chartOptions := []charts.ChartOption{
|
chartOptions := []charts.ChartOption{
|
||||||
// 普通折线图
|
|
||||||
{
|
{
|
||||||
Title: charts.TitleOption{
|
Title: charts.TitleOption{
|
||||||
Text: "Line",
|
Text: "Line",
|
||||||
|
|
@ -174,7 +172,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
Title: charts.TitleOption{
|
Title: charts.TitleOption{
|
||||||
Text: "Temperature Change in the Coming Week",
|
Text: "Temperature Change in the Coming Week",
|
||||||
},
|
},
|
||||||
Padding: chart.Box{
|
Padding: charts.Box{
|
||||||
Top: 20,
|
Top: 20,
|
||||||
Left: 20,
|
Left: 20,
|
||||||
Right: 30,
|
Right: 30,
|
||||||
|
|
@ -240,7 +238,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
"Rainfall",
|
"Rainfall",
|
||||||
"Evaporation",
|
"Evaporation",
|
||||||
},
|
},
|
||||||
Icon: charts.LegendIconRect,
|
Icon: charts.IconRect,
|
||||||
},
|
},
|
||||||
SeriesList: []charts.Series{
|
SeriesList: []charts.Series{
|
||||||
charts.NewSeriesFromValues([]float64{
|
charts.NewSeriesFromValues([]float64{
|
||||||
|
|
@ -260,8 +258,8 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Value: 190,
|
Value: 190,
|
||||||
Style: chart.Style{
|
Style: charts.Style{
|
||||||
FillColor: drawing.Color{
|
FillColor: charts.Color{
|
||||||
R: 169,
|
R: 169,
|
||||||
G: 0,
|
G: 0,
|
||||||
B: 0,
|
B: 0,
|
||||||
|
|
@ -288,13 +286,61 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 柱状图+mark
|
// 水平柱状图
|
||||||
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "World Population",
|
||||||
|
},
|
||||||
|
Padding: charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 40,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
},
|
||||||
|
Legend: charts.NewLegendOption([]string{
|
||||||
|
"2011",
|
||||||
|
"2012",
|
||||||
|
}),
|
||||||
|
YAxisOptions: charts.NewYAxisOptions([]string{
|
||||||
|
"Brazil",
|
||||||
|
"Indonesia",
|
||||||
|
"USA",
|
||||||
|
"India",
|
||||||
|
"China",
|
||||||
|
"World",
|
||||||
|
}),
|
||||||
|
SeriesList: []charts.Series{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeHorizontalBar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
18203,
|
||||||
|
23489,
|
||||||
|
29034,
|
||||||
|
104970,
|
||||||
|
131744,
|
||||||
|
630230,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeHorizontalBar,
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
19325,
|
||||||
|
23438,
|
||||||
|
31000,
|
||||||
|
121594,
|
||||||
|
134141,
|
||||||
|
681807,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 柱状图+标记
|
||||||
{
|
{
|
||||||
Title: charts.TitleOption{
|
Title: charts.TitleOption{
|
||||||
Text: "Rainfall vs Evaporation",
|
Text: "Rainfall vs Evaporation",
|
||||||
Subtext: "Fake Data",
|
Subtext: "Fake Data",
|
||||||
},
|
},
|
||||||
Padding: chart.Box{
|
Padding: charts.Box{
|
||||||
Top: 20,
|
Top: 20,
|
||||||
Right: 20,
|
Right: 20,
|
||||||
Bottom: 20,
|
Bottom: 20,
|
||||||
|
|
@ -371,6 +417,9 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
// 双Y轴示例
|
// 双Y轴示例
|
||||||
{
|
{
|
||||||
|
Title: charts.TitleOption{
|
||||||
|
Text: "Temperature",
|
||||||
|
},
|
||||||
XAxis: charts.NewXAxisOption([]string{
|
XAxis: charts.NewXAxisOption([]string{
|
||||||
"Jan",
|
"Jan",
|
||||||
"Feb",
|
"Feb",
|
||||||
|
|
@ -390,22 +439,22 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
"Precipitation",
|
"Precipitation",
|
||||||
"Temperature",
|
"Temperature",
|
||||||
}),
|
}),
|
||||||
YAxisList: []charts.YAxisOption{
|
YAxisOptions: []charts.YAxisOption{
|
||||||
{
|
{
|
||||||
Formatter: "{value}°C",
|
Formatter: "{value}ml",
|
||||||
Color: drawing.Color{
|
Color: charts.Color{
|
||||||
R: 250,
|
R: 84,
|
||||||
G: 200,
|
G: 112,
|
||||||
B: 88,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Formatter: "{value}ml",
|
Formatter: "{value}°C",
|
||||||
Color: drawing.Color{
|
Color: charts.Color{
|
||||||
R: 84,
|
R: 250,
|
||||||
G: 112,
|
G: 200,
|
||||||
B: 198,
|
B: 88,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -426,9 +475,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
20.0,
|
20.0,
|
||||||
6.4,
|
6.4,
|
||||||
3.3,
|
3.3,
|
||||||
10.2,
|
|
||||||
}),
|
}),
|
||||||
YAxisIndex: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: charts.ChartTypeBar,
|
Type: charts.ChartTypeBar,
|
||||||
|
|
@ -445,9 +492,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
18.8,
|
18.8,
|
||||||
6.0,
|
6.0,
|
||||||
2.3,
|
2.3,
|
||||||
20.2,
|
|
||||||
}),
|
}),
|
||||||
YAxisIndex: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Data: charts.NewSeriesDataFromValues([]float64{
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
|
@ -463,8 +508,8 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
16.5,
|
16.5,
|
||||||
12.0,
|
12.0,
|
||||||
6.2,
|
6.2,
|
||||||
30.3,
|
|
||||||
}),
|
}),
|
||||||
|
AxisIndex: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -572,6 +617,20 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
"Order",
|
"Order",
|
||||||
}),
|
}),
|
||||||
SeriesList: []charts.Series{
|
SeriesList: []charts.Series{
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Show",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
100,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.ChartTypeFunnel,
|
||||||
|
Name: "Click",
|
||||||
|
Data: charts.NewSeriesDataFromValues([]float64{
|
||||||
|
80,
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Type: charts.ChartTypeFunnel,
|
Type: charts.ChartTypeFunnel,
|
||||||
Name: "Visit",
|
Name: "Visit",
|
||||||
|
|
@ -593,20 +652,6 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
20,
|
20,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Type: charts.ChartTypeFunnel,
|
|
||||||
Name: "Click",
|
|
||||||
Data: charts.NewSeriesDataFromValues([]float64{
|
|
||||||
80,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: charts.ChartTypeFunnel,
|
|
||||||
Name: "Show",
|
|
||||||
Data: charts.NewSeriesDataFromValues([]float64{
|
|
||||||
100,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 多图展示
|
// 多图展示
|
||||||
|
|
@ -620,7 +665,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
"Walnut Brownie",
|
"Walnut Brownie",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Padding: chart.Box{
|
Padding: charts.Box{
|
||||||
Top: 100,
|
Top: 100,
|
||||||
Right: 10,
|
Right: 10,
|
||||||
Bottom: 10,
|
Bottom: 10,
|
||||||
|
|
@ -634,7 +679,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
"2016",
|
"2016",
|
||||||
"2017",
|
"2017",
|
||||||
}),
|
}),
|
||||||
YAxisList: []charts.YAxisOption{
|
YAxisOptions: []charts.YAxisOption{
|
||||||
{
|
{
|
||||||
|
|
||||||
Min: charts.NewFloatPoint(0),
|
Min: charts.NewFloatPoint(0),
|
||||||
|
|
@ -686,7 +731,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
"Walnut Brownie",
|
"Walnut Brownie",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Box: chart.Box{
|
Box: charts.Box{
|
||||||
Top: 20,
|
Top: 20,
|
||||||
Left: 400,
|
Left: 400,
|
||||||
Right: 500,
|
Right: 500,
|
||||||
|
|
@ -1011,6 +1056,64 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`,
|
}`,
|
||||||
|
`{
|
||||||
|
"title": {
|
||||||
|
"text": "World Population"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"trigger": "axis",
|
||||||
|
"axisPointer": {
|
||||||
|
"type": "shadow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"legend": {},
|
||||||
|
"grid": {
|
||||||
|
"left": "3%",
|
||||||
|
"right": "4%",
|
||||||
|
"bottom": "3%",
|
||||||
|
"containLabel": true
|
||||||
|
},
|
||||||
|
"xAxis": {
|
||||||
|
"type": "value"
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"type": "category",
|
||||||
|
"data": [
|
||||||
|
"Brazil",
|
||||||
|
"Indonesia",
|
||||||
|
"USA",
|
||||||
|
"India",
|
||||||
|
"China",
|
||||||
|
"World"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "2011",
|
||||||
|
"type": "bar",
|
||||||
|
"data": [
|
||||||
|
18203,
|
||||||
|
23489,
|
||||||
|
29034,
|
||||||
|
104970,
|
||||||
|
131744,
|
||||||
|
630230
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "2012",
|
||||||
|
"type": "bar",
|
||||||
|
"data": [
|
||||||
|
19325,
|
||||||
|
23438,
|
||||||
|
31000,
|
||||||
|
121594,
|
||||||
|
134141,
|
||||||
|
681807
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
`{
|
`{
|
||||||
"title": {
|
"title": {
|
||||||
"text": "Rainfall vs Evaporation",
|
"text": "Rainfall vs Evaporation",
|
||||||
|
|
@ -1172,12 +1275,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
23.2,
|
23.2,
|
||||||
25.6,
|
25.6,
|
||||||
76.7,
|
76.7,
|
||||||
135.6,
|
135.6
|
||||||
162.2,
|
|
||||||
32.6,
|
|
||||||
20,
|
|
||||||
6.4,
|
|
||||||
3.3
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1191,12 +1289,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
26.4,
|
26.4,
|
||||||
28.7,
|
28.7,
|
||||||
70.7,
|
70.7,
|
||||||
175.6,
|
175.6
|
||||||
182.2,
|
|
||||||
48.7,
|
|
||||||
18.8,
|
|
||||||
6,
|
|
||||||
2.3
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1211,12 +1304,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
4.5,
|
4.5,
|
||||||
6.3,
|
6.3,
|
||||||
10.2,
|
10.2,
|
||||||
20.3,
|
20.3
|
||||||
23.4,
|
|
||||||
23,
|
|
||||||
16.5,
|
|
||||||
12,
|
|
||||||
6.2
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,118 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
charts "github.com/vicanso/go-charts"
|
"github.com/vicanso/go-charts/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func echartsRender() ([]byte, error) {
|
func writeFile(buf []byte) error {
|
||||||
return charts.RenderEChartsToPNG(`{
|
tmpPath := "./tmp"
|
||||||
"title": {
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
"text": "用户访问次数",
|
if err != nil {
|
||||||
"textStyle": {
|
return err
|
||||||
"fontFamily": "chinese"
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"xAxis": {
|
file := filepath.Join(tmpPath, "chinese-line-chart.png")
|
||||||
"data": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
},
|
if err != nil {
|
||||||
"series": [
|
return err
|
||||||
{
|
|
||||||
"data": [150, 230, 224, 218, 135, 147, 260],
|
|
||||||
"label": {
|
|
||||||
"show": true
|
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
]
|
|
||||||
}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fontData, err := ioutil.ReadFile("/Users/darcy/Downloads/NotoSansCJKsc-VF.ttf")
|
// 字体文件需要自行下载
|
||||||
|
buf, err := ioutil.ReadFile("../NotoSansSC.ttf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Error when reading font file:", err)
|
panic(err)
|
||||||
|
}
|
||||||
|
err = charts.InstallFont("noto", buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := charts.InstallFont("chinese", fontData); err != nil {
|
values := [][]float64{
|
||||||
log.Fatalln("Error when instaling font:", err)
|
{
|
||||||
|
120,
|
||||||
|
132,
|
||||||
|
101,
|
||||||
|
134,
|
||||||
|
90,
|
||||||
|
230,
|
||||||
|
210,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
220,
|
||||||
|
182,
|
||||||
|
191,
|
||||||
|
234,
|
||||||
|
290,
|
||||||
|
330,
|
||||||
|
310,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
150,
|
||||||
|
232,
|
||||||
|
201,
|
||||||
|
154,
|
||||||
|
190,
|
||||||
|
330,
|
||||||
|
410,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
320,
|
||||||
|
332,
|
||||||
|
301,
|
||||||
|
334,
|
||||||
|
390,
|
||||||
|
330,
|
||||||
|
320,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
820,
|
||||||
|
932,
|
||||||
|
901,
|
||||||
|
934,
|
||||||
|
1290,
|
||||||
|
1330,
|
||||||
|
1320,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
p, err := charts.LineRender(
|
||||||
fileData, err := echartsRender()
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Line"),
|
||||||
|
charts.FontFamilyOptionFunc("noto"),
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
|
"星期一",
|
||||||
|
"星期二",
|
||||||
|
"星期三",
|
||||||
|
"星期四",
|
||||||
|
"星期五",
|
||||||
|
"星期六",
|
||||||
|
"星期日",
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"邮件",
|
||||||
|
"广告",
|
||||||
|
"视频广告",
|
||||||
|
"直接访问",
|
||||||
|
"搜索引擎",
|
||||||
|
}, charts.PositionCenter),
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Error when rendering image:", err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := ioutil.WriteFile("chinese.png", fileData, 0644); err != nil {
|
|
||||||
log.Fatalln("Error when save image to chinese.png:", err)
|
buf, err = p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
||||||
57
examples/funnel_chart/main.go
Normal file
57
examples/funnel_chart/main.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "funnel-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := []float64{
|
||||||
|
100,
|
||||||
|
80,
|
||||||
|
60,
|
||||||
|
40,
|
||||||
|
20,
|
||||||
|
}
|
||||||
|
p, err := charts.FunnelRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Funnel"),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Show",
|
||||||
|
"Click",
|
||||||
|
"Visit",
|
||||||
|
"Inquiry",
|
||||||
|
"Order",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
79
examples/horizontal_bar_chart/main.go
Normal file
79
examples/horizontal_bar_chart/main.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "horizontal-bar-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
18203,
|
||||||
|
23489,
|
||||||
|
29034,
|
||||||
|
104970,
|
||||||
|
131744,
|
||||||
|
630230,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
19325,
|
||||||
|
23438,
|
||||||
|
31000,
|
||||||
|
121594,
|
||||||
|
134141,
|
||||||
|
681807,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.HorizontalBarRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("World Population"),
|
||||||
|
charts.PaddingOptionFunc(charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 40,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"2011",
|
||||||
|
"2012",
|
||||||
|
}),
|
||||||
|
charts.YAxisDataOptionFunc([]string{
|
||||||
|
"Brazil",
|
||||||
|
"Indonesia",
|
||||||
|
"USA",
|
||||||
|
"India",
|
||||||
|
"China",
|
||||||
|
"World",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
107
examples/line_chart/main.go
Normal file
107
examples/line_chart/main.go
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "line-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
120,
|
||||||
|
132,
|
||||||
|
101,
|
||||||
|
134,
|
||||||
|
90,
|
||||||
|
230,
|
||||||
|
210,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
220,
|
||||||
|
182,
|
||||||
|
191,
|
||||||
|
234,
|
||||||
|
290,
|
||||||
|
330,
|
||||||
|
310,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
150,
|
||||||
|
232,
|
||||||
|
201,
|
||||||
|
154,
|
||||||
|
190,
|
||||||
|
330,
|
||||||
|
410,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
320,
|
||||||
|
332,
|
||||||
|
301,
|
||||||
|
334,
|
||||||
|
390,
|
||||||
|
330,
|
||||||
|
320,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
820,
|
||||||
|
932,
|
||||||
|
901,
|
||||||
|
934,
|
||||||
|
1290,
|
||||||
|
1330,
|
||||||
|
1320,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.LineRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Line"),
|
||||||
|
charts.XAxisDataOptionFunc([]string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
}),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
"Direct",
|
||||||
|
"Search Engine",
|
||||||
|
}, charts.PositionCenter),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
608
examples/painter/main.go
Normal file
608
examples/painter/main.go
Normal file
|
|
@ -0,0 +1,608 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
charts "github.com/vicanso/go-charts/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "painter.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
p, err := charts.NewPainter(charts.PainterOptions{
|
||||||
|
Width: 600,
|
||||||
|
Height: 2000,
|
||||||
|
Type: charts.ChartOutputPNG,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// 背景色
|
||||||
|
p.SetBackground(p.Width(), p.Height(), drawing.ColorWhite)
|
||||||
|
|
||||||
|
top := 0
|
||||||
|
|
||||||
|
// 画线
|
||||||
|
p.SetDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
})
|
||||||
|
p.LineStroke([]charts.Point{
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 100,
|
||||||
|
Y: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 200,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 300,
|
||||||
|
Y: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 圆滑曲线
|
||||||
|
// top += 50
|
||||||
|
// p.Child(charts.PainterPaddingOption(charts.Box{
|
||||||
|
// Top: top,
|
||||||
|
// })).SetDrawingStyle(charts.Style{
|
||||||
|
// StrokeColor: drawing.ColorBlack,
|
||||||
|
// FillColor: drawing.ColorBlack,
|
||||||
|
// StrokeWidth: 1,
|
||||||
|
// }).SmoothLineStroke([]charts.Point{
|
||||||
|
// {
|
||||||
|
// X: 0,
|
||||||
|
// Y: 0,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// X: 100,
|
||||||
|
// Y: 10,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// X: 200,
|
||||||
|
// Y: 0,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// X: 300,
|
||||||
|
// Y: 10,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// 标线
|
||||||
|
top += 50
|
||||||
|
p.Child(charts.PainterPaddingOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
})).SetDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
StrokeDashArray: []float64{
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
}).MarkLine(0, 0, p.Width())
|
||||||
|
|
||||||
|
top += 60
|
||||||
|
// Polygon
|
||||||
|
p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
})).SetDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
}).Polygon(charts.Point{
|
||||||
|
X: 100,
|
||||||
|
Y: 0,
|
||||||
|
}, 50, 6)
|
||||||
|
|
||||||
|
// FillArea
|
||||||
|
top += 60
|
||||||
|
p.Child(charts.PainterPaddingOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
})).SetDrawingStyle(charts.Style{
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
}).FillArea([]charts.Point{
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 100,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 150,
|
||||||
|
Y: 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 80,
|
||||||
|
Y: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 坐标轴的点
|
||||||
|
top += 50
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: 20,
|
||||||
|
}),
|
||||||
|
).SetDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
}).Ticks(charts.TicksOption{
|
||||||
|
Count: 7,
|
||||||
|
Length: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 坐标轴的点,每2格显示一个
|
||||||
|
top += 20
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: 20,
|
||||||
|
}),
|
||||||
|
).SetDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
}).Ticks(charts.TicksOption{
|
||||||
|
Unit: 2,
|
||||||
|
Count: 7,
|
||||||
|
Length: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 坐标轴的点,纵向
|
||||||
|
top += 20
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 100,
|
||||||
|
}),
|
||||||
|
).SetDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
}).Ticks(charts.TicksOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Count: 7,
|
||||||
|
Length: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 横向展示文本
|
||||||
|
top += 120
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: 20,
|
||||||
|
}),
|
||||||
|
).OverrideTextStyle(charts.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 10,
|
||||||
|
}).MultiText(charts.MultiTextOption{
|
||||||
|
TextList: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 横向显示文本,靠左
|
||||||
|
top += 20
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: 20,
|
||||||
|
}),
|
||||||
|
).OverrideTextStyle(charts.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 10,
|
||||||
|
}).MultiText(charts.MultiTextOption{
|
||||||
|
Position: charts.PositionLeft,
|
||||||
|
TextList: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 纵向显示文本
|
||||||
|
top += 20
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: 50,
|
||||||
|
Bottom: top + 150,
|
||||||
|
}),
|
||||||
|
).OverrideTextStyle(charts.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 10,
|
||||||
|
}).MultiText(charts.MultiTextOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Align: charts.AlignRight,
|
||||||
|
TextList: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// 纵向 文本居中
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 50,
|
||||||
|
Right: 100,
|
||||||
|
Bottom: top + 150,
|
||||||
|
}),
|
||||||
|
).OverrideTextStyle(charts.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 10,
|
||||||
|
}).MultiText(charts.MultiTextOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Align: charts.AlignCenter,
|
||||||
|
TextList: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// 纵向 文本置顶
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 100,
|
||||||
|
Right: 150,
|
||||||
|
Bottom: top + 150,
|
||||||
|
}),
|
||||||
|
).OverrideTextStyle(charts.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 10,
|
||||||
|
}).MultiText(charts.MultiTextOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Position: charts.PositionTop,
|
||||||
|
Align: charts.AlignCenter,
|
||||||
|
TextList: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// grid
|
||||||
|
top += 150
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 100,
|
||||||
|
}),
|
||||||
|
).OverrideTextStyle(charts.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 10,
|
||||||
|
}).Grid(charts.GridOption{
|
||||||
|
Column: 8,
|
||||||
|
IgnoreColumnLines: []int{0, 8},
|
||||||
|
Row: 8,
|
||||||
|
IgnoreRowLines: []int{0, 8},
|
||||||
|
})
|
||||||
|
|
||||||
|
// dots
|
||||||
|
top += 100
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 20,
|
||||||
|
}),
|
||||||
|
).OverrideDrawingStyle(charts.Style{
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
}).Dots([]charts.Point{
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 50,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 100,
|
||||||
|
Y: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// rect
|
||||||
|
top += 30
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: 200,
|
||||||
|
Bottom: top + 50,
|
||||||
|
}),
|
||||||
|
).OverrideDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
}).Rect(charts.Box{
|
||||||
|
Left: 10,
|
||||||
|
Top: 0,
|
||||||
|
Right: 110,
|
||||||
|
Bottom: 20,
|
||||||
|
})
|
||||||
|
// legend line dot
|
||||||
|
p.Child(
|
||||||
|
charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 200,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 50,
|
||||||
|
}),
|
||||||
|
).OverrideDrawingStyle(charts.Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FillColor: drawing.ColorBlack,
|
||||||
|
}).LegendLineDot(charts.Box{
|
||||||
|
Left: 10,
|
||||||
|
Top: 0,
|
||||||
|
Right: 50,
|
||||||
|
Bottom: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
// grid
|
||||||
|
top += 50
|
||||||
|
charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 100,
|
||||||
|
})), charts.GridPainterOption{
|
||||||
|
Row: 5,
|
||||||
|
IgnoreFirstRow: true,
|
||||||
|
IgnoreLastRow: true,
|
||||||
|
StrokeColor: drawing.ColorBlue,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// legend
|
||||||
|
top += 100
|
||||||
|
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 30,
|
||||||
|
})), charts.LegendPainterOption{
|
||||||
|
Left: "10",
|
||||||
|
Data: []string{
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
"Direct",
|
||||||
|
},
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// legend
|
||||||
|
top += 30
|
||||||
|
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 30,
|
||||||
|
})), charts.LegendPainterOption{
|
||||||
|
Left: charts.PositionRight,
|
||||||
|
Data: []string{
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
"Direct",
|
||||||
|
},
|
||||||
|
Align: charts.AlignRight,
|
||||||
|
FontSize: 16,
|
||||||
|
Icon: charts.IconRect,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// legend
|
||||||
|
top += 30
|
||||||
|
charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 100,
|
||||||
|
})), charts.LegendPainterOption{
|
||||||
|
Top: "10",
|
||||||
|
Data: []string{
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
"Direct",
|
||||||
|
},
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// axis bottom
|
||||||
|
top += 100
|
||||||
|
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 50,
|
||||||
|
})), charts.AxisOption{
|
||||||
|
Data: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// axis top
|
||||||
|
top += 50
|
||||||
|
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 1,
|
||||||
|
Right: p.Width() - 1,
|
||||||
|
Bottom: top + 50,
|
||||||
|
})), charts.AxisOption{
|
||||||
|
Position: charts.PositionTop,
|
||||||
|
BoundaryGap: charts.FalseFlag(),
|
||||||
|
Data: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// axis left
|
||||||
|
top += 50
|
||||||
|
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 10,
|
||||||
|
Right: 60,
|
||||||
|
Bottom: top + 200,
|
||||||
|
})), charts.AxisOption{
|
||||||
|
Position: charts.PositionLeft,
|
||||||
|
Data: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
// axis right
|
||||||
|
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 100,
|
||||||
|
Right: 150,
|
||||||
|
Bottom: top + 200,
|
||||||
|
})), charts.AxisOption{
|
||||||
|
Position: charts.PositionRight,
|
||||||
|
Data: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
// axis left no tick
|
||||||
|
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
|
||||||
|
Top: top,
|
||||||
|
Left: 150,
|
||||||
|
Right: 300,
|
||||||
|
Bottom: top + 200,
|
||||||
|
})), charts.AxisOption{
|
||||||
|
BoundaryGap: charts.FalseFlag(),
|
||||||
|
Position: charts.PositionLeft,
|
||||||
|
Data: []string{
|
||||||
|
"Mon",
|
||||||
|
"Tue",
|
||||||
|
"Wed",
|
||||||
|
"Thu",
|
||||||
|
"Fri",
|
||||||
|
"Sat",
|
||||||
|
"Sun",
|
||||||
|
},
|
||||||
|
FontSize: 12,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
SplitLineShow: true,
|
||||||
|
SplitLineColor: drawing.ColorBlack.WithAlpha(100),
|
||||||
|
}).Render()
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
examples/pie_chart/main.go
Normal file
72
examples/pie_chart/main.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "pie-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := []float64{
|
||||||
|
1048,
|
||||||
|
735,
|
||||||
|
580,
|
||||||
|
484,
|
||||||
|
300,
|
||||||
|
}
|
||||||
|
p, err := charts.PieRender(
|
||||||
|
values,
|
||||||
|
charts.TitleOptionFunc(charts.TitleOption{
|
||||||
|
Text: "Rainfall vs Evaporation",
|
||||||
|
Subtext: "Fake Data",
|
||||||
|
Left: charts.PositionCenter,
|
||||||
|
}),
|
||||||
|
charts.PaddingOptionFunc(charts.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 20,
|
||||||
|
Bottom: 20,
|
||||||
|
Left: 20,
|
||||||
|
}),
|
||||||
|
charts.LegendOptionFunc(charts.LegendOption{
|
||||||
|
Orient: charts.OrientVertical,
|
||||||
|
Data: []string{
|
||||||
|
"Search Engine",
|
||||||
|
"Direct",
|
||||||
|
"Email",
|
||||||
|
"Union Ads",
|
||||||
|
"Video Ads",
|
||||||
|
},
|
||||||
|
Left: charts.PositionLeft,
|
||||||
|
}),
|
||||||
|
charts.PieSeriesShowLabel(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
examples/radar_chart/main.go
Normal file
80
examples/radar_chart/main.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./tmp"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "radar-chart.png")
|
||||||
|
err = ioutil.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
values := [][]float64{
|
||||||
|
{
|
||||||
|
4200,
|
||||||
|
3000,
|
||||||
|
20000,
|
||||||
|
35000,
|
||||||
|
50000,
|
||||||
|
18000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
5000,
|
||||||
|
14000,
|
||||||
|
28000,
|
||||||
|
26000,
|
||||||
|
42000,
|
||||||
|
21000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := charts.RadarRender(
|
||||||
|
values,
|
||||||
|
charts.TitleTextOptionFunc("Basic Radar Chart"),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Allocated Budget",
|
||||||
|
"Actual Spending",
|
||||||
|
}),
|
||||||
|
charts.RadarIndicatorOptionFunc([]string{
|
||||||
|
"Sales",
|
||||||
|
"Administration",
|
||||||
|
"Information Technology",
|
||||||
|
"Customer Support",
|
||||||
|
"Development",
|
||||||
|
"Marketing",
|
||||||
|
}, []float64{
|
||||||
|
6500,
|
||||||
|
16000,
|
||||||
|
30000,
|
||||||
|
38000,
|
||||||
|
52000,
|
||||||
|
25000,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,34 +24,43 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type funnelChartOption struct {
|
type funnelChart struct {
|
||||||
Theme string
|
p *Painter
|
||||||
Font *truetype.Font
|
opt *FunnelChartOption
|
||||||
SeriesList SeriesList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func funnelChartRender(opt funnelChartOption, result *basicRenderResult) error {
|
func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
|
||||||
d, err := NewDraw(DrawOption{
|
if opt.Theme == nil {
|
||||||
Parent: result.d,
|
opt.Theme = defaultTheme
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Top: result.titleBox.Height(),
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
seriesList := make([]Series, len(opt.SeriesList))
|
return &funnelChart{
|
||||||
copy(seriesList, opt.SeriesList)
|
p: p,
|
||||||
sort.Slice(seriesList, func(i, j int) bool {
|
opt: &opt,
|
||||||
// 大的数据在前
|
}
|
||||||
return seriesList[i].Data[0].Value > seriesList[j].Data[0].Value
|
}
|
||||||
})
|
|
||||||
|
type FunnelChartOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
opt := f.opt
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
max := seriesList[0].Data[0].Value
|
max := seriesList[0].Data[0].Value
|
||||||
min := float64(0)
|
min := float64(0)
|
||||||
for _, item := range seriesList {
|
for _, item := range seriesList {
|
||||||
|
|
@ -62,11 +71,10 @@ func funnelChartRender(opt funnelChartOption, result *basicRenderResult) error {
|
||||||
min = *item.Min
|
min = *item.Min
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
theme := opt.Theme
|
||||||
theme := NewTheme(opt.Theme)
|
|
||||||
gap := 2
|
gap := 2
|
||||||
height := d.Box.Height()
|
height := seriesPainter.Height()
|
||||||
width := d.Box.Width()
|
width := seriesPainter.Width()
|
||||||
count := len(seriesList)
|
count := len(seriesList)
|
||||||
|
|
||||||
h := (height - gap*(count-1)) / count
|
h := (height - gap*(count-1)) / count
|
||||||
|
|
@ -116,26 +124,49 @@ func funnelChartRender(opt funnelChartOption, result *basicRenderResult) error {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
color := theme.GetSeriesColor(series.index)
|
color := theme.GetSeriesColor(series.index)
|
||||||
d.fill(points, chart.Style{
|
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
FillColor: color,
|
FillColor: color,
|
||||||
})
|
}).FillArea(points)
|
||||||
|
|
||||||
// 文本
|
// 文本
|
||||||
text := textList[index]
|
text := textList[index]
|
||||||
r := d.Render
|
seriesPainter.OverrideTextStyle(Style{
|
||||||
textStyle := chart.Style{
|
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: labelFontSize,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}
|
})
|
||||||
textStyle.GetTextOptions().WriteToRenderer(r)
|
textBox := seriesPainter.MeasureText(text)
|
||||||
textBox := r.MeasureText(text)
|
|
||||||
textX := width>>1 - textBox.Width()>>1
|
textX := width>>1 - textBox.Width()>>1
|
||||||
textY := y + h>>1
|
textY := y + h>>1
|
||||||
d.text(text, textX, textY)
|
seriesPainter.Text(text, textX, textY)
|
||||||
|
|
||||||
y += (h + gap)
|
y += (h + gap)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return f.p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *funnelChart) Render() (Box, error) {
|
||||||
|
p := f.p
|
||||||
|
opt := f.opt
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: XAxisOption{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
YAxisOptions: []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeFunnel)
|
||||||
|
return f.render(renderResult, seriesList)
|
||||||
}
|
}
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFunnelChartRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 250,
|
|
||||||
Height: 150,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
err = funnelChartRender(funnelChartOption{
|
|
||||||
Font: f,
|
|
||||||
SeriesList: []Series{
|
|
||||||
{
|
|
||||||
Type: ChartTypeFunnel,
|
|
||||||
Name: "Visit",
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
60,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: ChartTypeFunnel,
|
|
||||||
Name: "Inquiry",
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
40,
|
|
||||||
}),
|
|
||||||
index: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: ChartTypeFunnel,
|
|
||||||
Name: "Order",
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
20,
|
|
||||||
}),
|
|
||||||
index: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: ChartTypeFunnel,
|
|
||||||
Name: "Click",
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
80,
|
|
||||||
}),
|
|
||||||
index: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: ChartTypeFunnel,
|
|
||||||
Name: "Show",
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
100,
|
|
||||||
}),
|
|
||||||
index: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, &basicRenderResult{
|
|
||||||
d: d,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"250\" height=\"150\">\\n<path d=\"M 0 0\nL 250 0\nL 225 28\nL 25 28\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(115,192,222,1.0)\"/><text x=\"89\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Show(100%)</text><path d=\"M 25 30\nL 225 30\nL 200 58\nL 50 58\nL 25 30\" style=\"stroke-width:0;stroke:none;fill:rgba(238,102,102,1.0)\"/><text x=\"94\" y=\"44\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Click(80%)</text><path d=\"M 50 60\nL 200 60\nL 175 88\nL 75 88\nL 50 60\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,1.0)\"/><text x=\"96\" y=\"74\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Visit(60%)</text><path d=\"M 75 90\nL 175 90\nL 150 118\nL 100 118\nL 75 90\" style=\"stroke-width:0;stroke:none;fill:rgba(145,204,117,1.0)\"/><text x=\"89\" y=\"104\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Inquiry(40%)</text><path d=\"M 100 120\nL 150 120\nL 125 148\nL 125 148\nL 100 120\" style=\"stroke-width:0;stroke:none;fill:rgba(250,200,88,1.0)\"/><text x=\"93\" y=\"134\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Order(20%)</text></svg>", string(data))
|
|
||||||
}
|
|
||||||
8
go.mod
8
go.mod
|
|
@ -1,17 +1,17 @@
|
||||||
module github.com/vicanso/go-charts
|
module github.com/vicanso/go-charts/v2
|
||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.7.2
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.0
|
github.com/wcharczuk/go-chart/v2 v2.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
|
golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
15
go.sum
15
go.sum
|
|
@ -8,18 +8,17 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
|
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-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
|
golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw=
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
|
||||||
80
grid.go
Normal file
80
grid.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
type gridPainter struct {
|
||||||
|
p *Painter
|
||||||
|
opt *GridPainterOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type GridPainterOption struct {
|
||||||
|
StrokeWidth float64
|
||||||
|
StrokeColor Color
|
||||||
|
Column int
|
||||||
|
Row int
|
||||||
|
IgnoreFirstRow bool
|
||||||
|
IgnoreLastRow bool
|
||||||
|
IgnoreFirstColumn bool
|
||||||
|
IgnoreLastColumn bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter {
|
||||||
|
return &gridPainter{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gridPainter) Render() (Box, error) {
|
||||||
|
opt := g.opt
|
||||||
|
ignoreColumnLines := make([]int, 0)
|
||||||
|
if opt.IgnoreFirstColumn {
|
||||||
|
ignoreColumnLines = append(ignoreColumnLines, 0)
|
||||||
|
}
|
||||||
|
if opt.IgnoreLastColumn {
|
||||||
|
ignoreColumnLines = append(ignoreColumnLines, opt.Column)
|
||||||
|
}
|
||||||
|
ignoreRowLines := make([]int, 0)
|
||||||
|
if opt.IgnoreFirstRow {
|
||||||
|
ignoreRowLines = append(ignoreRowLines, 0)
|
||||||
|
}
|
||||||
|
if opt.IgnoreLastRow {
|
||||||
|
ignoreRowLines = append(ignoreRowLines, opt.Row)
|
||||||
|
}
|
||||||
|
strokeWidth := opt.StrokeWidth
|
||||||
|
if strokeWidth <= 0 {
|
||||||
|
strokeWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
g.p.SetDrawingStyle(Style{
|
||||||
|
StrokeWidth: strokeWidth,
|
||||||
|
StrokeColor: opt.StrokeColor,
|
||||||
|
})
|
||||||
|
g.p.Grid(GridOption{
|
||||||
|
Column: opt.Column,
|
||||||
|
Row: opt.Row,
|
||||||
|
IgnoreColumnLines: ignoreColumnLines,
|
||||||
|
IgnoreRowLines: ignoreRowLines,
|
||||||
|
})
|
||||||
|
return g.p.box, nil
|
||||||
|
}
|
||||||
148
horizontal_bar_chart.go
Normal file
148
horizontal_bar_chart.go
Normal file
|
|
@ -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"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type horizontalBarChart struct {
|
||||||
|
p *Painter
|
||||||
|
opt *HorizontalBarChartOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type HorizontalBarChartOption struct {
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
|
SeriesList SeriesList
|
||||||
|
// The x axis option
|
||||||
|
XAxis XAxisOption
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The y axis option
|
||||||
|
YAxisOptions []YAxisOption
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart {
|
||||||
|
if opt.Theme == nil {
|
||||||
|
opt.Theme = defaultTheme
|
||||||
|
}
|
||||||
|
return &horizontalBarChart{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
p := h.p
|
||||||
|
opt := h.opt
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
|
yRange := result.axisRanges[0]
|
||||||
|
y0, y1 := yRange.GetRange(0)
|
||||||
|
height := int(y1 - y0)
|
||||||
|
// 每一块之间的margin
|
||||||
|
margin := 10
|
||||||
|
// 每一个bar之间的margin
|
||||||
|
barMargin := 5
|
||||||
|
if height < 20 {
|
||||||
|
margin = 2
|
||||||
|
barMargin = 2
|
||||||
|
} else if height < 50 {
|
||||||
|
margin = 5
|
||||||
|
barMargin = 3
|
||||||
|
}
|
||||||
|
seriesCount := len(seriesList)
|
||||||
|
// 总的高度-两个margin-(总数-1)的barMargin
|
||||||
|
barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList)
|
||||||
|
|
||||||
|
theme := opt.Theme
|
||||||
|
|
||||||
|
max, min := seriesList.GetMaxMin(0)
|
||||||
|
xRange := NewRange(AxisRangeOption{
|
||||||
|
Min: min,
|
||||||
|
Max: max,
|
||||||
|
DivideCount: defaultAxisDivideCount,
|
||||||
|
Size: seriesPainter.Width(),
|
||||||
|
})
|
||||||
|
|
||||||
|
for index := range seriesList {
|
||||||
|
series := seriesList[index]
|
||||||
|
seriesColor := theme.GetSeriesColor(series.index)
|
||||||
|
divideValues := yRange.AutoDivide()
|
||||||
|
for j, item := range series.Data {
|
||||||
|
if j >= yRange.divideCount {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 显示位置切换
|
||||||
|
j = yRange.divideCount - j - 1
|
||||||
|
y := divideValues[j]
|
||||||
|
y += margin
|
||||||
|
if index != 0 {
|
||||||
|
y += index * (barHeight + barMargin)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := int(xRange.getHeight(item.Value))
|
||||||
|
fillColor := seriesColor
|
||||||
|
if !item.Style.FillColor.IsZero() {
|
||||||
|
fillColor = item.Style.FillColor
|
||||||
|
}
|
||||||
|
right := w
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
FillColor: fillColor,
|
||||||
|
}).Rect(chart.Box{
|
||||||
|
Top: y,
|
||||||
|
Left: 0,
|
||||||
|
Right: right,
|
||||||
|
Bottom: y + barHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *horizontalBarChart) Render() (Box, error) {
|
||||||
|
p := h.p
|
||||||
|
opt := h.opt
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
YAxisOptions: opt.YAxisOptions,
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
axisReversed: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeHorizontalBar)
|
||||||
|
return h.render(renderResult, seriesList)
|
||||||
|
}
|
||||||
275
legend.go
275
legend.go
|
|
@ -25,16 +25,18 @@ package charts
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type legendPainter struct {
|
||||||
|
p *Painter
|
||||||
|
opt *LegendOption
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconRect = "rect"
|
||||||
|
const IconLineDot = "lineDot"
|
||||||
|
|
||||||
type LegendOption struct {
|
type LegendOption struct {
|
||||||
theme string
|
Theme ColorPalette
|
||||||
// Legend show flag, if nil or true, the legend will be shown
|
|
||||||
Show *bool
|
|
||||||
// Legend text style
|
|
||||||
Style chart.Style
|
|
||||||
// Text array of legend
|
// Text array of legend
|
||||||
Data []string
|
Data []string
|
||||||
// Distance between legend component and the left side of the container.
|
// Distance between legend component and the left side of the container.
|
||||||
|
|
@ -50,177 +52,170 @@ type LegendOption struct {
|
||||||
Orient string
|
Orient string
|
||||||
// Icon of the legend.
|
// Icon of the legend.
|
||||||
Icon string
|
Icon string
|
||||||
|
// Font size of legend text
|
||||||
|
FontSize float64
|
||||||
|
// FontColor color of legend text
|
||||||
|
FontColor Color
|
||||||
|
// The flag for show legend, set this to *false will hide legend
|
||||||
|
Show *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func NewLegendOption(labels []string, left ...string) LegendOption {
|
||||||
LegendIconRect = "rect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewLegendOption creates a new legend option by legend text list
|
|
||||||
func NewLegendOption(data []string, position ...string) LegendOption {
|
|
||||||
opt := LegendOption{
|
opt := LegendOption{
|
||||||
Data: data,
|
Data: labels,
|
||||||
}
|
}
|
||||||
if len(position) != 0 {
|
if len(left) != 0 {
|
||||||
opt.Left = position[0]
|
opt.Left = left[0]
|
||||||
}
|
}
|
||||||
return opt
|
return opt
|
||||||
}
|
}
|
||||||
|
|
||||||
type legend struct {
|
func (opt *LegendOption) IsEmpty() bool {
|
||||||
d *Draw
|
isEmpty := true
|
||||||
opt *LegendOption
|
for _, v := range opt.Data {
|
||||||
|
if v != "" {
|
||||||
|
isEmpty = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLegend(d *Draw, opt LegendOption) *legend {
|
func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
|
||||||
return &legend{
|
return &legendPainter{
|
||||||
d: d,
|
p: p,
|
||||||
opt: &opt,
|
opt: &opt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *legend) Render() (chart.Box, error) {
|
func (l *legendPainter) Render() (Box, error) {
|
||||||
d := l.d
|
|
||||||
opt := l.opt
|
opt := l.opt
|
||||||
if len(opt.Data) == 0 || isFalse(opt.Show) {
|
theme := opt.Theme
|
||||||
return chart.BoxZero, nil
|
if opt.IsEmpty() ||
|
||||||
|
(opt.Show != nil && !*opt.Show) {
|
||||||
|
return BoxZero, nil
|
||||||
}
|
}
|
||||||
theme := NewTheme(opt.theme)
|
if theme == nil {
|
||||||
padding := opt.Style.Padding
|
theme = l.p.theme
|
||||||
legendDraw, err := NewDraw(DrawOption{
|
|
||||||
Parent: d,
|
|
||||||
}, PaddingOption(padding))
|
|
||||||
if err != nil {
|
|
||||||
return chart.BoxZero, err
|
|
||||||
}
|
}
|
||||||
r := legendDraw.Render
|
if opt.FontSize == 0 {
|
||||||
opt.Style.GetTextOptions().WriteToRenderer(r)
|
opt.FontSize = theme.GetFontSize()
|
||||||
|
|
||||||
x := 0
|
|
||||||
y := 0
|
|
||||||
top := 0
|
|
||||||
// TODO TOP 暂只支持数值
|
|
||||||
if opt.Top != "" {
|
|
||||||
top, _ = strconv.Atoi(opt.Top)
|
|
||||||
y += top
|
|
||||||
}
|
}
|
||||||
legendWidth := 30
|
if opt.FontColor.IsZero() {
|
||||||
legendDotHeight := 5
|
opt.FontColor = theme.GetTextColor()
|
||||||
textPadding := 5
|
}
|
||||||
legendMargin := 10
|
if opt.Left == "" {
|
||||||
// 往下移2倍dot的高度
|
opt.Left = PositionCenter
|
||||||
y += 2 * legendDotHeight
|
}
|
||||||
|
p := l.p.Child(PainterPaddingOption(Box{
|
||||||
widthCount := 0
|
Top: 5,
|
||||||
|
}))
|
||||||
|
p.SetTextStyle(Style{
|
||||||
|
FontSize: opt.FontSize,
|
||||||
|
FontColor: opt.FontColor,
|
||||||
|
})
|
||||||
|
measureList := make([]Box, len(opt.Data))
|
||||||
maxTextWidth := 0
|
maxTextWidth := 0
|
||||||
// 文本宽度
|
for index, text := range opt.Data {
|
||||||
for _, text := range opt.Data {
|
b := p.MeasureText(text)
|
||||||
b := r.MeasureText(text)
|
|
||||||
if b.Width() > maxTextWidth {
|
if b.Width() > maxTextWidth {
|
||||||
maxTextWidth = b.Width()
|
maxTextWidth = b.Width()
|
||||||
}
|
}
|
||||||
widthCount += b.Width()
|
measureList[index] = b
|
||||||
}
|
|
||||||
if opt.Orient == OrientVertical {
|
|
||||||
widthCount = maxTextWidth + legendWidth + textPadding
|
|
||||||
} else {
|
|
||||||
// 加上标记
|
|
||||||
widthCount += legendWidth * len(opt.Data)
|
|
||||||
// 文本的padding
|
|
||||||
widthCount += 2 * textPadding * len(opt.Data)
|
|
||||||
// margin的宽度
|
|
||||||
widthCount += legendMargin * (len(opt.Data) - 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算展示的宽高
|
||||||
|
width := 0
|
||||||
|
height := 0
|
||||||
|
offset := 20
|
||||||
|
textOffset := 2
|
||||||
|
legendWidth := 30
|
||||||
|
legendHeight := 20
|
||||||
|
for _, item := range measureList {
|
||||||
|
if opt.Orient == OrientVertical {
|
||||||
|
height += item.Height()
|
||||||
|
} else {
|
||||||
|
width += item.Width()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opt.Orient == OrientVertical {
|
||||||
|
width = maxTextWidth + textOffset + legendWidth
|
||||||
|
height = offset * len(opt.Data)
|
||||||
|
} else {
|
||||||
|
height = legendHeight
|
||||||
|
offsetValue := (len(opt.Data) - 1) * (offset + textOffset)
|
||||||
|
allLegendWidth := len(opt.Data) * legendWidth
|
||||||
|
width += (offsetValue + allLegendWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算开始的位置
|
||||||
left := 0
|
left := 0
|
||||||
switch opt.Left {
|
switch opt.Left {
|
||||||
case PositionRight:
|
case PositionRight:
|
||||||
left = legendDraw.Box.Width() - widthCount
|
left = p.Width() - width
|
||||||
case PositionCenter:
|
case PositionCenter:
|
||||||
left = (legendDraw.Box.Width() - widthCount) >> 1
|
left = (p.Width() - width) >> 1
|
||||||
default:
|
default:
|
||||||
if strings.HasSuffix(opt.Left, "%") {
|
if strings.HasSuffix(opt.Left, "%") {
|
||||||
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
|
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
|
||||||
left = legendDraw.Box.Width() * value / 100
|
left = p.Width() * value / 100
|
||||||
} else {
|
} else {
|
||||||
value, _ := strconv.Atoi(opt.Left)
|
value, _ := strconv.Atoi(opt.Left)
|
||||||
left = value
|
left = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x = left
|
top, _ := strconv.Atoi(opt.Top)
|
||||||
|
|
||||||
|
x := int(left)
|
||||||
|
y := int(top) + 10
|
||||||
|
x0 := x
|
||||||
|
y0 := y
|
||||||
|
|
||||||
|
drawIcon := func(top, left int) int {
|
||||||
|
if opt.Icon == IconRect {
|
||||||
|
p.Rect(Box{
|
||||||
|
Top: top - legendHeight + 8,
|
||||||
|
Left: left,
|
||||||
|
Right: left + legendWidth,
|
||||||
|
Bottom: top + 1,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
p.LegendLineDot(Box{
|
||||||
|
Top: top + 1,
|
||||||
|
Left: left,
|
||||||
|
Right: left + legendWidth,
|
||||||
|
Bottom: top + legendHeight + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return left + legendWidth
|
||||||
|
}
|
||||||
for index, text := range opt.Data {
|
for index, text := range opt.Data {
|
||||||
textBox := r.MeasureText(text)
|
color := theme.GetSeriesColor(index)
|
||||||
var renderText func()
|
p.SetDrawingStyle(Style{
|
||||||
|
FillColor: color,
|
||||||
|
StrokeColor: color,
|
||||||
|
})
|
||||||
|
if opt.Align != AlignRight {
|
||||||
|
x0 = drawIcon(y0, x0)
|
||||||
|
x0 += textOffset
|
||||||
|
}
|
||||||
|
p.Text(text, x0, y0)
|
||||||
|
x0 += measureList[index].Width()
|
||||||
|
if opt.Align == AlignRight {
|
||||||
|
x0 += textOffset
|
||||||
|
x0 = drawIcon(0, x0)
|
||||||
|
}
|
||||||
if opt.Orient == OrientVertical {
|
if opt.Orient == OrientVertical {
|
||||||
// 垂直
|
y0 += offset
|
||||||
// 重置x的位置
|
x0 = x
|
||||||
x = left
|
} else {
|
||||||
renderText = func() {
|
x0 += offset
|
||||||
x += textPadding
|
y0 = y
|
||||||
legendDraw.text(text, x, y+legendDotHeight)
|
}
|
||||||
x += textBox.Width()
|
|
||||||
y += (2*legendDotHeight + legendMargin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
return Box{
|
||||||
// 水平
|
Right: width,
|
||||||
if index != 0 {
|
Bottom: height,
|
||||||
x += legendMargin
|
}, nil
|
||||||
}
|
|
||||||
renderText = func() {
|
|
||||||
x += textPadding
|
|
||||||
legendDraw.text(text, x, y+legendDotHeight)
|
|
||||||
x += textBox.Width()
|
|
||||||
x += textPadding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if opt.Align == PositionRight {
|
|
||||||
renderText()
|
|
||||||
}
|
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
|
||||||
fillColor := seriesColor
|
|
||||||
if !theme.IsDark() {
|
|
||||||
fillColor = theme.GetBackgroundColor()
|
|
||||||
}
|
|
||||||
style := chart.Style{
|
|
||||||
StrokeColor: seriesColor,
|
|
||||||
FillColor: fillColor,
|
|
||||||
StrokeWidth: 3,
|
|
||||||
}
|
|
||||||
if opt.Icon == LegendIconRect {
|
|
||||||
style.FillColor = seriesColor
|
|
||||||
style.StrokeWidth = 1
|
|
||||||
}
|
|
||||||
style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
|
|
||||||
if opt.Icon == LegendIconRect {
|
|
||||||
legendDraw.moveTo(x, y-legendDotHeight)
|
|
||||||
legendDraw.lineTo(x+legendWidth, y-legendDotHeight)
|
|
||||||
legendDraw.lineTo(x+legendWidth, y+legendDotHeight)
|
|
||||||
legendDraw.lineTo(x, y+legendDotHeight)
|
|
||||||
legendDraw.lineTo(x, y-legendDotHeight)
|
|
||||||
r.FillStroke()
|
|
||||||
} else {
|
|
||||||
legendDraw.moveTo(x, y)
|
|
||||||
legendDraw.lineTo(x+legendWidth, y)
|
|
||||||
r.Stroke()
|
|
||||||
legendDraw.circle(float64(legendDotHeight), x+legendWidth>>1, y)
|
|
||||||
r.FillStroke()
|
|
||||||
}
|
|
||||||
x += legendWidth
|
|
||||||
|
|
||||||
if opt.Align != PositionRight {
|
|
||||||
renderText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
legendBox := padding.Clone()
|
|
||||||
// 计算展示区域
|
|
||||||
if opt.Orient == OrientVertical {
|
|
||||||
legendBox.Right = legendBox.Left + left + maxTextWidth + legendWidth + textPadding
|
|
||||||
legendBox.Bottom = legendBox.Top + y
|
|
||||||
} else {
|
|
||||||
legendBox.Right = legendBox.Left + x
|
|
||||||
legendBox.Bottom = legendBox.Top + 2*legendDotHeight + top + textPadding
|
|
||||||
}
|
|
||||||
return legendBox, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
185
legend_test.go
185
legend_test.go
|
|
@ -1,185 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewLegendOption(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
opt := NewLegendOption([]string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}, PositionRight)
|
|
||||||
assert.Equal(LegendOption{
|
|
||||||
Data: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
},
|
|
||||||
Left: PositionRight,
|
|
||||||
}, opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLegendRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
newDraw := func() *Draw {
|
|
||||||
d, _ := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
style := chart.Style{
|
|
||||||
FontSize: 10,
|
|
||||||
FontColor: drawing.ColorBlack,
|
|
||||||
}
|
|
||||||
style.Font, _ = chart.GetDefaultFont()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
newDraw func() *Draw
|
|
||||||
newLegend func(*Draw) *legend
|
|
||||||
box chart.Box
|
|
||||||
result string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newLegend: func(d *Draw) *legend {
|
|
||||||
return NewLegend(d, LegendOption{
|
|
||||||
Top: "10",
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
},
|
|
||||||
Style: style,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 0 20\nL 30 20\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 76 20\nL 106 20\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"91\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"111\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 148 20\nL 178 20\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"163\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"183\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 214,
|
|
||||||
Bottom: 25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newLegend: func(d *Draw) *legend {
|
|
||||||
return NewLegend(d, LegendOption{
|
|
||||||
Top: "10",
|
|
||||||
Left: PositionRight,
|
|
||||||
Align: PositionRight,
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
},
|
|
||||||
Style: style,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"191\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 222 20\nL 252 20\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"237\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"267\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 294 20\nL 324 20\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"309\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"339\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><path d=\"M 370 20\nL 400 20\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"385\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 400,
|
|
||||||
Bottom: 25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newLegend: func(d *Draw) *legend {
|
|
||||||
return NewLegend(d, LegendOption{
|
|
||||||
Top: "10",
|
|
||||||
Left: PositionCenter,
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
},
|
|
||||||
Style: style,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 93 20\nL 123 20\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"108\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"128\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 169 20\nL 199 20\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"184\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"204\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 241 20\nL 271 20\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"256\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"276\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 307,
|
|
||||||
Bottom: 25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newLegend: func(d *Draw) *legend {
|
|
||||||
return NewLegend(d, LegendOption{
|
|
||||||
Top: "10",
|
|
||||||
Left: PositionLeft,
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
},
|
|
||||||
Style: style,
|
|
||||||
Orient: OrientVertical,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 0 20\nL 30 20\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 0 40\nL 30 40\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"40\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"45\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 0 60\nL 30 60\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"60\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"65\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 61,
|
|
||||||
Bottom: 80,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newLegend: func(d *Draw) *legend {
|
|
||||||
return NewLegend(d, LegendOption{
|
|
||||||
Top: "10",
|
|
||||||
Left: "10%",
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
},
|
|
||||||
Style: style,
|
|
||||||
Orient: OrientVertical,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 101,
|
|
||||||
Bottom: 80,
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 40 20\nL 70 20\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"55\" cy=\"20\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"75\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 40 40\nL 70 40\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"55\" cy=\"40\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"75\" y=\"45\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 40 60\nL 70 60\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"55\" cy=\"60\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"75\" y=\"65\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
d := tt.newDraw()
|
|
||||||
b, err := tt.newLegend(d).Render()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(tt.box, b)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.NotEmpty(data)
|
|
||||||
assert.Equal(tt.result, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
103
line.go
103
line.go
|
|
@ -1,103 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LineStyle struct {
|
|
||||||
ClassName string
|
|
||||||
StrokeDashArray []float64
|
|
||||||
StrokeColor drawing.Color
|
|
||||||
StrokeWidth float64
|
|
||||||
FillColor drawing.Color
|
|
||||||
DotWidth float64
|
|
||||||
DotColor drawing.Color
|
|
||||||
DotFillColor drawing.Color
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *LineStyle) Style() chart.Style {
|
|
||||||
return chart.Style{
|
|
||||||
ClassName: ls.ClassName,
|
|
||||||
StrokeDashArray: ls.StrokeDashArray,
|
|
||||||
StrokeColor: ls.StrokeColor,
|
|
||||||
StrokeWidth: ls.StrokeWidth,
|
|
||||||
FillColor: ls.FillColor,
|
|
||||||
DotWidth: ls.DotWidth,
|
|
||||||
DotColor: ls.DotColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) lineFill(points []Point, style LineStyle) {
|
|
||||||
s := style.Style()
|
|
||||||
if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newPoints := make([]Point, len(points))
|
|
||||||
copy(newPoints, points)
|
|
||||||
x0 := points[0].X
|
|
||||||
y0 := points[0].Y
|
|
||||||
height := d.Box.Height()
|
|
||||||
newPoints = append(newPoints, Point{
|
|
||||||
X: points[len(points)-1].X,
|
|
||||||
Y: height,
|
|
||||||
}, Point{
|
|
||||||
X: x0,
|
|
||||||
Y: height,
|
|
||||||
}, Point{
|
|
||||||
X: x0,
|
|
||||||
Y: y0,
|
|
||||||
})
|
|
||||||
d.fill(newPoints, style.Style())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) lineDot(points []Point, style LineStyle) {
|
|
||||||
s := style.Style()
|
|
||||||
if !s.ShouldDrawDot() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r := d.Render
|
|
||||||
dotWith := s.GetDotWidth()
|
|
||||||
|
|
||||||
s.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
|
||||||
for _, point := range points {
|
|
||||||
if !style.DotFillColor.IsZero() {
|
|
||||||
r.SetFillColor(style.DotFillColor)
|
|
||||||
}
|
|
||||||
r.SetStrokeColor(s.DotColor)
|
|
||||||
d.circle(dotWith, point.X, point.Y)
|
|
||||||
r.FillStroke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Draw) Line(points []Point, style LineStyle) {
|
|
||||||
if len(points) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.lineFill(points, style)
|
|
||||||
d.lineStroke(points, style)
|
|
||||||
d.lineDot(points, style)
|
|
||||||
}
|
|
||||||
210
line_chart.go
210
line_chart.go
|
|
@ -24,108 +24,146 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type lineChartOption struct {
|
type lineChart struct {
|
||||||
Theme string
|
p *Painter
|
||||||
SeriesList SeriesList
|
opt *LineChartOption
|
||||||
Font *truetype.Font
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPointRenderOption, error) {
|
func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
|
||||||
|
if opt.Theme == nil {
|
||||||
theme := NewTheme(opt.Theme)
|
opt.Theme = defaultTheme
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Parent: result.d,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Top: result.titleBox.Height(),
|
|
||||||
Left: YAxisWidth,
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
seriesNames := opt.SeriesList.Names()
|
return &lineChart{
|
||||||
|
p: p,
|
||||||
r := d.Render
|
opt: &opt,
|
||||||
xRange := result.xRange
|
|
||||||
markPointRenderOptions := make([]markPointRenderOption, 0)
|
|
||||||
for i, s := range opt.SeriesList {
|
|
||||||
// 由于series是for range,为同一个数据,因此需要clone
|
|
||||||
// 后续需要使用,如mark point
|
|
||||||
series := s
|
|
||||||
index := series.index
|
|
||||||
if index == 0 {
|
|
||||||
index = i
|
|
||||||
}
|
}
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
}
|
||||||
|
|
||||||
yRange := result.getYRange(series.YAxisIndex)
|
type LineChartOption struct {
|
||||||
points := make([]Point, 0, len(series.Data))
|
Theme ColorPalette
|
||||||
// mark line
|
// The font size
|
||||||
markLineRender(markLineRenderOption{
|
Font *truetype.Font
|
||||||
Draw: d,
|
// The data series list
|
||||||
FillColor: seriesColor,
|
SeriesList SeriesList
|
||||||
FontColor: theme.GetTextColor(),
|
// The x axis option
|
||||||
|
XAxis XAxisOption
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The y axis option
|
||||||
|
YAxisOptions []YAxisOption
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
// background is filled
|
||||||
|
backgroundIsFilled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
p := l.p
|
||||||
|
opt := l.opt
|
||||||
|
boundaryGap := true
|
||||||
|
if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap {
|
||||||
|
boundaryGap = false
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesPainter := result.seriesPainter
|
||||||
|
|
||||||
|
xDivideCount := len(opt.XAxis.Data)
|
||||||
|
if !boundaryGap {
|
||||||
|
xDivideCount--
|
||||||
|
}
|
||||||
|
xDivideValues := autoDivide(seriesPainter.Width(), xDivideCount)
|
||||||
|
xValues := make([]int, len(xDivideValues)-1)
|
||||||
|
if boundaryGap {
|
||||||
|
for i := 0; i < len(xDivideValues)-1; i++ {
|
||||||
|
xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
xValues = xDivideValues
|
||||||
|
}
|
||||||
|
markPointPainter := NewMarkPointPainter(seriesPainter)
|
||||||
|
markLinePainter := NewMarkLinePainter(seriesPainter)
|
||||||
|
rendererList := []Renderer{
|
||||||
|
markPointPainter,
|
||||||
|
markLinePainter,
|
||||||
|
}
|
||||||
|
for index := range seriesList {
|
||||||
|
series := seriesList[index]
|
||||||
|
seriesColor := opt.Theme.GetSeriesColor(series.index)
|
||||||
|
drawingStyle := Style{
|
||||||
StrokeColor: seriesColor,
|
StrokeColor: seriesColor,
|
||||||
Font: opt.Font,
|
StrokeWidth: defaultStrokeWidth,
|
||||||
Series: &series,
|
|
||||||
Range: yRange,
|
|
||||||
})
|
|
||||||
|
|
||||||
for j, item := range series.Data {
|
|
||||||
if j >= xRange.divideCount {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
y := yRange.getRestHeight(item.Value)
|
|
||||||
x := xRange.getWidth(float64(j))
|
|
||||||
points = append(points, Point{
|
|
||||||
Y: y,
|
|
||||||
X: x,
|
|
||||||
})
|
|
||||||
if !series.Label.Show {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
distance := series.Label.Distance
|
|
||||||
if distance == 0 {
|
|
||||||
distance = 5
|
|
||||||
}
|
|
||||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
|
||||||
labelStyle := chart.Style{
|
|
||||||
FontColor: theme.GetTextColor(),
|
|
||||||
FontSize: labelFontSize,
|
|
||||||
Font: opt.Font,
|
|
||||||
}
|
|
||||||
if !series.Label.Color.IsZero() {
|
|
||||||
labelStyle.FontColor = series.Label.Color
|
|
||||||
}
|
|
||||||
labelStyle.GetTextOptions().WriteToRenderer(r)
|
|
||||||
textBox := r.MeasureText(text)
|
|
||||||
d.text(text, x-textBox.Width()>>1, y-distance)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dotFillColor := drawing.ColorWhite
|
seriesPainter.SetDrawingStyle(drawingStyle)
|
||||||
if theme.IsDark() {
|
yRange := result.axisRanges[series.AxisIndex]
|
||||||
dotFillColor = seriesColor
|
points := make([]Point, 0)
|
||||||
|
for i, item := range series.Data {
|
||||||
|
h := yRange.getRestHeight(item.Value)
|
||||||
|
p := Point{
|
||||||
|
X: xValues[i],
|
||||||
|
Y: h,
|
||||||
}
|
}
|
||||||
d.Line(points, LineStyle{
|
points = append(points, p)
|
||||||
StrokeColor: seriesColor,
|
}
|
||||||
StrokeWidth: 2,
|
// 画线
|
||||||
DotColor: seriesColor,
|
seriesPainter.LineStroke(points)
|
||||||
DotWidth: defaultDotWidth,
|
|
||||||
DotFillColor: dotFillColor,
|
// 画点
|
||||||
})
|
if opt.Theme.IsDark() {
|
||||||
// draw mark point
|
drawingStyle.FillColor = drawingStyle.StrokeColor
|
||||||
markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{
|
} else {
|
||||||
Draw: d,
|
drawingStyle.FillColor = drawing.ColorWhite
|
||||||
|
}
|
||||||
|
drawingStyle.StrokeWidth = 1
|
||||||
|
seriesPainter.SetDrawingStyle(drawingStyle)
|
||||||
|
seriesPainter.Dots(points)
|
||||||
|
markPointPainter.Add(markPointRenderOption{
|
||||||
FillColor: seriesColor,
|
FillColor: seriesColor,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
Points: points,
|
Points: points,
|
||||||
Series: &series,
|
Series: series,
|
||||||
|
})
|
||||||
|
markLinePainter.Add(markLineRenderOption{
|
||||||
|
FillColor: seriesColor,
|
||||||
|
FontColor: opt.Theme.GetTextColor(),
|
||||||
|
StrokeColor: seriesColor,
|
||||||
|
Font: opt.Font,
|
||||||
|
Series: series,
|
||||||
|
Range: yRange,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 最大、最小的mark point
|
||||||
|
err := doRender(rendererList...)
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
|
||||||
return markPointRenderOptions, nil
|
return p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lineChart) Render() (Box, error) {
|
||||||
|
p := l.p
|
||||||
|
opt := l.opt
|
||||||
|
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: opt.XAxis,
|
||||||
|
YAxisOptions: opt.YAxisOptions,
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
backgroundIsFilled: opt.backgroundIsFilled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeLine)
|
||||||
|
|
||||||
|
return l.render(renderResult, seriesList)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLineChartRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
width := 400
|
|
||||||
height := 300
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
result := basicRenderResult{
|
|
||||||
xRange: &Range{
|
|
||||||
Min: 0,
|
|
||||||
Max: 4,
|
|
||||||
divideCount: 4,
|
|
||||||
Size: width,
|
|
||||||
Boundary: true,
|
|
||||||
},
|
|
||||||
yRangeList: []*Range{
|
|
||||||
{
|
|
||||||
divideCount: 6,
|
|
||||||
Max: 100,
|
|
||||||
Min: 0,
|
|
||||||
Size: height,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
d: d,
|
|
||||||
}
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
_, err = lineChartRender(lineChartOption{
|
|
||||||
Font: f,
|
|
||||||
SeriesList: SeriesList{
|
|
||||||
{
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
Color: drawing.ColorBlue,
|
|
||||||
},
|
|
||||||
MarkLine: NewMarkLine(
|
|
||||||
SeriesMarkDataTypeAverage,
|
|
||||||
),
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 60,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 90,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NewSeriesFromValues([]float64{
|
|
||||||
40,
|
|
||||||
60,
|
|
||||||
70,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}, &result)
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"40\" cy=\"130\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 45 130\nL 382 130\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 382 125\nL 398 130\nL 382 135\nL 387 130\nL 382 125\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"400\" y=\"134\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">56.66</text><text x=\"83\" y=\"235\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text><text x=\"183\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">60</text><text x=\"283\" y=\"25\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">90</text><path d=\"M 90 240\nL 190 120\nL 290 30\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"90\" cy=\"240\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"190\" cy=\"120\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"290\" cy=\"30\" r=\"2\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"M 90 180\nL 190 120\nL 290 90\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:none\"/><circle cx=\"90\" cy=\"180\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"190\" cy=\"120\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"290\" cy=\"90\" r=\"2\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
165
line_test.go
165
line_test.go
|
|
@ -1,165 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLineStyle(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
ls := LineStyle{
|
|
||||||
ClassName: "test",
|
|
||||||
StrokeDashArray: []float64{
|
|
||||||
1.0,
|
|
||||||
},
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: drawing.ColorBlack.WithAlpha(60),
|
|
||||||
DotWidth: 2,
|
|
||||||
DotColor: drawing.ColorBlack,
|
|
||||||
DotFillColor: drawing.ColorWhite,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(chart.Style{
|
|
||||||
ClassName: "test",
|
|
||||||
StrokeDashArray: []float64{
|
|
||||||
1.0,
|
|
||||||
},
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: drawing.ColorBlack.WithAlpha(60),
|
|
||||||
DotWidth: 2,
|
|
||||||
DotColor: drawing.ColorBlack,
|
|
||||||
}, ls.Style())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrawLineFill(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
ls := LineStyle{
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: drawing.ColorBlack.WithAlpha(60),
|
|
||||||
DotWidth: 2,
|
|
||||||
DotColor: drawing.ColorBlack,
|
|
||||||
DotFillColor: drawing.ColorWhite,
|
|
||||||
}
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
d.lineFill([]Point{
|
|
||||||
{
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 10,
|
|
||||||
Y: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 50,
|
|
||||||
Y: 60,
|
|
||||||
},
|
|
||||||
}, ls)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 0 0\nL 10 20\nL 50 60\nL 50 300\nL 0 300\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,0.2)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrawLineDot(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
ls := LineStyle{
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: drawing.ColorBlack.WithAlpha(60),
|
|
||||||
DotWidth: 2,
|
|
||||||
DotColor: drawing.ColorBlack,
|
|
||||||
DotFillColor: drawing.ColorWhite,
|
|
||||||
}
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
d.lineDot([]Point{
|
|
||||||
{
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 10,
|
|
||||||
Y: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 50,
|
|
||||||
Y: 60,
|
|
||||||
},
|
|
||||||
}, ls)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"0\" cy=\"0\" r=\"2\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"10\" cy=\"20\" r=\"2\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"50\" cy=\"60\" r=\"2\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrawLine(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
ls := LineStyle{
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: drawing.ColorBlack.WithAlpha(60),
|
|
||||||
DotWidth: 2,
|
|
||||||
DotColor: drawing.ColorBlack,
|
|
||||||
DotFillColor: drawing.ColorWhite,
|
|
||||||
}
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
d.Line([]Point{
|
|
||||||
{
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 10,
|
|
||||||
Y: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 50,
|
|
||||||
Y: 60,
|
|
||||||
},
|
|
||||||
}, ls)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 0 0\nL 10 20\nL 50 60\nL 50 300\nL 0 300\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,0.2)\"/><path d=\"M 0 0\nL 10 20\nL 50 60\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><circle cx=\"0\" cy=\"0\" r=\"2\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"10\" cy=\"20\" r=\"2\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"50\" cy=\"60\" r=\"2\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(data))
|
|
||||||
}
|
|
||||||
71
mark_line.go
71
mark_line.go
|
|
@ -24,8 +24,6 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
|
func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
|
||||||
|
|
@ -40,38 +38,54 @@ func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type markLineRenderOption struct {
|
type markLinePainter struct {
|
||||||
Draw *Draw
|
p *Painter
|
||||||
FillColor drawing.Color
|
options []markLineRenderOption
|
||||||
FontColor drawing.Color
|
|
||||||
StrokeColor drawing.Color
|
|
||||||
Font *truetype.Font
|
|
||||||
Series *Series
|
|
||||||
Range *Range
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func markLineRender(opt markLineRenderOption) {
|
func (m *markLinePainter) Add(opt markLineRenderOption) {
|
||||||
d := opt.Draw
|
m.options = append(m.options, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarkLinePainter(p *Painter) *markLinePainter {
|
||||||
|
return &markLinePainter{
|
||||||
|
p: p,
|
||||||
|
options: make([]markLineRenderOption, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type markLineRenderOption struct {
|
||||||
|
FillColor Color
|
||||||
|
FontColor Color
|
||||||
|
StrokeColor Color
|
||||||
|
Font *truetype.Font
|
||||||
|
Series Series
|
||||||
|
Range axisRange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markLinePainter) Render() (Box, error) {
|
||||||
|
painter := m.p
|
||||||
|
for _, opt := range m.options {
|
||||||
s := opt.Series
|
s := opt.Series
|
||||||
if len(s.MarkLine.Data) == 0 {
|
if len(s.MarkLine.Data) == 0 {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
r := d.Render
|
|
||||||
summary := s.Summary()
|
summary := s.Summary()
|
||||||
for _, markLine := range s.MarkLine.Data {
|
for _, markLine := range s.MarkLine.Data {
|
||||||
// 由于mark line会修改style,因此每次重新设置
|
// 由于mark line会修改style,因此每次重新设置
|
||||||
chart.Style{
|
painter.OverrideDrawingStyle(Style{
|
||||||
FillColor: opt.FillColor,
|
FillColor: opt.FillColor,
|
||||||
FontColor: opt.FontColor,
|
|
||||||
FontSize: labelFontSize,
|
|
||||||
StrokeColor: opt.StrokeColor,
|
StrokeColor: opt.StrokeColor,
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
Font: opt.Font,
|
|
||||||
StrokeDashArray: []float64{
|
StrokeDashArray: []float64{
|
||||||
4,
|
4,
|
||||||
2,
|
2,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(r)
|
}).OverrideTextStyle(Style{
|
||||||
|
Font: opt.Font,
|
||||||
|
FontColor: opt.FontColor,
|
||||||
|
FontSize: labelFontSize,
|
||||||
|
})
|
||||||
value := float64(0)
|
value := float64(0)
|
||||||
switch markLine.Type {
|
switch markLine.Type {
|
||||||
case SeriesMarkDataTypeMax:
|
case SeriesMarkDataTypeMax:
|
||||||
|
|
@ -82,11 +96,22 @@ func markLineRender(opt markLineRenderOption) {
|
||||||
value = summary.AverageValue
|
value = summary.AverageValue
|
||||||
}
|
}
|
||||||
y := opt.Range.getRestHeight(value)
|
y := opt.Range.getRestHeight(value)
|
||||||
width := d.Box.Width()
|
width := painter.Width()
|
||||||
text := commafWithDigits(value)
|
text := commafWithDigits(value)
|
||||||
textBox := r.MeasureText(text)
|
textBox := painter.MeasureText(text)
|
||||||
d.makeLine(0, y, width-2)
|
painter.MarkLine(0, y, width-2)
|
||||||
d.text(text, width, y+textBox.Height()>>1-2)
|
painter.Text(text, width, y+textBox.Height()>>1-2)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return BoxZero, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func markLineRender(opt markLineRenderOption) {
|
||||||
|
// d := opt.Draw
|
||||||
|
// s := opt.Series
|
||||||
|
// if len(s.MarkLine.Data) == 0 {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// r := d.Render
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewMarkLine(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
markLine := NewMarkLine(
|
|
||||||
SeriesMarkDataTypeMax,
|
|
||||||
SeriesMarkDataTypeMin,
|
|
||||||
SeriesMarkDataTypeAverage,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.Equal(SeriesMarkLine{
|
|
||||||
Data: []SeriesMarkData{
|
|
||||||
{
|
|
||||||
Type: SeriesMarkDataTypeMax,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: SeriesMarkDataTypeMin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: SeriesMarkDataTypeAverage,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, markLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkLineRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Left: 20,
|
|
||||||
Right: 20,
|
|
||||||
}))
|
|
||||||
assert.Nil(err)
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
|
|
||||||
markLineRender(markLineRenderOption{
|
|
||||||
Draw: d,
|
|
||||||
FillColor: drawing.ColorBlack,
|
|
||||||
FontColor: drawing.ColorBlack,
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
Font: f,
|
|
||||||
Series: &Series{
|
|
||||||
MarkLine: NewMarkLine(
|
|
||||||
SeriesMarkDataTypeMax,
|
|
||||||
SeriesMarkDataTypeMin,
|
|
||||||
SeriesMarkDataTypeAverage,
|
|
||||||
),
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
5,
|
|
||||||
7,
|
|
||||||
9,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Range: &Range{
|
|
||||||
Min: 0,
|
|
||||||
Max: 10,
|
|
||||||
Size: 200,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"20\" cy=\"20\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 25 20\nL 362 20\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path d=\"M 362 15\nL 378 20\nL 362 25\nL 367 20\nL 362 15\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"380\" y=\"24\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">9</text><circle cx=\"20\" cy=\"180\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 25 180\nL 362 180\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path d=\"M 362 175\nL 378 180\nL 362 185\nL 367 180\nL 362 175\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"380\" y=\"184\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">1</text><circle cx=\"20\" cy=\"100\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 25 100\nL 362 100\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path d=\"M 362 95\nL 378 100\nL 362 105\nL 367 100\nL 362 95\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"380\" y=\"104\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">5</text></svg>", string(data))
|
|
||||||
}
|
|
||||||
|
|
@ -24,8 +24,6 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint {
|
func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint {
|
||||||
|
|
@ -40,19 +38,36 @@ func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type markPointPainter struct {
|
||||||
|
p *Painter
|
||||||
|
options []markPointRenderOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markPointPainter) Add(opt markPointRenderOption) {
|
||||||
|
m.options = append(m.options, opt)
|
||||||
|
}
|
||||||
|
|
||||||
type markPointRenderOption struct {
|
type markPointRenderOption struct {
|
||||||
Draw *Draw
|
FillColor Color
|
||||||
FillColor drawing.Color
|
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
Series *Series
|
Series Series
|
||||||
Points []Point
|
Points []Point
|
||||||
}
|
}
|
||||||
|
|
||||||
func markPointRender(opt markPointRenderOption) {
|
func NewMarkPointPainter(p *Painter) *markPointPainter {
|
||||||
d := opt.Draw
|
return &markPointPainter{
|
||||||
|
p: p,
|
||||||
|
options: make([]markPointRenderOption, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markPointPainter) Render() (Box, error) {
|
||||||
|
painter := m.p
|
||||||
|
theme := m.p.theme
|
||||||
|
for _, opt := range m.options {
|
||||||
s := opt.Series
|
s := opt.Series
|
||||||
if len(s.MarkPoint.Data) == 0 {
|
if len(s.MarkPoint.Data) == 0 {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
points := opt.Points
|
points := opt.Points
|
||||||
summary := s.Summary()
|
summary := s.Summary()
|
||||||
|
|
@ -60,18 +75,14 @@ func markPointRender(opt markPointRenderOption) {
|
||||||
if symbolSize == 0 {
|
if symbolSize == 0 {
|
||||||
symbolSize = 30
|
symbolSize = 30
|
||||||
}
|
}
|
||||||
r := d.Render
|
painter.OverrideDrawingStyle(Style{
|
||||||
// 设置填充样式
|
|
||||||
chart.Style{
|
|
||||||
FillColor: opt.FillColor,
|
FillColor: opt.FillColor,
|
||||||
}.WriteToRenderer(r)
|
}).OverrideTextStyle(Style{
|
||||||
// 设置文本样式
|
FontColor: theme.GetTextColor(),
|
||||||
chart.Style{
|
|
||||||
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
|
||||||
FontSize: labelFontSize,
|
FontSize: labelFontSize,
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}.WriteTextOptionsToRenderer(r)
|
})
|
||||||
for _, markPointData := range s.MarkPoint.Data {
|
for _, markPointData := range s.MarkPoint.Data {
|
||||||
p := points[summary.MinIndex]
|
p := points[summary.MinIndex]
|
||||||
value := summary.MinValue
|
value := summary.MinValue
|
||||||
|
|
@ -81,9 +92,11 @@ func markPointRender(opt markPointRenderOption) {
|
||||||
value = summary.MaxValue
|
value = summary.MaxValue
|
||||||
}
|
}
|
||||||
|
|
||||||
d.pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
||||||
text := commafWithDigits(value)
|
text := commafWithDigits(value)
|
||||||
textBox := r.MeasureText(text)
|
textBox := painter.MeasureText(text)
|
||||||
d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return BoxZero, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewMarkPoint(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
markPoint := NewMarkPoint(
|
|
||||||
SeriesMarkDataTypeMax,
|
|
||||||
SeriesMarkDataTypeMin,
|
|
||||||
SeriesMarkDataTypeAverage,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.Equal(SeriesMarkPoint{
|
|
||||||
Data: []SeriesMarkData{
|
|
||||||
{
|
|
||||||
Type: SeriesMarkDataTypeMax,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: SeriesMarkDataTypeMin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: SeriesMarkDataTypeAverage,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, markPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarkPointRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Left: 20,
|
|
||||||
Right: 20,
|
|
||||||
}))
|
|
||||||
assert.Nil(err)
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
|
|
||||||
markPointRender(markPointRenderOption{
|
|
||||||
Draw: d,
|
|
||||||
FillColor: drawing.ColorBlack,
|
|
||||||
Font: f,
|
|
||||||
Series: &Series{
|
|
||||||
MarkPoint: NewMarkPoint(
|
|
||||||
SeriesMarkDataTypeMax,
|
|
||||||
SeriesMarkDataTypeMin,
|
|
||||||
),
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
5,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Points: []Point{
|
|
||||||
{
|
|
||||||
X: 1,
|
|
||||||
Y: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 100,
|
|
||||||
Y: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 200,
|
|
||||||
Y: 200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 217 192\nA 15 15 330.00 1 1 223 192\nL 220 178\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><path d=\"M 205 178\nQ220,215 235,178\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><text x=\"216\" y=\"183\" style=\"stroke-width:0;stroke:none;fill:rgba(238,238,238,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">5</text><path d=\"M 18 42\nA 15 15 330.00 1 1 24 42\nL 21 28\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><path d=\"M 6 28\nQ21,65 36,28\nZ\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0)\"/><text x=\"17\" y=\"33\" style=\"stroke-width:0;stroke:none;fill:rgba(238,238,238,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">1</text></svg>", string(data))
|
|
||||||
}
|
|
||||||
736
painter.go
Normal file
736
painter.go
Normal file
|
|
@ -0,0 +1,736 @@
|
||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Painter struct {
|
||||||
|
render chart.Renderer
|
||||||
|
box Box
|
||||||
|
font *truetype.Font
|
||||||
|
parent *Painter
|
||||||
|
style Style
|
||||||
|
theme ColorPalette
|
||||||
|
}
|
||||||
|
|
||||||
|
type PainterOptions struct {
|
||||||
|
// Draw type, "svg" or "png", default type is "svg"
|
||||||
|
Type string
|
||||||
|
// The width of draw painter
|
||||||
|
Width int
|
||||||
|
// The height of draw painter
|
||||||
|
Height int
|
||||||
|
// The font for painter
|
||||||
|
Font *truetype.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
type PainterOption func(*Painter)
|
||||||
|
|
||||||
|
type TicksOption struct {
|
||||||
|
Length int
|
||||||
|
Orient string
|
||||||
|
Count int
|
||||||
|
Unit int
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultiTextOption struct {
|
||||||
|
TextList []string
|
||||||
|
Orient string
|
||||||
|
Unit int
|
||||||
|
Position string
|
||||||
|
Align string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GridOption struct {
|
||||||
|
Column int
|
||||||
|
Row int
|
||||||
|
// 忽略不展示的column
|
||||||
|
IgnoreColumnLines []int
|
||||||
|
// 忽略不展示的row
|
||||||
|
IgnoreRowLines []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// PainterPaddingOption sets the padding of draw painter
|
||||||
|
func PainterPaddingOption(padding Box) PainterOption {
|
||||||
|
return func(p *Painter) {
|
||||||
|
p.box.Left += padding.Left
|
||||||
|
p.box.Top += padding.Top
|
||||||
|
p.box.Right -= padding.Right
|
||||||
|
p.box.Bottom -= padding.Bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PainterBoxOption sets the box of draw painter
|
||||||
|
func PainterBoxOption(box Box) PainterOption {
|
||||||
|
return func(p *Painter) {
|
||||||
|
if box.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.box = box
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PainterFontOption sets the font of draw painter
|
||||||
|
func PainterFontOption(font *truetype.Font) PainterOption {
|
||||||
|
return func(p *Painter) {
|
||||||
|
if font == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.font = font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PainterStyleOption sets the style of draw painter
|
||||||
|
func PainterStyleOption(style Style) PainterOption {
|
||||||
|
return func(p *Painter) {
|
||||||
|
p.SetStyle(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PainterThemeOption sets the theme of draw painter
|
||||||
|
func PainterThemeOption(theme ColorPalette) PainterOption {
|
||||||
|
return func(p *Painter) {
|
||||||
|
if theme == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.theme = theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PainterWidthHeightOption set width or height of draw painter
|
||||||
|
func PainterWidthHeightOption(width, height int) PainterOption {
|
||||||
|
return func(p *Painter) {
|
||||||
|
if width > 0 {
|
||||||
|
p.box.Right = p.box.Left + width
|
||||||
|
}
|
||||||
|
if height > 0 {
|
||||||
|
p.box.Bottom = p.box.Top + height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPainter creates a new painter
|
||||||
|
func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
|
||||||
|
if opts.Width <= 0 || opts.Height <= 0 {
|
||||||
|
return nil, errors.New("width/height can not be nil")
|
||||||
|
}
|
||||||
|
font := opts.Font
|
||||||
|
if font == nil {
|
||||||
|
f, err := chart.GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
font = f
|
||||||
|
}
|
||||||
|
fn := chart.PNG
|
||||||
|
if opts.Type == ChartOutputSVG {
|
||||||
|
fn = chart.SVG
|
||||||
|
}
|
||||||
|
width := opts.Width
|
||||||
|
height := opts.Height
|
||||||
|
r, err := fn(width, height)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.SetFont(font)
|
||||||
|
|
||||||
|
p := &Painter{
|
||||||
|
render: r,
|
||||||
|
box: Box{
|
||||||
|
Right: opts.Width,
|
||||||
|
Bottom: opts.Height,
|
||||||
|
},
|
||||||
|
font: font,
|
||||||
|
}
|
||||||
|
p.setOptions(opt...)
|
||||||
|
if p.theme == nil {
|
||||||
|
p.theme = NewTheme(ThemeLight)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
func (p *Painter) setOptions(opts ...PainterOption) {
|
||||||
|
for _, fn := range opts {
|
||||||
|
fn(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Child(opt ...PainterOption) *Painter {
|
||||||
|
child := &Painter{
|
||||||
|
render: p.render,
|
||||||
|
box: p.box.Clone(),
|
||||||
|
font: p.font,
|
||||||
|
parent: p,
|
||||||
|
style: p.style,
|
||||||
|
theme: p.theme,
|
||||||
|
}
|
||||||
|
child.setOptions(opt...)
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) SetStyle(style Style) {
|
||||||
|
if style.Font == nil {
|
||||||
|
style.Font = p.font
|
||||||
|
}
|
||||||
|
p.style = style
|
||||||
|
style.WriteToRenderer(p.render)
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideStyle(defaultStyle Style, style Style) Style {
|
||||||
|
if style.StrokeWidth == 0 {
|
||||||
|
style.StrokeWidth = defaultStyle.StrokeWidth
|
||||||
|
}
|
||||||
|
if style.StrokeColor.IsZero() {
|
||||||
|
style.StrokeColor = defaultStyle.StrokeColor
|
||||||
|
}
|
||||||
|
if style.StrokeDashArray == nil {
|
||||||
|
style.StrokeDashArray = defaultStyle.StrokeDashArray
|
||||||
|
}
|
||||||
|
if style.DotColor.IsZero() {
|
||||||
|
style.DotColor = defaultStyle.DotColor
|
||||||
|
}
|
||||||
|
if style.DotWidth == 0 {
|
||||||
|
style.DotWidth = defaultStyle.DotWidth
|
||||||
|
}
|
||||||
|
if style.FillColor.IsZero() {
|
||||||
|
style.FillColor = defaultStyle.FillColor
|
||||||
|
}
|
||||||
|
if style.FontSize == 0 {
|
||||||
|
style.FontSize = defaultStyle.FontSize
|
||||||
|
}
|
||||||
|
if style.FontColor.IsZero() {
|
||||||
|
style.FontColor = defaultStyle.FontColor
|
||||||
|
}
|
||||||
|
if style.Font == nil {
|
||||||
|
style.Font = defaultStyle.Font
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) OverrideDrawingStyle(style Style) *Painter {
|
||||||
|
s := overrideStyle(p.style, style)
|
||||||
|
p.SetDrawingStyle(s)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) SetDrawingStyle(style Style) *Painter {
|
||||||
|
style.WriteDrawingOptionsToRenderer(p.render)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) SetTextStyle(style Style) *Painter {
|
||||||
|
if style.Font == nil {
|
||||||
|
style.Font = p.font
|
||||||
|
}
|
||||||
|
style.WriteTextOptionsToRenderer(p.render)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Painter) OverrideTextStyle(style Style) *Painter {
|
||||||
|
s := overrideStyle(p.style, style)
|
||||||
|
p.SetTextStyle(s)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) ResetStyle() *Painter {
|
||||||
|
p.style.WriteToRenderer(p.render)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the data of draw canvas
|
||||||
|
func (p *Painter) Bytes() ([]byte, error) {
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
err := p.render.Save(&buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo moves the cursor to a given point
|
||||||
|
func (p *Painter) MoveTo(x, y int) *Painter {
|
||||||
|
p.render.MoveTo(x+p.box.Left, y+p.box.Top)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) *Painter {
|
||||||
|
p.render.ArcTo(cx+p.box.Left, cy+p.box.Top, rx, ry, startAngle, delta)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) LineTo(x, y int) *Painter {
|
||||||
|
p.render.LineTo(x+p.box.Left, y+p.box.Top)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) QuadCurveTo(cx, cy, x, y int) *Painter {
|
||||||
|
p.render.QuadCurveTo(cx+p.box.Left, cy+p.box.Top, x+p.box.Left, y+p.box.Top)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Pin(x, y, width int) *Painter {
|
||||||
|
r := float64(width) / 2
|
||||||
|
y -= width / 4
|
||||||
|
angle := chart.DegreesToRadians(15)
|
||||||
|
box := p.box
|
||||||
|
|
||||||
|
startAngle := math.Pi/2 + angle
|
||||||
|
delta := 2*math.Pi - 2*angle
|
||||||
|
p.ArcTo(x, y, r, r, startAngle, delta)
|
||||||
|
p.LineTo(x, y)
|
||||||
|
p.Close()
|
||||||
|
p.FillStroke()
|
||||||
|
|
||||||
|
startX := x - int(r)
|
||||||
|
startY := y
|
||||||
|
endX := x + int(r)
|
||||||
|
endY := y
|
||||||
|
p.MoveTo(startX, startY)
|
||||||
|
|
||||||
|
left := box.Left
|
||||||
|
top := box.Top
|
||||||
|
cx := x
|
||||||
|
cy := y + int(r*2.5)
|
||||||
|
p.render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
|
||||||
|
p.Close()
|
||||||
|
p.Fill()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) arrow(x, y, width, height int, direction string) *Painter {
|
||||||
|
halfWidth := width >> 1
|
||||||
|
halfHeight := height >> 1
|
||||||
|
if direction == PositionTop || direction == PositionBottom {
|
||||||
|
x0 := x - halfWidth
|
||||||
|
x1 := x0 + width
|
||||||
|
dy := -height / 3
|
||||||
|
y0 := y
|
||||||
|
y1 := y0 - height
|
||||||
|
if direction == PositionBottom {
|
||||||
|
y0 = y - height
|
||||||
|
y1 = y
|
||||||
|
dy = 2 * dy
|
||||||
|
}
|
||||||
|
p.MoveTo(x0, y0)
|
||||||
|
p.LineTo(x0+halfWidth, y1)
|
||||||
|
p.LineTo(x1, y0)
|
||||||
|
p.LineTo(x0+halfWidth, y+dy)
|
||||||
|
p.LineTo(x0, y0)
|
||||||
|
} else {
|
||||||
|
x0 := x + width
|
||||||
|
x1 := x0 - width
|
||||||
|
y0 := y - halfHeight
|
||||||
|
dx := -width / 3
|
||||||
|
if direction == PositionRight {
|
||||||
|
x0 = x - width
|
||||||
|
dx = -dx
|
||||||
|
x1 = x0 + width
|
||||||
|
}
|
||||||
|
p.MoveTo(x0, y0)
|
||||||
|
p.LineTo(x1, y0+halfHeight)
|
||||||
|
p.LineTo(x0, y0+height)
|
||||||
|
p.LineTo(x0+dx, y0+halfHeight)
|
||||||
|
p.LineTo(x0, y0)
|
||||||
|
}
|
||||||
|
p.FillStroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) ArrowLeft(x, y, width, height int) *Painter {
|
||||||
|
p.arrow(x, y, width, height, PositionLeft)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) ArrowRight(x, y, width, height int) *Painter {
|
||||||
|
p.arrow(x, y, width, height, PositionRight)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) ArrowTop(x, y, width, height int) *Painter {
|
||||||
|
p.arrow(x, y, width, height, PositionTop)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Painter) ArrowBottom(x, y, width, height int) *Painter {
|
||||||
|
p.arrow(x, y, width, height, PositionBottom)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Circle(radius float64, x, y int) *Painter {
|
||||||
|
p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Stroke() *Painter {
|
||||||
|
p.render.Stroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Close() *Painter {
|
||||||
|
p.render.Close()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) FillStroke() *Painter {
|
||||||
|
p.render.FillStroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Fill() *Painter {
|
||||||
|
p.render.Fill()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Width() int {
|
||||||
|
return p.box.Width()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Height() int {
|
||||||
|
return p.box.Height()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) MeasureText(text string) Box {
|
||||||
|
return p.render.MeasureText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) {
|
||||||
|
maxWidth := 0
|
||||||
|
maxHeight := 0
|
||||||
|
for _, text := range textList {
|
||||||
|
box := p.MeasureText(text)
|
||||||
|
if maxWidth < box.Width() {
|
||||||
|
maxWidth = box.Width()
|
||||||
|
}
|
||||||
|
if maxHeight < box.Height() {
|
||||||
|
maxHeight = box.Height()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxWidth, maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) LineStroke(points []Point) *Painter {
|
||||||
|
for index, point := range points {
|
||||||
|
x := point.X
|
||||||
|
y := point.Y
|
||||||
|
if index == 0 {
|
||||||
|
p.MoveTo(x, y)
|
||||||
|
} else {
|
||||||
|
p.LineTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Stroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) SmoothLineStroke(points []Point) *Painter {
|
||||||
|
prevX := 0
|
||||||
|
prevY := 0
|
||||||
|
// TODO 如何生成平滑的折线
|
||||||
|
for index, point := range points {
|
||||||
|
x := point.X
|
||||||
|
y := point.Y
|
||||||
|
if index == 0 {
|
||||||
|
p.MoveTo(x, y)
|
||||||
|
} else {
|
||||||
|
cx := prevX + (x-prevX)/5
|
||||||
|
cy := y + (y-prevY)/2
|
||||||
|
p.QuadCurveTo(cx, cy, x, y)
|
||||||
|
}
|
||||||
|
prevX = x
|
||||||
|
prevY = y
|
||||||
|
}
|
||||||
|
p.Stroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) SetBackground(width, height int, color Color) *Painter {
|
||||||
|
r := p.render
|
||||||
|
s := chart.Style{
|
||||||
|
FillColor: color,
|
||||||
|
}
|
||||||
|
// 背景色
|
||||||
|
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)
|
||||||
|
p.FillStroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
func (p *Painter) MarkLine(x, y, width int) *Painter {
|
||||||
|
arrowWidth := 16
|
||||||
|
arrowHeight := 10
|
||||||
|
endX := x + width
|
||||||
|
radius := 3
|
||||||
|
p.Circle(3, x+radius, y)
|
||||||
|
p.render.Fill()
|
||||||
|
p.MoveTo(x+radius*3, y)
|
||||||
|
p.LineTo(endX-arrowWidth, y)
|
||||||
|
p.Stroke()
|
||||||
|
p.ArrowRight(endX, y, arrowWidth, arrowHeight)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Polygon(center Point, radius float64, sides int) *Painter {
|
||||||
|
points := getPolygonPoints(center, radius, sides)
|
||||||
|
for i, item := range points {
|
||||||
|
if i == 0 {
|
||||||
|
p.MoveTo(item.X, item.Y)
|
||||||
|
} else {
|
||||||
|
p.LineTo(item.X, item.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.LineTo(points[0].X, points[0].Y)
|
||||||
|
p.Stroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) FillArea(points []Point) *Painter {
|
||||||
|
var x, y int
|
||||||
|
for index, point := range points {
|
||||||
|
x = point.X
|
||||||
|
y = point.Y
|
||||||
|
if index == 0 {
|
||||||
|
p.MoveTo(x, y)
|
||||||
|
} else {
|
||||||
|
p.LineTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Fill()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Text(body string, x, y int) *Painter {
|
||||||
|
p.render.Text(body, x+p.box.Left, y+p.box.Top)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
|
||||||
|
style := p.style
|
||||||
|
textWarp := style.TextWrap
|
||||||
|
style.TextWrap = chart.TextWrapWord
|
||||||
|
r := p.render
|
||||||
|
lines := chart.Text.WrapFit(r, body, width, style)
|
||||||
|
p.SetTextStyle(style)
|
||||||
|
var output chart.Box
|
||||||
|
|
||||||
|
for index, line := range lines {
|
||||||
|
x0 := x
|
||||||
|
y0 := y + output.Height()
|
||||||
|
p.Text(line, x0, y0)
|
||||||
|
lineBox := r.MeasureText(line)
|
||||||
|
output.Right = chart.MaxInt(lineBox.Right, output.Right)
|
||||||
|
output.Bottom += lineBox.Height()
|
||||||
|
if index < len(lines)-1 {
|
||||||
|
output.Bottom += +style.GetTextLineSpacing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.style.TextWrap = textWarp
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Ticks(opt TicksOption) *Painter {
|
||||||
|
if opt.Count <= 0 || opt.Length <= 0 {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
count := opt.Count
|
||||||
|
width := p.Width()
|
||||||
|
height := p.Height()
|
||||||
|
unit := 1
|
||||||
|
if opt.Unit > 1 {
|
||||||
|
unit = opt.Unit
|
||||||
|
}
|
||||||
|
var values []int
|
||||||
|
isVertical := opt.Orient == OrientVertical
|
||||||
|
if isVertical {
|
||||||
|
values = autoDivide(height, count)
|
||||||
|
} else {
|
||||||
|
values = autoDivide(width, count)
|
||||||
|
}
|
||||||
|
for index, value := range values {
|
||||||
|
if index%unit != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isVertical {
|
||||||
|
p.LineStroke([]Point{
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: opt.Length,
|
||||||
|
Y: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
p.LineStroke([]Point{
|
||||||
|
{
|
||||||
|
X: value,
|
||||||
|
Y: opt.Length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: value,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) MultiText(opt MultiTextOption) *Painter {
|
||||||
|
if len(opt.TextList) == 0 {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
count := len(opt.TextList)
|
||||||
|
positionCenter := true
|
||||||
|
if containsString([]string{
|
||||||
|
PositionLeft,
|
||||||
|
PositionTop,
|
||||||
|
}, opt.Position) {
|
||||||
|
positionCenter = false
|
||||||
|
count--
|
||||||
|
}
|
||||||
|
width := p.Width()
|
||||||
|
height := p.Height()
|
||||||
|
var values []int
|
||||||
|
isVertical := opt.Orient == OrientVertical
|
||||||
|
if isVertical {
|
||||||
|
values = autoDivide(height, count)
|
||||||
|
} else {
|
||||||
|
values = autoDivide(width, count)
|
||||||
|
}
|
||||||
|
for index, text := range opt.TextList {
|
||||||
|
if index%opt.Unit != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
box := p.MeasureText(text)
|
||||||
|
start := values[index]
|
||||||
|
if positionCenter {
|
||||||
|
start = (values[index] + values[index+1]) >> 1
|
||||||
|
}
|
||||||
|
x := 0
|
||||||
|
y := 0
|
||||||
|
if isVertical {
|
||||||
|
y = start + box.Height()>>1
|
||||||
|
switch opt.Align {
|
||||||
|
case AlignRight:
|
||||||
|
x = width - box.Width()
|
||||||
|
case AlignCenter:
|
||||||
|
x = width - box.Width()>>1
|
||||||
|
default:
|
||||||
|
x = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
x = start - box.Width()>>1
|
||||||
|
}
|
||||||
|
p.Text(text, x, y)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Grid(opt GridOption) *Painter {
|
||||||
|
width := p.Width()
|
||||||
|
height := p.Height()
|
||||||
|
drawLines := func(values []int, ignoreIndexList []int, isVertical bool) {
|
||||||
|
for index, v := range values {
|
||||||
|
if containsInt(ignoreIndexList, index) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x0 := 0
|
||||||
|
y0 := 0
|
||||||
|
x1 := 0
|
||||||
|
y1 := 0
|
||||||
|
if isVertical {
|
||||||
|
|
||||||
|
x0 = v
|
||||||
|
x1 = v
|
||||||
|
y1 = height
|
||||||
|
} else {
|
||||||
|
x1 = width
|
||||||
|
y0 = v
|
||||||
|
y1 = v
|
||||||
|
}
|
||||||
|
p.LineStroke([]Point{
|
||||||
|
{
|
||||||
|
X: x0,
|
||||||
|
Y: y0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: x1,
|
||||||
|
Y: y1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opt.Column > 0 {
|
||||||
|
values := autoDivide(width, opt.Column)
|
||||||
|
drawLines(values, opt.IgnoreColumnLines, true)
|
||||||
|
}
|
||||||
|
if opt.Row > 0 {
|
||||||
|
values := autoDivide(height, opt.Row)
|
||||||
|
drawLines(values, opt.IgnoreRowLines, false)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Dots(points []Point) *Painter {
|
||||||
|
for _, item := range points {
|
||||||
|
p.Circle(2, item.X, item.Y)
|
||||||
|
}
|
||||||
|
p.FillStroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) Rect(box Box) *Painter {
|
||||||
|
p.MoveTo(box.Left, box.Top)
|
||||||
|
p.LineTo(box.Right, box.Top)
|
||||||
|
p.LineTo(box.Right, box.Bottom)
|
||||||
|
p.LineTo(box.Left, box.Bottom)
|
||||||
|
p.LineTo(box.Left, box.Top)
|
||||||
|
p.FillStroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Painter) LegendLineDot(box Box) *Painter {
|
||||||
|
width := box.Width()
|
||||||
|
height := box.Height()
|
||||||
|
strokeWidth := 3
|
||||||
|
dotHeight := 5
|
||||||
|
|
||||||
|
p.render.SetStrokeWidth(float64(strokeWidth))
|
||||||
|
center := (height-strokeWidth)>>1 - 1
|
||||||
|
p.MoveTo(box.Left, box.Top-center)
|
||||||
|
p.LineTo(box.Right, box.Top-center)
|
||||||
|
p.Stroke()
|
||||||
|
p.Circle(float64(dotHeight), box.Left+width>>1, box.Top-center)
|
||||||
|
p.FillStroke()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
@ -26,217 +26,84 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParentOption(t *testing.T) {
|
func TestPainterOption(t *testing.T) {
|
||||||
assert := assert.New(t)
|
|
||||||
p, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Parent: p,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(p, d.parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWidthHeightOption(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
// no parent
|
font := &truetype.Font{}
|
||||||
width := 300
|
d, err := NewPainter(PainterOptions{
|
||||||
height := 200
|
Width: 800,
|
||||||
d, err := NewDraw(DrawOption{
|
Height: 600,
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Top: 0,
|
|
||||||
Left: 0,
|
|
||||||
Right: width,
|
|
||||||
Bottom: height,
|
|
||||||
}, d.Box)
|
|
||||||
|
|
||||||
width = 500
|
|
||||||
height = 600
|
|
||||||
// with parent
|
|
||||||
p, err := NewDraw(
|
|
||||||
DrawOption{
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
},
|
},
|
||||||
PaddingOption(chart.NewBox(5, 5, 5, 5)),
|
PainterBoxOption(Box{
|
||||||
)
|
|
||||||
assert.Nil(err)
|
|
||||||
d, err = NewDraw(
|
|
||||||
DrawOption{
|
|
||||||
Parent: p,
|
|
||||||
},
|
|
||||||
PaddingOption(chart.NewBox(1, 2, 3, 4)),
|
|
||||||
)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Top: 6,
|
|
||||||
Left: 7,
|
|
||||||
Right: 492,
|
|
||||||
Bottom: 591,
|
|
||||||
}, d.Box)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBoxOption(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
err = BoxOption(chart.Box{
|
|
||||||
Left: 10,
|
|
||||||
Top: 20,
|
|
||||||
Right: 50,
|
|
||||||
Bottom: 100,
|
|
||||||
})(d)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Left: 10,
|
|
||||||
Top: 20,
|
|
||||||
Right: 50,
|
|
||||||
Bottom: 100,
|
|
||||||
}, d.Box)
|
|
||||||
|
|
||||||
// zero box will be ignored
|
|
||||||
err = BoxOption(chart.Box{})(d)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Left: 10,
|
|
||||||
Top: 20,
|
|
||||||
Right: 50,
|
|
||||||
Bottom: 100,
|
|
||||||
}, d.Box)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPaddingOption(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
// 默认的box
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Right: 400,
|
Right: 400,
|
||||||
Bottom: 300,
|
Bottom: 300,
|
||||||
}, d.Box)
|
}),
|
||||||
|
PainterPaddingOption(Box{
|
||||||
// 设置padding之后的box
|
|
||||||
d, err = NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Left: 1,
|
|
||||||
Top: 2,
|
|
||||||
Right: 3,
|
|
||||||
Bottom: 4,
|
|
||||||
}))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(chart.Box{
|
|
||||||
Top: 2,
|
|
||||||
Left: 1,
|
|
||||||
Right: 397,
|
|
||||||
Bottom: 296,
|
|
||||||
}, d.Box)
|
|
||||||
|
|
||||||
p := d
|
|
||||||
// 设置父元素之后的box
|
|
||||||
d, err = NewDraw(
|
|
||||||
DrawOption{
|
|
||||||
Parent: p,
|
|
||||||
},
|
|
||||||
PaddingOption(chart.Box{
|
|
||||||
Left: 1,
|
Left: 1,
|
||||||
Top: 2,
|
Top: 2,
|
||||||
Right: 3,
|
Right: 3,
|
||||||
Bottom: 4,
|
Bottom: 4,
|
||||||
}),
|
}),
|
||||||
|
PainterFontOption(font),
|
||||||
|
PainterStyleOption(Style{
|
||||||
|
ClassName: "test",
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal(chart.Box{
|
assert.Equal(Box{
|
||||||
Top: 4,
|
Left: 1,
|
||||||
Left: 2,
|
Top: 2,
|
||||||
Right: 394,
|
Right: 397,
|
||||||
Bottom: 292,
|
Bottom: 296,
|
||||||
}, d.Box)
|
}, d.box)
|
||||||
|
assert.Equal(font, d.font)
|
||||||
|
assert.Equal("test", d.style.ClassName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParentTop(t *testing.T) {
|
func TestPainter(t *testing.T) {
|
||||||
assert := assert.New(t)
|
|
||||||
d1, err := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
d2, err := NewDraw(DrawOption{
|
|
||||||
Parent: d1,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
d3, err := NewDraw(DrawOption{
|
|
||||||
Parent: d2,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
assert.Equal(d2, d3.Parent())
|
|
||||||
assert.Equal(d1, d2.Parent())
|
|
||||||
assert.Equal(d1, d3.Top())
|
|
||||||
assert.Equal(d1, d2.Top())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDraw(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
fn func(d *Draw)
|
fn func(*Painter)
|
||||||
result string
|
result string
|
||||||
}{
|
}{
|
||||||
// moveTo, lineTo
|
// moveTo, lineTo
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
d.moveTo(1, 1)
|
p.MoveTo(1, 1)
|
||||||
d.lineTo(2, 2)
|
p.LineTo(2, 2)
|
||||||
d.Render.Stroke()
|
p.Stroke()
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 6 11\nL 7 12\" style=\"stroke-width:0;stroke:none;fill:none\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 6 11\nL 7 12\" style=\"stroke-width:0;stroke:none;fill:none\"/></svg>",
|
||||||
},
|
},
|
||||||
// circle
|
// circle
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
d.circle(5, 2, 3)
|
p.Circle(5, 2, 3)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"7\" cy=\"13\" r=\"5\" style=\"stroke-width:0;stroke:none;fill:none\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"7\" cy=\"13\" r=\"5\" style=\"stroke-width:0;stroke:none;fill:none\"/></svg>",
|
||||||
},
|
},
|
||||||
// text
|
// text
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
d.text("hello world!", 3, 6)
|
p.Text("hello world!", 3, 6)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"8\" y=\"16\" style=\"stroke-width:0;stroke:none;fill:none\">hello world!</text></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"8\" y=\"16\" style=\"stroke-width:0;stroke:none;fill:none;font-family:'Roboto Medium',sans-serif\">hello world!</text></svg>",
|
||||||
},
|
},
|
||||||
// line stroke
|
// line stroke
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
d.lineStroke([]Point{
|
p.SetDrawingStyle(Style{
|
||||||
|
StrokeColor: drawing.ColorBlack,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
})
|
||||||
|
p.LineStroke([]Point{
|
||||||
{
|
{
|
||||||
X: 1,
|
X: 1,
|
||||||
Y: 2,
|
Y: 2,
|
||||||
|
|
@ -245,38 +112,35 @@ func TestDraw(t *testing.T) {
|
||||||
X: 3,
|
X: 3,
|
||||||
Y: 4,
|
Y: 4,
|
||||||
},
|
},
|
||||||
}, LineStyle{
|
|
||||||
StrokeColor: drawing.ColorBlack,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 6 12\nL 8 14\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 6 12\nL 8 14\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/></svg>",
|
||||||
},
|
},
|
||||||
// set background
|
// set background
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
d.setBackground(400, 300, chart.ColorWhite)
|
p.SetBackground(400, 300, chart.ColorWhite)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 0 0\nL 400 0\nL 400 300\nL 0 300\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 0 0\nL 400 0\nL 400 300\nL 0 300\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// arcTo
|
// arcTo
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.ColorBlack,
|
StrokeColor: drawing.ColorBlack,
|
||||||
FillColor: drawing.ColorBlue,
|
FillColor: drawing.ColorBlue,
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.arcTo(100, 100, 100, 100, 0, math.Pi/2)
|
p.ArcTo(100, 100, 100, 100, 0, math.Pi/2)
|
||||||
d.Render.Close()
|
p.Close()
|
||||||
d.Render.FillStroke()
|
p.FillStroke()
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 205 110\nA 100 100 90.00 0 1 105 210\nZ\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,255,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 205 110\nA 100 100 90.00 0 1 105 210\nZ\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,255,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// pin
|
// pin
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -290,15 +154,15 @@ func TestDraw(t *testing.T) {
|
||||||
B: 198,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.pin(30, 30, 30)
|
p.Pin(30, 30, 30)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 32 47\nA 15 15 330.00 1 1 38 47\nL 35 33\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 20 33\nQ35,70 50,33\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 32 47\nA 15 15 330.00 1 1 38 47\nL 35 33\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 20 33\nQ35,70 50,33\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// arrow left
|
// arrow left
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -312,15 +176,15 @@ func TestDraw(t *testing.T) {
|
||||||
B: 198,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.arrowLeft(30, 30, 16, 10)
|
p.ArrowLeft(30, 30, 16, 10)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 51 35\nL 35 40\nL 51 45\nL 46 40\nL 51 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 51 35\nL 35 40\nL 51 45\nL 46 40\nL 51 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// arrow right
|
// arrow right
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -334,15 +198,15 @@ func TestDraw(t *testing.T) {
|
||||||
B: 198,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.arrowRight(30, 30, 16, 10)
|
p.ArrowRight(30, 30, 16, 10)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 19 35\nL 35 40\nL 19 45\nL 24 40\nL 19 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 19 35\nL 35 40\nL 19 45\nL 24 40\nL 19 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// arrow top
|
// arrow top
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -356,15 +220,15 @@ func TestDraw(t *testing.T) {
|
||||||
B: 198,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.arrowTop(30, 30, 10, 16)
|
p.ArrowTop(30, 30, 10, 16)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 30 40\nL 35 24\nL 40 40\nL 35 35\nL 30 40\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 30 40\nL 35 24\nL 40 40\nL 35 35\nL 30 40\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// arrow bottom
|
// arrow bottom
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -378,15 +242,15 @@ func TestDraw(t *testing.T) {
|
||||||
B: 198,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.arrowBottom(30, 30, 10, 16)
|
p.ArrowBottom(30, 30, 10, 16)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 30 24\nL 35 40\nL 40 24\nL 35 30\nL 30 24\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 30 24\nL 35 40\nL 40 24\nL 35 30\nL 30 24\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// mark line
|
// mark line
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -404,15 +268,15 @@ func TestDraw(t *testing.T) {
|
||||||
4,
|
4,
|
||||||
2,
|
2,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.makeLine(0, 20, 300)
|
p.MarkLine(0, 20, 300)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"5\" cy=\"30\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 10 30\nL 289 30\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 289 25\nL 305 30\nL 289 35\nL 294 30\nL 289 25\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"8\" cy=\"30\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 14 30\nL 289 30\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 289 25\nL 305 30\nL 289 35\nL 294 30\nL 289 25\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// polygon
|
// polygon
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
chart.Style{
|
p.SetStyle(Style{
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
StrokeColor: drawing.Color{
|
StrokeColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
|
|
@ -420,35 +284,18 @@ func TestDraw(t *testing.T) {
|
||||||
B: 198,
|
B: 198,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
}.WriteToRenderer(d.Render)
|
})
|
||||||
d.polygon(Point{
|
p.Polygon(Point{
|
||||||
X: 100,
|
X: 100,
|
||||||
Y: 100,
|
Y: 100,
|
||||||
}, 50, 6)
|
}, 50, 6)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 105 60\nL 148 85\nL 148 134\nL 105 160\nL 62 135\nL 62 86\nL 105 60\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:none\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 105 60\nL 148 85\nL 148 134\nL 105 160\nL 62 135\nL 62 86\nL 105 60\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:none\"/></svg>",
|
||||||
},
|
},
|
||||||
// fill
|
// FillArea
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(p *Painter) {
|
||||||
d.fill([]Point{
|
p.SetDrawingStyle(Style{
|
||||||
{
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 0,
|
|
||||||
Y: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 100,
|
|
||||||
Y: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
X: 0,
|
|
||||||
Y: 0,
|
|
||||||
},
|
|
||||||
}, chart.Style{
|
|
||||||
FillColor: drawing.Color{
|
FillColor: drawing.Color{
|
||||||
R: 84,
|
R: 84,
|
||||||
G: 112,
|
G: 112,
|
||||||
|
|
@ -456,15 +303,33 @@ func TestDraw(t *testing.T) {
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
p.FillArea([]Point{
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 100,
|
||||||
|
Y: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 10\nL 5 110\nL 105 110\nL 5 10\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 5 10\nL 5 110\nL 105 110\nL 5 10\" style=\"stroke-width:0;stroke:none;fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewPainter(PainterOptions{
|
||||||
Width: 400,
|
Width: 400,
|
||||||
Height: 300,
|
Height: 300,
|
||||||
}, PaddingOption(chart.Box{
|
}, PainterPaddingOption(chart.Box{
|
||||||
Left: 5,
|
Left: 5,
|
||||||
Top: 10,
|
Top: 10,
|
||||||
}))
|
}))
|
||||||
|
|
@ -476,32 +341,33 @@ func TestDraw(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDrawTextFit(t *testing.T) {
|
func TestPainterTextFit(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
d, err := NewDraw(DrawOption{
|
p, err := NewPainter(PainterOptions{
|
||||||
Width: 400,
|
Width: 400,
|
||||||
Height: 300,
|
Height: 300,
|
||||||
})
|
})
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
f, _ := chart.GetDefaultFont()
|
f, _ := chart.GetDefaultFont()
|
||||||
style := chart.Style{
|
style := Style{
|
||||||
FontSize: 12,
|
FontSize: 12,
|
||||||
FontColor: chart.ColorBlack,
|
FontColor: chart.ColorBlack,
|
||||||
Font: f,
|
Font: f,
|
||||||
}
|
}
|
||||||
box := d.textFit("Hello World!", 0, 20, 80, style)
|
p.SetStyle(style)
|
||||||
|
box := p.TextFit("Hello World!", 0, 20, 80)
|
||||||
assert.Equal(chart.Box{
|
assert.Equal(chart.Box{
|
||||||
Right: 45,
|
Right: 45,
|
||||||
Bottom: 35,
|
Bottom: 35,
|
||||||
}, box)
|
}, box)
|
||||||
|
|
||||||
box = d.textFit("Hello World!", 0, 100, 200, style)
|
box = p.TextFit("Hello World!", 0, 100, 200)
|
||||||
assert.Equal(chart.Box{
|
assert.Equal(chart.Box{
|
||||||
Right: 84,
|
Right: 84,
|
||||||
Bottom: 15,
|
Bottom: 15,
|
||||||
}, box)
|
}, box)
|
||||||
|
|
||||||
buf, err := d.Bytes()
|
buf, err := p.Bytes()
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300">\n<text x="0" y="20" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Hello</text><text x="0" y="40" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">World!</text><text x="0" y="100" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Hello World!</text></svg>`, string(buf))
|
assert.Equal(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="300">\n<text x="0" y="20" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Hello</text><text x="0" y="40" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">World!</text><text x="0" y="100" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif">Hello World!</text></svg>`, string(buf))
|
||||||
}
|
}
|
||||||
146
pie_chart.go
146
pie_chart.go
|
|
@ -30,35 +30,43 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPieStyle(theme *Theme, index int) chart.Style {
|
type pieChart struct {
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
p *Painter
|
||||||
return chart.Style{
|
opt *PieChartOption
|
||||||
StrokeColor: seriesColor,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
FillColor: seriesColor,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type pieChartOption struct {
|
type PieChartOption struct {
|
||||||
Theme string
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
SeriesList SeriesList
|
SeriesList SeriesList
|
||||||
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
// background is filled
|
||||||
|
backgroundIsFilled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
func NewPieChart(p *Painter, opt PieChartOption) *pieChart {
|
||||||
d, err := NewDraw(DrawOption{
|
if opt.Theme == nil {
|
||||||
Parent: result.d,
|
opt.Theme = defaultTheme
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Top: result.titleBox.Height(),
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
return &pieChart{
|
||||||
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
values := make([]float64, len(opt.SeriesList))
|
func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
opt := p.opt
|
||||||
|
values := make([]float64, len(seriesList))
|
||||||
total := float64(0)
|
total := float64(0)
|
||||||
radiusValue := ""
|
radiusValue := ""
|
||||||
for index, series := range opt.SeriesList {
|
for index, series := range seriesList {
|
||||||
if len(series.Radius) != 0 {
|
if len(series.Radius) != 0 {
|
||||||
radiusValue = series.Radius
|
radiusValue = series.Radius
|
||||||
}
|
}
|
||||||
|
|
@ -70,16 +78,13 @@ func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
total += value
|
total += value
|
||||||
}
|
}
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
return errors.New("The sum value of pie chart should gt 0")
|
return BoxZero, errors.New("The sum value of pie chart should gt 0")
|
||||||
}
|
}
|
||||||
r := d.Render
|
seriesPainter := result.seriesPainter
|
||||||
theme := NewTheme(opt.Theme)
|
cx := seriesPainter.Width() >> 1
|
||||||
|
cy := seriesPainter.Height() >> 1
|
||||||
|
|
||||||
box := d.Box
|
diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height())
|
||||||
cx := box.Width() >> 1
|
|
||||||
cy := box.Height() >> 1
|
|
||||||
|
|
||||||
diameter := chart.MinInt(box.Width(), box.Height())
|
|
||||||
radius := getRadius(float64(diameter), radiusValue)
|
radius := getRadius(float64(diameter), radiusValue)
|
||||||
|
|
||||||
labelLineWidth := 15
|
labelLineWidth := 15
|
||||||
|
|
@ -87,32 +92,40 @@ func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
labelLineWidth = 10
|
labelLineWidth = 10
|
||||||
}
|
}
|
||||||
labelRadius := radius + float64(labelLineWidth)
|
labelRadius := radius + float64(labelLineWidth)
|
||||||
|
seriesNames := opt.Legend.Data
|
||||||
seriesNames := opt.SeriesList.Names()
|
if len(seriesNames) == 0 {
|
||||||
|
seriesNames = seriesList.Names()
|
||||||
|
}
|
||||||
|
theme := opt.Theme
|
||||||
if len(values) == 1 {
|
if len(values) == 1 {
|
||||||
getPieStyle(theme, 0).WriteToRenderer(r)
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
d.moveTo(cx, cy)
|
StrokeWidth: 1,
|
||||||
d.circle(radius, cx, cy)
|
StrokeColor: theme.GetSeriesColor(0),
|
||||||
|
FillColor: theme.GetSeriesColor(0),
|
||||||
|
})
|
||||||
|
seriesPainter.MoveTo(cx, cy).
|
||||||
|
Circle(radius, cx, cy)
|
||||||
} else {
|
} else {
|
||||||
currentValue := float64(0)
|
currentValue := float64(0)
|
||||||
prevEndX := 0
|
prevEndX := 0
|
||||||
prevEndY := 0
|
prevEndY := 0
|
||||||
for index, v := range values {
|
for index, v := range values {
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
pieStyle := getPieStyle(theme, index)
|
StrokeWidth: 1,
|
||||||
pieStyle.WriteToRenderer(r)
|
StrokeColor: theme.GetSeriesColor(index),
|
||||||
d.moveTo(cx, cy)
|
FillColor: theme.GetSeriesColor(index),
|
||||||
|
})
|
||||||
|
seriesPainter.MoveTo(cx, cy)
|
||||||
start := chart.PercentToRadians(currentValue/total) - math.Pi/2
|
start := chart.PercentToRadians(currentValue/total) - math.Pi/2
|
||||||
currentValue += v
|
currentValue += v
|
||||||
percent := (v / total)
|
percent := (v / total)
|
||||||
delta := chart.PercentToRadians(percent)
|
delta := chart.PercentToRadians(percent)
|
||||||
d.arcTo(cx, cy, radius, radius, start, delta)
|
seriesPainter.ArcTo(cx, cy, radius, radius, start, delta).
|
||||||
d.lineTo(cx, cy)
|
LineTo(cx, cy).
|
||||||
r.Close()
|
Close().
|
||||||
r.FillStroke()
|
FillStroke()
|
||||||
|
|
||||||
series := opt.SeriesList[index]
|
series := seriesList[index]
|
||||||
// 是否显示label
|
// 是否显示label
|
||||||
showLabel := series.Label.Show
|
showLabel := series.Label.Show
|
||||||
if !showLabel {
|
if !showLabel {
|
||||||
|
|
@ -134,17 +147,19 @@ func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
}
|
}
|
||||||
prevEndX = endx
|
prevEndX = endx
|
||||||
prevEndY = endy
|
prevEndY = endy
|
||||||
d.moveTo(startx, starty)
|
|
||||||
d.lineTo(endx, endy)
|
seriesPainter.MoveTo(startx, starty)
|
||||||
|
seriesPainter.LineTo(endx, endy)
|
||||||
offset := labelLineWidth
|
offset := labelLineWidth
|
||||||
if endx < cx {
|
if endx < cx {
|
||||||
offset *= -1
|
offset *= -1
|
||||||
}
|
}
|
||||||
d.moveTo(endx, endy)
|
seriesPainter.MoveTo(endx, endy)
|
||||||
endx += offset
|
endx += offset
|
||||||
d.lineTo(endx, endy)
|
seriesPainter.LineTo(endx, endy)
|
||||||
r.Stroke()
|
seriesPainter.Stroke()
|
||||||
textStyle := chart.Style{
|
|
||||||
|
textStyle := Style{
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: labelFontSize,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
|
|
@ -152,9 +167,9 @@ func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
if !series.Label.Color.IsZero() {
|
if !series.Label.Color.IsZero() {
|
||||||
textStyle.FontColor = series.Label.Color
|
textStyle.FontColor = series.Label.Color
|
||||||
}
|
}
|
||||||
textStyle.GetTextOptions().WriteToRenderer(r)
|
seriesPainter.OverrideTextStyle(textStyle)
|
||||||
text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent)
|
text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent)
|
||||||
textBox := r.MeasureText(text)
|
textBox := seriesPainter.MeasureText(text)
|
||||||
textMargin := 3
|
textMargin := 3
|
||||||
x := endx + textMargin
|
x := endx + textMargin
|
||||||
y := endy + textBox.Height()>>1 - 1
|
y := endy + textBox.Height()>>1 - 1
|
||||||
|
|
@ -162,8 +177,35 @@ func pieChartRender(opt pieChartOption, result *basicRenderResult) error {
|
||||||
textWidth := textBox.Width()
|
textWidth := textBox.Width()
|
||||||
x = endx - textWidth - textMargin
|
x = endx - textWidth - textMargin
|
||||||
}
|
}
|
||||||
d.text(text, x, y)
|
seriesPainter.Text(text, x, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return p.p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pieChart) Render() (Box, error) {
|
||||||
|
opt := p.opt
|
||||||
|
|
||||||
|
renderResult, err := defaultRender(p.p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: XAxisOption{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
YAxisOptions: []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
backgroundIsFilled: opt.backgroundIsFilled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypePie)
|
||||||
|
return p.render(renderResult, seriesList)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPieChartRender(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: 250,
|
|
||||||
Height: 150,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
|
|
||||||
err = pieChartRender(pieChartOption{
|
|
||||||
Font: f,
|
|
||||||
SeriesList: NewPieSeriesList([]float64{
|
|
||||||
5,
|
|
||||||
10,
|
|
||||||
0,
|
|
||||||
}, PieSeriesOption{
|
|
||||||
Names: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
"c",
|
|
||||||
},
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
Color: drawing.ColorRed,
|
|
||||||
},
|
|
||||||
Radius: "20%",
|
|
||||||
}),
|
|
||||||
}, &basicRenderResult{
|
|
||||||
d: d,
|
|
||||||
})
|
|
||||||
assert.Nil(err)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"250\" height=\"150\">\\n<path d=\"M 125 75\nL 125 45\nA 30 30 120.00 0 1 150 89\nL 125 75\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 150 60\nL 159 55\nM 159 55\nL 169 55\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"172\" y=\"60\" style=\"stroke-width:0;stroke:none;fill:rgba(255,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">a: 33.33%</text><path d=\"M 125 75\nL 150 89\nA 30 30 240.00 1 1 125 45\nL 125 75\nZ\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"M 100 90\nL 91 95\nM 91 95\nL 81 95\" style=\"stroke-width:1;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><text x=\"22\" y=\"100\" style=\"stroke-width:0;stroke:none;fill:rgba(255,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">b: 66.66%</text><path d=\"M 125 75\nL 125 45\nA 30 30 0.00 0 1 125 45\nL 125 75\nZ\" style=\"stroke-width:1;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><path d=\"M 125 45\nL 125 35\nM 125 35\nL 135 35\" style=\"stroke-width:1;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><text x=\"138\" y=\"40\" style=\"stroke-width:0;stroke:none;fill:rgba(255,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">c: 0%</text></svg>", string(data))
|
|
||||||
}
|
|
||||||
152
radar_chart.go
152
radar_chart.go
|
|
@ -30,6 +30,11 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type radarChart struct {
|
||||||
|
p *Painter
|
||||||
|
opt *RadarChartOption
|
||||||
|
}
|
||||||
|
|
||||||
type RadarIndicator struct {
|
type RadarIndicator struct {
|
||||||
// Indicator's name
|
// Indicator's name
|
||||||
Name string
|
Name string
|
||||||
|
|
@ -39,89 +44,101 @@ type RadarIndicator struct {
|
||||||
Min float64
|
Min float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type radarChartOption struct {
|
type RadarChartOption struct {
|
||||||
Theme string
|
Theme ColorPalette
|
||||||
|
// The font size
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
|
// The data series list
|
||||||
SeriesList SeriesList
|
SeriesList SeriesList
|
||||||
Indicators []RadarIndicator
|
// The padding of line chart
|
||||||
|
Padding Box
|
||||||
|
// The option of title
|
||||||
|
Title TitleOption
|
||||||
|
// The legend option
|
||||||
|
Legend LegendOption
|
||||||
|
// The radar indicator list
|
||||||
|
RadarIndicators []RadarIndicator
|
||||||
|
// background is filled
|
||||||
|
backgroundIsFilled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart {
|
||||||
sides := len(opt.Indicators)
|
if opt.Theme == nil {
|
||||||
if sides < 3 {
|
opt.Theme = defaultTheme
|
||||||
return errors.New("The count of indicator should be >= 3")
|
|
||||||
}
|
}
|
||||||
maxValues := make([]float64, len(opt.Indicators))
|
return &radarChart{
|
||||||
for _, series := range opt.SeriesList {
|
p: p,
|
||||||
|
opt: &opt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
|
||||||
|
opt := r.opt
|
||||||
|
indicators := opt.RadarIndicators
|
||||||
|
sides := len(indicators)
|
||||||
|
if sides < 3 {
|
||||||
|
return BoxZero, errors.New("The count of indicator should be >= 3")
|
||||||
|
}
|
||||||
|
maxValues := make([]float64, len(indicators))
|
||||||
|
for _, series := range seriesList {
|
||||||
for index, item := range series.Data {
|
for index, item := range series.Data {
|
||||||
if index < len(maxValues) && item.Value > maxValues[index] {
|
if index < len(maxValues) && item.Value > maxValues[index] {
|
||||||
maxValues[index] = item.Value
|
maxValues[index] = item.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index, indicator := range opt.Indicators {
|
for index, indicator := range indicators {
|
||||||
if indicator.Max <= 0 {
|
if indicator.Max <= 0 {
|
||||||
opt.Indicators[index].Max = maxValues[index]
|
indicators[index].Max = maxValues[index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Parent: result.d,
|
|
||||||
}, PaddingOption(chart.Box{
|
|
||||||
Top: result.titleBox.Height(),
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
radiusValue := ""
|
radiusValue := ""
|
||||||
for _, series := range opt.SeriesList {
|
for _, series := range seriesList {
|
||||||
if len(series.Radius) != 0 {
|
if len(series.Radius) != 0 {
|
||||||
radiusValue = series.Radius
|
radiusValue = series.Radius
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
box := d.Box
|
seriesPainter := result.seriesPainter
|
||||||
cx := box.Width() >> 1
|
theme := opt.Theme
|
||||||
cy := box.Height() >> 1
|
|
||||||
diameter := chart.MinInt(box.Width(), box.Height())
|
|
||||||
radius := getRadius(float64(diameter), radiusValue)
|
|
||||||
|
|
||||||
theme := NewTheme(opt.Theme)
|
cx := seriesPainter.Width() >> 1
|
||||||
|
cy := seriesPainter.Height() >> 1
|
||||||
|
diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height())
|
||||||
|
radius := getRadius(float64(diameter), radiusValue)
|
||||||
|
|
||||||
divideCount := 5
|
divideCount := 5
|
||||||
divideRadius := float64(int(radius / float64(divideCount)))
|
divideRadius := float64(int(radius / float64(divideCount)))
|
||||||
radius = divideRadius * float64(divideCount)
|
radius = divideRadius * float64(divideCount)
|
||||||
|
|
||||||
style := chart.Style{
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
StrokeColor: theme.GetAxisSplitLineColor(),
|
StrokeColor: theme.GetAxisSplitLineColor(),
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
}
|
})
|
||||||
r := d.Render
|
|
||||||
style.WriteToRenderer(r)
|
|
||||||
center := Point{
|
center := Point{
|
||||||
X: cx,
|
X: cx,
|
||||||
Y: cy,
|
Y: cy,
|
||||||
}
|
}
|
||||||
for i := 0; i < divideCount; i++ {
|
for i := 0; i < divideCount; i++ {
|
||||||
d.polygon(center, divideRadius*float64(i+1), sides)
|
seriesPainter.Polygon(center, divideRadius*float64(i+1), sides)
|
||||||
}
|
}
|
||||||
points := getPolygonPoints(center, radius, sides)
|
points := getPolygonPoints(center, radius, sides)
|
||||||
for _, p := range points {
|
for _, p := range points {
|
||||||
d.moveTo(center.X, center.Y)
|
seriesPainter.MoveTo(center.X, center.Y)
|
||||||
d.lineTo(p.X, p.Y)
|
seriesPainter.LineTo(p.X, p.Y)
|
||||||
d.Render.Stroke()
|
seriesPainter.Stroke()
|
||||||
}
|
}
|
||||||
// 文本
|
seriesPainter.OverrideTextStyle(Style{
|
||||||
textStyle := chart.Style{
|
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: labelFontSize,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}
|
})
|
||||||
textStyle.GetTextOptions().WriteToRenderer(r)
|
|
||||||
offset := 5
|
offset := 5
|
||||||
// 文本生成
|
// 文本生成
|
||||||
for index, p := range points {
|
for index, p := range points {
|
||||||
name := opt.Indicators[index].Name
|
name := indicators[index].Name
|
||||||
b := r.MeasureText(name)
|
b := seriesPainter.MeasureText(name)
|
||||||
isXCenter := p.X == center.X
|
isXCenter := p.X == center.X
|
||||||
isYCenter := p.Y == center.Y
|
isYCenter := p.Y == center.Y
|
||||||
isRight := p.X > center.X
|
isRight := p.X > center.X
|
||||||
|
|
@ -153,19 +170,19 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
||||||
if isLeft {
|
if isLeft {
|
||||||
x -= (b.Width() + offset)
|
x -= (b.Width() + offset)
|
||||||
}
|
}
|
||||||
d.text(name, x, y)
|
seriesPainter.Text(name, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 雷达图
|
// 雷达图
|
||||||
angles := getPolygonPointAngles(sides)
|
angles := getPolygonPointAngles(sides)
|
||||||
maxCount := len(opt.Indicators)
|
maxCount := len(indicators)
|
||||||
for _, series := range opt.SeriesList {
|
for _, series := range seriesList {
|
||||||
linePoints := make([]Point, 0, maxCount)
|
linePoints := make([]Point, 0, maxCount)
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
if j >= maxCount {
|
if j >= maxCount {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
indicator := opt.Indicators[j]
|
indicator := indicators[j]
|
||||||
percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min)
|
percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min)
|
||||||
r := percent * radius
|
r := percent * radius
|
||||||
p := getPolygonPoint(center, r, angles[j])
|
p := getPolygonPoint(center, r, angles[j])
|
||||||
|
|
@ -177,17 +194,52 @@ func radarChartRender(opt radarChartOption, result *basicRenderResult) error {
|
||||||
dotFillColor = color
|
dotFillColor = color
|
||||||
}
|
}
|
||||||
linePoints = append(linePoints, linePoints[0])
|
linePoints = append(linePoints, linePoints[0])
|
||||||
s := LineStyle{
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
StrokeColor: color,
|
StrokeColor: color,
|
||||||
StrokeWidth: defaultStrokeWidth,
|
StrokeWidth: defaultStrokeWidth,
|
||||||
DotWidth: defaultDotWidth,
|
DotWidth: defaultDotWidth,
|
||||||
DotColor: color,
|
DotColor: color,
|
||||||
DotFillColor: dotFillColor,
|
|
||||||
FillColor: color.WithAlpha(20),
|
FillColor: color.WithAlpha(20),
|
||||||
|
})
|
||||||
|
seriesPainter.LineStroke(linePoints).
|
||||||
|
FillArea(linePoints)
|
||||||
|
dotWith := 2.0
|
||||||
|
seriesPainter.OverrideDrawingStyle(Style{
|
||||||
|
StrokeWidth: defaultStrokeWidth,
|
||||||
|
StrokeColor: color,
|
||||||
|
FillColor: dotFillColor,
|
||||||
|
})
|
||||||
|
for _, point := range linePoints {
|
||||||
|
seriesPainter.Circle(dotWith, point.X, point.Y)
|
||||||
|
seriesPainter.FillStroke()
|
||||||
}
|
}
|
||||||
d.lineStroke(linePoints, s)
|
|
||||||
d.fill(linePoints, s.Style())
|
|
||||||
d.lineDot(linePoints[0:len(linePoints)-1], s)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return r.p.box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *radarChart) Render() (Box, error) {
|
||||||
|
p := r.p
|
||||||
|
opt := r.opt
|
||||||
|
renderResult, err := defaultRender(p, defaultRenderOption{
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Padding: opt.Padding,
|
||||||
|
SeriesList: opt.SeriesList,
|
||||||
|
XAxis: XAxisOption{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
YAxisOptions: []YAxisOption{
|
||||||
|
{
|
||||||
|
Show: FalseFlag(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TitleOption: opt.Title,
|
||||||
|
LegendOption: opt.Legend,
|
||||||
|
backgroundIsFilled: opt.backgroundIsFilled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return BoxZero, err
|
||||||
|
}
|
||||||
|
seriesList := opt.SeriesList.Filter(ChartTypeRadar)
|
||||||
|
return r.render(renderResult, seriesList)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
60
range.go
60
range.go
|
|
@ -26,15 +26,31 @@ import (
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Range struct {
|
const defaultAxisDivideCount = 6
|
||||||
|
|
||||||
|
type axisRange struct {
|
||||||
divideCount int
|
divideCount int
|
||||||
|
min float64
|
||||||
|
max float64
|
||||||
|
size int
|
||||||
|
boundary bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type AxisRangeOption struct {
|
||||||
Min float64
|
Min float64
|
||||||
Max float64
|
Max float64
|
||||||
Size int
|
Size int
|
||||||
Boundary bool
|
Boundary bool
|
||||||
|
DivideCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRange(min, max float64, divideCount int) Range {
|
func NewRange(opt AxisRangeOption) axisRange {
|
||||||
|
max := opt.Max
|
||||||
|
min := opt.Min
|
||||||
|
|
||||||
|
max += math.Abs(max * 0.1)
|
||||||
|
min -= math.Abs(min * 0.1)
|
||||||
|
divideCount := opt.DivideCount
|
||||||
r := math.Abs(max - min)
|
r := math.Abs(max - min)
|
||||||
|
|
||||||
// 最小单位计算
|
// 最小单位计算
|
||||||
|
|
@ -63,47 +79,49 @@ func NewRange(min, max float64, divideCount int) Range {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
max = min + float64(unit*divideCount)
|
max = min + float64(unit*divideCount)
|
||||||
return Range{
|
return axisRange{
|
||||||
Min: min,
|
|
||||||
Max: max,
|
|
||||||
divideCount: divideCount,
|
divideCount: divideCount,
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
size: opt.Size,
|
||||||
|
boundary: opt.Boundary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Range) Values() []string {
|
func (r axisRange) Values() []string {
|
||||||
offset := (r.Max - r.Min) / float64(r.divideCount)
|
offset := (r.max - r.min) / float64(r.divideCount)
|
||||||
values := make([]string, 0)
|
values := make([]string, 0)
|
||||||
for i := 0; i <= r.divideCount; i++ {
|
for i := 0; i <= r.divideCount; i++ {
|
||||||
v := r.Min + float64(i)*offset
|
v := r.min + float64(i)*offset
|
||||||
value := commafWithDigits(v)
|
value := commafWithDigits(v)
|
||||||
values = append(values, value)
|
values = append(values, value)
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range) getHeight(value float64) int {
|
func (r *axisRange) getHeight(value float64) int {
|
||||||
v := (value - r.Min) / (r.Max - r.Min)
|
v := (value - r.min) / (r.max - r.min)
|
||||||
return int(v * float64(r.Size))
|
return int(v * float64(r.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range) getRestHeight(value float64) int {
|
func (r *axisRange) getRestHeight(value float64) int {
|
||||||
return r.Size - r.getHeight(value)
|
return r.size - r.getHeight(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range) GetRange(index int) (float64, float64) {
|
func (r *axisRange) GetRange(index int) (float64, float64) {
|
||||||
unit := float64(r.Size) / float64(r.divideCount)
|
unit := float64(r.size) / float64(r.divideCount)
|
||||||
return unit * float64(index), unit * float64(index+1)
|
return unit * float64(index), unit * float64(index+1)
|
||||||
}
|
}
|
||||||
func (r *Range) AutoDivide() []int {
|
func (r *axisRange) AutoDivide() []int {
|
||||||
return autoDivide(r.Size, r.divideCount)
|
return autoDivide(r.size, r.divideCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range) getWidth(value float64) int {
|
func (r *axisRange) getWidth(value float64) int {
|
||||||
v := value / (r.Max - r.Min)
|
v := value / (r.max - r.min)
|
||||||
// 移至居中
|
// 移至居中
|
||||||
if r.Boundary &&
|
if r.boundary &&
|
||||||
r.divideCount != 0 {
|
r.divideCount != 0 {
|
||||||
v += 1 / float64(r.divideCount*2)
|
v += 1 / float64(r.divideCount*2)
|
||||||
}
|
}
|
||||||
return int(v * float64(r.Size))
|
return int(v * float64(r.size))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRange(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
r := NewRange(0, 8, 6)
|
|
||||||
assert.Equal(0.0, r.Min)
|
|
||||||
assert.Equal(12.0, r.Max)
|
|
||||||
|
|
||||||
r = NewRange(0, 12, 6)
|
|
||||||
assert.Equal(0.0, r.Min)
|
|
||||||
assert.Equal(24.0, r.Max)
|
|
||||||
|
|
||||||
r = NewRange(-13, 18, 6)
|
|
||||||
assert.Equal(-20.0, r.Min)
|
|
||||||
assert.Equal(40.0, r.Max)
|
|
||||||
|
|
||||||
r = NewRange(0, 150, 6)
|
|
||||||
assert.Equal(0.0, r.Min)
|
|
||||||
assert.Equal(180.0, r.Max)
|
|
||||||
|
|
||||||
r = NewRange(0, 400, 6)
|
|
||||||
assert.Equal(0.0, r.Min)
|
|
||||||
assert.Equal(480.0, r.Max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRangeHeightWidth(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
r := NewRange(0, 8, 6)
|
|
||||||
r.Size = 100
|
|
||||||
|
|
||||||
assert.Equal(33, r.getHeight(4))
|
|
||||||
assert.Equal(67, r.getRestHeight(4))
|
|
||||||
|
|
||||||
assert.Equal(33, r.getWidth(4))
|
|
||||||
r.Boundary = true
|
|
||||||
assert.Equal(41, r.getWidth(4))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRangeGetRange(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
r := NewRange(0, 8, 6)
|
|
||||||
r.Size = 120
|
|
||||||
|
|
||||||
f1, f2 := r.GetRange(0)
|
|
||||||
assert.Equal(0.0, f1)
|
|
||||||
assert.Equal(20.0, f2)
|
|
||||||
|
|
||||||
f1, f2 = r.GetRange(2)
|
|
||||||
assert.Equal(40.0, f1)
|
|
||||||
assert.Equal(60.0, f2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRangeAutoDivide(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
r := Range{
|
|
||||||
Size: 120,
|
|
||||||
divideCount: 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal([]int{0, 20, 40, 60, 80, 100, 120}, r.AutoDivide())
|
|
||||||
|
|
||||||
r.Size = 130
|
|
||||||
assert.Equal([]int{0, 22, 44, 66, 88, 109, 130}, r.AutoDivide())
|
|
||||||
}
|
|
||||||
66
series.go
66
series.go
|
|
@ -19,7 +19,6 @@
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
// 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
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -28,14 +27,21 @@ import (
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SeriesData struct {
|
type SeriesData struct {
|
||||||
// The value of series data
|
// The value of series data
|
||||||
Value float64
|
Value float64
|
||||||
// The style of series data
|
// The style of series data
|
||||||
Style chart.Style
|
Style Style
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList {
|
||||||
|
seriesList := make(SeriesList, len(values))
|
||||||
|
for index, value := range values {
|
||||||
|
seriesList[index] = NewSeriesFromValues(value, chartType...)
|
||||||
|
}
|
||||||
|
return seriesList
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSeriesFromValues(values []float64, chartType ...string) Series {
|
func NewSeriesFromValues(values []float64, chartType ...string) Series {
|
||||||
|
|
@ -65,7 +71,7 @@ type SeriesLabel struct {
|
||||||
// {d}: the percent of a data item(pie chart).
|
// {d}: the percent of a data item(pie chart).
|
||||||
Formatter string
|
Formatter string
|
||||||
// The color for label
|
// The color for label
|
||||||
Color drawing.Color
|
Color Color
|
||||||
// Show flag for label
|
// Show flag for label
|
||||||
Show bool
|
Show bool
|
||||||
// Distance to the host graphic element.
|
// Distance to the host graphic element.
|
||||||
|
|
@ -101,8 +107,8 @@ type Series struct {
|
||||||
// The data list of series
|
// The data list of series
|
||||||
Data []SeriesData
|
Data []SeriesData
|
||||||
// The Y axis index, it should be 0 or 1.
|
// The Y axis index, it should be 0 or 1.
|
||||||
// Default value is 1
|
// Default value is 0
|
||||||
YAxisIndex int
|
AxisIndex int
|
||||||
// The style for series
|
// The style for series
|
||||||
Style chart.Style
|
Style chart.Style
|
||||||
// The label for series
|
// The label for series
|
||||||
|
|
@ -122,6 +128,54 @@ type Series struct {
|
||||||
}
|
}
|
||||||
type SeriesList []Series
|
type SeriesList []Series
|
||||||
|
|
||||||
|
func (sl SeriesList) init() {
|
||||||
|
if sl[len(sl)-1].index != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < len(sl); i++ {
|
||||||
|
if sl[i].Type == "" {
|
||||||
|
sl[i].Type = ChartTypeLine
|
||||||
|
}
|
||||||
|
sl[i].index = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl SeriesList) reverse() {
|
||||||
|
for i, j := 0, len(sl)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
sl[i], sl[j] = sl[j], sl[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl SeriesList) Filter(chartType string) SeriesList {
|
||||||
|
arr := make(SeriesList, 0)
|
||||||
|
for index, item := range sl {
|
||||||
|
if item.Type == chartType {
|
||||||
|
arr = append(arr, sl[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxMin get max and min value of series list
|
||||||
|
func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) {
|
||||||
|
min := math.MaxFloat64
|
||||||
|
max := -math.MaxFloat64
|
||||||
|
for _, series := range sl {
|
||||||
|
if series.AxisIndex != axisIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, item := range series.Data {
|
||||||
|
if item.Value > max {
|
||||||
|
max = item.Value
|
||||||
|
}
|
||||||
|
if item.Value < min {
|
||||||
|
min = item.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max, min
|
||||||
|
}
|
||||||
|
|
||||||
type PieSeriesOption struct {
|
type PieSeriesOption struct {
|
||||||
Radius string
|
Radius string
|
||||||
Label SeriesLabel
|
Label SeriesLabel
|
||||||
|
|
|
||||||
166
series_test.go
166
series_test.go
|
|
@ -1,166 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewSeriesFromValues(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal(Series{
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Type: ChartTypeBar,
|
|
||||||
}, NewSeriesFromValues([]float64{
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
}, ChartTypeBar))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSeriesDataFromValues(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal([]SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
}, NewSeriesDataFromValues([]float64{
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPieSeriesList(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal(SeriesList{
|
|
||||||
{
|
|
||||||
Type: ChartTypePie,
|
|
||||||
Name: "a",
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
},
|
|
||||||
Radius: "30%",
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: ChartTypePie,
|
|
||||||
Name: "b",
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
},
|
|
||||||
Radius: "30%",
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, NewPieSeriesList([]float64{
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
}, PieSeriesOption{
|
|
||||||
Radius: "30%",
|
|
||||||
Label: SeriesLabel{
|
|
||||||
Show: true,
|
|
||||||
},
|
|
||||||
Names: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeriesSummary(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
s := Series{
|
|
||||||
Data: NewSeriesDataFromValues([]float64{
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
5,
|
|
||||||
7,
|
|
||||||
9,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
assert.Equal(seriesSummary{
|
|
||||||
MaxIndex: 4,
|
|
||||||
MaxValue: 9,
|
|
||||||
MinIndex: 0,
|
|
||||||
MinValue: 1,
|
|
||||||
AverageValue: 5,
|
|
||||||
}, s.Summary())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetSeriesNames(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
sl := SeriesList{
|
|
||||||
{
|
|
||||||
Name: "a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "b",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal([]string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}, sl.Names())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPieLabelFormatter(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
fn := NewPieLabelFormatter([]string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}, "")
|
|
||||||
assert.Equal("a: 35%", fn(0, 1.2, 0.35))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewValueLabelFormater(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
fn := NewValueLabelFormater([]string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}, "")
|
|
||||||
assert.Equal("1.2", fn(0, 1.2, 0.35))
|
|
||||||
}
|
|
||||||
145
table.go
145
table.go
|
|
@ -1,145 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TableOption struct {
|
|
||||||
// draw
|
|
||||||
Draw *Draw
|
|
||||||
// The width of table
|
|
||||||
Width int
|
|
||||||
// The header of table
|
|
||||||
Header []string
|
|
||||||
// The style of table
|
|
||||||
Style chart.Style
|
|
||||||
ColumnWidths []float64
|
|
||||||
// 是否仅测量高度
|
|
||||||
measurement bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrTableColumnWidthInvalid = errors.New("table column width is invalid")
|
|
||||||
|
|
||||||
func tableDivideWidth(width, size int, columnWidths []float64) ([]int, error) {
|
|
||||||
widths := make([]int, size)
|
|
||||||
|
|
||||||
autoFillCount := size
|
|
||||||
restWidth := width
|
|
||||||
if len(columnWidths) != 0 {
|
|
||||||
for index, v := range columnWidths {
|
|
||||||
if v <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
autoFillCount--
|
|
||||||
// 小于1的表示占比
|
|
||||||
if v < 1 {
|
|
||||||
widths[index] = int(v * float64(width))
|
|
||||||
} else {
|
|
||||||
widths[index] = int(v)
|
|
||||||
}
|
|
||||||
restWidth -= widths[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if restWidth < 0 {
|
|
||||||
return nil, ErrTableColumnWidthInvalid
|
|
||||||
}
|
|
||||||
// 填充其它未指定的宽度
|
|
||||||
if autoFillCount > 0 {
|
|
||||||
autoWidth := restWidth / autoFillCount
|
|
||||||
for index, v := range widths {
|
|
||||||
if v == 0 {
|
|
||||||
widths[index] = autoWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
widthSum := 0
|
|
||||||
for _, v := range widths {
|
|
||||||
widthSum += v
|
|
||||||
}
|
|
||||||
if widthSum > width {
|
|
||||||
return nil, ErrTableColumnWidthInvalid
|
|
||||||
}
|
|
||||||
return widths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TableMeasure(opt TableOption) (chart.Box, error) {
|
|
||||||
d, err := NewDraw(DrawOption{
|
|
||||||
Width: opt.Width,
|
|
||||||
Height: 600,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return chart.BoxZero, err
|
|
||||||
}
|
|
||||||
opt.Draw = d
|
|
||||||
opt.measurement = true
|
|
||||||
return tableRender(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableRender(opt TableOption) (chart.Box, error) {
|
|
||||||
if opt.Draw == nil {
|
|
||||||
return chart.BoxZero, errors.New("draw can not be nil")
|
|
||||||
}
|
|
||||||
if len(opt.Header) == 0 {
|
|
||||||
return chart.BoxZero, errors.New("header can not be nil")
|
|
||||||
}
|
|
||||||
width := opt.Width
|
|
||||||
if width == 0 {
|
|
||||||
width = opt.Draw.Box.Width()
|
|
||||||
}
|
|
||||||
|
|
||||||
columnWidths, err := tableDivideWidth(width, len(opt.Header), opt.ColumnWidths)
|
|
||||||
if err != nil {
|
|
||||||
return chart.BoxZero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
d := opt.Draw
|
|
||||||
style := opt.Style
|
|
||||||
y := 0
|
|
||||||
x := 0
|
|
||||||
|
|
||||||
headerMaxHeight := 0
|
|
||||||
for index, text := range opt.Header {
|
|
||||||
var box chart.Box
|
|
||||||
w := columnWidths[index]
|
|
||||||
y0 := y + int(opt.Style.FontSize)
|
|
||||||
if opt.measurement {
|
|
||||||
box = d.measureTextFit(text, x, y0, w, style)
|
|
||||||
} else {
|
|
||||||
box = d.textFit(text, x, y0, w, style)
|
|
||||||
}
|
|
||||||
if box.Height() > headerMaxHeight {
|
|
||||||
headerMaxHeight = box.Height()
|
|
||||||
}
|
|
||||||
x += w
|
|
||||||
}
|
|
||||||
y += headerMaxHeight
|
|
||||||
|
|
||||||
return chart.Box{
|
|
||||||
Right: width,
|
|
||||||
Bottom: y,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
166
theme.go
166
theme.go
|
|
@ -23,6 +23,8 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -31,23 +33,45 @@ const ThemeLight = "light"
|
||||||
const ThemeGrafana = "grafana"
|
const ThemeGrafana = "grafana"
|
||||||
const ThemeAnt = "ant"
|
const ThemeAnt = "ant"
|
||||||
|
|
||||||
type Theme struct {
|
type ColorPalette interface {
|
||||||
palette *themeColorPalette
|
IsDark() bool
|
||||||
|
GetAxisStrokeColor() Color
|
||||||
|
GetAxisSplitLineColor() Color
|
||||||
|
GetSeriesColor(int) Color
|
||||||
|
GetBackgroundColor() Color
|
||||||
|
GetTextColor() Color
|
||||||
|
GetFontSize() float64
|
||||||
|
GetFont() *truetype.Font
|
||||||
}
|
}
|
||||||
|
|
||||||
type themeColorPalette struct {
|
type themeColorPalette struct {
|
||||||
isDarkMode bool
|
isDarkMode bool
|
||||||
axisStrokeColor drawing.Color
|
axisStrokeColor Color
|
||||||
axisSplitLineColor drawing.Color
|
axisSplitLineColor Color
|
||||||
backgroundColor drawing.Color
|
backgroundColor Color
|
||||||
textColor drawing.Color
|
textColor Color
|
||||||
seriesColors []drawing.Color
|
seriesColors []Color
|
||||||
|
fontSize float64
|
||||||
|
font *truetype.Font
|
||||||
}
|
}
|
||||||
|
|
||||||
var palettes = map[string]*themeColorPalette{}
|
type ThemeOption struct {
|
||||||
|
IsDarkMode bool
|
||||||
|
AxisStrokeColor Color
|
||||||
|
AxisSplitLineColor Color
|
||||||
|
BackgroundColor Color
|
||||||
|
TextColor Color
|
||||||
|
SeriesColors []Color
|
||||||
|
}
|
||||||
|
|
||||||
|
var palettes = map[string]ColorPalette{}
|
||||||
|
|
||||||
|
const defaultFontSize = 12.0
|
||||||
|
|
||||||
|
var defaultTheme ColorPalette
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
echartSeriesColors := []drawing.Color{
|
echartSeriesColors := []Color{
|
||||||
parseColor("#5470c6"),
|
parseColor("#5470c6"),
|
||||||
parseColor("#91cc75"),
|
parseColor("#91cc75"),
|
||||||
parseColor("#fac858"),
|
parseColor("#fac858"),
|
||||||
|
|
@ -58,7 +82,7 @@ func init() {
|
||||||
parseColor("#9a60b4"),
|
parseColor("#9a60b4"),
|
||||||
parseColor("#ea7ccc"),
|
parseColor("#ea7ccc"),
|
||||||
}
|
}
|
||||||
grafanaSeriesColors := []drawing.Color{
|
grafanaSeriesColors := []Color{
|
||||||
parseColor("#7EB26D"),
|
parseColor("#7EB26D"),
|
||||||
parseColor("#EAB839"),
|
parseColor("#EAB839"),
|
||||||
parseColor("#6ED0E0"),
|
parseColor("#6ED0E0"),
|
||||||
|
|
@ -68,7 +92,7 @@ func init() {
|
||||||
parseColor("#705DA0"),
|
parseColor("#705DA0"),
|
||||||
parseColor("#508642"),
|
parseColor("#508642"),
|
||||||
}
|
}
|
||||||
antSeriesColors := []drawing.Color{
|
antSeriesColors := []Color{
|
||||||
parseColor("#5b8ff9"),
|
parseColor("#5b8ff9"),
|
||||||
parseColor("#5ad8a6"),
|
parseColor("#5ad8a6"),
|
||||||
parseColor("#5d7092"),
|
parseColor("#5d7092"),
|
||||||
|
|
@ -80,155 +104,181 @@ func init() {
|
||||||
}
|
}
|
||||||
AddTheme(
|
AddTheme(
|
||||||
ThemeDark,
|
ThemeDark,
|
||||||
true,
|
ThemeOption{
|
||||||
drawing.Color{
|
IsDarkMode: true,
|
||||||
|
AxisStrokeColor: Color{
|
||||||
R: 185,
|
R: 185,
|
||||||
G: 184,
|
G: 184,
|
||||||
B: 206,
|
B: 206,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
AxisSplitLineColor: Color{
|
||||||
R: 72,
|
R: 72,
|
||||||
G: 71,
|
G: 71,
|
||||||
B: 83,
|
B: 83,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
BackgroundColor: Color{
|
||||||
R: 16,
|
R: 16,
|
||||||
G: 12,
|
G: 12,
|
||||||
B: 42,
|
B: 42,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
TextColor: Color{
|
||||||
R: 238,
|
R: 238,
|
||||||
G: 238,
|
G: 238,
|
||||||
B: 238,
|
B: 238,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
echartSeriesColors,
|
SeriesColors: echartSeriesColors,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
AddTheme(
|
AddTheme(
|
||||||
ThemeLight,
|
ThemeLight,
|
||||||
false,
|
ThemeOption{
|
||||||
drawing.Color{
|
IsDarkMode: false,
|
||||||
|
AxisStrokeColor: Color{
|
||||||
R: 110,
|
R: 110,
|
||||||
G: 112,
|
G: 112,
|
||||||
B: 121,
|
B: 121,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
AxisSplitLineColor: Color{
|
||||||
R: 224,
|
R: 224,
|
||||||
G: 230,
|
G: 230,
|
||||||
B: 242,
|
B: 242,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.ColorWhite,
|
BackgroundColor: drawing.ColorWhite,
|
||||||
drawing.Color{
|
TextColor: Color{
|
||||||
R: 70,
|
R: 70,
|
||||||
G: 70,
|
G: 70,
|
||||||
B: 70,
|
B: 70,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
echartSeriesColors,
|
SeriesColors: echartSeriesColors,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
AddTheme(
|
AddTheme(
|
||||||
ThemeAnt,
|
ThemeAnt,
|
||||||
false,
|
ThemeOption{
|
||||||
drawing.Color{
|
IsDarkMode: false,
|
||||||
|
AxisStrokeColor: Color{
|
||||||
R: 110,
|
R: 110,
|
||||||
G: 112,
|
G: 112,
|
||||||
B: 121,
|
B: 121,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
AxisSplitLineColor: Color{
|
||||||
R: 224,
|
R: 224,
|
||||||
G: 230,
|
G: 230,
|
||||||
B: 242,
|
B: 242,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.ColorWhite,
|
BackgroundColor: drawing.ColorWhite,
|
||||||
drawing.Color{
|
TextColor: drawing.Color{
|
||||||
R: 70,
|
R: 70,
|
||||||
G: 70,
|
G: 70,
|
||||||
B: 70,
|
B: 70,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
antSeriesColors,
|
SeriesColors: antSeriesColors,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
AddTheme(
|
AddTheme(
|
||||||
ThemeGrafana,
|
ThemeGrafana,
|
||||||
true,
|
ThemeOption{
|
||||||
drawing.Color{
|
IsDarkMode: true,
|
||||||
|
AxisStrokeColor: Color{
|
||||||
R: 185,
|
R: 185,
|
||||||
G: 184,
|
G: 184,
|
||||||
B: 206,
|
B: 206,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
AxisSplitLineColor: Color{
|
||||||
R: 68,
|
R: 68,
|
||||||
G: 67,
|
G: 67,
|
||||||
B: 67,
|
B: 67,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
BackgroundColor: drawing.Color{
|
||||||
R: 31,
|
R: 31,
|
||||||
G: 29,
|
G: 29,
|
||||||
B: 29,
|
B: 29,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
drawing.Color{
|
TextColor: Color{
|
||||||
R: 216,
|
R: 216,
|
||||||
G: 217,
|
G: 217,
|
||||||
B: 218,
|
B: 218,
|
||||||
A: 255,
|
A: 255,
|
||||||
},
|
},
|
||||||
grafanaSeriesColors,
|
SeriesColors: grafanaSeriesColors,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
SetDefaultTheme(ThemeLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddTheme(name string, isDarkMode bool, axisStrokeColor, axisSplitLineColor, backgroundColor, textColor drawing.Color, seriesColors []drawing.Color) {
|
func SetDefaultTheme(name string) {
|
||||||
|
defaultTheme = NewTheme(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddTheme(name string, opt ThemeOption) {
|
||||||
palettes[name] = &themeColorPalette{
|
palettes[name] = &themeColorPalette{
|
||||||
isDarkMode: isDarkMode,
|
isDarkMode: opt.IsDarkMode,
|
||||||
axisStrokeColor: axisStrokeColor,
|
axisStrokeColor: opt.AxisStrokeColor,
|
||||||
axisSplitLineColor: axisSplitLineColor,
|
axisSplitLineColor: opt.AxisSplitLineColor,
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: opt.BackgroundColor,
|
||||||
textColor: textColor,
|
textColor: opt.TextColor,
|
||||||
seriesColors: seriesColors,
|
seriesColors: opt.SeriesColors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTheme(name string) *Theme {
|
func NewTheme(name string) ColorPalette {
|
||||||
p, ok := palettes[name]
|
p, ok := palettes[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
p = palettes[ThemeLight]
|
p = palettes[ThemeLight]
|
||||||
}
|
}
|
||||||
return &Theme{
|
return p
|
||||||
palette: p,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) IsDark() bool {
|
func (t *themeColorPalette) IsDark() bool {
|
||||||
return t.palette.isDarkMode
|
return t.isDarkMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) GetAxisStrokeColor() drawing.Color {
|
func (t *themeColorPalette) GetAxisStrokeColor() Color {
|
||||||
return t.palette.axisStrokeColor
|
return t.axisStrokeColor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) GetAxisSplitLineColor() drawing.Color {
|
func (t *themeColorPalette) GetAxisSplitLineColor() Color {
|
||||||
return t.palette.axisSplitLineColor
|
return t.axisSplitLineColor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) GetSeriesColor(index int) drawing.Color {
|
func (t *themeColorPalette) GetSeriesColor(index int) Color {
|
||||||
colors := t.palette.seriesColors
|
colors := t.seriesColors
|
||||||
return colors[index%len(colors)]
|
return colors[index%len(colors)]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) GetBackgroundColor() drawing.Color {
|
func (t *themeColorPalette) GetBackgroundColor() Color {
|
||||||
return t.palette.backgroundColor
|
return t.backgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Theme) GetTextColor() drawing.Color {
|
func (t *themeColorPalette) GetTextColor() Color {
|
||||||
return t.palette.textColor
|
return t.textColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *themeColorPalette) GetFontSize() float64 {
|
||||||
|
if t.fontSize != 0 {
|
||||||
|
return t.fontSize
|
||||||
|
}
|
||||||
|
return defaultFontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *themeColorPalette) GetFont() *truetype.Font {
|
||||||
|
if t.font != nil {
|
||||||
|
return t.font
|
||||||
|
}
|
||||||
|
f, _ := chart.GetDefaultFont()
|
||||||
|
return f
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTheme(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
darkTheme := NewTheme(ThemeDark)
|
|
||||||
lightTheme := NewTheme(ThemeLight)
|
|
||||||
|
|
||||||
assert.True(darkTheme.IsDark())
|
|
||||||
assert.False(lightTheme.IsDark())
|
|
||||||
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 185,
|
|
||||||
G: 184,
|
|
||||||
B: 206,
|
|
||||||
A: 255,
|
|
||||||
}, darkTheme.GetAxisStrokeColor())
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 110,
|
|
||||||
G: 112,
|
|
||||||
B: 121,
|
|
||||||
A: 255,
|
|
||||||
}, lightTheme.GetAxisStrokeColor())
|
|
||||||
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 72,
|
|
||||||
G: 71,
|
|
||||||
B: 83,
|
|
||||||
A: 255,
|
|
||||||
}, darkTheme.GetAxisSplitLineColor())
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 224,
|
|
||||||
G: 230,
|
|
||||||
B: 242,
|
|
||||||
A: 255,
|
|
||||||
}, lightTheme.GetAxisSplitLineColor())
|
|
||||||
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 16,
|
|
||||||
G: 12,
|
|
||||||
B: 42,
|
|
||||||
A: 255,
|
|
||||||
}, darkTheme.GetBackgroundColor())
|
|
||||||
assert.Equal(drawing.ColorWhite, lightTheme.GetBackgroundColor())
|
|
||||||
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 238,
|
|
||||||
G: 238,
|
|
||||||
B: 238,
|
|
||||||
A: 255,
|
|
||||||
}, darkTheme.GetTextColor())
|
|
||||||
assert.Equal(drawing.Color{
|
|
||||||
R: 70,
|
|
||||||
G: 70,
|
|
||||||
B: 70,
|
|
||||||
A: 255,
|
|
||||||
}, lightTheme.GetTextColor())
|
|
||||||
}
|
|
||||||
112
title.go
112
title.go
|
|
@ -26,18 +26,20 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/golang/freetype/truetype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TitleOption struct {
|
type TitleOption struct {
|
||||||
|
// The theme of chart
|
||||||
|
Theme ColorPalette
|
||||||
// Title text, support \n for new line
|
// Title text, support \n for new line
|
||||||
Text string
|
Text string
|
||||||
// Subtitle text, support \n for new line
|
// Subtitle text, support \n for new line
|
||||||
Subtext string
|
Subtext string
|
||||||
// Title style
|
// // Title style
|
||||||
Style chart.Style
|
// Style Style
|
||||||
// Subtitle style
|
// // Subtitle style
|
||||||
SubtextStyle chart.Style
|
// SubtextStyle Style
|
||||||
// Distance between title component and the left side of the container.
|
// Distance between title component and the left side of the container.
|
||||||
// It can be pixel value: 20, percentage value: 20%,
|
// It can be pixel value: 20, percentage value: 20%,
|
||||||
// or position value: right, center.
|
// or position value: right, center.
|
||||||
|
|
@ -45,12 +47,23 @@ type TitleOption struct {
|
||||||
// Distance between title component and the top side of the container.
|
// Distance between title component and the top side of the container.
|
||||||
// It can be pixel value: 20.
|
// It can be pixel value: 20.
|
||||||
Top string
|
Top string
|
||||||
|
// The font of label
|
||||||
|
Font *truetype.Font
|
||||||
|
// The font size of label
|
||||||
|
FontSize float64
|
||||||
|
// The color of label
|
||||||
|
FontColor Color
|
||||||
|
// The subtext font size of label
|
||||||
|
SubtextFontSize float64
|
||||||
|
// The subtext font color of label
|
||||||
|
SubtextFontColor Color
|
||||||
}
|
}
|
||||||
|
|
||||||
type titleMeasureOption struct {
|
type titleMeasureOption struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
text string
|
text string
|
||||||
style chart.Style
|
style Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitTitleText(text string) []string {
|
func splitTitleText(text string) []string {
|
||||||
|
|
@ -66,44 +79,74 @@ func splitTitleText(text string) []string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) {
|
type titlePainter struct {
|
||||||
if len(opt.Text) == 0 {
|
p *Painter
|
||||||
return chart.BoxZero, nil
|
opt *TitleOption
|
||||||
}
|
}
|
||||||
|
|
||||||
padding := opt.Style.Padding
|
func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter {
|
||||||
d, err := NewDraw(DrawOption{
|
return &titlePainter{
|
||||||
Parent: p,
|
p: p,
|
||||||
}, PaddingOption(padding))
|
opt: &opt,
|
||||||
if err != nil {
|
|
||||||
return chart.BoxZero, err
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r := d.Render
|
func (t *titlePainter) Render() (Box, error) {
|
||||||
|
opt := t.opt
|
||||||
|
p := t.p
|
||||||
|
theme := opt.Theme
|
||||||
|
|
||||||
|
if opt.Text == "" && opt.Subtext == "" {
|
||||||
|
return BoxZero, nil
|
||||||
|
}
|
||||||
|
|
||||||
measureOptions := make([]titleMeasureOption, 0)
|
measureOptions := make([]titleMeasureOption, 0)
|
||||||
|
|
||||||
|
if opt.Font == nil {
|
||||||
|
opt.Font = theme.GetFont()
|
||||||
|
}
|
||||||
|
if opt.FontColor.IsZero() {
|
||||||
|
opt.FontColor = theme.GetTextColor()
|
||||||
|
}
|
||||||
|
if opt.FontSize == 0 {
|
||||||
|
opt.FontSize = theme.GetFontSize()
|
||||||
|
}
|
||||||
|
if opt.SubtextFontColor.IsZero() {
|
||||||
|
opt.SubtextFontColor = opt.FontColor
|
||||||
|
}
|
||||||
|
if opt.SubtextFontSize == 0 {
|
||||||
|
opt.SubtextFontSize = opt.FontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
titleTextStyle := Style{
|
||||||
|
Font: opt.Font,
|
||||||
|
FontSize: opt.FontSize,
|
||||||
|
FontColor: opt.FontColor,
|
||||||
|
}
|
||||||
// 主标题
|
// 主标题
|
||||||
for _, v := range splitTitleText(opt.Text) {
|
for _, v := range splitTitleText(opt.Text) {
|
||||||
measureOptions = append(measureOptions, titleMeasureOption{
|
measureOptions = append(measureOptions, titleMeasureOption{
|
||||||
text: v,
|
text: v,
|
||||||
style: opt.Style.GetTextOptions(),
|
style: titleTextStyle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
subtextStyle := Style{
|
||||||
|
Font: opt.Font,
|
||||||
|
FontSize: opt.SubtextFontSize,
|
||||||
|
FontColor: opt.SubtextFontColor,
|
||||||
|
}
|
||||||
// 副标题
|
// 副标题
|
||||||
for _, v := range splitTitleText(opt.Subtext) {
|
for _, v := range splitTitleText(opt.Subtext) {
|
||||||
measureOptions = append(measureOptions, titleMeasureOption{
|
measureOptions = append(measureOptions, titleMeasureOption{
|
||||||
text: v,
|
text: v,
|
||||||
style: opt.SubtextStyle.GetTextOptions(),
|
style: subtextStyle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
textMaxWidth := 0
|
textMaxWidth := 0
|
||||||
textMaxHeight := 0
|
textMaxHeight := 0
|
||||||
width := 0
|
|
||||||
for index, item := range measureOptions {
|
for index, item := range measureOptions {
|
||||||
item.style.WriteTextOptionsToRenderer(r)
|
p.OverrideTextStyle(item.style)
|
||||||
textBox := r.MeasureText(item.text)
|
textBox := p.MeasureText(item.text)
|
||||||
|
|
||||||
w := textBox.Width()
|
w := textBox.Width()
|
||||||
h := textBox.Height()
|
h := textBox.Height()
|
||||||
|
|
@ -116,18 +159,18 @@ func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) {
|
||||||
measureOptions[index].height = h
|
measureOptions[index].height = h
|
||||||
measureOptions[index].width = w
|
measureOptions[index].width = w
|
||||||
}
|
}
|
||||||
width = textMaxWidth
|
width := textMaxWidth
|
||||||
|
|
||||||
titleX := 0
|
titleX := 0
|
||||||
b := d.Box
|
|
||||||
switch opt.Left {
|
switch opt.Left {
|
||||||
case PositionRight:
|
case PositionRight:
|
||||||
titleX = b.Width() - textMaxWidth
|
titleX = p.Width() - textMaxWidth
|
||||||
case PositionCenter:
|
case PositionCenter:
|
||||||
titleX = b.Width()>>1 - (textMaxWidth >> 1)
|
titleX = p.Width()>>1 - (textMaxWidth >> 1)
|
||||||
default:
|
default:
|
||||||
if strings.HasSuffix(opt.Left, "%") {
|
if strings.HasSuffix(opt.Left, "%") {
|
||||||
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
|
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
|
||||||
titleX = b.Width() * value / 100
|
titleX = p.Width() * value / 100
|
||||||
} else {
|
} else {
|
||||||
value, _ := strconv.Atoi(opt.Left)
|
value, _ := strconv.Atoi(opt.Left)
|
||||||
titleX = value
|
titleX = value
|
||||||
|
|
@ -140,16 +183,15 @@ func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) {
|
||||||
titleY += value
|
titleY += value
|
||||||
}
|
}
|
||||||
for _, item := range measureOptions {
|
for _, item := range measureOptions {
|
||||||
item.style.WriteTextOptionsToRenderer(r)
|
p.OverrideTextStyle(item.style)
|
||||||
x := titleX + (textMaxWidth-item.width)>>1
|
x := titleX + (textMaxWidth-item.width)>>1
|
||||||
y := titleY + item.height
|
y := titleY + item.height
|
||||||
d.text(item.text, x, y)
|
p.Text(item.text, x, y)
|
||||||
titleY += item.height
|
titleY += item.height
|
||||||
}
|
}
|
||||||
height := titleY + padding.Top + padding.Bottom
|
|
||||||
box := padding.Clone()
|
|
||||||
box.Right = box.Left + titleX + width
|
|
||||||
box.Bottom = box.Top + height
|
|
||||||
|
|
||||||
return box, nil
|
return Box{
|
||||||
|
Bottom: titleY,
|
||||||
|
Right: titleX + width,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
142
title_test.go
142
title_test.go
|
|
@ -1,142 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSplitTitleText(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal([]string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}, splitTitleText("a\nb"))
|
|
||||||
assert.Equal([]string{
|
|
||||||
"a",
|
|
||||||
}, splitTitleText("a\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrawTitle(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
newOption := func() *TitleOption {
|
|
||||||
f, _ := chart.GetDefaultFont()
|
|
||||||
return &TitleOption{
|
|
||||||
Text: "title\nHello",
|
|
||||||
Subtext: "subtitle\nWorld!",
|
|
||||||
Style: chart.Style{
|
|
||||||
FontSize: 14,
|
|
||||||
Font: f,
|
|
||||||
FontColor: drawing.ColorBlack,
|
|
||||||
},
|
|
||||||
SubtextStyle: chart.Style{
|
|
||||||
FontSize: 10,
|
|
||||||
Font: f,
|
|
||||||
FontColor: drawing.ColorBlue,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newDraw := func() *Draw {
|
|
||||||
d, _ := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
newDraw func() *Draw
|
|
||||||
newOption func() *TitleOption
|
|
||||||
result string
|
|
||||||
box chart.Box
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: newOption,
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"6\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"0\" y=\"34\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">Hello</text><text x=\"0\" y=\"46\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">subtitle</text><text x=\"3\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">World!</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 43,
|
|
||||||
Bottom: 58,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *TitleOption {
|
|
||||||
opt := newOption()
|
|
||||||
opt.Left = PositionRight
|
|
||||||
opt.Top = "50"
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"363\" y=\"67\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"357\" y=\"84\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">Hello</text><text x=\"357\" y=\"96\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">subtitle</text><text x=\"360\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">World!</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 400,
|
|
||||||
Bottom: 108,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *TitleOption {
|
|
||||||
opt := newOption()
|
|
||||||
opt.Left = PositionCenter
|
|
||||||
opt.Top = "10"
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"185\" y=\"27\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"179\" y=\"44\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">Hello</text><text x=\"179\" y=\"56\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">subtitle</text><text x=\"182\" y=\"68\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">World!</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 222,
|
|
||||||
Bottom: 68,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *TitleOption {
|
|
||||||
opt := newOption()
|
|
||||||
opt.Left = "10%"
|
|
||||||
opt.Top = "10"
|
|
||||||
return opt
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"46\" y=\"27\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">title</text><text x=\"40\" y=\"44\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:17.9px;font-family:'Roboto Medium',sans-serif\">Hello</text><text x=\"40\" y=\"56\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">subtitle</text><text x=\"43\" y=\"68\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,255,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">World!</text></svg>",
|
|
||||||
box: chart.Box{
|
|
||||||
Right: 83,
|
|
||||||
Bottom: 68,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
d := tt.newDraw()
|
|
||||||
o := tt.newOption()
|
|
||||||
b, err := drawTitle(d, o)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(tt.box, b)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.NotEmpty(data)
|
|
||||||
assert.Equal(tt.result, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
util.go
41
util.go
|
|
@ -43,6 +43,24 @@ func FalseFlag() *bool {
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsInt(values []int, value int) bool {
|
||||||
|
for _, v := range values {
|
||||||
|
if v == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(values []string, value string) bool {
|
||||||
|
for _, v := range values {
|
||||||
|
if v == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func ceilFloatToInt(value float64) int {
|
func ceilFloatToInt(value float64) int {
|
||||||
i := int(value)
|
i := int(value)
|
||||||
if value == float64(i) {
|
if value == float64(i) {
|
||||||
|
|
@ -59,28 +77,25 @@ func getDefaultInt(value, defaultValue int) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoDivide(max, size int) []int {
|
func autoDivide(max, size int) []int {
|
||||||
unit := max / size
|
unit := float64(max) / float64(size)
|
||||||
|
|
||||||
rest := max - unit*size
|
|
||||||
values := make([]int, size+1)
|
values := make([]int, size+1)
|
||||||
value := 0
|
for i := 0; i < size+1; i++ {
|
||||||
for i := 0; i < size; i++ {
|
if i == size {
|
||||||
values[i] = value
|
values[i] = max
|
||||||
if i < rest {
|
} else {
|
||||||
value++
|
values[i] = int(float64(i) * unit)
|
||||||
}
|
}
|
||||||
value += unit
|
|
||||||
}
|
}
|
||||||
values[size] = max
|
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
|
// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
|
||||||
func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) {
|
func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) {
|
||||||
maxWidth := 0
|
maxWidth := 0
|
||||||
maxHeight := 0
|
maxHeight := 0
|
||||||
for _, text := range textList {
|
for _, text := range textList {
|
||||||
box := r.MeasureText(text)
|
box := p.MeasureText(text)
|
||||||
maxWidth = chart.MaxInt(maxWidth, box.Width())
|
maxWidth = chart.MaxInt(maxWidth, box.Width())
|
||||||
maxHeight = chart.MaxInt(maxHeight, box.Height())
|
maxHeight = chart.MaxInt(maxHeight, box.Height())
|
||||||
}
|
}
|
||||||
|
|
@ -134,8 +149,8 @@ func commafWithDigits(value float64) string {
|
||||||
return humanize.CommafWithDigits(value, decimals)
|
return humanize.CommafWithDigits(value, decimals)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseColor(color string) drawing.Color {
|
func parseColor(color string) Color {
|
||||||
c := drawing.Color{}
|
c := Color{}
|
||||||
if color == "" {
|
if color == "" {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
util_test.go
22
util_test.go
|
|
@ -60,12 +60,12 @@ func TestAutoDivide(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal([]int{
|
assert.Equal([]int{
|
||||||
0,
|
0,
|
||||||
86,
|
85,
|
||||||
172,
|
171,
|
||||||
258,
|
257,
|
||||||
344,
|
342,
|
||||||
430,
|
428,
|
||||||
515,
|
514,
|
||||||
600,
|
600,
|
||||||
}, autoDivide(600, 7))
|
}, autoDivide(600, 7))
|
||||||
}
|
}
|
||||||
|
|
@ -80,13 +80,15 @@ func TestGetRadius(t *testing.T) {
|
||||||
|
|
||||||
func TestMeasureTextMaxWidthHeight(t *testing.T) {
|
func TestMeasureTextMaxWidthHeight(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
r, err := chart.SVG(400, 300)
|
p, err := NewPainter(PainterOptions{
|
||||||
|
Width: 400,
|
||||||
|
Height: 300,
|
||||||
|
})
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
style := chart.Style{
|
style := chart.Style{
|
||||||
FontSize: 10,
|
FontSize: 10,
|
||||||
}
|
}
|
||||||
style.Font, _ = chart.GetDefaultFont()
|
p.SetStyle(style)
|
||||||
style.WriteToRenderer(r)
|
|
||||||
|
|
||||||
maxWidth, maxHeight := measureTextMaxWidthHeight([]string{
|
maxWidth, maxHeight := measureTextMaxWidthHeight([]string{
|
||||||
"Mon",
|
"Mon",
|
||||||
|
|
@ -96,7 +98,7 @@ func TestMeasureTextMaxWidthHeight(t *testing.T) {
|
||||||
"Fri",
|
"Fri",
|
||||||
"Sat",
|
"Sat",
|
||||||
"Sun",
|
"Sun",
|
||||||
}, r)
|
}, p)
|
||||||
assert.Equal(26, maxWidth)
|
assert.Equal(26, maxWidth)
|
||||||
assert.Equal(12, maxHeight)
|
assert.Equal(12, maxHeight)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
xaxis.go
84
xaxis.go
|
|
@ -24,10 +24,10 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type XAxisOption struct {
|
type XAxisOption struct {
|
||||||
|
// The font of x axis
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
// The boundary gap on both sides of a coordinate axis.
|
// The boundary gap on both sides of a coordinate axis.
|
||||||
// Nil or *true means the center part of two axis ticks
|
// Nil or *true means the center part of two axis ticks
|
||||||
|
|
@ -35,13 +35,24 @@ type XAxisOption struct {
|
||||||
// The data value of x axis
|
// The data value of x axis
|
||||||
Data []string
|
Data []string
|
||||||
// The theme of chart
|
// The theme of chart
|
||||||
Theme string
|
Theme ColorPalette
|
||||||
// Hidden x axis
|
// The font size of x axis label
|
||||||
Hidden bool
|
FontSize float64
|
||||||
|
// The flag for show axis, set this to *false will hide axis
|
||||||
|
Show *bool
|
||||||
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
|
||||||
SplitNumber int
|
SplitNumber int
|
||||||
|
// The position of axis, it can be 'top' or 'bottom'
|
||||||
|
Position string
|
||||||
|
// The line color of axis
|
||||||
|
StrokeColor Color
|
||||||
|
// The color of label
|
||||||
|
FontColor Color
|
||||||
|
isValueAxis bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultXAxisHeight = 30
|
||||||
|
|
||||||
func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
|
func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
|
||||||
opt := XAxisOption{
|
opt := XAxisOption{
|
||||||
Data: data,
|
Data: data,
|
||||||
|
|
@ -52,51 +63,32 @@ func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption {
|
||||||
return opt
|
return opt
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawXAxis draws x axis, and returns the height, range of if.
|
func (opt *XAxisOption) ToAxisOption() AxisOption {
|
||||||
func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) {
|
position := PositionBottom
|
||||||
if opt.Hidden {
|
if opt.Position == PositionTop {
|
||||||
return 0, nil, nil
|
position = PositionTop
|
||||||
}
|
}
|
||||||
left := YAxisWidth
|
axisOpt := AxisOption{
|
||||||
right := (yAxisCount - 1) * YAxisWidth
|
Theme: opt.Theme,
|
||||||
dXAxis, err := NewDraw(
|
Data: opt.Data,
|
||||||
DrawOption{
|
|
||||||
Parent: p,
|
|
||||||
},
|
|
||||||
PaddingOption(chart.Box{
|
|
||||||
Left: left,
|
|
||||||
Right: right,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if opt.Font != nil {
|
|
||||||
dXAxis.Font = opt.Font
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, err
|
|
||||||
}
|
|
||||||
theme := NewTheme(opt.Theme)
|
|
||||||
data := NewAxisDataListFromStringList(opt.Data)
|
|
||||||
style := AxisOption{
|
|
||||||
BoundaryGap: opt.BoundaryGap,
|
BoundaryGap: opt.BoundaryGap,
|
||||||
StrokeColor: theme.GetAxisStrokeColor(),
|
Position: position,
|
||||||
FontColor: theme.GetAxisStrokeColor(),
|
|
||||||
StrokeWidth: 1,
|
|
||||||
SplitNumber: opt.SplitNumber,
|
SplitNumber: opt.SplitNumber,
|
||||||
|
StrokeColor: opt.StrokeColor,
|
||||||
|
FontSize: opt.FontSize,
|
||||||
|
Font: opt.Font,
|
||||||
|
FontColor: opt.FontColor,
|
||||||
|
Show: opt.Show,
|
||||||
|
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||||
}
|
}
|
||||||
|
if opt.isValueAxis {
|
||||||
boundary := true
|
axisOpt.SplitLineShow = true
|
||||||
max := float64(len(opt.Data))
|
axisOpt.StrokeWidth = -1
|
||||||
if isFalse(opt.BoundaryGap) {
|
axisOpt.BoundaryGap = FalseFlag()
|
||||||
boundary = false
|
|
||||||
max--
|
|
||||||
}
|
}
|
||||||
axis := NewAxis(dXAxis, data, style)
|
return axisOpt
|
||||||
axis.Render()
|
}
|
||||||
return axis.measure().Height, &Range{
|
|
||||||
divideCount: len(opt.Data),
|
func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter {
|
||||||
Min: 0,
|
return NewAxisPainter(p, opt.ToAxisOption())
|
||||||
Max: max,
|
|
||||||
Size: dXAxis.Box.Width(),
|
|
||||||
Boundary: boundary,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
xaxis_test.go
108
xaxis_test.go
|
|
@ -1,108 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewXAxisOption(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
opt := NewXAxisOption([]string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
}, FalseFlag())
|
|
||||||
|
|
||||||
assert.Equal(XAxisOption{
|
|
||||||
Data: []string{
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
},
|
|
||||||
BoundaryGap: FalseFlag(),
|
|
||||||
}, opt)
|
|
||||||
|
|
||||||
}
|
|
||||||
func TestDrawXAxis(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
newDraw := func() *Draw {
|
|
||||||
d, _ := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
newDraw func() *Draw
|
|
||||||
newOption func() *XAxisOption
|
|
||||||
result string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *XAxisOption {
|
|
||||||
return &XAxisOption{
|
|
||||||
BoundaryGap: FalseFlag(),
|
|
||||||
Data: []string{
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 40 275\nL 400 275\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 40 275\nL 40 280\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 400 275\nL 400 280\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"27\" y=\"292\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"389\" y=\"292\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text></svg>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *XAxisOption {
|
|
||||||
return &XAxisOption{
|
|
||||||
Data: []string{
|
|
||||||
"01-01",
|
|
||||||
"01-02",
|
|
||||||
"01-03",
|
|
||||||
"01-04",
|
|
||||||
"01-05",
|
|
||||||
"01-06",
|
|
||||||
"01-07",
|
|
||||||
"01-08",
|
|
||||||
"01-09",
|
|
||||||
},
|
|
||||||
SplitNumber: 3,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 40 275\nL 400 275\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 40 275\nL 40 280\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 160 275\nL 160 280\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 280 275\nL 280 280\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><path d=\"M 400 275\nL 400 280\" style=\"stroke-width:1;stroke:rgba(110,112,121,1.0);fill:none\"/><text x=\"83\" y=\"292\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-02</text><text x=\"203\" y=\"292\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-05</text><text x=\"323\" y=\"292\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">01-08</text></svg>",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
d := tt.newDraw()
|
|
||||||
height, _, err := drawXAxis(d, tt.newOption(), 1)
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(25, height)
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(tt.result, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
130
yaxis.go
130
yaxis.go
|
|
@ -22,84 +22,92 @@
|
||||||
|
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import "github.com/golang/freetype/truetype"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type YAxisOption struct {
|
type YAxisOption struct {
|
||||||
// The minimun value of axis.
|
// The minimun value of axis.
|
||||||
Min *float64
|
Min *float64
|
||||||
// The maximum value of axis.
|
// The maximum value of axis.
|
||||||
Max *float64
|
Max *float64
|
||||||
// Hidden y axis
|
// The font of y axis
|
||||||
Hidden bool
|
Font *truetype.Font
|
||||||
|
// The data value of x axis
|
||||||
|
Data []string
|
||||||
|
// The theme of chart
|
||||||
|
Theme ColorPalette
|
||||||
|
// The font size of x axis label
|
||||||
|
FontSize float64
|
||||||
|
// The position of axis, it can be 'left' or 'right'
|
||||||
|
Position string
|
||||||
|
// The color of label
|
||||||
|
FontColor Color
|
||||||
// Formatter for y axis text value
|
// Formatter for y axis text value
|
||||||
Formatter string
|
Formatter string
|
||||||
// Color for y axis
|
// Color for y axis
|
||||||
Color drawing.Color
|
Color Color
|
||||||
|
// The flag for show axis, set this to *false will hide axis
|
||||||
|
Show *bool
|
||||||
|
isCategoryAxis bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 长度是否可以变化
|
func NewYAxisOptions(data []string, others ...[]string) []YAxisOption {
|
||||||
const YAxisWidth = 40
|
arr := [][]string{
|
||||||
|
data,
|
||||||
func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) {
|
|
||||||
theme := NewTheme(opt.Theme)
|
|
||||||
yRange := opt.newYRange(axisIndex)
|
|
||||||
values := yRange.Values()
|
|
||||||
yAxis := opt.YAxisList[axisIndex]
|
|
||||||
formatter := yAxis.Formatter
|
|
||||||
if len(formatter) != 0 {
|
|
||||||
for index, text := range values {
|
|
||||||
values[index] = strings.ReplaceAll(formatter, "{value}", text)
|
|
||||||
}
|
}
|
||||||
|
arr = append(arr, others...)
|
||||||
|
opts := make([]YAxisOption, 0)
|
||||||
|
for _, data := range arr {
|
||||||
|
opts = append(opts, YAxisOption{
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
data := NewAxisDataListFromStringList(values)
|
func (opt *YAxisOption) ToAxisOption() AxisOption {
|
||||||
style := AxisOption{
|
position := PositionLeft
|
||||||
Position: PositionLeft,
|
if opt.Position == PositionRight {
|
||||||
|
position = PositionRight
|
||||||
|
}
|
||||||
|
axisOpt := AxisOption{
|
||||||
|
Formatter: opt.Formatter,
|
||||||
|
Theme: opt.Theme,
|
||||||
|
Data: opt.Data,
|
||||||
|
Position: position,
|
||||||
|
FontSize: opt.FontSize,
|
||||||
|
StrokeWidth: -1,
|
||||||
|
Font: opt.Font,
|
||||||
|
FontColor: opt.FontColor,
|
||||||
BoundaryGap: FalseFlag(),
|
BoundaryGap: FalseFlag(),
|
||||||
FontColor: theme.GetAxisStrokeColor(),
|
|
||||||
TickShow: FalseFlag(),
|
|
||||||
StrokeWidth: 1,
|
|
||||||
SplitLineColor: theme.GetAxisSplitLineColor(),
|
|
||||||
SplitLineShow: true,
|
SplitLineShow: true,
|
||||||
|
SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
|
||||||
|
Show: opt.Show,
|
||||||
}
|
}
|
||||||
if !yAxis.Color.IsZero() {
|
if !opt.Color.IsZero() {
|
||||||
style.FontColor = yAxis.Color
|
axisOpt.FontColor = opt.Color
|
||||||
style.StrokeColor = yAxis.Color
|
axisOpt.StrokeColor = opt.Color
|
||||||
}
|
}
|
||||||
width := NewAxis(p, data, style).measure().Width
|
if opt.isCategoryAxis {
|
||||||
|
axisOpt.BoundaryGap = TrueFlag()
|
||||||
yAxisCount := len(opt.YAxisList)
|
axisOpt.StrokeWidth = 1
|
||||||
boxWidth := p.Box.Width()
|
axisOpt.SplitLineShow = false
|
||||||
if axisIndex > 0 {
|
|
||||||
style.SplitLineShow = false
|
|
||||||
style.Position = PositionRight
|
|
||||||
padding.Right += (axisIndex - 1) * YAxisWidth
|
|
||||||
} else {
|
|
||||||
boxWidth = p.Box.Width() - (yAxisCount-1)*YAxisWidth
|
|
||||||
padding.Left += (YAxisWidth - width)
|
|
||||||
}
|
}
|
||||||
|
return axisOpt
|
||||||
dYAxis, err := NewDraw(
|
}
|
||||||
DrawOption{
|
|
||||||
Parent: p,
|
func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||||
Width: boxWidth,
|
p = p.Child(PainterPaddingOption(Box{
|
||||||
// 减去x轴的高
|
Bottom: defaultXAxisHeight,
|
||||||
Height: p.Box.Height() - xAxisHeight,
|
}))
|
||||||
},
|
return NewAxisPainter(p, opt.ToAxisOption())
|
||||||
PaddingOption(padding),
|
}
|
||||||
)
|
|
||||||
if err != nil {
|
func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
|
||||||
return nil, err
|
p = p.Child(PainterPaddingOption(Box{
|
||||||
}
|
Bottom: defaultXAxisHeight,
|
||||||
if opt.Font != nil {
|
}))
|
||||||
dYAxis.Font = opt.Font
|
axisOpt := opt.ToAxisOption()
|
||||||
}
|
axisOpt.Position = PositionRight
|
||||||
NewAxis(dYAxis, data, style).Render()
|
axisOpt.SplitLineShow = false
|
||||||
yRange.Size = dYAxis.Box.Height()
|
return NewAxisPainter(p, axisOpt)
|
||||||
return &yRange, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
119
yaxis_test.go
119
yaxis_test.go
|
|
@ -1,119 +0,0 @@
|
||||||
// MIT License
|
|
||||||
|
|
||||||
// Copyright (c) 2022 Tree Xie
|
|
||||||
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
package charts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDrawYAxis(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
newDraw := func() *Draw {
|
|
||||||
d, _ := NewDraw(DrawOption{
|
|
||||||
Width: 400,
|
|
||||||
Height: 300,
|
|
||||||
})
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
newDraw func() *Draw
|
|
||||||
newOption func() *ChartOption
|
|
||||||
axisIndex int
|
|
||||||
xAxisHeight int
|
|
||||||
result string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *ChartOption {
|
|
||||||
return &ChartOption{
|
|
||||||
YAxisList: []YAxisOption{
|
|
||||||
{
|
|
||||||
Max: NewFloatPoint(20),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SeriesList: []Series{
|
|
||||||
{
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 50 10\nL 50 290\" style=\"stroke-width:1;stroke:none;fill:none\"/><path d=\"M 50 10\nL 390 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 57\nL 390 57\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 104\nL 390 104\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 151\nL 390 151\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 198\nL 390 198\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 244\nL 390 244\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"36\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0</text><text x=\"18\" y=\"248\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3.33</text><text x=\"18\" y=\"202\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6.66</text><text x=\"29\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10</text><text x=\"11\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">13.33</text><text x=\"11\" y=\"61\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">16.66</text><text x=\"29\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text></svg>",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newDraw: newDraw,
|
|
||||||
newOption: func() *ChartOption {
|
|
||||||
return &ChartOption{
|
|
||||||
YAxisList: []YAxisOption{
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
Max: NewFloatPoint(20),
|
|
||||||
Formatter: "{value} C",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SeriesList: []Series{
|
|
||||||
{
|
|
||||||
YAxisIndex: 1,
|
|
||||||
Data: []SeriesData{
|
|
||||||
{
|
|
||||||
Value: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisIndex: 1,
|
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 337 10\nL 337 290\" style=\"stroke-width:1;stroke:none;fill:none\"/><text x=\"345\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0 C</text><text x=\"345\" y=\"248\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3.33 C</text><text x=\"345\" y=\"202\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6.66 C</text><text x=\"345\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10 C</text><text x=\"345\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">13.33 C</text><text x=\"345\" y=\"61\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">16.66 C</text><text x=\"345\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20 C</text></svg>",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
d := tt.newDraw()
|
|
||||||
r, err := drawYAxis(d, tt.newOption(), tt.axisIndex, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(&Range{
|
|
||||||
divideCount: 6,
|
|
||||||
Max: 20,
|
|
||||||
Size: 280,
|
|
||||||
}, r)
|
|
||||||
|
|
||||||
data, err := d.Bytes()
|
|
||||||
assert.Nil(err)
|
|
||||||
assert.Equal(tt.result, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue