refactor: support pie chart
This commit is contained in:
parent
be19cd728a
commit
4d8086a283
5 changed files with 195 additions and 48 deletions
38
axis.go
38
axis.go
|
|
@ -35,8 +35,9 @@ type (
|
|||
// 'category' 类目轴,适用于离散的类目数据。为该类型时类目数据可自动从 series.data 或 dataset.source 中取,或者可通过 xAxis.data 设置类目数据。
|
||||
// 'time' 时间轴,适用于连续的时序数据,与数值轴相比时间轴带有时间的格式化,在刻度计算上也有所不同,例如会根据跨度的范围来决定使用月,星期,日还是小时范围的刻度。
|
||||
// 'log' 对数轴。适用于对数数据。
|
||||
Type string
|
||||
Data []string
|
||||
Type string
|
||||
Data []string
|
||||
SplitNumber int
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ const axisStrokeWidth = 1
|
|||
|
||||
func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme string) (chart.XAxis, []float64) {
|
||||
data := xAxis.Data
|
||||
originalSize := len(data)
|
||||
// 如果居中,则需要多添加一个值
|
||||
if tickPosition == chart.TickPositionBetweenTicks {
|
||||
data = append([]string{
|
||||
|
|
@ -51,14 +53,36 @@ func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme strin
|
|||
}, data...)
|
||||
}
|
||||
|
||||
xValues := make([]float64, len(data))
|
||||
ticks := make([]chart.Tick, len(data))
|
||||
size := len(data)
|
||||
|
||||
xValues := make([]float64, size)
|
||||
ticks := make([]chart.Tick, 0)
|
||||
|
||||
maxTicks := xAxis.SplitNumber
|
||||
if maxTicks == 0 {
|
||||
maxTicks = 10
|
||||
}
|
||||
|
||||
// 计息最多每个unit至少放多个
|
||||
minUnitSize := originalSize / maxTicks
|
||||
if originalSize%maxTicks != 0 {
|
||||
minUnitSize++
|
||||
}
|
||||
unitSize := minUnitSize
|
||||
for i := minUnitSize; i < 2*minUnitSize; i++ {
|
||||
if originalSize%i == 0 {
|
||||
unitSize = i
|
||||
}
|
||||
}
|
||||
|
||||
for index, key := range data {
|
||||
f := float64(index)
|
||||
xValues[index] = f
|
||||
ticks[index] = chart.Tick{
|
||||
Value: f,
|
||||
Label: key,
|
||||
if index%unitSize == 0 || index == size-1 {
|
||||
ticks = append(ticks, chart.Tick{
|
||||
Value: f,
|
||||
Label: key,
|
||||
})
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ import (
|
|||
|
||||
const defaultBarMargin = 10
|
||||
|
||||
type BarSeriesCustomStyle struct {
|
||||
PointIndex int
|
||||
Index int
|
||||
Style chart.Style
|
||||
}
|
||||
|
||||
type BarSeries struct {
|
||||
BaseSeries
|
||||
Count int
|
||||
|
|
@ -37,7 +43,17 @@ type BarSeries struct {
|
|||
// 偏移量
|
||||
Offset int
|
||||
// 宽度
|
||||
BarWidth int
|
||||
BarWidth int
|
||||
CustomStyles []BarSeriesCustomStyle
|
||||
}
|
||||
|
||||
func (bs BarSeries) GetBarStyle(index, pointIndex int) chart.Style {
|
||||
for _, item := range bs.CustomStyles {
|
||||
if item.Index == index && item.PointIndex == pointIndex {
|
||||
return item.Style
|
||||
}
|
||||
}
|
||||
return chart.Style{}
|
||||
}
|
||||
|
||||
func (bs BarSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) {
|
||||
|
|
@ -79,6 +95,12 @@ func (bs BarSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange
|
|||
|
||||
for i := 0; i < bs.Len(); i++ {
|
||||
vx, vy := bs.GetValues(i)
|
||||
customStyle := bs.GetBarStyle(bs.Index, i)
|
||||
cloneStyle := style
|
||||
if !customStyle.IsZero() {
|
||||
cloneStyle.FillColor = customStyle.FillColor
|
||||
cloneStyle.StrokeColor = customStyle.StrokeColor
|
||||
}
|
||||
|
||||
x := cl + xrange.Translate(vx)
|
||||
// 由于bar是居中展示,因此需要往前移一个显示块
|
||||
|
|
@ -92,6 +114,6 @@ func (bs BarSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange
|
|||
Top: y,
|
||||
Right: x + barWidth,
|
||||
Bottom: canvasBox.Bottom - 1,
|
||||
}, style)
|
||||
}, cloneStyle)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
91
charts.go
91
charts.go
|
|
@ -24,10 +24,17 @@ package charts
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
ThemeLight = "light"
|
||||
ThemeDark = "dark"
|
||||
)
|
||||
|
||||
type (
|
||||
Title struct {
|
||||
Text string
|
||||
|
|
@ -46,28 +53,43 @@ type (
|
|||
Legend Legend
|
||||
TickPosition chart.TickPosition
|
||||
}
|
||||
ECharOption struct {
|
||||
Title struct {
|
||||
Text string
|
||||
TextStyle struct {
|
||||
Color string
|
||||
FontFamily string
|
||||
}
|
||||
}
|
||||
XAxis struct {
|
||||
Type string
|
||||
BoundaryGap bool
|
||||
Data []string
|
||||
)
|
||||
|
||||
type Chart interface {
|
||||
Render(rp chart.RendererProvider, w io.Writer) error
|
||||
}
|
||||
|
||||
type ECharOption struct {
|
||||
Title struct {
|
||||
Text string
|
||||
TextStyle struct {
|
||||
Color string
|
||||
FontFamily string
|
||||
}
|
||||
}
|
||||
)
|
||||
XAxis struct {
|
||||
Type string
|
||||
BoundaryGap *bool
|
||||
SplitNumber int
|
||||
Data []string
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ThemeLight = "light"
|
||||
ThemeDark = "dark"
|
||||
)
|
||||
func (o *Option) validate() error {
|
||||
xAxisCount := len(o.XAxis.Data)
|
||||
if len(o.Series) == 0 {
|
||||
return errors.New("series can not be empty")
|
||||
}
|
||||
|
||||
func render(c *chart.Chart, rp chart.RendererProvider) ([]byte, error) {
|
||||
for _, item := range o.Series {
|
||||
if len(item.Data) != xAxisCount {
|
||||
return errors.New("series and xAxis is not matched")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func render(c Chart, rp chart.RendererProvider) ([]byte, error) {
|
||||
buf := bytes.Buffer{}
|
||||
err := c.Render(rp, &buf)
|
||||
if err != nil {
|
||||
|
|
@ -76,14 +98,18 @@ func render(c *chart.Chart, rp chart.RendererProvider) ([]byte, error) {
|
|||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func ToPNG(c *chart.Chart) ([]byte, error) {
|
||||
func ToPNG(c Chart) ([]byte, error) {
|
||||
return render(c, chart.PNG)
|
||||
}
|
||||
|
||||
func ToSVG(c *chart.Chart) ([]byte, error) {
|
||||
func ToSVG(c Chart) ([]byte, error) {
|
||||
return render(c, chart.SVG)
|
||||
}
|
||||
func New(opt Option) *chart.Chart {
|
||||
func New(opt Option) (Chart, error) {
|
||||
err := opt.validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tickPosition := opt.TickPosition
|
||||
|
||||
xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme)
|
||||
|
|
@ -97,6 +123,25 @@ func New(opt Option) *chart.Chart {
|
|||
opt.Series[index].Name = opt.Legend.Data[index]
|
||||
}
|
||||
}
|
||||
if opt.Series[0].Type == SeriesPie {
|
||||
values := make(chart.Values, len(opt.Series))
|
||||
for index, item := range opt.Series {
|
||||
values[index] = chart.Value{
|
||||
Value: item.Data[0].Value,
|
||||
Label: item.Name,
|
||||
}
|
||||
}
|
||||
c := &chart.PieChart{
|
||||
Title: opt.Title.Text,
|
||||
Width: opt.Width,
|
||||
Height: opt.Height,
|
||||
Values: values,
|
||||
ColorPalette: &ThemeColorPalette{
|
||||
Theme: opt.Theme,
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
c := &chart.Chart{
|
||||
Title: opt.Title.Text,
|
||||
|
|
@ -106,7 +151,7 @@ func New(opt Option) *chart.Chart {
|
|||
YAxis: GetYAxis(opt.Theme),
|
||||
Series: GetSeries(opt.Series, tickPosition, opt.Theme),
|
||||
}
|
||||
// TODO 校验xAxis与yAxis的数量是否一致
|
||||
|
||||
// 设置secondary的样式
|
||||
c.YAxisSecondary.Style = c.YAxis.Style
|
||||
if legendSize != 0 {
|
||||
|
|
@ -114,5 +159,5 @@ func New(opt Option) *chart.Chart {
|
|||
DefaultLegend(c),
|
||||
}
|
||||
}
|
||||
return c
|
||||
return c, nil
|
||||
}
|
||||
|
|
|
|||
44
series.go
44
series.go
|
|
@ -24,13 +24,16 @@ package charts
|
|||
|
||||
import (
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
type SeriesData struct {
|
||||
Value float64
|
||||
Style chart.Style
|
||||
}
|
||||
type Series struct {
|
||||
Type string
|
||||
Name string
|
||||
Data []float64
|
||||
Data []SeriesData
|
||||
XValues []float64
|
||||
}
|
||||
|
||||
|
|
@ -40,15 +43,9 @@ const dotWith = 2
|
|||
const (
|
||||
SeriesBar = "bar"
|
||||
SeriesLine = "line"
|
||||
SeriesPie = "pie"
|
||||
)
|
||||
|
||||
func getSeriesColor(theme string, index int) drawing.Color {
|
||||
// TODO
|
||||
if theme == ThemeDark {
|
||||
}
|
||||
return SeriesColorsLight[index%len(SeriesColorsLight)]
|
||||
}
|
||||
|
||||
func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) []chart.Series {
|
||||
arr := make([]chart.Series, len(series))
|
||||
barCount := 0
|
||||
|
|
@ -58,34 +55,51 @@ func GetSeries(series []Series, tickPosition chart.TickPosition, theme string) [
|
|||
barCount++
|
||||
}
|
||||
}
|
||||
|
||||
for index, item := range series {
|
||||
style := chart.Style{
|
||||
StrokeWidth: lineStrokeWidth,
|
||||
StrokeColor: getSeriesColor(theme, index),
|
||||
// FillColor: getSeriesColor(theme, index),
|
||||
// TODO 调整为通过dot with color 生成
|
||||
DotColor: getSeriesColor(theme, index),
|
||||
DotWidth: dotWith,
|
||||
}
|
||||
// 如果居中,需要多增加一个点
|
||||
if tickPosition == chart.TickPositionBetweenTicks {
|
||||
item.Data = append([]float64{
|
||||
0.0,
|
||||
item.Data = append([]SeriesData{
|
||||
{
|
||||
Value: 0.0,
|
||||
},
|
||||
}, item.Data...)
|
||||
}
|
||||
yValues := make([]float64, len(item.Data))
|
||||
barCustomStyles := make([]BarSeriesCustomStyle, 0)
|
||||
for i, item := range item.Data {
|
||||
yValues[i] = item.Value
|
||||
if !item.Style.IsZero() {
|
||||
barCustomStyles = append(barCustomStyles, BarSeriesCustomStyle{
|
||||
PointIndex: i,
|
||||
Index: barIndex,
|
||||
Style: item.Style,
|
||||
})
|
||||
}
|
||||
}
|
||||
baseSeries := BaseSeries{
|
||||
Name: item.Name,
|
||||
XValues: item.XValues,
|
||||
Style: style,
|
||||
YValues: item.Data,
|
||||
YValues: yValues,
|
||||
TickPosition: tickPosition,
|
||||
}
|
||||
// TODO 判断类型
|
||||
switch item.Type {
|
||||
case SeriesBar:
|
||||
arr[index] = BarSeries{
|
||||
Count: barCount,
|
||||
Index: barIndex,
|
||||
BaseSeries: baseSeries,
|
||||
Count: barCount,
|
||||
Index: barIndex,
|
||||
BaseSeries: baseSeries,
|
||||
CustomStyles: barCustomStyles,
|
||||
}
|
||||
barIndex++
|
||||
default:
|
||||
|
|
|
|||
44
theme.go
44
theme.go
|
|
@ -22,7 +22,10 @@
|
|||
|
||||
package charts
|
||||
|
||||
import "github.com/wcharczuk/go-chart/v2/drawing"
|
||||
import (
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
var hiddenColor = drawing.Color{R: 110, G: 112, B: 121, A: 0}
|
||||
|
||||
|
|
@ -65,3 +68,42 @@ var SeriesColorsLight = []drawing.Color{
|
|||
A: 255,
|
||||
},
|
||||
}
|
||||
|
||||
type ThemeColorPalette struct {
|
||||
Theme string
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) BackgroundColor() drawing.Color {
|
||||
return chart.DefaultBackgroundColor
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) BackgroundStrokeColor() drawing.Color {
|
||||
return chart.DefaultBackgroundStrokeColor
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) CanvasColor() drawing.Color {
|
||||
return chart.DefaultCanvasColor
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) CanvasStrokeColor() drawing.Color {
|
||||
return chart.DefaultCanvasStrokeColor
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) AxisStrokeColor() drawing.Color {
|
||||
return chart.DefaultAxisColor
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) TextColor() drawing.Color {
|
||||
return chart.DefaultTextColor
|
||||
}
|
||||
|
||||
func (tp ThemeColorPalette) GetSeriesColor(index int) drawing.Color {
|
||||
return getSeriesColor(tp.Theme, index)
|
||||
}
|
||||
|
||||
func getSeriesColor(theme string, index int) drawing.Color {
|
||||
// TODO
|
||||
if theme == ThemeDark {
|
||||
}
|
||||
return SeriesColorsLight[index%len(SeriesColorsLight)]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue