Compare commits

...

38 commits
v2.5.2 ... main

Author SHA1 Message Date
958172a1f1 update URL in examples
Some checks failed
Test / Build (push) Has been cancelled
2025-05-13 21:53:31 -05:00
0eacc8e394 start migration to our packages
Some checks are pending
Test / Build (push) Waiting to run
2025-05-13 21:46:02 -05:00
vicanso
d25a827706 fix: fix label position of pie, #86 2024-08-15 20:37:07 +08:00
vicanso
5842c71b1d refactor: remove unused code 2024-08-01 21:44:52 +08:00
vicanso
e7dc4189d5 feat: support bar margin 2024-06-07 20:35:03 +08:00
vicanso
32e6dd52d0 refactor: export GetRenderer function to get chart renderer 2024-06-05 21:13:03 +08:00
vicanso
9614835723 feat: support rounded rect for horizontal bar chart 2024-05-21 20:26:40 +08:00
vicanso
9b7634c2c2 feat: support rounded rect for bar chart 2024-05-16 20:02:24 +08:00
Tree Xie
5a69c3e5a3
Merge pull request #72 from euerla/main
fix: Label position of the pie chart
2024-03-23 10:33:19 +08:00
Alexander Heidrich
8c6c4e007c fix: Label position of the pie chart 2024-03-22 08:27:06 +01:00
Tree Xie
765febd03a
Merge pull request #71 from euerla/main
fix: Label position of the pie chart
2024-03-09 08:13:02 +08:00
Alexander Heidrich
19a4d783fd fix: Label position of the pie chart 2024-03-08 20:24:13 +01:00
vicanso
06fe1006d5 chore: update test go version 2024-02-11 12:39:39 +08:00
vicanso
f1a231ff4b feat: support split line show option for charts, #69 2024-02-11 12:36:26 +08:00
Tree Xie
c7c0655113
Merge pull request #67 from ssexuejinwei/main
support dash line for line chart
2024-01-02 19:35:16 +08:00
xuejinwei.1112
310800a5f0 support dash line for line chart 2024-01-02 12:32:56 +08:00
vicanso
e09ab2c3c7 Revert "chore: update modules"
This reverts commit c2f709a742.
2023-12-27 20:37:18 +08:00
vicanso
c2f709a742 chore: update modules 2023-12-27 20:34:05 +08:00
vicanso
98af9866a4 refactor: support label show for radar chart, #62 2023-12-27 20:33:12 +08:00
Tree Xie
c302d0ffa4
Merge pull request #65 from vicanso/revert-56-xAxisImprovements
Revert "Improvements to how the X Axis is rendered"
2023-12-27 18:21:05 +08:00
Tree Xie
8bcb584aba
Revert "Improvements to how the X Axis is rendered" 2023-12-27 18:20:55 +08:00
vicanso
0ddb9e4ef1 chore: update modules 2023-05-12 20:31:42 +08:00
Tree Xie
18d8ee51fb
Merge pull request #56 from jentfoo/xAxisImprovements
Improvements to how the X Axis is rendered
2023-05-06 19:41:25 +08:00
Mike Jensen
687baad0af
Unit test fixes
Unit tests updated for new tick positions and in a couple cases additional one X axis sample.
2023-05-05 10:19:01 -06:00
Mike Jensen
a158191faf
Add Unit to XAxis as a publicly visible parameter
In some cases the XAxis may have a single long title.  This can result in very few increments being shown.
In order to be more flexible for those cases this allows the XAxis Tick frequency to be able to be directly controlled.
2023-05-05 09:55:55 -06:00
Mike Jensen
c810369730
Change ticks to avoid values impacting each other
The recently introduced logic has an incorrect understanding of the `unit` parameter.
This would result in too many ticks being outputted, particularly as datasets got larger.
This fixes it by re-calculating the tick count using the `unit` param as originally intended.
2023-05-05 09:44:09 -06:00
Mike Jensen
19173dfd37
painter.go: Optimize isTick function
This reduces the loop frequency to one or two iterations in all cases.
I have been unable to find any single line equation that can produce this same behavior, but one likely exists.
2023-05-04 17:59:11 -06:00
Mike Jensen
e7a49c2c21
Improvements to how the X Axis is rendered
This provides two improvements to how the X Axis is rendered:
* The calculation for where a tick should exist has been improved.  It now will ensure a tick is always at both the start of the axis and the end of the axis.  This makes it clear exactly what data span is captured in the graph.
* The second improvement is how the label on the last tick is written.  It used to often get partially cut off, and with the change to ensure a tick is always at the end this could be seen more easily.  Now the last tick has it's label written to the left so that it can be fully displayed.
2023-05-04 12:52:28 -06:00
vicanso
20e8d4a078 feat: support to set the first axis 2023-02-25 14:04:30 +08:00
vicanso
29a5ece545 chore: update go modules 2023-02-14 20:35:54 +08:00
vicanso
d3f7a773af fix: fix zero value of funnel chart, #43 2023-01-12 20:20:36 +08:00
vicanso
8ba9e2e1b2 fix: fix x axis label of horizontal bar chart, #42 2023-01-11 20:41:16 +08:00
vicanso
e10175594b feat: support label format for funnel chart, #41 2023-01-05 19:15:58 +08:00
Tree Xie
b3cb5a75cb
Merge pull request #40 from junglerider/main
added option for line chart bg fill opacity
2022-12-27 08:29:19 +08:00
Thomas Knierim
a767b3e1af added option for line chart bg fill opacity 2022-12-26 15:06:53 +07:00
vicanso
830d4bdd21 fix: fix test for text roration 2022-12-11 14:59:37 +08:00
vicanso
d5533447f5 feat: support text rotation for series label, #38 2022-12-11 14:57:05 +08:00
vicanso
ef04ac14ab feat: support font size for series label, #38 2022-12-09 20:08:47 +08:00
49 changed files with 1129 additions and 231 deletions

