diff --git a/axis.go b/axis.go index 5937ef0..775747b 100644 --- a/axis.go +++ b/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 diff --git a/bar_series.go b/bar_series.go index a2274f7..72f4cb1 100644 --- a/bar_series.go +++ b/bar_series.go @@ -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) } } diff --git a/charts.go b/charts.go index e0d2925..e396d73 100644 --- a/charts.go +++ b/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 } diff --git a/series.go b/series.go index 538f929..09a6c54 100644 --- a/series.go +++ b/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: diff --git a/theme.go b/theme.go index 4d86a64..1962a3f 100644 --- a/theme.go +++ b/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)] +}