View file

@ -14,13 +14,12 @@ jobs:
strategy: strategy:
matrix: matrix:
go: go:
- '1.22'
- '1.21'
- '1.20'
- '1.19' - '1.19'
- '1.18' - '1.18'
- '1.17' - '1.17'
- '1.16'
- '1.15'
- '1.14'
- '1.13'
steps: steps:
- name: Go ${{ matrix.go }} test - name: Go ${{ matrix.go }} test

View file

@ -1,5 +1,7 @@
# go-charts # go-charts
Clone from https://github.com/vicanso/go-charts
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE)
[![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) [![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions)
@ -33,7 +35,7 @@ More examples can be found in the [./examples/](./examples/) directory.
package main package main
import ( import (
charts "github.com/vicanso/go-charts/v2" charts "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -99,7 +101,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -174,7 +176,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -231,7 +233,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -286,7 +288,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -344,7 +346,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -384,7 +386,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -449,7 +451,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {

View file

@ -32,7 +32,7 @@
package main package main
import ( import (
charts "github.com/vicanso/go-charts/v2" charts "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -98,7 +98,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -173,7 +173,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -230,7 +230,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -285,7 +285,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -343,7 +343,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -383,7 +383,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {
@ -447,7 +447,7 @@ func main() {
package main package main
import ( import (
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func main() { func main() {

View file

@ -23,8 +23,8 @@
package charts package charts
import ( import (
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
type Box = chart.Box type Box = chart.Box

32
axis.go
View file

@ -26,7 +26,7 @@ import (
"strings" "strings"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type axisPainter struct { type axisPainter struct {
@ -63,6 +63,8 @@ type AxisOption struct {
StrokeWidth float64 StrokeWidth float64
// The length of the axis tick // The length of the axis tick
TickLength int TickLength int
// The first axis
FirstAxis int
// The margin value of label // The margin value of label
LabelMargin int LabelMargin int
// The font size of label // The font size of label
@ -75,7 +77,11 @@ type AxisOption struct {
SplitLineShow bool SplitLineShow bool
// The color of split line // The color of split line
SplitLineColor Color SplitLineColor Color
Unit int // The text rotation of label
TextRotation float64
// The offset of label
LabelOffset Box
Unit int
} }
func (a *axisPainter) Render() (Box, error) { func (a *axisPainter) Render() (Box, error) {
@ -153,7 +159,15 @@ func (a *axisPainter) Render() (Box, error) {
} }
top.SetDrawingStyle(style).OverrideTextStyle(style) top.SetDrawingStyle(style).OverrideTextStyle(style)
isTextRotation := opt.TextRotation != 0
if isTextRotation {
top.SetTextRotation(opt.TextRotation)
}
textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data)
if isTextRotation {
top.ClearTextRotation()
}
// 增加30px来计算文本展示区域 // 增加30px来计算文本展示区域
textFillWidth := float64(textMaxWidth + 20) textFillWidth := float64(textMaxWidth + 20)
@ -243,6 +257,7 @@ func (a *axisPainter) Render() (Box, error) {
Length: tickLength, Length: tickLength,
Unit: unit, Unit: unit,
Orient: orient, Orient: orient,
First: opt.FirstAxis,
}) })
p.LineStroke([]Point{ p.LineStroke([]Point{
{ {
@ -261,11 +276,14 @@ func (a *axisPainter) Render() (Box, error) {
Top: labelPaddingTop, Top: labelPaddingTop,
Right: labelPaddingRight, Right: labelPaddingRight,
})).MultiText(MultiTextOption{ })).MultiText(MultiTextOption{
Align: textAlign, First: opt.FirstAxis,
TextList: data, Align: textAlign,
Orient: orient, TextList: data,
Unit: unit, Orient: orient,
Position: labelPosition, Unit: unit,
Position: labelPosition,
TextRotation: opt.TextRotation,
Offset: opt.LabelOffset,
}) })
// 显示辅助线 // 显示辅助线
if opt.SplitLineShow { if opt.SplitLineShow {

View file

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestAxis(t *testing.T) { func TestAxis(t *testing.T) {

View file

@ -26,7 +26,7 @@ import (
"math" "math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type barChart struct { type barChart struct {
@ -63,6 +63,8 @@ type BarChartOption struct {
// The legend option // The legend option
Legend LegendOption Legend LegendOption
BarWidth int BarWidth int
// Margin of bar
BarMargin int
} }
func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@ -88,6 +90,9 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
margin = 5 margin = 5
barMargin = 3 barMargin = 3
} }
if opt.BarMargin > 0 {
barMargin = opt.BarMargin
}
seriesCount := len(seriesList) seriesCount := len(seriesList)
// 总的宽度-两个margin-(总数-1)的barMargin // 总的宽度-两个margin-(总数-1)的barMargin
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount
@ -142,14 +147,25 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
} }
top := barMaxHeight - h top := barMaxHeight - h
seriesPainter.OverrideDrawingStyle(Style{ if series.RoundRadius <= 0 {
FillColor: fillColor, seriesPainter.OverrideDrawingStyle(Style{
}).Rect(chart.Box{ FillColor: fillColor,
Top: top, }).Rect(chart.Box{
Left: x, Top: top,
Right: x + barWidth, Left: x,
Bottom: barMaxHeight - 1, Right: x + barWidth,
}) Bottom: barMaxHeight - 1,
})
} else {
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).RoundedRect(chart.Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
}, series.RoundRadius)
}
// 用于生成marker point // 用于生成marker point
points[j] = Point{ points[j] = Point{
// 居中的位置 // 居中的位置
@ -189,6 +205,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
Radians: radians, Radians: radians,
FontColor: fontColor, FontColor: fontColor,
Offset: series.Label.Offset, Offset: series.Label.Offset,
FontSize: series.Label.FontSize,
}) })
} }

File diff suppressed because one or more lines are too long

View file

@ -67,10 +67,14 @@ type ChartOption struct {
LineStrokeWidth float64 LineStrokeWidth float64
// The bar with of bar chart // The bar with of bar chart
BarWidth int BarWidth int
// The margin of each bar
BarMargin int
// The bar height of horizontal bar chart // The bar height of horizontal bar chart
BarHeight int BarHeight int
// Fill the area of line chart // Fill the area of line chart
FillArea bool FillArea bool
// background fill (alpha) opacity
Opacity uint8
// The child charts // The child charts
Children []ChartOption Children []ChartOption
// The value formatter // The value formatter

View file

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestChartOption(t *testing.T) { func TestChartOption(t *testing.T) {

View file

@ -27,7 +27,7 @@ import (
"math" "math"
"sort" "sort"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
const labelFontSize = 10 const labelFontSize = 10
@ -215,7 +215,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
yAxisOption.Data = r.Values() yAxisOption.Data = r.Values()
} else { } else {
yAxisOption.isCategoryAxis = true yAxisOption.isCategoryAxis = true
opt.XAxis.Data = r.Values() // 由于x轴为value部分因此计算其label单独处理
opt.XAxis.Data = NewRange(AxisRangeOption{
Painter: p,
Min: min,
Max: max,
// 高度需要减去x轴的高度
Size: rangeHeight,
// 分隔数量
DivideCount: defaultAxisDivideCount,
}).Values()
opt.XAxis.isValueAxis = true opt.XAxis.isValueAxis = true
} }
reverseStringSlice(yAxisOption.Data) reverseStringSlice(yAxisOption.Data)
@ -366,10 +375,11 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
if len(barSeriesList) != 0 { if len(barSeriesList) != 0 {
handler.Add(func() error { handler.Add(func() error {
_, err := NewBarChart(p, BarChartOption{ _, err := NewBarChart(p, BarChartOption{
Theme: opt.theme, Theme: opt.theme,
Font: opt.font, Font: opt.font,
XAxis: opt.XAxis, XAxis: opt.XAxis,
BarWidth: opt.BarWidth, BarWidth: opt.BarWidth,
BarMargin: opt.BarMargin,
}).render(renderResult, barSeriesList) }).render(renderResult, barSeriesList)
return err return err
}) })
@ -382,6 +392,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
Theme: opt.theme, Theme: opt.theme,
Font: opt.font, Font: opt.font,
BarHeight: opt.BarHeight, BarHeight: opt.BarHeight,
BarMargin: opt.BarMargin,
YAxisOptions: opt.YAxisOptions, YAxisOptions: opt.YAxisOptions,
}).render(renderResult, horizontalBarSeriesList) }).render(renderResult, horizontalBarSeriesList)
return err return err
@ -409,6 +420,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
SymbolShow: opt.SymbolShow, SymbolShow: opt.SymbolShow,
StrokeWidth: opt.LineStrokeWidth, StrokeWidth: opt.LineStrokeWidth,
FillArea: opt.FillArea, FillArea: opt.FillArea,
Opacity: opt.Opacity,
}).render(renderResult, lineSeriesList) }).render(renderResult, lineSeriesList)
return err return err
}) })

View file

@ -26,7 +26,7 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
func BenchmarkMultiChartPNGRender(b *testing.B) { func BenchmarkMultiChartPNGRender(b *testing.B) {

View file

@ -29,7 +29,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
func convertToArray(data []byte) []byte { func convertToArray(data []byte) []byte {
@ -344,6 +344,11 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
Data: NewSeriesDataFromValues(dataItem.Value.values), Data: NewSeriesDataFromValues(dataItem.Value.values),
Max: item.Max, Max: item.Max,
Min: item.Min, Min: item.Min,
Label: SeriesLabel{
Color: parseColor(item.Label.Color),
Show: item.Label.Show,
Distance: item.Label.Distance,
},
}) })
} }
continue continue

View file

@ -27,7 +27,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestConvertToArray(t *testing.T) { func TestConvertToArray(t *testing.T) {

View file

@ -1,11 +1,10 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "area-line-chart.png") file := filepath.Join(tmpPath, "area-line-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,11 +1,10 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "bar-chart.png") file := filepath.Join(tmpPath, "bar-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
charts "github.com/vicanso/go-charts/v2" charts "git.smarteching.com/zeni/go-charts/v2"
) )
var html = `<!DOCTYPE html> var html = `<!DOCTYPE html>

View file

@ -5,7 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +16,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "chinese-line-chart.png") file := filepath.Join(tmpPath, "chinese-line-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,11 +1,10 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "funnel-chart.png") file := filepath.Join(tmpPath, "funnel-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }
@ -30,6 +29,8 @@ func main() {
60, 60,
40, 40,
20, 20,
10,
0,
} }
p, err := charts.FunnelRender( p, err := charts.FunnelRender(
values, values,
@ -40,6 +41,8 @@ func main() {
"Visit", "Visit",
"Inquiry", "Inquiry",
"Order", "Order",
"Pay",
"Cancel",
}), }),
) )
if err != nil { if err != nil {

View file

@ -1,11 +1,10 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "horizontal-bar-chart.png") file := filepath.Join(tmpPath, "horizontal-bar-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }
@ -26,22 +25,22 @@ func writeFile(buf []byte) error {
func main() { func main() {
values := [][]float64{ values := [][]float64{
{ {
8203, 10,
18203, 30,
23489, 50,
29034, 70,
104970, 90,
131744, 110,
630230, 130,
}, },
{ {
9325, 20,
19325, 40,
23438, 60,
31000, 80,
121594, 100,
134141, 120,
681807, 140,
}, },
} }
p, err := charts.HorizontalBarRender( p, err := charts.HorizontalBarRender(
@ -66,6 +65,9 @@ func main() {
"China", "China",
"World", "World",
}), }),
func(opt *charts.ChartOption) {
opt.SeriesList[0].RoundRadius = 5
},
) )
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -2,11 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -17,7 +16,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "line-chart.png") file := filepath.Join(tmpPath, "line-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }
@ -97,6 +96,11 @@ func main() {
Top: 5, Top: 5,
Bottom: 10, Bottom: 10,
} }
opt.YAxisOptions = []charts.YAxisOption{
{
SplitLineShow: charts.FalseFlag(),
},
}
opt.SymbolShow = charts.FalseFlag() opt.SymbolShow = charts.FalseFlag()
opt.LineStrokeWidth = 1 opt.LineStrokeWidth = 1
opt.ValueFormatter = func(f float64) string { opt.ValueFormatter = func(f float64) string {

View file

@ -1,12 +1,11 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
charts "github.com/vicanso/go-charts/v2" charts "git.smarteching.com/zeni/go-charts/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -17,7 +16,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "painter.png") file := filepath.Join(tmpPath, "painter.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,11 +1,10 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "pie-chart.png") file := filepath.Join(tmpPath, "pie-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,11 +1,10 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
) )
func writeFile(buf []byte) error { func writeFile(buf []byte) error {
@ -16,7 +15,7 @@ func writeFile(buf []byte) error {
} }
file := filepath.Join(tmpPath, "radar-chart.png") file := filepath.Join(tmpPath, "radar-chart.png")
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,14 +1,13 @@
package main package main
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/vicanso/go-charts/v2" "git.smarteching.com/zeni/go-charts/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func writeFile(buf []byte, filename string) error { func writeFile(buf []byte, filename string) error {
@ -19,7 +18,7 @@ func writeFile(buf []byte, filename string) error {
} }
file := filepath.Join(tmpPath, filename) file := filepath.Join(tmpPath, filename)
err = ioutil.WriteFile(file, buf, 0600) err = os.WriteFile(file, buf, 0600)
if err != nil { if err != nil {
return err return err
} }

View file

@ -0,0 +1,81 @@
package main
import (
"crypto/rand"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
"git.smarteching.com/zeni/go-charts/v2"
)
func writeFile(buf []byte) error {
tmpPath := "./tmp"
err := os.MkdirAll(tmpPath, 0700)
if err != nil {
return err
}
file := filepath.Join(tmpPath, "time-line-chart.png")
err = os.WriteFile(file, buf, 0600)
if err != nil {
return err
}
return nil
}
func main() {
xAxisValue := []string{}
values := []float64{}
now := time.Now()
firstAxis := 0
for i := 0; i < 300; i++ {
// 设置首个axis为xx:00的时间点
if firstAxis == 0 && now.Minute() == 0 {
firstAxis = i
}
xAxisValue = append(xAxisValue, now.Format("15:04"))
now = now.Add(time.Minute)
value, _ := rand.Int(rand.Reader, big.NewInt(100))
values = append(values, float64(value.Int64()))
}
p, err := charts.LineRender(
[][]float64{
values,
},
charts.TitleTextOptionFunc("Line"),
charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()),
charts.LegendLabelsOptionFunc([]string{
"Demo",
}, "50"),
func(opt *charts.ChartOption) {
opt.XAxis.FirstAxis = firstAxis
// 必须要比计算得来的最小值更大(每60分钟)
opt.XAxis.SplitNumber = 60
opt.Legend.Padding = charts.Box{
Top: 5,
Bottom: 10,
}
opt.SymbolShow = charts.FalseFlag()
opt.LineStrokeWidth = 1
opt.ValueFormatter = func(f float64) string {
return fmt.Sprintf("%.0f", f)
}
},
)
if err != nil {
panic(err)
}
buf, err := p.Bytes()
if err != nil {
panic(err)
}
err = writeFile(buf)
if err != nil {
panic(err)
}
}

View file

@ -27,7 +27,7 @@ import (
"sync" "sync"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2/roboto" "git.smarteching.com/zeni/go-chart/v2/roboto"
) )
var fonts = sync.Map{} var fonts = sync.Map{}

View file

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/roboto" "git.smarteching.com/zeni/go-chart/v2/roboto"
) )
func TestInstallFont(t *testing.T) { func TestInstallFont(t *testing.T) {

View file

@ -23,9 +23,6 @@
package charts package charts
import ( import (
"fmt"
"github.com/dustin/go-humanize"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
) )
@ -95,13 +92,23 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList)
y := 0 y := 0
widthList := make([]int, len(seriesList)) widthList := make([]int, len(seriesList))
textList := make([]string, len(seriesList)) textList := make([]string, len(seriesList))
seriesNames := seriesList.Names()
offset := max - min
for index, item := range seriesList { for index, item := range seriesList {
value := item.Data[0].Value value := item.Data[0].Value
widthPercent := (value - min) / (max - min) // 最大最小值一致则为100%
widthPercent := 100.0
if offset != 0 {
widthPercent = (value - min) / offset
}
w := int(widthPercent * float64(width)) w := int(widthPercent * float64(width))
widthList[index] = w widthList[index] = w
p := humanize.CommafWithDigits(value/max*100, 2) + "%" // 如果最大值为0则占比100%
textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) percent := 1.0
if max != 0 {
percent = value / max
}
textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent)
} }
for index, w := range widthList { for index, w := range widthList {

12
go.mod
View file

@ -1,17 +1,17 @@
module github.com/vicanso/go-charts/v2 module git.smarteching.com/zeni/go-charts/v2
go 1.17 go 1.24.1
require ( require (
github.com/dustin/go-humanize v1.0.0 git.smarteching.com/zeni/go-chart/v2 v2.1.4
github.com/dustin/go-humanize v1.0.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.10.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-20220902085622-e7cb96979f69 // indirect golang.org/x/image v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

25
go.sum
View file

@ -1,27 +1,18 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q=
git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestGrid(t *testing.T) { func TestGrid(t *testing.T) {

View file

@ -24,7 +24,7 @@ package charts
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type horizontalBarChart struct { type horizontalBarChart struct {
@ -50,6 +50,8 @@ type HorizontalBarChartOption struct {
// The legend option // The legend option
Legend LegendOption Legend LegendOption
BarHeight int BarHeight int
// Margin of bar
BarMargin int
} }
// NewHorizontalBarChart returns a horizontal bar chart renderer // NewHorizontalBarChart returns a horizontal bar chart renderer
@ -81,6 +83,9 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
margin = 5 margin = 5
barMargin = 3 barMargin = 3
} }
if opt.BarMargin > 0 {
barMargin = opt.BarMargin
}
seriesCount := len(seriesList) seriesCount := len(seriesList)
// 总的高度-两个margin-(总数-1)的barMargin // 总的高度-两个margin-(总数-1)的barMargin
barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount
@ -136,14 +141,26 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
fillColor = item.Style.FillColor fillColor = item.Style.FillColor
} }
right := w right := w
seriesPainter.OverrideDrawingStyle(Style{ if series.RoundRadius <= 0 {
FillColor: fillColor, seriesPainter.OverrideDrawingStyle(Style{
}).Rect(chart.Box{ FillColor: fillColor,
Top: y, }).Rect(chart.Box{
Left: 0, Top: y,
Right: right, Left: 0,
Bottom: y + barHeight, Right: right,
}) Bottom: y + barHeight,
})
} else {
seriesPainter.OverrideDrawingStyle(Style{
FillColor: fillColor,
}).RoundedRect(chart.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
}, series.RoundRadius)
}
// 如果label不需要展示则返回 // 如果label不需要展示则返回
if labelPainter == nil { if labelPainter == nil {
continue continue
@ -156,6 +173,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
Y: y + barHeight>>1, Y: y + barHeight>>1,
Offset: series.Label.Offset, Offset: series.Label.Offset,
FontColor: series.Label.Color, FontColor: series.Label.Color,
FontSize: series.Label.FontSize,
} }
if series.Label.Position == PositionLeft { if series.Label.Position == PositionLeft {
labelValue.X = 0 labelValue.X = 0

View file

@ -26,7 +26,7 @@ import (
"math" "math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
type lineChart struct { type lineChart struct {
@ -70,6 +70,8 @@ type LineChartOption struct {
FillArea bool FillArea bool
// background is filled // background is filled
backgroundIsFilled bool backgroundIsFilled bool
// background fill (alpha) opacity
Opacity uint8
} }
func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@ -113,6 +115,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
StrokeColor: seriesColor, StrokeColor: seriesColor,
StrokeWidth: strokeWidth, StrokeWidth: strokeWidth,
} }
if len(series.Style.StrokeDashArray) > 0 {
drawingStyle.StrokeDashArray = series.Style.StrokeDashArray
}
yRange := result.axisRanges[series.AxisIndex] yRange := result.axisRanges[series.AxisIndex]
points := make([]Point, 0) points := make([]Point, 0)
@ -147,6 +152,8 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
Value: item.Value, Value: item.Value,
X: p.X, X: p.X,
Y: p.Y, Y: p.Y,
// 字体大小
FontSize: series.Label.FontSize,
}) })
} }
// 如果需要填充区域 // 如果需要填充区域
@ -154,6 +161,10 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
areaPoints := make([]Point, len(points)) areaPoints := make([]Point, len(points))
copy(areaPoints, points) copy(areaPoints, points)
bottomY := yRange.getRestHeight(yRange.min) bottomY := yRange.getRestHeight(yRange.min)
var opacity uint8 = 200
if opt.Opacity != 0 {
opacity = opt.Opacity
}
areaPoints = append(areaPoints, Point{ areaPoints = append(areaPoints, Point{
X: areaPoints[len(areaPoints)-1].X, X: areaPoints[len(areaPoints)-1].X,
Y: bottomY, Y: bottomY,
@ -162,7 +173,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
Y: bottomY, Y: bottomY,
}, areaPoints[0]) }, areaPoints[0])
seriesPainter.SetDrawingStyle(Style{ seriesPainter.SetDrawingStyle(Style{
FillColor: seriesColor.WithAlpha(200), FillColor: seriesColor.WithAlpha(opacity),
}) })
seriesPainter.FillArea(areaPoints) seriesPainter.FillArea(areaPoints)
} }

View file

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestMarkLine(t *testing.T) { func TestMarkLine(t *testing.T) {

View file

@ -26,7 +26,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestMarkPoint(t *testing.T) { func TestMarkPoint(t *testing.T) {

View file

@ -28,7 +28,7 @@ import (
"math" "math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type ValueFormatter func(float64) string type ValueFormatter func(float64) string
@ -59,6 +59,8 @@ type PainterOptions struct {
type PainterOption func(*Painter) type PainterOption func(*Painter)
type TicksOption struct { type TicksOption struct {
// the first tick
First int
Length int Length int
Orient string Orient string
Count int Count int
@ -71,6 +73,11 @@ type MultiTextOption struct {
Unit int Unit int
Position string Position string
Align string Align string
// The text rotation of label
TextRotation float64
Offset Box
// The first text index
First int
} }
type GridOption struct { type GridOption struct {
@ -613,6 +620,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter {
return p return p
} }
count := opt.Count count := opt.Count
first := opt.First
width := p.Width() width := p.Width()
height := p.Height() height := p.Height()
unit := 1 unit := 1
@ -627,7 +635,10 @@ func (p *Painter) Ticks(opt TicksOption) *Painter {
values = autoDivide(width, count) values = autoDivide(width, count)
} }
for index, value := range values { for index, value := range values {
if index%unit != 0 { if index < first {
continue
}
if (index-first)%unit != 0 {
continue continue
} }
if isVertical { if isVertical {
@ -682,10 +693,19 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
} else { } else {
values = autoDivide(width, count) values = autoDivide(width, count)
} }
isTextRotation := opt.TextRotation != 0
offset := opt.Offset
for index, text := range opt.TextList { for index, text := range opt.TextList {
if opt.Unit != 0 && index%opt.Unit != showIndex { if index < opt.First {
continue continue
} }
if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex {
continue
}
if isTextRotation {
p.ClearTextRotation()
p.SetTextRotation(opt.TextRotation)
}
box := p.MeasureText(text) box := p.MeasureText(text)
start := values[index] start := values[index]
if positionCenter { if positionCenter {
@ -706,8 +726,13 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
} else { } else {
x = start - box.Width()>>1 x = start - box.Width()>>1
} }
x += offset.Left
y += offset.Top
p.Text(text, x, y) p.Text(text, x, y)
} }
if isTextRotation {
p.ClearTextRotation()
}
return p return p
} }
@ -778,6 +803,48 @@ func (p *Painter) Rect(box Box) *Painter {
return p return p
} }
func (p *Painter) RoundedRect(box Box, radius int) *Painter {
r := (box.Right - box.Left) / 2
if radius > r {
radius = r
}
rx := float64(radius)
ry := float64(radius)
p.MoveTo(box.Left+radius, box.Top)
p.LineTo(box.Right-radius, box.Top)
cx := box.Right - radius
cy := box.Top + radius
// right top
p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2)
p.LineTo(box.Right, box.Bottom-radius)
// right bottom
cx = box.Right - radius
cy = box.Bottom - radius
p.ArcTo(cx, cy, rx, ry, 0.0, math.Pi/2)
p.LineTo(box.Left+radius, box.Bottom)
// left bottom
cx = box.Left + radius
cy = box.Bottom - radius
p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2)
p.LineTo(box.Left, box.Top+radius)
// left top
cx = box.Left + radius
cy = box.Top + radius
p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2)
p.Close()
p.FillStroke()
p.Fill()
return p
}
func (p *Painter) LegendLineDot(box Box) *Painter { func (p *Painter) LegendLineDot(box Box) *Painter {
width := box.Width() width := box.Width()
height := box.Height() height := box.Height()
@ -793,3 +860,7 @@ func (p *Painter) LegendLineDot(box Box) *Painter {
p.FillStroke() p.FillStroke()
return p return p
} }
func (p *Painter) GetRenderer() chart.Renderer {
return p.render
}

View file

@ -28,8 +28,8 @@ import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestPainterOption(t *testing.T) { func TestPainterOption(t *testing.T) {
@ -343,6 +343,29 @@ func TestPainter(t *testing.T) {
} }
} }
func TestRoundedRect(t *testing.T) {
assert := assert.New(t)
p, err := NewPainter(PainterOptions{
Width: 400,
Height: 300,
Type: ChartOutputSVG,
})
assert.Nil(err)
p.OverrideDrawingStyle(Style{
FillColor: drawing.ColorWhite,
StrokeWidth: 1,
StrokeColor: drawing.ColorWhite,
}).RoundedRect(Box{
Left: 10,
Right: 30,
Bottom: 150,
Top: 10,
}, 5)
buf, err := p.Bytes()
assert.Nil(err)
assert.Equal("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 15 10\nL 25 10\nL 25 10\nA 5 5 90.00 0 1 30 15\nL 30 145\nL 30 145\nA 5 5 90.00 0 1 25 150\nL 15 150\nL 15 150\nA 5 5 90.00 0 1 10 145\nL 10 15\nL 10 15\nA 5 5 90.00 0 1 15 10\nZ\" style=\"stroke-width:1;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:1;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)\"/></svg>", string(buf))
}
func TestPainterTextFit(t *testing.T) { func TestPainterTextFit(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
p, err := NewPainter(PainterOptions{ p, err := NewPainter(PainterOptions{

View file

@ -27,7 +27,7 @@ import (
"math" "math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type pieChart struct { type pieChart struct {
@ -63,6 +63,96 @@ func NewPieChart(p *Painter, opt PieChartOption) *pieChart {
} }
} }
type sector struct {
value float64
percent float64
cx int
cy int
rx float64
ry float64
start float64
delta float64
offset int
quadrant int
lineStartX int
lineStartY int
lineBranchX int
lineBranchY int
lineEndX int
lineEndY int
showLabel bool
label string
series Series
color Color
}
func NewSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector {
s := sector{}
s.value = value
s.percent = value / totalValue
s.cx = cx
s.cy = cy
s.rx = radius
s.ry = radius
p := (currentValue + value/2) / totalValue
if p < 0.25 {
s.quadrant = 1
} else if p < 0.5 {
s.quadrant = 4
} else if p < 0.75 {
s.quadrant = 3
} else {
s.quadrant = 2
}
s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2
s.delta = chart.PercentToRadians(value / totalValue)
angle := s.start + s.delta/2
s.lineStartX = cx + int(radius*math.Cos(angle))
s.lineStartY = cy + int(radius*math.Sin(angle))
s.lineBranchX = cx + int(labelRadius*math.Cos(angle))
s.lineBranchY = cy + int(labelRadius*math.Sin(angle))
s.offset = labelLineLength
if s.lineBranchX <= cx {
s.offset *= -1
}
s.lineEndX = s.lineBranchX + s.offset
s.lineEndY = s.lineBranchY
s.series = series
s.color = color
s.showLabel = series.Label.Show
s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent)
return s
}
func (s *sector) calculateY(prevY int) int {
for i := 0; i <= s.cy; i++ {
if s.quadrant <= 2 {
if (prevY - s.lineBranchY) > labelFontSize+5 {
break
}
s.lineBranchY -= 1
} else {
if (s.lineBranchY - prevY) > labelFontSize+5 {
break
}
s.lineBranchY += 1
}
}
s.lineEndY = s.lineBranchY
return s.lineBranchY
}
func (s *sector) calculateTextXY(textBox Box) (x int, y int) {
textMargin := 3
x = s.lineEndX + textMargin
y = s.lineEndY + textBox.Height()>>1 - 1
if s.offset < 0 {
textWidth := textBox.Width()
x = s.lineEndX - textWidth - textMargin
}
return
}
func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
opt := p.opt opt := p.opt
values := make([]float64, len(seriesList)) values := make([]float64, len(seriesList))
@ -101,98 +191,103 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
theme := opt.Theme theme := opt.Theme
currentValue := float64(0) currentValue := float64(0)
prevPoints := make([]Point, 0)
isOverride := func(x, y int) bool { var quadrant1, quadrant2, quadrant3, quadrant4 []sector
for _, p := range prevPoints { for index, v := range values {
if math.Abs(float64(p.Y-y)) > labelFontSize { series := seriesList[index]
continue color := theme.GetSeriesColor(index)
} if index == len(values)-1 {
// label可能较多内容不好计算横向占用空间 if color == theme.GetSeriesColor(0) {
// 因此x的位置需要中间位置两侧否则认为override color = theme.GetSeriesColor(1)
if (p.X <= cx && x <= cx) ||
(p.X > cx && x > cx) {
return true
} }
} }
return false s := NewSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color)
switch quadrant := s.quadrant; quadrant {
case 1:
quadrant1 = append([]sector{s}, quadrant1...)
case 2:
quadrant2 = append(quadrant2, s)
case 3:
quadrant3 = append([]sector{s}, quadrant3...)
case 4:
quadrant4 = append(quadrant4, s)
}
currentValue += v
} }
sectors := append(quadrant1, quadrant4...)
sectors = append(sectors, quadrant3...)
sectors = append(sectors, quadrant2...)
for index, v := range values { currentQuadrant := 0
prevY := 0
maxY := 0
minY := 0
for _, s := range sectors {
seriesPainter.OverrideDrawingStyle(Style{ seriesPainter.OverrideDrawingStyle(Style{
StrokeWidth: 1, StrokeWidth: 1,
StrokeColor: theme.GetSeriesColor(index), StrokeColor: s.color,
FillColor: theme.GetSeriesColor(index), FillColor: s.color,
}) })
seriesPainter.MoveTo(cx, cy) seriesPainter.MoveTo(s.cx, s.cy)
start := chart.PercentToRadians(currentValue/total) - math.Pi/2 seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke()
currentValue += v if !s.showLabel {
percent := (v / total)
delta := chart.PercentToRadians(percent)
seriesPainter.ArcTo(cx, cy, radius, radius, start, delta).
LineTo(cx, cy).
Close().
FillStroke()
series := seriesList[index]
// 是否显示label
showLabel := series.Label.Show
if !showLabel {
continue continue
} }
if currentQuadrant != s.quadrant {
// label的角度为饼块中间 if s.quadrant == 1 {
angle := start + delta/2 minY = cy * 2
startx := cx + int(radius*math.Cos(angle)) maxY = 0
starty := cy + int(radius*math.Sin(angle)) prevY = cy * 2
endx := cx + int(labelRadius*math.Cos(angle))
endy := cy + int(labelRadius*math.Sin(angle))
// 计算是否有重叠如果有则调整y坐标位置
// 最多只尝试5次
for i := 0; i < 5; i++ {
if !isOverride(endx, endy) {
break
} }
endy -= (labelFontSize << 1) if s.quadrant == 2 {
if currentQuadrant != 3 {
prevY = s.lineEndY
} else {
prevY = minY
}
}
if s.quadrant == 3 {
if currentQuadrant != 4 {
prevY = s.lineEndY
} else {
minY = cy * 2
maxY = 0
prevY = 0
}
}
if s.quadrant == 4 {
if currentQuadrant != 1 {
prevY = s.lineEndY
} else {
prevY = maxY
}
}
currentQuadrant = s.quadrant
} }
prevPoints = append(prevPoints, Point{ prevY = s.calculateY(prevY)
X: endx, if prevY > maxY {
Y: endy, maxY = prevY
})
seriesPainter.MoveTo(startx, starty)
seriesPainter.LineTo(endx, endy)
offset := labelLineWidth
if endx < cx {
offset *= -1
} }
seriesPainter.MoveTo(endx, endy) if prevY < minY {
endx += offset minY = prevY
seriesPainter.LineTo(endx, endy) }
seriesPainter.MoveTo(s.lineStartX, s.lineStartY)
seriesPainter.LineTo(s.lineBranchX, s.lineBranchY)
seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY)
seriesPainter.LineTo(s.lineEndX, s.lineEndY)
seriesPainter.Stroke() seriesPainter.Stroke()
textStyle := Style{ textStyle := Style{
FontColor: theme.GetTextColor(), FontColor: theme.GetTextColor(),
FontSize: labelFontSize, FontSize: labelFontSize,
Font: opt.Font, Font: opt.Font,
} }
if !series.Label.Color.IsZero() { if !s.series.Label.Color.IsZero() {
textStyle.FontColor = series.Label.Color textStyle.FontColor = s.series.Label.Color
} }
seriesPainter.OverrideTextStyle(textStyle) seriesPainter.OverrideTextStyle(textStyle)
text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label))
textBox := seriesPainter.MeasureText(text) seriesPainter.Text(s.label, x, y)
textMargin := 3
x := endx + textMargin
y := endy + textBox.Height()>>1 - 1
if offset < 0 {
textWidth := textBox.Width()
x = endx - textWidth - textMargin
}
seriesPainter.Text(text, x, y)
} }
return p.p.box, nil return p.p.box, nil
} }

File diff suppressed because one or more lines are too long

View file

@ -25,9 +25,10 @@ package charts
import ( import (
"errors" "errors"
"github.com/dustin/go-humanize"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
type radarChart struct { type radarChart struct {
@ -230,9 +231,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
StrokeColor: color, StrokeColor: color,
FillColor: dotFillColor, FillColor: dotFillColor,
}) })
for _, point := range linePoints { for index, point := range linePoints {
seriesPainter.Circle(dotWith, point.X, point.Y) seriesPainter.Circle(dotWith, point.X, point.Y)
seriesPainter.FillStroke() seriesPainter.FillStroke()
if series.Label.Show && index < len(series.Data) {
value := humanize.FtoaWithDigits(series.Data[index].Value, 2)
b := seriesPainter.MeasureText(value)
seriesPainter.Text(value, point.X-b.Width()/2, point.Y)
}
} }
} }

View file

@ -26,7 +26,7 @@ import (
"strings" "strings"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type SeriesData struct { type SeriesData struct {
@ -83,6 +83,8 @@ type SeriesLabel struct {
Position string Position string
// The offset of label's position // The offset of label's position
Offset Box Offset Box
// The font size of label
FontSize float64
} }
const ( const (
@ -124,6 +126,8 @@ type Series struct {
Name string Name string
// Radius for Pie chart, e.g.: 40%, default is "40%" // Radius for Pie chart, e.g.: 40%, default is "40%"
Radius string Radius string
// Round for bar chart
RoundRadius int
// Mark point for series // Mark point for series
MarkPoint SeriesMarkPoint MarkPoint SeriesMarkPoint
// Make line for series // Make line for series
@ -277,6 +281,14 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
return NewLabelFormatter(seriesNames, layout) return NewLabelFormatter(seriesNames, layout)
} }
// NewFunnelLabelFormatter returns a funner label formatter
func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter {
if len(layout) == 0 {
layout = "{b}({d})"
}
return NewLabelFormatter(seriesNames, layout)
}
// NewValueLabelFormatter returns a value formatter // NewValueLabelFormatter returns a value formatter
func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter {
if len(layout) == 0 { if len(layout) == 0 {

View file

@ -24,7 +24,7 @@ package charts
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
) )
type labelRenderValue struct { type labelRenderValue struct {
@ -45,8 +45,10 @@ type LabelValue struct {
Radians float64 Radians float64
// 字体颜色 // 字体颜色
FontColor Color FontColor Color
Orient string // 字体大小
Offset Box FontSize float64
Orient string
Offset Box
} }
type SeriesLabelPainter struct { type SeriesLabelPainter struct {
@ -89,6 +91,9 @@ func (o *SeriesLabelPainter) Add(value LabelValue) {
FontSize: labelFontSize, FontSize: labelFontSize,
Font: o.font, Font: o.font,
} }
if value.FontSize != 0 {
labelStyle.FontSize = value.FontSize
}
if !value.FontColor.IsZero() { if !value.FontColor.IsZero() {
label.Color = value.FontColor label.Color = value.FontColor
} }

View file

@ -26,8 +26,8 @@ import (
"errors" "errors"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
type tableChart struct { type tableChart struct {

View file

@ -24,7 +24,7 @@ package charts
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
const ThemeDark = "dark" const ThemeDark = "dark"

View file

@ -29,8 +29,8 @@ import (
"strings" "strings"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TrueFlag() *bool { func TrueFlag() *bool {

View file

@ -26,8 +26,8 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2" "git.smarteching.com/zeni/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "git.smarteching.com/zeni/go-chart/v2/drawing"
) )
func TestGetDefaultInt(t *testing.T) { func TestGetDefaultInt(t *testing.T) {

View file

@ -47,7 +47,13 @@ type XAxisOption struct {
// The line color of axis // The line color of axis
StrokeColor Color StrokeColor Color
// The color of label // The color of label
FontColor Color FontColor Color
// The text rotation of label
TextRotation float64
// The first axis
FirstAxis int
// The offset of label
LabelOffset Box
isValueAxis bool isValueAxis bool
} }
@ -81,6 +87,9 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
FontColor: opt.FontColor, FontColor: opt.FontColor,
Show: opt.Show, Show: opt.Show,
SplitLineColor: opt.Theme.GetAxisSplitLineColor(), SplitLineColor: opt.Theme.GetAxisSplitLineColor(),
TextRotation: opt.TextRotation,
LabelOffset: opt.LabelOffset,
FirstAxis: opt.FirstAxis,
} }
if opt.isValueAxis { if opt.isValueAxis {
axisOpt.SplitLineShow = true axisOpt.SplitLineShow = true

View file

@ -50,6 +50,8 @@ type YAxisOption struct {
DivideCount int DivideCount int
Unit int Unit int
isCategoryAxis bool isCategoryAxis bool
// The flag for show axis split line, set this to true will show axis split line
SplitLineShow *bool
} }
// NewYAxisOptions returns a y axis option // NewYAxisOptions returns a y axis option
@ -100,6 +102,9 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
axisOpt.StrokeWidth = 1 axisOpt.StrokeWidth = 1
axisOpt.SplitLineShow = false axisOpt.SplitLineShow = false
} }
if opt.SplitLineShow != nil {
axisOpt.SplitLineShow = *opt.SplitLineShow
}
return axisOpt return axisOpt
} }