From 92458aece27cff52dc3f6b4a2de1df677cf13276 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 22 Jun 2022 20:30:10 +0800 Subject: [PATCH 01/87] refactor: adjust font size of mark point --- README.md | 2 +- README_zh.md | 2 +- chart_option_test.go | 2 +- charts.go | 1 + echarts_test.go | 2 +- mark_point.go | 25 +++++++++++++++++++------ mark_point_test.go | 2 +- 7 files changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8183871..d7d94f7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [中文](./README_zh.md) -`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. +`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. The default format is `png` and the default theme is `light`. `Apache ECharts` is popular among Front-end developers, so `go-charts` supports the option of `Apache ECharts`. Developers can generate charts almost the same as `Apache ECharts`. diff --git a/README_zh.md b/README_zh.md index fed2d61..dbdaaf3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -3,7 +3,7 @@ [![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) -`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。 +`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。 `Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana): diff --git a/chart_option_test.go b/chart_option_test.go index c77bb4f..5e53e46 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/charts.go b/charts.go index cd1ac2b..a2e0ec0 100644 --- a/charts.go +++ b/charts.go @@ -28,6 +28,7 @@ import ( ) const labelFontSize = 10 +const smallLabelFontSize = 8 const defaultDotWidth = 2.0 const defaultStrokeWidth = 2.0 diff --git a/echarts_test.go b/echarts_test.go index 9c31286..4d50d9e 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } diff --git a/mark_point.go b/mark_point.go index 3d43a73..014b17f 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,6 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2/drawing" ) func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { @@ -63,7 +64,6 @@ func NewMarkPointPainter(p *Painter) *markPointPainter { func (m *markPointPainter) Render() (Box, error) { painter := m.p - theme := m.p.theme for _, opt := range m.options { s := opt.Series if len(s.MarkPoint.Data) == 0 { @@ -75,15 +75,23 @@ func (m *markPointPainter) Render() (Box, error) { if symbolSize == 0 { symbolSize = 30 } - painter.OverrideDrawingStyle(Style{ - FillColor: opt.FillColor, - }).OverrideTextStyle(Style{ - FontColor: theme.GetTextColor(), + textStyle := Style{ + FontColor: drawing.Color{ + R: 238, + G: 238, + B: 238, + A: 255, + }, FontSize: labelFontSize, StrokeWidth: 1, Font: opt.Font, - }) + } + painter.OverrideDrawingStyle(Style{ + FillColor: opt.FillColor, + }).OverrideTextStyle(textStyle) for _, markPointData := range s.MarkPoint.Data { + textStyle.FontSize = labelFontSize + painter.OverrideTextStyle(textStyle) p := points[summary.MinIndex] value := summary.MinValue switch markPointData.Type { @@ -95,6 +103,11 @@ func (m *markPointPainter) Render() (Box, error) { painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize) text := commafWithDigits(value) textBox := painter.MeasureText(text) + if textBox.Width() > symbolSize { + textStyle.FontSize = smallLabelFontSize + painter.OverrideTextStyle(textStyle) + textBox = painter.MeasureText(text) + } painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) } } diff --git a/mark_point_test.go b/mark_point_test.go index 1a810cf..ffa01a7 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -69,7 +69,7 @@ func TestMarkPoint(t *testing.T) { } return p.Bytes() }, - result: "\\n3", + result: "\\n3", }, } From 706896737b5f46a3581b061cb6ca25169032d847 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 22 Jun 2022 21:04:16 +0800 Subject: [PATCH 02/87] docs: update documents --- bar_chart.go | 4 +++- charts.go | 3 +++ funnel_chart.go | 3 +++ grid.go | 23 ++++++++++++++++------- horizontal_bar_chart.go | 2 ++ legend.go | 4 ++++ line_chart.go | 2 ++ mark_line.go | 2 ++ mark_point.go | 2 ++ painter.go | 2 +- pie_chart.go | 2 ++ radar_chart.go | 3 +++ range.go | 18 ++++++++++++++---- series.go | 24 +++++++++++++++++++----- theme.go | 1 + title.go | 1 + xaxis.go | 2 ++ yaxis.go | 3 +++ 18 files changed, 83 insertions(+), 18 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 0ac9f47..26f8da5 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -32,6 +32,7 @@ type barChart struct { opt *BarChartOption } +// NewBarChart returns a bar chart renderer func NewBarChart(p *Painter, opt BarChartOption) *barChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -43,6 +44,7 @@ func NewBarChart(p *Painter, opt BarChartOption) *barChart { } type BarChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -155,7 +157,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if distance == 0 { distance = 5 } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1) + text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) labelStyle := Style{ FontColor: theme.GetTextColor(), FontSize: labelFontSize, diff --git a/charts.go b/charts.go index a2e0ec0..6c1c92b 100644 --- a/charts.go +++ b/charts.go @@ -35,11 +35,14 @@ const defaultStrokeWidth = 2.0 var defaultChartWidth = 600 var defaultChartHeight = 400 +// SetDefaultWidth sets default width of chart func SetDefaultWidth(width int) { if width > 0 { defaultChartWidth = width } } + +// SetDefaultHeight sets default height of chart func SetDefaultHeight(height int) { if height > 0 { defaultChartHeight = height diff --git a/funnel_chart.go b/funnel_chart.go index 7c04bfe..719853a 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -34,6 +34,7 @@ type funnelChart struct { opt *FunnelChartOption } +// NewFunnelSeriesList returns a series list for funnel func NewFunnelSeriesList(values []float64) SeriesList { seriesList := make(SeriesList, len(values)) for index, value := range values { @@ -44,6 +45,7 @@ func NewFunnelSeriesList(values []float64) SeriesList { return seriesList } +// NewFunnelChart returns a funnel chart renderer func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -55,6 +57,7 @@ func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { } type FunnelChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font diff --git a/grid.go b/grid.go index 252fe2e..fb5dad6 100644 --- a/grid.go +++ b/grid.go @@ -28,16 +28,25 @@ type gridPainter struct { } type GridPainterOption struct { - StrokeWidth float64 - StrokeColor Color - Column int - Row int - IgnoreFirstRow bool - IgnoreLastRow bool + // The stroke width + StrokeWidth float64 + // The stroke color + StrokeColor Color + // The column of grid + Column int + // The row of grid + Row int + // Ignore first row + IgnoreFirstRow bool + // Ignore last row + IgnoreLastRow bool + // Ignore first column IgnoreFirstColumn bool - IgnoreLastColumn bool + // Ignore last column + IgnoreLastColumn bool } +// NewGridPainter returns new a grid renderer func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter { return &gridPainter{ p: p, diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index fb23734..30a9b7d 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -33,6 +33,7 @@ type horizontalBarChart struct { } type HorizontalBarChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -50,6 +51,7 @@ type HorizontalBarChartOption struct { Legend LegendOption } +// NewHorizontalBarChart returns a horizontal bar chart renderer func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/legend.go b/legend.go index 65db102..2acd35b 100644 --- a/legend.go +++ b/legend.go @@ -36,6 +36,7 @@ const IconRect = "rect" const IconLineDot = "lineDot" type LegendOption struct { + // The theme Theme ColorPalette // Text array of legend Data []string @@ -60,6 +61,7 @@ type LegendOption struct { Show *bool } +// NewLegendOption returns a legend option func NewLegendOption(labels []string, left ...string) LegendOption { opt := LegendOption{ Data: labels, @@ -70,6 +72,7 @@ func NewLegendOption(labels []string, left ...string) LegendOption { return opt } +// IsEmpty checks legend is empty func (opt *LegendOption) IsEmpty() bool { isEmpty := true for _, v := range opt.Data { @@ -81,6 +84,7 @@ func (opt *LegendOption) IsEmpty() bool { return isEmpty } +// NewLegendPainter returns a legend renderer func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { return &legendPainter{ p: p, diff --git a/line_chart.go b/line_chart.go index f171813..0770447 100644 --- a/line_chart.go +++ b/line_chart.go @@ -32,6 +32,7 @@ type lineChart struct { opt *LineChartOption } +// NewLineChart returns a line chart render func NewLineChart(p *Painter, opt LineChartOption) *lineChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -43,6 +44,7 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart { } type LineChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font diff --git a/mark_line.go b/mark_line.go index a0efcfb..af1062d 100644 --- a/mark_line.go +++ b/mark_line.go @@ -27,6 +27,7 @@ import ( "github.com/wcharczuk/go-chart/v2" ) +// NewMarkLine returns a series mark line func NewMarkLine(markLineTypes ...string) SeriesMarkLine { data := make([]SeriesMarkData, len(markLineTypes)) for index, t := range markLineTypes { @@ -48,6 +49,7 @@ func (m *markLinePainter) Add(opt markLineRenderOption) { m.options = append(m.options, opt) } +// NewMarkLinePainter returns a mark line renderer func NewMarkLinePainter(p *Painter) *markLinePainter { return &markLinePainter{ p: p, diff --git a/mark_point.go b/mark_point.go index 014b17f..f6c93f3 100644 --- a/mark_point.go +++ b/mark_point.go @@ -27,6 +27,7 @@ import ( "github.com/wcharczuk/go-chart/v2/drawing" ) +// NewMarkPoint returns a series mark point func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { data := make([]SeriesMarkData, len(markPointTypes)) for index, t := range markPointTypes { @@ -55,6 +56,7 @@ type markPointRenderOption struct { Points []Point } +// NewMarkPointPainter returns a mark point renderer func NewMarkPointPainter(p *Painter) *markPointPainter { return &markPointPainter{ p: p, diff --git a/painter.go b/painter.go index da07007..c250369 100644 --- a/painter.go +++ b/painter.go @@ -136,7 +136,7 @@ func PainterWidthHeightOption(width, height int) PainterOption { } } -// NewPainter creates a new painter +// NewPainter creates a painter func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { if opts.Width <= 0 || opts.Height <= 0 { return nil, errors.New("width/height can not be nil") diff --git a/pie_chart.go b/pie_chart.go index 972b4c1..6382140 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -36,6 +36,7 @@ type pieChart struct { } type PieChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -51,6 +52,7 @@ type PieChartOption struct { backgroundIsFilled bool } +// NewPieChart returns a pie chart renderer func NewPieChart(p *Painter, opt PieChartOption) *pieChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/radar_chart.go b/radar_chart.go index 5b8aa85..eab70d5 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -45,6 +45,7 @@ type RadarIndicator struct { } type RadarChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -62,6 +63,7 @@ type RadarChartOption struct { backgroundIsFilled bool } +// NewRadarIndicators returns a radar indicator list func NewRadarIndicators(names []string, values []float64) []RadarIndicator { if len(names) != len(values) { return nil @@ -76,6 +78,7 @@ func NewRadarIndicators(names []string, values []float64) []RadarIndicator { return indicators } +// NewRadarChart returns a radar chart renderer func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/range.go b/range.go index d5a9ef7..579a77f 100644 --- a/range.go +++ b/range.go @@ -37,13 +37,19 @@ type axisRange struct { } type AxisRangeOption struct { - Min float64 - Max float64 - Size int - Boundary bool + // The min value of axis + Min float64 + // The max value of axis + Max float64 + // The size of axis + Size int + // Boundary gap + Boundary bool + // The count of divide DivideCount int } +// NewRange returns a axis range func NewRange(opt AxisRangeOption) axisRange { max := opt.Max min := opt.Min @@ -88,6 +94,7 @@ func NewRange(opt AxisRangeOption) axisRange { } } +// Values returns values of range func (r axisRange) Values() []string { offset := (r.max - r.min) / float64(r.divideCount) values := make([]string, 0) @@ -108,10 +115,13 @@ func (r *axisRange) getRestHeight(value float64) int { return r.size - r.getHeight(value) } +// GetRange returns a range of index func (r *axisRange) GetRange(index int) (float64, float64) { unit := float64(r.size) / float64(r.divideCount) return unit * float64(index), unit * float64(index+1) } + +// AutoDivide divides the axis func (r *axisRange) AutoDivide() []int { return autoDivide(r.size, r.divideCount) } diff --git a/series.go b/series.go index 44c4749..87a719f 100644 --- a/series.go +++ b/series.go @@ -36,6 +36,7 @@ type SeriesData struct { Style Style } +// NewSeriesListDataFromValues returns a series list func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList { seriesList := make(SeriesList, len(values)) for index, value := range values { @@ -44,6 +45,7 @@ func NewSeriesListDataFromValues(values [][]float64, chartType ...string) Series return seriesList } +// NewSeriesFromValues returns a series func NewSeriesFromValues(values []float64, chartType ...string) Series { s := Series{ Data: NewSeriesDataFromValues(values), @@ -54,6 +56,7 @@ func NewSeriesFromValues(values []float64, chartType ...string) Series { return s } +// NewSeriesDataFromValues return a series data func NewSeriesDataFromValues(values []float64) []SeriesData { data := make([]SeriesData, len(values)) for index, value := range values { @@ -204,13 +207,19 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { } type seriesSummary struct { - MaxIndex int - MaxValue float64 - MinIndex int - MinValue float64 + // The index of max value + MaxIndex int + // The max value + MaxValue float64 + // The index of min value + MinIndex int + // The min value + MinValue float64 + // THe average value AverageValue float64 } +// Summary get summary of series func (s *Series) Summary() seriesSummary { minIndex := -1 maxIndex := -1 @@ -237,6 +246,7 @@ func (s *Series) Summary() seriesSummary { } } +// Names returns the names of series list func (sl SeriesList) Names() []string { names := make([]string, len(sl)) for index, s := range sl { @@ -245,8 +255,10 @@ func (sl SeriesList) Names() []string { return names } +// LabelFormatter label formatter type LabelFormatter func(index int, value float64, percent float64) string +// NewPieLabelFormatter returns a pie label formatter func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { layout = "{b}: {d}" @@ -254,13 +266,15 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { return NewLabelFormatter(seriesNames, layout) } -func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter { +// NewValueLabelFormatter returns a value formatter +func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { layout = "{c}" } return NewLabelFormatter(seriesNames, layout) } +// NewLabelFormatter returns a label formaatter func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { return func(index int, value, percent float64) string { // 如果无percent的则设置为<0 diff --git a/theme.go b/theme.go index 26786b9..31c3bf8 100644 --- a/theme.go +++ b/theme.go @@ -220,6 +220,7 @@ func init() { SetDefaultTheme(ThemeLight) } +// SetDefaultTheme sets default theme func SetDefaultTheme(name string) { defaultTheme = NewTheme(name) } diff --git a/title.go b/title.go index a805c55..5af4c39 100644 --- a/title.go +++ b/title.go @@ -84,6 +84,7 @@ type titlePainter struct { opt *TitleOption } +// NewTitlePainter returns a title renderer func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter { return &titlePainter{ p: p, diff --git a/xaxis.go b/xaxis.go index bfb57cb..00636a5 100644 --- a/xaxis.go +++ b/xaxis.go @@ -53,6 +53,7 @@ type XAxisOption struct { const defaultXAxisHeight = 30 +// NewXAxisOption returns a x axis option func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { opt := XAxisOption{ Data: data, @@ -89,6 +90,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { return axisOpt } +// NewBottomXAxis returns a bottom x axis renderer func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { return NewAxisPainter(p, opt.ToAxisOption()) } diff --git a/yaxis.go b/yaxis.go index 265ac59..b0bfa86 100644 --- a/yaxis.go +++ b/yaxis.go @@ -50,6 +50,7 @@ type YAxisOption struct { isCategoryAxis bool } +// NewYAxisOptions returns a y axis option func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { arr := [][]string{ data, @@ -95,6 +96,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { return axisOpt } +// NewLeftYAxis returns a left y axis renderer func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, @@ -102,6 +104,7 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { return NewAxisPainter(p, opt.ToAxisOption()) } +// NewRightYAxis returns a right y axis renderer func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, From 8c5647f65f160118c285a28b4e59a8ec6700ecc6 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 23 Jun 2022 20:32:25 +0800 Subject: [PATCH 03/87] test: add test for axis --- grid.go | 3 ++ grid_test.go | 18 ++++++++++ painter.go | 16 ++++++--- series_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++ table.go | 53 ++++++++++++++++++++++++++++ title.go | 3 ++ title_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ util.go | 24 +++++++++++++ yaxis.go | 14 +++++--- yaxis_test.go | 70 +++++++++++++++++++++++++++++++++++++ 10 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 series_test.go create mode 100644 table.go create mode 100644 title_test.go create mode 100644 yaxis_test.go diff --git a/grid.go b/grid.go index fb5dad6..0ebd226 100644 --- a/grid.go +++ b/grid.go @@ -32,6 +32,8 @@ type GridPainterOption struct { StrokeWidth float64 // The stroke color StrokeColor Color + // The spans of column + ColumnSpans []int // The column of grid Column int // The row of grid @@ -81,6 +83,7 @@ func (g *gridPainter) Render() (Box, error) { }) g.p.Grid(GridOption{ Column: opt.Column, + ColumnSpans: opt.ColumnSpans, Row: opt.Row, IgnoreColumnLines: ignoreColumnLines, IgnoreRowLines: ignoreRowLines, diff --git a/grid_test.go b/grid_test.go index f6880dc..3110a2b 100644 --- a/grid_test.go +++ b/grid_test.go @@ -54,6 +54,24 @@ func TestGrid(t *testing.T) { }, result: "\\n", }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewGridPainter(p, GridPainterOption{ + StrokeColor: drawing.ColorBlack, + ColumnSpans: []int{ + 2, + 5, + 3, + }, + Row: 6, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n", + }, } for _, tt := range tests { p, err := NewPainter(PainterOptions{ diff --git a/painter.go b/painter.go index c250369..1f9d418 100644 --- a/painter.go +++ b/painter.go @@ -69,8 +69,9 @@ type MultiTextOption struct { } type GridOption struct { - Column int - Row int + Column int + Row int + ColumnSpans []int // 忽略不展示的column IgnoreColumnLines []int // 忽略不展示的row @@ -542,6 +543,9 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box { var output chart.Box for index, line := range lines { + if line == "" { + continue + } x0 := x y0 := y + output.Height() p.Text(line, x0, y0) @@ -690,8 +694,12 @@ func (p *Painter) Grid(opt GridOption) *Painter { }) } } - if opt.Column > 0 { - values := autoDivide(width, opt.Column) + columnCount := sumInt(opt.ColumnSpans) + if columnCount == 0 { + columnCount = opt.Column + } + if columnCount > 0 { + values := autoDivideSpans(width, columnCount, opt.ColumnSpans) drawLines(values, opt.IgnoreColumnLines, true) } if opt.Row > 0 { diff --git a/series_test.go b/series_test.go new file mode 100644 index 0000000..40d2f91 --- /dev/null +++ b/series_test.go @@ -0,0 +1,89 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +package charts + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSeriesListDataFromValues(t *testing.T) { + assert := assert.New(t) + + assert.Equal(SeriesList{ + { + Type: ChartTypeBar, + Data: []SeriesData{ + { + Value: 1.0, + }, + }, + }, + }, NewSeriesListDataFromValues([][]float64{ + { + 1, + }, + }, ChartTypeBar)) +} + +func TestSeriesLists(t *testing.T) { + assert := assert.New(t) + seriesList := NewSeriesListDataFromValues([][]float64{ + { + 1, + 2, + }, + { + 10, + }, + }, ChartTypeBar) + + assert.Equal(2, len(seriesList.Filter(ChartTypeBar))) + assert.Equal(0, len(seriesList.Filter(ChartTypeLine))) + + max, min := seriesList.GetMaxMin(0) + assert.Equal(float64(10), max) + assert.Equal(float64(1), min) + + assert.Equal(seriesSummary{ + MaxIndex: 1, + MaxValue: 2, + MinIndex: 0, + MinValue: 1, + AverageValue: 1.5, + }, seriesList[0].Summary()) +} + +func TestFormatter(t *testing.T) { + assert := assert.New(t) + + assert.Equal("a: 12%", NewPieLabelFormatter([]string{ + "a", + "b", + }, "")(0, 10, 0.12)) + + assert.Equal("10", NewValueLabelFormatter([]string{ + "a", + "b", + }, "")(0, 10, 0.12)) +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..e47914c --- /dev/null +++ b/table.go @@ -0,0 +1,53 @@ +// 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 tableChart struct { + p *Painter + opt *TableChartOption +} + +func NewTableChart(p *Painter, opt TableChartOption) *tableChart { + if opt.Theme == nil { + opt.Theme = defaultTheme + } + return &tableChart{ + p: p, + opt: &opt, + } +} + +type TableChartOption struct { + // The theme + Theme ColorPalette + // The padding of table cell + Padding Box + // The header data of table + HeaderData []string + // The data of table + Data [][]string +} + +func (c *tableChart) Render() (Box, error) { + return BoxZero, nil +} diff --git a/title.go b/title.go index 5af4c39..5cdd161 100644 --- a/title.go +++ b/title.go @@ -97,6 +97,9 @@ func (t *titlePainter) Render() (Box, error) { p := t.p theme := opt.Theme + if theme == nil { + theme = p.theme + } if opt.Text == "" && opt.Subtext == "" { return BoxZero, nil } diff --git a/title_test.go b/title_test.go new file mode 100644 index 0000000..add8163 --- /dev/null +++ b/title_test.go @@ -0,0 +1,93 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTitleRenderer(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: "20", + Top: "20", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: "20%", + Top: "20", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: PositionRight, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/util.go b/util.go index adfa9fd..a33c6d2 100644 --- a/util.go +++ b/util.go @@ -90,6 +90,30 @@ func autoDivide(max, size int) []int { return values } +func autoDivideSpans(max, size int, spans []int) []int { + values := autoDivide(max, size) + // 重新合并 + if len(spans) != 0 { + newValues := make([]int, len(spans)+1) + newValues[0] = 0 + end := 0 + for index, v := range spans { + end += v + newValues[index+1] = values[end] + } + values = newValues + } + return values +} + +func sumInt(values []int) int { + sum := 0 + for _, v := range values { + sum += v + } + return sum +} + // measureTextMaxWidthHeight returns maxWidth and maxHeight of text list func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) { maxWidth := 0 diff --git a/yaxis.go b/yaxis.go index b0bfa86..eb9034c 100644 --- a/yaxis.go +++ b/yaxis.go @@ -65,14 +65,18 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { return opts } -func (opt *YAxisOption) ToAxisOption() AxisOption { +func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { position := PositionLeft if opt.Position == PositionRight { position = PositionRight } + theme := opt.Theme + if theme == nil { + theme = p.theme + } axisOpt := AxisOption{ Formatter: opt.Formatter, - Theme: opt.Theme, + Theme: theme, Data: opt.Data, Position: position, FontSize: opt.FontSize, @@ -81,7 +85,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { FontColor: opt.FontColor, BoundaryGap: FalseFlag(), SplitLineShow: true, - SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + SplitLineColor: theme.GetAxisSplitLineColor(), Show: opt.Show, } if !opt.Color.IsZero() { @@ -101,7 +105,7 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - return NewAxisPainter(p, opt.ToAxisOption()) + return NewAxisPainter(p, opt.ToAxisOption(p)) } // NewRightYAxis returns a right y axis renderer @@ -109,7 +113,7 @@ func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - axisOpt := opt.ToAxisOption() + axisOpt := opt.ToAxisOption(p) axisOpt.Position = PositionRight axisOpt.SplitLineShow = false return NewAxisPainter(p, axisOpt) diff --git a/yaxis_test.go b/yaxis_test.go new file mode 100644 index 0000000..0f565ac --- /dev/null +++ b/yaxis_test.go @@ -0,0 +1,70 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRightYAxis(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + opt := NewYAxisOptions([]string{ + "a", + "b", + "c", + "d", + })[0] + _, err := NewRightYAxis(p, opt).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nabcd", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme), PainterPaddingOption(Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + })) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 2fb0ebcbf7a5f8efd8c4f14c1b3056e2e29af688 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 23 Jun 2022 23:29:13 +0800 Subject: [PATCH 04/87] feat: support table render --- chart_option.go | 69 ++++++++++++++ examples/table/main.go | 79 ++++++++++++++++ painter.go | 22 +++-- painter_test.go | 28 +++--- table.go | 201 ++++++++++++++++++++++++++++++++++++++++- table_test.go | 100 ++++++++++++++++++++ 6 files changed, 476 insertions(+), 23 deletions(-) create mode 100644 examples/table/main.go create mode 100644 table_test.go diff --git a/chart_option.go b/chart_option.go index f2f3b51..b7b4714 100644 --- a/chart_option.go +++ b/chart_option.go @@ -23,6 +23,7 @@ package charts import ( + "fmt" "sort" "github.com/golang/freetype/truetype" @@ -336,3 +337,71 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { SeriesList: seriesList, }, opts...) } + +func TableRender(opt TableChartOption) (*Painter, error) { + if opt.Type == "" { + opt.Type = ChartOutputPNG + } + if opt.Width <= 0 { + opt.Width = defaultChartWidth + } + if opt.Height <= 0 { + opt.Height = defaultChartHeight + } + if opt.Font == nil { + opt.Font, _ = chart.GetDefaultFont() + } + + p, err := NewPainter(PainterOptions{ + Type: opt.Type, + Width: opt.Width, + Height: opt.Height, + Font: opt.Font, + }) + if err != nil { + return nil, err + } + info, err := NewTableChart(p, opt).render() + if err != nil { + return nil, err + } + fmt.Println(*info) + fmt.Println(info.Height) + + p, err = NewPainter(PainterOptions{ + Type: opt.Type, + Width: info.Width, + Height: info.Height, + Font: opt.Font, + }) + if err != nil { + return nil, err + } + _, err = NewTableChart(p, opt).Render() + if err != nil { + return nil, err + } + + // opt := ChartOption{} + // for _, fn := range opts { + // fn(&opt) + // } + // opt.fillDefault() + // p, err := NewPainter(PainterOptions{ + // Type: opt.Type, + // Width: opt.Width, + // Height: opt.Height, + // Font: opt.font, + // }) + // if err != nil { + // return nil, err + // } + // _, err = NewTableChart(p, TableChartOption{ + // Header: header, + // Data: data, + // }).Render() + // if err != nil { + // return nil, err + // } + return p, nil +} diff --git a/examples/table/main.go b/examples/table/main.go new file mode 100644 index 0000000..650566c --- /dev/null +++ b/examples/table/main.go @@ -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, "table.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.TableRender(charts.TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Spans: []int{ + 1, + 1, + 2, + 1, + 1, + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/painter.go b/painter.go index 1f9d418..c787315 100644 --- a/painter.go +++ b/painter.go @@ -468,7 +468,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter { return p } -func (p *Painter) SetBackground(width, height int, color Color) *Painter { +func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter { r := p.render s := chart.Style{ FillColor: color, @@ -476,12 +476,20 @@ func (p *Painter) SetBackground(width, height int, color Color) *Painter { // 背景色 p.SetDrawingStyle(s) defer p.ResetStyle() - // 设置背景色不使用box,因此不直接使用Painter - r.MoveTo(0, 0) - r.LineTo(width, 0) - r.LineTo(width, height) - r.LineTo(0, height) - r.LineTo(0, 0) + if len(inside) != 0 && inside[0] { + p.MoveTo(0, 0) + p.LineTo(width, 0) + p.LineTo(width, height) + p.LineTo(0, height) + p.LineTo(0, 0) + } else { + // 设置背景色不使用box,因此不直接使用Painter + r.MoveTo(0, 0) + r.LineTo(width, 0) + r.LineTo(width, height) + r.LineTo(0, height) + r.LineTo(0, 0) + } p.FillStroke() return p } diff --git a/painter_test.go b/painter_test.go index 8892563..96e41ef 100644 --- a/painter_test.go +++ b/painter_test.go @@ -143,13 +143,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -165,13 +165,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -187,13 +187,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -209,13 +209,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -231,13 +231,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -253,13 +253,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -279,7 +279,7 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, @@ -297,7 +297,7 @@ func TestPainter(t *testing.T) { { fn: func(p *Painter) { p.SetDrawingStyle(Style{ - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, diff --git a/table.go b/table.go index e47914c..34dee67 100644 --- a/table.go +++ b/table.go @@ -22,6 +22,14 @@ package charts +import ( + "errors" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) + type tableChart struct { p *Painter opt *TableChartOption @@ -38,16 +46,205 @@ func NewTableChart(p *Painter, opt TableChartOption) *tableChart { } type TableChartOption struct { + // The output type + Type string + // The width of table + Width int + // The height of table + Height int // The theme Theme ColorPalette // The padding of table cell Padding Box // The header data of table - HeaderData []string + Header []string // The data of table Data [][]string + // The span of table column + Spans []int + // The font size of table + FontSize float64 + Font *truetype.Font + // The font color of table + FontColor Color + // The background color of header + HeaderBackgroundColor Color + // The header font color + HeaderFontColor Color + // The background color of row + RowBackgroundColors []Color + // The background color + BackgroundColor Color +} + +var defaultTableHeaderColor = Color{ + R: 34, + G: 34, + B: 34, + A: 255, +} +var defaultTableRowColors = []Color{ + drawing.ColorWhite, + { + R: 242, + G: 242, + B: 242, + A: 255, + }, +} +var defaultTablePadding = Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, +} + +type renderInfo struct { + Width int + Height int + HeaderHeight int + RowHeights []int +} + +func (c *tableChart) render() (*renderInfo, error) { + info := renderInfo{ + RowHeights: make([]int, 0), + } + p := c.p + opt := c.opt + if len(opt.Header) == 0 { + return nil, errors.New("header can not be nil") + } + theme := opt.Theme + if theme == nil { + theme = p.theme + } + fontSize := opt.FontSize + if fontSize == 0 { + fontSize = 12 + } + fontColor := opt.FontColor + if fontColor.IsZero() { + fontColor = theme.GetTextColor() + } + font := opt.Font + if font == nil { + font = theme.GetFont() + } + headerFontColor := opt.HeaderFontColor + if opt.HeaderFontColor.IsZero() { + headerFontColor = drawing.ColorWhite + } + + spans := opt.Spans + if len(spans) != 0 && len(spans) != len(opt.Header) { + newSpans := make([]int, len(opt.Header)) + for index := range opt.Header { + if len(spans) < index { + newSpans[index] = 1 + } else { + newSpans[index] = spans[index] + } + } + spans = newSpans + } + + values := autoDivideSpans(p.Width(), len(opt.Header)+1, spans) + height := 0 + textStyle := Style{ + FontSize: fontSize, + FontColor: headerFontColor, + FillColor: headerFontColor, + Font: font, + } + p.SetStyle(textStyle) + + headerHeight := 0 + padding := opt.Padding + if padding.IsZero() { + padding = defaultTablePadding + } + + renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { + cellMaxHeight := 0 + paddingHeight := cellPadding.Top + cellPadding.Bottom + paddingWidth := cellPadding.Left + cellPadding.Right + for index, text := range textList { + x := values[index] + y := currentHeight + cellPadding.Top + width := values[index+1] - x + x += cellPadding.Left + width -= paddingWidth + box := p.TextFit(text, x, y+int(fontSize), width) + if box.Height()+paddingHeight > cellMaxHeight { + cellMaxHeight = box.Height() + paddingHeight + } + } + return cellMaxHeight + } + + headerHeight = renderTableCells(opt.Header, height, padding) + height += headerHeight + info.HeaderHeight = headerHeight + + textStyle.FontColor = fontColor + textStyle.FillColor = fontColor + p.SetStyle(textStyle) + for _, textList := range opt.Data { + cellHeight := renderTableCells(textList, height, padding) + info.RowHeights = append(info.RowHeights, cellHeight) + height += cellHeight + } + + info.Width = p.Width() + info.Height = height + return &info, nil } func (c *tableChart) Render() (Box, error) { - return BoxZero, nil + p := c.p + opt := c.opt + if !opt.BackgroundColor.IsZero() { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } + headerBGColor := opt.HeaderBackgroundColor + if headerBGColor.IsZero() { + headerBGColor = defaultTableHeaderColor + } + + r := p.render + newRender, err := chart.SVG(p.Width(), 100) + if err != nil { + return BoxZero, err + } + p.render = newRender + info, err := c.render() + if err != nil { + return BoxZero, err + } + p.render = r + // 如果设置表头背景色 + p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) + currentHeight := info.HeaderHeight + rowColors := opt.RowBackgroundColors + if len(rowColors) == 0 { + rowColors = defaultTableRowColors + } + for index, h := range info.RowHeights { + color := rowColors[index%len(rowColors)] + child := p.Child(PainterPaddingOption(Box{ + Top: currentHeight, + })) + child.SetBackground(p.Width(), h, color, true) + currentHeight += h + } + _, err = c.render() + if err != nil { + return BoxZero, err + } + + return Box{ + Right: info.Width, + Bottom: info.Height, + }, nil } diff --git a/table_test.go b/table_test.go new file mode 100644 index 0000000..c54de25 --- /dev/null +++ b/table_test.go @@ -0,0 +1,100 @@ +// 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 ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTableChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTableChart(p, TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Spans: []int{ + 1, + 1, + 2, + 1, + 1, + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + fmt.Println(string(data)) + assert.Equal(tt.result, string(data)) + } + +} From b3a3018ea2eb6f71b126ce053f1d9410225a0689 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:21:27 +0800 Subject: [PATCH 05/87] feat: support table redner --- README.md | 68 +++++++++++++++ README_zh.md | 64 ++++++++++++++ chart_option.go | 61 ++++++------- examples/charts/main.go | 42 +++++++++ examples/table/main.go | 76 ++++++++-------- painter.go | 4 + table.go | 188 ++++++++++++++++++++++++++++++---------- table_test.go | 46 +++++++++- 8 files changed, 433 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index d7d94f7..d0549f0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Screenshot of common charts, the left part is light theme, the right part is gra go-charts

+

+ go-table +

## Chart Type These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. @@ -374,6 +377,71 @@ func main() { } ``` +### Table + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + ### ECharts Render ```go diff --git a/README_zh.md b/README_zh.md index dbdaaf3..487a365 100644 --- a/README_zh.md +++ b/README_zh.md @@ -373,6 +373,70 @@ func main() { } ``` +### Table + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` ### ECharts Render ```go diff --git a/chart_option.go b/chart_option.go index b7b4714..39de686 100644 --- a/chart_option.go +++ b/chart_option.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "sort" "github.com/golang/freetype/truetype" @@ -338,24 +337,44 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { }, opts...) } -func TableRender(opt TableChartOption) (*Painter, error) { +// TableRender table chart render +func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) { + opt := TableChartOption{ + Header: header, + Data: data, + } + if len(spanMaps) != 0 { + spanMap := spanMaps[0] + spans := make([]int, len(opt.Header)) + for index := range spans { + v, ok := spanMap[index] + if !ok { + v = 1 + } + spans[index] = v + } + opt.Spans = spans + } + return TableOptionRender(opt) +} + +// TableOptionRender table render with option +func TableOptionRender(opt TableChartOption) (*Painter, error) { if opt.Type == "" { opt.Type = ChartOutputPNG } if opt.Width <= 0 { opt.Width = defaultChartWidth } - if opt.Height <= 0 { - opt.Height = defaultChartHeight - } if opt.Font == nil { opt.Font, _ = chart.GetDefaultFont() } p, err := NewPainter(PainterOptions{ - Type: opt.Type, - Width: opt.Width, - Height: opt.Height, + Type: opt.Type, + Width: opt.Width, + // 仅用于计算表格高度,因此随便设置即可 + Height: 100, Font: opt.Font, }) if err != nil { @@ -365,8 +384,6 @@ func TableRender(opt TableChartOption) (*Painter, error) { if err != nil { return nil, err } - fmt.Println(*info) - fmt.Println(info.Height) p, err = NewPainter(PainterOptions{ Type: opt.Type, @@ -377,31 +394,9 @@ func TableRender(opt TableChartOption) (*Painter, error) { if err != nil { return nil, err } - _, err = NewTableChart(p, opt).Render() + _, err = NewTableChart(p, opt).renderWithInfo(info) if err != nil { return nil, err } - - // opt := ChartOption{} - // for _, fn := range opts { - // fn(&opt) - // } - // opt.fillDefault() - // p, err := NewPainter(PainterOptions{ - // Type: opt.Type, - // Width: opt.Width, - // Height: opt.Height, - // Font: opt.font, - // }) - // if err != nil { - // return nil, err - // } - // _, err = NewTableChart(p, TableChartOption{ - // Header: header, - // Data: data, - // }).Render() - // if err != nil { - // return nil, err - // } return p, nil } diff --git a/examples/charts/main.go b/examples/charts/main.go index 0e1d48e..7b14919 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -92,6 +92,48 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha bytesList = append(bytesList, buf) } + p, err := charts.TableOptionRender(charts.TableChartOption{ + Type: charts.ChartOutputSVG, + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }) + if err != nil { + panic(err) + } + buf, err := p.Bytes() + if err != nil { + panic(err) + } + bytesList = append(bytesList, buf) + data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) w.Header().Set("Content-Type", "text/html") w.Write(data) diff --git a/examples/table/main.go b/examples/table/main.go index 650566c..ee8147e 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -24,45 +24,49 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.TableRender(charts.TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", + charts.SetDefaultWidth(810) + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", }, - Spans: []int{ - 1, - 1, - 2, - 1, - 1, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", }, - }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, ) if err != nil { panic(err) diff --git a/painter.go b/painter.go index c787315..06973b6 100644 --- a/painter.go +++ b/painter.go @@ -38,6 +38,8 @@ type Painter struct { parent *Painter style Style theme ColorPalette + // 类型 + outputType string } type PainterOptions struct { @@ -169,6 +171,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { Bottom: opts.Height, }, font: font, + // 类型 + outputType: opts.Type, } p.setOptions(opt...) if p.theme == nil { diff --git a/table.go b/table.go index 34dee67..ffa7013 100644 --- a/table.go +++ b/table.go @@ -35,6 +35,7 @@ type tableChart struct { opt *TableChartOption } +// NewTableChart returns a table chart render func NewTableChart(p *Painter, opt TableChartOption) *tableChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -50,8 +51,6 @@ type TableChartOption struct { Type string // The width of table Width int - // The height of table - Height int // The theme Theme ColorPalette // The padding of table cell @@ -64,7 +63,9 @@ type TableChartOption struct { Spans []int // The font size of table FontSize float64 - Font *truetype.Font + // The font family, which should be installed first + FontFamily string + Font *truetype.Font // The font color of table FontColor Color // The background color of header @@ -77,26 +78,101 @@ type TableChartOption struct { BackgroundColor Color } -var defaultTableHeaderColor = Color{ - R: 34, - G: 34, - B: 34, - A: 255, +type TableSetting struct { + // The color of header + HeaderColor Color + // The color of heder text + HeaderFontColor Color + // The color of table text + FontColor Color + // The color list of row + RowColors []Color + // The padding of cell + Padding Box } -var defaultTableRowColors = []Color{ - drawing.ColorWhite, - { - R: 242, - G: 242, - B: 242, + +var TableLightThemeSetting = TableSetting{ + HeaderColor: Color{ + R: 240, + G: 240, + B: 240, A: 255, }, + HeaderFontColor: Color{ + R: 98, + G: 105, + B: 118, + A: 255, + }, + FontColor: Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + RowColors: []Color{ + drawing.ColorWhite, + { + R: 247, + G: 247, + B: 247, + A: 255, + }, + }, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, } -var defaultTablePadding = Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, + +var TableDarkThemeSetting = TableSetting{ + HeaderColor: Color{ + R: 38, + G: 38, + B: 42, + A: 255, + }, + HeaderFontColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + FontColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + RowColors: []Color{ + { + R: 24, + G: 24, + B: 28, + A: 255, + }, + { + R: 38, + G: 38, + B: 42, + A: 255, + }, + }, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, +} + +var tableDefaultSetting = TableLightThemeSetting + +// SetDefaultTableSetting sets the default setting for table +func SetDefaultTableSetting(setting TableSetting) { + tableDefaultSetting = setting } type renderInfo struct { @@ -106,12 +182,12 @@ type renderInfo struct { RowHeights []int } -func (c *tableChart) render() (*renderInfo, error) { +func (t *tableChart) render() (*renderInfo, error) { info := renderInfo{ RowHeights: make([]int, 0), } - p := c.p - opt := c.opt + p := t.p + opt := t.opt if len(opt.Header) == 0 { return nil, errors.New("header can not be nil") } @@ -125,7 +201,7 @@ func (c *tableChart) render() (*renderInfo, error) { } fontColor := opt.FontColor if fontColor.IsZero() { - fontColor = theme.GetTextColor() + fontColor = tableDefaultSetting.FontColor } font := opt.Font if font == nil { @@ -133,14 +209,14 @@ func (c *tableChart) render() (*renderInfo, error) { } headerFontColor := opt.HeaderFontColor if opt.HeaderFontColor.IsZero() { - headerFontColor = drawing.ColorWhite + headerFontColor = tableDefaultSetting.HeaderFontColor } spans := opt.Spans - if len(spans) != 0 && len(spans) != len(opt.Header) { + if len(spans) != len(opt.Header) { newSpans := make([]int, len(opt.Header)) for index := range opt.Header { - if len(spans) < index { + if index >= len(spans) { newSpans[index] = 1 } else { newSpans[index] = spans[index] @@ -149,7 +225,8 @@ func (c *tableChart) render() (*renderInfo, error) { spans = newSpans } - values := autoDivideSpans(p.Width(), len(opt.Header)+1, spans) + sum := sumInt(spans) + values := autoDivideSpans(p.Width(), sum, spans) height := 0 textStyle := Style{ FontSize: fontSize, @@ -162,7 +239,7 @@ func (c *tableChart) render() (*renderInfo, error) { headerHeight := 0 padding := opt.Padding if padding.IsZero() { - padding = defaultTablePadding + padding = tableDefaultSetting.Padding } renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { @@ -201,34 +278,23 @@ func (c *tableChart) render() (*renderInfo, error) { return &info, nil } -func (c *tableChart) Render() (Box, error) { - p := c.p - opt := c.opt +func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { + p := t.p + opt := t.opt if !opt.BackgroundColor.IsZero() { p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) } headerBGColor := opt.HeaderBackgroundColor if headerBGColor.IsZero() { - headerBGColor = defaultTableHeaderColor + headerBGColor = tableDefaultSetting.HeaderColor } - r := p.render - newRender, err := chart.SVG(p.Width(), 100) - if err != nil { - return BoxZero, err - } - p.render = newRender - info, err := c.render() - if err != nil { - return BoxZero, err - } - p.render = r // 如果设置表头背景色 p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) currentHeight := info.HeaderHeight rowColors := opt.RowBackgroundColors if len(rowColors) == 0 { - rowColors = defaultTableRowColors + rowColors = tableDefaultSetting.RowColors } for index, h := range info.RowHeights { color := rowColors[index%len(rowColors)] @@ -238,7 +304,7 @@ func (c *tableChart) Render() (Box, error) { child.SetBackground(p.Width(), h, color, true) currentHeight += h } - _, err = c.render() + _, err := t.render() if err != nil { return BoxZero, err } @@ -248,3 +314,35 @@ func (c *tableChart) Render() (Box, error) { Bottom: info.Height, }, nil } + +func (t *tableChart) Render() (Box, error) { + p := t.p + opt := t.opt + if !opt.BackgroundColor.IsZero() { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } + headerBGColor := opt.HeaderBackgroundColor + if headerBGColor.IsZero() { + headerBGColor = tableDefaultSetting.HeaderColor + } + if opt.Font == nil && opt.FontFamily != "" { + opt.Font, _ = GetFont(opt.FontFamily) + } + + r := p.render + fn := chart.PNG + if p.outputType == ChartOutputSVG { + fn = chart.SVG + } + newRender, err := fn(p.Width(), 100) + if err != nil { + return BoxZero, err + } + p.render = newRender + info, err := t.render() + if err != nil { + return BoxZero, err + } + p.render = r + return t.renderWithInfo(info) +} diff --git a/table_test.go b/table_test.go index c54de25..41a857c 100644 --- a/table_test.go +++ b/table_test.go @@ -51,7 +51,8 @@ func TestTableChart(t *testing.T) { 1, 2, 1, - 1, + // span和header不匹配,最后自动设置为1 + // 1, }, Data: [][]string{ { @@ -82,6 +83,48 @@ func TestTableChart(t *testing.T) { } return p.Bytes() }, + result: "\\nNameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTableChart(p, TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nNameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", }, } for _, tt := range tests { @@ -96,5 +139,4 @@ func TestTableChart(t *testing.T) { fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } - } From da3ad16c23abb669361295e92a38680409dbe01e Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:23:50 +0800 Subject: [PATCH 06/87] chore: upload table preview --- assets/go-table.png | Bin 0 -> 17387 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/go-table.png diff --git a/assets/go-table.png b/assets/go-table.png new file mode 100644 index 0000000000000000000000000000000000000000..5848ad88b41c8266171e4211002499fea1244304 GIT binary patch literal 17387 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 literal 0 HcmV?d00001 From f1276067d763e612b927c839c7e29b5af9ff7a23 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:33:05 +0800 Subject: [PATCH 07/87] fix: fix lint --- README.md | 3 ++- README_zh.md | 6 +++++- table.go | 4 ---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d0549f0..3e713a6 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ Screenshot of common charts, the left part is light theme, the right part is gra

go-table

+ ## Chart Type -These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. +These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel` and `table`. ## Example diff --git a/README_zh.md b/README_zh.md index 487a365..94a9e14 100644 --- a/README_zh.md +++ b/README_zh.md @@ -12,9 +12,13 @@ go-charts

+

+ go-table +

Date: Sat, 25 Jun 2022 08:49:00 +0800 Subject: [PATCH 08/87] test: add bench mark --- charts_test.go | 255 +++++++++++++++++++++++++++++++++++++++++++++++++ table_test.go | 2 - 2 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 charts_test.go diff --git a/charts_test.go b/charts_test.go new file mode 100644 index 0000000..da75ee5 --- /dev/null +++ b/charts_test.go @@ -0,0 +1,255 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "errors" + "testing" + + "github.com/wcharczuk/go-chart/v2" +) + +func BenchmarkMultiChartPNGRender(b *testing.B) { + for i := 0; i < b.N; i++ { + opt := ChartOption{ + Type: ChartOutputPNG, + Legend: LegendOption{ + Top: "-90", + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Padding: chart.Box{ + Top: 100, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: NewXAxisOption([]string{ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017", + }), + YAxisOptions: []YAxisOption{ + { + + Min: NewFloatPoint(0), + Max: NewFloatPoint(90), + }, + }, + SeriesList: []Series{ + NewSeriesFromValues([]float64{ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1, + }), + NewSeriesFromValues([]float64{ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7, + }), + NewSeriesFromValues([]float64{ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5, + }, ChartTypeBar), + NewSeriesFromValues([]float64{ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1, + }, ChartTypeBar), + }, + Children: []ChartOption{ + { + Legend: LegendOption{ + Show: FalseFlag(), + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Box: chart.Box{ + Top: 20, + Left: 400, + Right: 500, + Bottom: 120, + }, + SeriesList: NewPieSeriesList([]float64{ + 435.9, + 354.3, + 285.9, + 204.5, + }, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }, + }, + } + d, err := Render(opt) + if err != nil { + panic(err) + } + buf, err := d.Bytes() + if err != nil { + panic(err) + } + if len(buf) == 0 { + panic(errors.New("data is nil")) + } + } +} + +func BenchmarkMultiChartSVGRender(b *testing.B) { + for i := 0; i < b.N; i++ { + opt := ChartOption{ + Legend: LegendOption{ + Top: "-90", + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Padding: chart.Box{ + Top: 100, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: NewXAxisOption([]string{ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017", + }), + YAxisOptions: []YAxisOption{ + { + + Min: NewFloatPoint(0), + Max: NewFloatPoint(90), + }, + }, + SeriesList: []Series{ + NewSeriesFromValues([]float64{ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1, + }), + NewSeriesFromValues([]float64{ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7, + }), + NewSeriesFromValues([]float64{ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5, + }, ChartTypeBar), + NewSeriesFromValues([]float64{ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1, + }, ChartTypeBar), + }, + Children: []ChartOption{ + { + Legend: LegendOption{ + Show: FalseFlag(), + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Box: chart.Box{ + Top: 20, + Left: 400, + Right: 500, + Bottom: 120, + }, + SeriesList: NewPieSeriesList([]float64{ + 435.9, + 354.3, + 285.9, + 204.5, + }, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }, + }, + } + d, err := Render(opt) + if err != nil { + panic(err) + } + buf, err := d.Bytes() + if err != nil { + panic(err) + } + if len(buf) == 0 { + panic(errors.New("data is nil")) + } + } +} diff --git a/table_test.go b/table_test.go index 41a857c..a958c95 100644 --- a/table_test.go +++ b/table_test.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -136,7 +135,6 @@ func TestTableChart(t *testing.T) { assert.Nil(err) data, err := tt.render(p) assert.Nil(err) - fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } } From aed2250cb847817cf46508c4798cc17816f53ef5 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:51:49 +0800 Subject: [PATCH 09/87] docs: update documents --- README.md | 2 +- README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e713a6..1e4ea8b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Screenshot of common charts, the left part is light theme, the right part is gra ## Chart Type -These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel` and `table`. +These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`. ## Example diff --git a/README_zh.md b/README_zh.md index 94a9e14..87c42fa 100644 --- a/README_zh.md +++ b/README_zh.md @@ -18,7 +18,7 @@ ## 支持图表类型 -支持以下的图表类型:`line`, `bar`, `pie`, `radar`, `funnel` 以及 `table` +支持以下的图表类型:`line`, `bar`, `horizontal bar`, `pie`, `radar`, `funnel` 以及 `table` ## 示例 From 0eecb6c5b7002dba33c8b2d7a9c06341f8395d25 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 09:06:50 +0800 Subject: [PATCH 10/87] docs: update document --- assets/go-table.png | Bin 17387 -> 37028 bytes examples/table/main.go | 1 + 2 files changed, 1 insertion(+) diff --git a/assets/go-table.png b/assets/go-table.png index 5848ad88b41c8266171e4211002499fea1244304..b05a3d02eac00825c3125810803aab41e4d70a4e 100644 GIT binary patch literal 37028 zcmaI7bx<5Z*YJzG6WoF)xVr@nZoy@7ch@BlT!Onh!QEkTcY?cH2oQW>;oIlA@B7uQ z`^W8?+M4d|nQA$G`kY^%iBeOM!$c!RgMop;l>aKN0Rsci1Oo%hf&vFEDR!JIfq_Y| zmzVzX%?I|+GAAqy3Xon008r!+yAEo z$7@Eit)m!qa78~no@8JOnw~QTp}#)p@0zOb^NG2#HCp_xJayXh1M>U#e`b1xU9hm@ z&$EoxXJ%(@{fas+DT+6QyWV)&fApIO2nn^a@A=Kw{W~v97)!|+P-uG#Ng@S{2VRyr zwR7t3tV&m>OxHMOH9&d(H4#qZ!#bZB&T8V zZ9lf7h@Jcf(&t~Isl3zwGWvPEe3aHF=R!-5v~01k-oJqGG;V7T=;DA|{-muzE3cVmHn@+sy8_vb?+ffdsAhv>#8#= z0d)1esKTzYzgQO+Zqr?yTb|#VO4Qr)BO1 zM0_6zv3(r`t z6_OIkhS;N`_WkNmGb2PuK)|Bqgi!1L4igvW__s|UJZ*sBD`aa+v^P1i;j>UbbVvaz zUO+)7NdF$^*|%$0CcCUEI|GLDlg;quW&S4sVkz%y-N6BVUAY4X*wfE%M4RO&-`LPDC)@J&|M}gKIoWH6V0k`ZTyroA>*F42Ys`OqUuWb1r)FurPUfbsHZU?+ zi=3sC^MN)4%WVr|Ui{AO63%@nARuseqgoV*jlndmx zWM@On1Q~b{H1pn{XV8I-D`nQlJsEZVrz-;s6EzY2Dn`?6bLw{-%xYl0h{E|_-vqWG zPT;bJ4z_*c5`f~j0EQBU*Ho-w0{Su>Q#U)*7?m+M;jnV+6H6XuP!8pCxDC_iSG|0t zg;Syx*ASs!2%+>YD(&;^bl?F`tU&OBr<>clxB$(<{1?hi?)( zVN5u~`{^uXXP!2~+uB?F#9P}~Q2D7vesaY&aPAkCv*^b5w&hM^9S}ry*xXo;gfo=< zotTfw!Jq@>PO6{{9j93yb7(M(ji|MS4yQgn;|f z`4JwH0?ZsC%nFlm6^c`x36(CLE14mQOZxJchZAt)y?K^?eoK^iO7FmS&9@||0D6j- zvGioFy3if4K?-AhwyTr{?P_e^FI69v03ylHgr?)a;Eu<7Jz`aU6jbrx`N)!(7k<2A zr=5r{8YCA)BvcoJad}PmalPb76D2cJdBRJdf6d=XLu$@XR0CXEoTc9qNgRpImZpEr&InAmkcb7t&p1hT{x(BBnuLN`-Se4l!rV^d$@8>(i+uh@aOfP209-i z8t&28+$+OsdpS9|eKix5tc&3kOj(ewO3LD*rmfX++Kq0qVpW~9LkHUkHQ0_oJ1c*I zeR1`g+ONGyKAD+j0vYpwi6=dnmh8-_=Bd(P&IuP%h@};cHB_3-GWlcyiE>Ojrxro1;5TK2cQ7vXW`-o>O?HE7|n%nrsI zOZ&-z68M+Doww^>-wU9Z|3saV!-1!x|3njIq+CdIY)RAWYvK@~FEa33^o%HtBd=S{ zu0##mGy3Y(@5AWaRH)LaLd*}&3+yzxXV3Re-#2tpa*PX>Th2SRG3%uW%uW}G-=V4x zsS9JUah_#)X_o>raG@+-jSdd0wOYH7q`&+jbVl)}0#Y)Mk5yI>Dj~Wu3>HJt0B5}RO$a=#8HNY9bVb#&f;p8c3thmOui zM%i~tzszEBC=dEABBimdoYhEdrnjAUr|9^S5&F∓=KP*rXQ_7C$MY4kkTJ>x@hK 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%0A4C`wRf5K}TaGSwU<#E097z}`w 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 diff --git a/examples/table/main.go b/examples/table/main.go index ee8147e..4207b01 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -24,6 +24,7 @@ func writeFile(buf []byte) error { } func main() { + // charts.SetDefaultTableSetting(charts.TableDarkThemeSetting) charts.SetDefaultWidth(810) header := []string{ "Name", From d53fa1a329bed63efb02afdcfbb4cc338f233990 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 28 Jun 2022 20:21:06 +0800 Subject: [PATCH 11/87] feat: support customize table cell style --- examples/table/main.go | 55 ++++++++++++++++++++++-- table.go | 96 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 9 deletions(-) diff --git a/examples/table/main.go b/examples/table/main.go index 4207b01..f332851 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -4,18 +4,20 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "github.com/vicanso/go-charts/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) -func writeFile(buf []byte) error { +func writeFile(buf []byte, filename string) error { tmpPath := "./tmp" err := os.MkdirAll(tmpPath, 0700) if err != nil { return err } - file := filepath.Join(tmpPath, "table.png") + file := filepath.Join(tmpPath, filename) err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err @@ -77,7 +79,54 @@ func main() { if err != nil { panic(err) } - err = writeFile(buf) + err = writeFile(buf, "table.png") + if err != nil { + panic(err) + } + + p, err = charts.TableOptionRender(charts.TableChartOption{ + Header: header, + Data: data, + CellTextStyle: func(tc charts.TableCell) *charts.Style { + row := tc.Row + column := tc.Column + style := tc.Style + if column == 1 && row != 0 { + age, _ := strconv.Atoi(tc.Text) + if age < 40 { + style.FontColor = drawing.ColorGreen + } else { + style.FontColor = drawing.ColorRed + } + return &style + } + return nil + }, + CellStyle: func(tc charts.TableCell) *charts.Style { + row := tc.Row + column := tc.Column + if row == 2 && column == 1 { + return &charts.Style{ + FillColor: drawing.ColorBlue, + } + } + if row == 3 && column == 4 { + return &charts.Style{ + FillColor: drawing.ColorRed.WithAlpha(100), + } + } + return nil + }, + }) + if err != nil { + panic(err) + } + + buf, err = p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf, "table-color.png") if err != nil { panic(err) } diff --git a/table.go b/table.go index 0e72be5..d1af5f9 100644 --- a/table.go +++ b/table.go @@ -46,6 +46,17 @@ func NewTableChart(p *Painter, opt TableChartOption) *tableChart { } } +type TableCell struct { + // Text the text of table cell + Text string + // Style the current style of table cell + Style Style + // Row the row index of table cell + Row int + // Column the column index of table cell + Column int +} + type TableChartOption struct { // The output type Type string @@ -76,6 +87,10 @@ type TableChartOption struct { RowBackgroundColors []Color // The background color BackgroundColor Color + // CellTextStyle customize text style of table cell + CellTextStyle func(TableCell) *Style + // CellStyle customize drawing style of table cell + CellStyle func(TableCell) *Style } type TableSetting struct { @@ -180,6 +195,7 @@ type renderInfo struct { Height int HeaderHeight int RowHeights []int + ColumnWidths []int } func (t *tableChart) render() (*renderInfo, error) { @@ -227,6 +243,15 @@ func (t *tableChart) render() (*renderInfo, error) { sum := sumInt(spans) values := autoDivideSpans(p.Width(), sum, spans) + columnWidths := make([]int, 0) + for index, v := range values { + if index == len(values)-1 { + break + } + columnWidths = append(columnWidths, values[index+1]-v) + } + info.ColumnWidths = columnWidths + height := 0 textStyle := Style{ FontSize: fontSize, @@ -234,25 +259,48 @@ func (t *tableChart) render() (*renderInfo, error) { FillColor: headerFontColor, Font: font, } - p.SetStyle(textStyle) headerHeight := 0 padding := opt.Padding if padding.IsZero() { padding = tableDefaultSetting.Padding } + getCellTextStyle := opt.CellTextStyle + if getCellTextStyle == nil { + getCellTextStyle = func(_ TableCell) *Style { + return nil + } + } - renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { + // 表格单元的处理 + renderTableCells := func( + currentStyle Style, + rowIndex int, + textList []string, + currentHeight int, + cellPadding Box, + ) int { cellMaxHeight := 0 paddingHeight := cellPadding.Top + cellPadding.Bottom paddingWidth := cellPadding.Left + cellPadding.Right for index, text := range textList { + cellStyle := getCellTextStyle(TableCell{ + Text: text, + Row: rowIndex, + Column: index, + Style: currentStyle, + }) + if cellStyle == nil { + cellStyle = ¤tStyle + } + p.SetStyle(*cellStyle) x := values[index] y := currentHeight + cellPadding.Top width := values[index+1] - x x += cellPadding.Left width -= paddingWidth box := p.TextFit(text, x, y+int(fontSize), width) + // 计算最高的高度 if box.Height()+paddingHeight > cellMaxHeight { cellMaxHeight = box.Height() + paddingHeight } @@ -260,15 +308,16 @@ func (t *tableChart) render() (*renderInfo, error) { return cellMaxHeight } - headerHeight = renderTableCells(opt.Header, height, padding) + // 表头的处理 + headerHeight = renderTableCells(textStyle, 0, opt.Header, height, padding) height += headerHeight info.HeaderHeight = headerHeight + // 表格内容的处理 textStyle.FontColor = fontColor textStyle.FillColor = fontColor - p.SetStyle(textStyle) - for _, textList := range opt.Data { - cellHeight := renderTableCells(textList, height, padding) + for index, textList := range opt.Data { + cellHeight := renderTableCells(textStyle, index+1, textList, height, padding) info.RowHeights = append(info.RowHeights, cellHeight) height += cellHeight } @@ -304,6 +353,41 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { child.SetBackground(p.Width(), h, color, true) currentHeight += h } + // 根据是否有设置表格样式调整背景色 + getCellStyle := opt.CellStyle + if getCellStyle != nil { + arr := [][]string{ + opt.Header, + } + arr = append(arr, opt.Data...) + top := 0 + heights := []int{ + info.HeaderHeight, + } + heights = append(heights, info.RowHeights...) + // 循环所有表格单元,生成背景色 + for i, textList := range arr { + left := 0 + for j, v := range textList { + style := getCellStyle(TableCell{ + Text: v, + Row: i, + Column: j, + }) + if style != nil && !style.FillColor.IsZero() { + child := p.Child(PainterPaddingOption(Box{ + Top: top, + Left: left, + })) + w := info.ColumnWidths[j] + h := heights[i] + child.SetBackground(w, h, style.FillColor, true) + } + left += info.ColumnWidths[j] + } + top += heights[i] + } + } _, err := t.render() if err != nil { return BoxZero, err From f483e2a850e363b848fd09d4521266fdc608e41a Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 29 Jun 2022 20:15:58 +0800 Subject: [PATCH 12/87] feat: support text align for table cell --- assets/go-table.png | Bin 37028 -> 30679 bytes examples/table/main.go | 98 ++++++++++++++++++++++++++++++----------- painter.go | 14 +++++- table.go | 24 +++++++--- 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/assets/go-table.png b/assets/go-table.png index b05a3d02eac00825c3125810803aab41e4d70a4e..88ec49c43ae25b60269ad5800bc06154d491c194 100644 GIT binary patch literal 30679 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 RhT9cA%JLd=r84Fp{|_XcF>?R_ literal 37028 zcmaI7bx<5Z*YJzG6WoF)xVr@nZoy@7ch@BlT!Onh!QEkTcY?cH2oQW>;oIlA@B7uQ z`^W8?+M4d|nQA$G`kY^%iBeOM!$c!RgMop;l>aKN0Rsci1Oo%hf&vFEDR!JIfq_Y| zmzVzX%?I|+GAAqy3Xon008r!+yAEo z$7@Eit)m!qa78~no@8JOnw~QTp}#)p@0zOb^NG2#HCp_xJayXh1M>U#e`b1xU9hm@ z&$EoxXJ%(@{fas+DT+6QyWV)&fApIO2nn^a@A=Kw{W~v97)!|+P-uG#Ng@S{2VRyr zwR7t3tV&m>OxHMOH9&d(H4#qZ!#bZB&T8V zZ9lf7h@Jcf(&t~Isl3zwGWvPEe3aHF=R!-5v~01k-oJqGG;V7T=;DA|{-muzE3cVmHn@+sy8_vb?+ffdsAhv>#8#= z0d)1esKTzYzgQO+Zqr?yTb|#VO4Qr)BO1 zM0_6zv3(r`t z6_OIkhS;N`_WkNmGb2PuK)|Bqgi!1L4igvW__s|UJZ*sBD`aa+v^P1i;j>UbbVvaz zUO+)7NdF$^*|%$0CcCUEI|GLDlg;quW&S4sVkz%y-N6BVUAY4X*wfE%M4RO&-`LPDC)@J&|M}gKIoWH6V0k`ZTyroA>*F42Ys`OqUuWb1r)FurPUfbsHZU?+ zi=3sC^MN)4%WVr|Ui{AO63%@nARuseqgoV*jlndmx zWM@On1Q~b{H1pn{XV8I-D`nQlJsEZVrz-;s6EzY2Dn`?6bLw{-%xYl0h{E|_-vqWG zPT;bJ4z_*c5`f~j0EQBU*Ho-w0{Su>Q#U)*7?m+M;jnV+6H6XuP!8pCxDC_iSG|0t zg;Syx*ASs!2%+>YD(&;^bl?F`tU&OBr<>clxB$(<{1?hi?)( zVN5u~`{^uXXP!2~+uB?F#9P}~Q2D7vesaY&aPAkCv*^b5w&hM^9S}ry*xXo;gfo=< zotTfw!Jq@>PO6{{9j93yb7(M(ji|MS4yQgn;|f z`4JwH0?ZsC%nFlm6^c`x36(CLE14mQOZxJchZAt)y?K^?eoK^iO7FmS&9@||0D6j- zvGioFy3if4K?-AhwyTr{?P_e^FI69v03ylHgr?)a;Eu<7Jz`aU6jbrx`N)!(7k<2A zr=5r{8YCA)BvcoJad}PmalPb76D2cJdBRJdf6d=XLu$@XR0CXEoTc9qNgRpImZpEr&InAmkcb7t&p1hT{x(BBnuLN`-Se4l!rV^d$@8>(i+uh@aOfP209-i z8t&28+$+OsdpS9|eKix5tc&3kOj(ewO3LD*rmfX++Kq0qVpW~9LkHUkHQ0_oJ1c*I zeR1`g+ONGyKAD+j0vYpwi6=dnmh8-_=Bd(P&IuP%h@};cHB_3-GWlcyiE>Ojrxro1;5TK2cQ7vXW`-o>O?HE7|n%nrsI zOZ&-z68M+Doww^>-wU9Z|3saV!-1!x|3njIq+CdIY)RAWYvK@~FEa33^o%HtBd=S{ zu0##mGy3Y(@5AWaRH)LaLd*}&3+yzxXV3Re-#2tpa*PX>Th2SRG3%uW%uW}G-=V4x zsS9JUah_#)X_o>raG@+-jSdd0wOYH7q`&+jbVl)}0#Y)Mk5yI>Dj~Wu3>HJt0B5}RO$a=#8HNY9bVb#&f;p8c3thmOui zM%i~tzszEBC=dEABBimdoYhEdrnjAUr|9^S5&F∓=KP*rXQ_7C$MY4kkTJ>x@hK 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%0A4C`wRf5K}TaGSwU<#E097z}`w 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 0 { + style.FillColor = charts.Color{ + R: 179, + G: 53, + B: 20, + A: 255, + } + } else if value < 0 { + style.FillColor = charts.Color{ + R: 33, + G: 124, + B: 50, + A: 255, } } - if row == 3 && column == 4 { - return &charts.Style{ - FillColor: drawing.ColorRed.WithAlpha(100), - } - } - return nil + return &style }, }) if err != nil { diff --git a/painter.go b/painter.go index 06973b6..62a4378 100644 --- a/painter.go +++ b/painter.go @@ -545,7 +545,7 @@ func (p *Painter) Text(body string, x, y int) *Painter { return p } -func (p *Painter) TextFit(body string, x, y, width int) chart.Box { +func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box { style := p.style textWarp := style.TextWrap style.TextWrap = chart.TextWrapWord @@ -554,14 +554,24 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box { p.SetTextStyle(style) var output chart.Box + textAlign := "" + if len(textAligns) != 0 { + textAlign = textAligns[0] + } for index, line := range lines { if line == "" { continue } x0 := x y0 := y + output.Height() - p.Text(line, x0, y0) lineBox := r.MeasureText(line) + switch textAlign { + case AlignRight: + x0 += width - lineBox.Width() + case AlignCenter: + x0 += (width - lineBox.Width()) >> 1 + } + p.Text(line, x0, y0) output.Right = chart.MaxInt(lineBox.Right, output.Right) output.Bottom += lineBox.Height() if index < len(lines)-1 { diff --git a/table.go b/table.go index d1af5f9..86ef569 100644 --- a/table.go +++ b/table.go @@ -70,8 +70,10 @@ type TableChartOption struct { Header []string // The data of table Data [][]string - // The span of table column + // The span list of table column Spans []int + // The text align list of table cell + TextAligns []string // The font size of table FontSize float64 // The font family, which should be installed first @@ -271,6 +273,13 @@ func (t *tableChart) render() (*renderInfo, error) { return nil } } + // textAligns := opt.TextAligns + getTextAlign := func(index int) string { + if len(opt.TextAligns) <= index { + return "" + } + return opt.TextAligns[index] + } // 表格单元的处理 renderTableCells := func( @@ -299,7 +308,7 @@ func (t *tableChart) render() (*renderInfo, error) { width := values[index+1] - x x += cellPadding.Left width -= paddingWidth - box := p.TextFit(text, x, y+int(fontSize), width) + box := p.TextFit(text, x, y+int(fontSize), width, getTextAlign(index)) // 计算最高的高度 if box.Height()+paddingHeight > cellMaxHeight { cellMaxHeight = box.Height() + paddingHeight @@ -342,7 +351,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) currentHeight := info.HeaderHeight rowColors := opt.RowBackgroundColors - if len(rowColors) == 0 { + if rowColors == nil { rowColors = tableDefaultSetting.RowColors } for index, h := range info.RowHeights { @@ -375,12 +384,13 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { Column: j, }) if style != nil && !style.FillColor.IsZero() { + padding := style.Padding child := p.Child(PainterPaddingOption(Box{ - Top: top, - Left: left, + Top: top + padding.Top, + Left: left + padding.Left, })) - w := info.ColumnWidths[j] - h := heights[i] + w := info.ColumnWidths[j] - padding.Left - padding.Top + h := heights[i] - padding.Top - padding.Bottom child.SetBackground(w, h, style.FillColor, true) } left += info.ColumnWidths[j] From c862467a5bc1a07f21c3d022c8f2368655dbb650 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 1 Jul 2022 20:41:55 +0800 Subject: [PATCH 13/87] fix: fix only one data of pie chart, #12 --- pie_chart.go | 145 ++++++++++++++++++++++++--------------------------- 1 file changed, 68 insertions(+), 77 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index 6382140..0075ffc 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -99,88 +99,79 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B seriesNames = seriesList.Names() } theme := opt.Theme - if len(values) == 1 { + + currentValue := float64(0) + prevEndX := 0 + prevEndY := 0 + for index, v := range values { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(0), - FillColor: theme.GetSeriesColor(0), + StrokeColor: theme.GetSeriesColor(index), + FillColor: theme.GetSeriesColor(index), }) - seriesPainter.MoveTo(cx, cy). - Circle(radius, cx, cy) - } else { - currentValue := float64(0) - prevEndX := 0 - prevEndY := 0 - for index, v := range values { - seriesPainter.OverrideDrawingStyle(Style{ - StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(index), - FillColor: theme.GetSeriesColor(index), - }) - seriesPainter.MoveTo(cx, cy) - start := chart.PercentToRadians(currentValue/total) - math.Pi/2 - currentValue += v - percent := (v / total) - delta := chart.PercentToRadians(percent) - seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). - LineTo(cx, cy). - Close(). - FillStroke() + seriesPainter.MoveTo(cx, cy) + start := chart.PercentToRadians(currentValue/total) - math.Pi/2 + currentValue += v + percent := (v / total) + delta := chart.PercentToRadians(percent) + seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). + LineTo(cx, cy). + Close(). + FillStroke() - series := seriesList[index] - // 是否显示label - showLabel := series.Label.Show - if !showLabel { - continue - } - - // label的角度为饼块中间 - angle := start + delta/2 - startx := cx + int(radius*math.Cos(angle)) - starty := cy + int(radius*math.Sin(angle)) - - endx := cx + int(labelRadius*math.Cos(angle)) - endy := cy + int(labelRadius*math.Sin(angle)) - // 计算是否有重叠,如果有则调整y坐标位置 - if index != 0 && - math.Abs(float64(endx-prevEndX)) < labelFontSize && - math.Abs(float64(endy-prevEndY)) < labelFontSize { - endy -= (labelFontSize << 1) - } - prevEndX = endx - prevEndY = endy - - seriesPainter.MoveTo(startx, starty) - seriesPainter.LineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 - } - seriesPainter.MoveTo(endx, endy) - endx += offset - seriesPainter.LineTo(endx, endy) - seriesPainter.Stroke() - - textStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color - } - seriesPainter.OverrideTextStyle(textStyle) - text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) - textBox := seriesPainter.MeasureText(text) - textMargin := 3 - x := endx + textMargin - y := endy + textBox.Height()>>1 - 1 - if offset < 0 { - textWidth := textBox.Width() - x = endx - textWidth - textMargin - } - seriesPainter.Text(text, x, y) + series := seriesList[index] + // 是否显示label + showLabel := series.Label.Show + if !showLabel { + continue } + + // label的角度为饼块中间 + angle := start + delta/2 + startx := cx + int(radius*math.Cos(angle)) + starty := cy + int(radius*math.Sin(angle)) + + endx := cx + int(labelRadius*math.Cos(angle)) + endy := cy + int(labelRadius*math.Sin(angle)) + // 计算是否有重叠,如果有则调整y坐标位置 + if index != 0 && + math.Abs(float64(endx-prevEndX)) < labelFontSize && + math.Abs(float64(endy-prevEndY)) < labelFontSize { + endy -= (labelFontSize << 1) + } + prevEndX = endx + prevEndY = endy + + seriesPainter.MoveTo(startx, starty) + seriesPainter.LineTo(endx, endy) + offset := labelLineWidth + if endx < cx { + offset *= -1 + } + seriesPainter.MoveTo(endx, endy) + endx += offset + seriesPainter.LineTo(endx, endy) + seriesPainter.Stroke() + + textStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + textStyle.FontColor = series.Label.Color + } + seriesPainter.OverrideTextStyle(textStyle) + text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) + textBox := seriesPainter.MeasureText(text) + textMargin := 3 + x := endx + textMargin + y := endy + textBox.Height()>>1 - 1 + if offset < 0 { + textWidth := textBox.Width() + x = endx - textWidth - textMargin + } + seriesPainter.Text(text, x, y) } return p.p.box, nil From b56d0c546028facd0790ce334336129c88cba0e8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 4 Jul 2022 20:39:10 +0800 Subject: [PATCH 14/87] fix: fix init fail for empty series list --- series.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/series.go b/series.go index 87a719f..ea71869 100644 --- a/series.go +++ b/series.go @@ -132,6 +132,9 @@ type Series struct { type SeriesList []Series func (sl SeriesList) init() { + if len(sl) == 0 { + return + } if sl[len(sl)-1].index != 0 { return } From eef3a2f97b1ca9d1e337a025a493ee24c8832af4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 6 Jul 2022 20:28:46 +0800 Subject: [PATCH 15/87] fix: fix label overflow, #13 --- legend.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/legend.go b/legend.go index 2acd35b..d3b135e 100644 --- a/legend.go +++ b/legend.go @@ -135,7 +135,11 @@ func (l *legendPainter) Render() (Box, error) { textOffset := 2 legendWidth := 30 legendHeight := 20 + itemMaxHeight := 0 for _, item := range measureList { + if item.Height() > itemMaxHeight { + itemMaxHeight = item.Height() + } if opt.Orient == OrientVertical { height += item.Height() } else { @@ -170,6 +174,10 @@ func (l *legendPainter) Render() (Box, error) { } top, _ := strconv.Atoi(opt.Top) + if left < 0 { + left = 0 + } + x := int(left) y := int(top) + 10 x0 := x @@ -199,6 +207,10 @@ func (l *legendPainter) Render() (Box, error) { FillColor: color, StrokeColor: color, }) + if x0+measureList[index].Width() > p.Width() { + x0 = 0 + y0 += itemMaxHeight + } if opt.Align != AlignRight { x0 = drawIcon(y0, x0) x0 += textOffset From 0a3ac7096a30b41861faa8cb96bfa1bf84a7465c Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 6 Jul 2022 20:44:52 +0800 Subject: [PATCH 16/87] refactor: adjust text render of axis --- axis.go | 8 +++++++- painter.go | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/axis.go b/axis.go index 53b5362..17e8e9f 100644 --- a/axis.go +++ b/axis.go @@ -153,8 +153,14 @@ func (a *axisPainter) Render() (Box, error) { top.SetDrawingStyle(style).OverrideTextStyle(style) textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) - textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth)) + + textFillWidth := float64(textMaxWidth) * 1.3 + textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) + // 偶数 + if unit%2 == 0 && dataCount%(unit+1) == 0 { + unit++ + } width := 0 height := 0 diff --git a/painter.go b/painter.go index 62a4378..0771288 100644 --- a/painter.go +++ b/painter.go @@ -653,8 +653,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } + showIndex := opt.Unit / 2 for index, text := range opt.TextList { - if opt.Unit != 0 && index%opt.Unit != 0 { + if opt.Unit != 0 && index%opt.Unit != showIndex { continue } box := p.MeasureText(text) From c220b10ae600c68112792277c5a1103499964e38 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 7 Jul 2022 20:50:29 +0800 Subject: [PATCH 17/87] refactor: adjust label padding of axis --- axis.go | 3 ++- bar_chart_test.go | 2 +- chart_option_test.go | 2 +- echarts_test.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/axis.go b/axis.go index 17e8e9f..ebc6782 100644 --- a/axis.go +++ b/axis.go @@ -154,7 +154,8 @@ func (a *axisPainter) Render() (Box, error) { textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) - textFillWidth := float64(textMaxWidth) * 1.3 + // 增加30px来计算文本展示区域 + textFillWidth := float64(textMaxWidth + 20) textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) // 偶数 diff --git a/bar_chart_test.go b/bar_chart_test.go index 138b3ca..f1bd688 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index 5e53e46..1238422 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index 4d50d9e..8deda2d 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } From 959377542e2a1fa1daaec7defdfd56195d3ea4d9 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 8 Jul 2022 21:11:47 +0800 Subject: [PATCH 18/87] fix: fix multi line label --- chart_option_test.go | 4 ++-- legend.go | 7 +++++-- line_chart_test.go | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index 1238422..0cdc2aa 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/legend.go b/legend.go index d3b135e..820f1b5 100644 --- a/legend.go +++ b/legend.go @@ -146,6 +146,8 @@ func (l *legendPainter) Render() (Box, error) { width += item.Width() } } + // 增加padding + itemMaxHeight += 10 if opt.Orient == OrientVertical { width = maxTextWidth + textOffset + legendWidth height = offset * len(opt.Data) @@ -207,9 +209,10 @@ func (l *legendPainter) Render() (Box, error) { FillColor: color, StrokeColor: color, }) - if x0+measureList[index].Width() > p.Width() { + if x0+measureList[index].Width()+textOffset+offset+legendWidth > p.Width() { x0 = 0 - y0 += itemMaxHeight + y += itemMaxHeight + y0 = y } if opt.Align != AlignRight { x0 = drawIcon(y0, x0) diff --git a/line_chart_test.go b/line_chart_test.go index 856cdf3..ff80741 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } From 805f4381a31f3f46c0a711623ed6f7b94ac98ffa Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 11 Jul 2022 20:20:41 +0800 Subject: [PATCH 19/87] fix: fix multi line legend --- chart_option_test.go | 6 +++--- charts.go | 9 +++++++-- legend.go | 9 ++++++++- line_chart_test.go | 4 ++-- pie_chart_test.go | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index 0cdc2aa..a025c25 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { diff --git a/charts.go b/charts.go index 6c1c92b..41802d9 100644 --- a/charts.go +++ b/charts.go @@ -25,6 +25,8 @@ package charts import ( "errors" "sort" + + "github.com/wcharczuk/go-chart/v2" ) const labelFontSize = 10 @@ -110,14 +112,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e p = p.Child(PainterPaddingOption(opt.Padding)) } + legendHeight := 0 if len(opt.LegendOption.Data) != 0 { if opt.LegendOption.Theme == nil { opt.LegendOption.Theme = opt.Theme } - _, err := NewLegendPainter(p, opt.LegendOption).Render() + legendResult, err := NewLegendPainter(p, opt.LegendOption).Render() if err != nil { return nil, err } + legendHeight = legendResult.Height() } // 如果有标题 @@ -131,9 +135,10 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if err != nil { return nil, err } + p = p.Child(PainterPaddingOption(Box{ // 标题下留白 - Top: titleBox.Height() + 20, + Top: chart.MaxInt(legendHeight, titleBox.Height()) + 20, })) } diff --git a/legend.go b/legend.go index 820f1b5..4e2bc82 100644 --- a/legend.go +++ b/legend.go @@ -182,6 +182,7 @@ func (l *legendPainter) Render() (Box, error) { x := int(left) y := int(top) + 10 + startY := y x0 := x y0 := y @@ -203,13 +204,18 @@ func (l *legendPainter) Render() (Box, error) { } return left + legendWidth } + lastIndex := len(opt.Data) - 1 for index, text := range opt.Data { color := theme.GetSeriesColor(index) p.SetDrawingStyle(Style{ FillColor: color, StrokeColor: color, }) - if x0+measureList[index].Width()+textOffset+offset+legendWidth > p.Width() { + itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth + if lastIndex == index { + itemWidth = x0 + measureList[index].Width() + legendWidth + } + if itemWidth > p.Width() { x0 = 0 y += itemMaxHeight y0 = y @@ -231,6 +237,7 @@ func (l *legendPainter) Render() (Box, error) { x0 += offset y0 = y } + height = y0 - startY + 10 } return Box{ diff --git a/line_chart_test.go b/line_chart_test.go index ff80741..856cdf3 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } diff --git a/pie_chart_test.go b/pie_chart_test.go index c373a7e..070fb03 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -78,7 +78,7 @@ func TestPieChart(t *testing.T) { } return p.Bytes() }, - result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, } for _, tt := range tests { From b5b2d37e875cf765e04ba2a312a8b1efdc6d5d03 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 11 Jul 2022 20:44:28 +0800 Subject: [PATCH 20/87] fix: fix axis boundary gap, #13 --- painter.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/painter.go b/painter.go index 0771288..1a954e2 100644 --- a/painter.go +++ b/painter.go @@ -637,12 +637,15 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true + showIndex := opt.Unit / 2 if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- + // 非居中 + showIndex = 0 } width := p.Width() height := p.Height() @@ -653,7 +656,6 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } - showIndex := opt.Unit / 2 for index, text := range opt.TextList { if opt.Unit != 0 && index%opt.Unit != showIndex { continue From 3af0d4d4450652f132a8f5482aa30e0eb39ba9d4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 14 Jul 2022 20:14:32 +0800 Subject: [PATCH 21/87] fix: fix pie chart legend --- chart_option_test.go | 2 +- charts.go | 7 ++++++- pie_chart_test.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index a025c25..1238422 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { diff --git a/charts.go b/charts.go index 41802d9..36bb17e 100644 --- a/charts.go +++ b/charts.go @@ -136,9 +136,14 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e return nil, err } + top := chart.MaxInt(legendHeight, titleBox.Height()) + // 如果是垂直方式,则不计算legend高度 + if opt.LegendOption.Orient == OrientVertical { + top = titleBox.Height() + } p = p.Child(PainterPaddingOption(Box{ // 标题下留白 - Top: chart.MaxInt(legendHeight, titleBox.Height()) + 20, + Top: top + 20, })) } diff --git a/pie_chart_test.go b/pie_chart_test.go index 070fb03..c373a7e 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -78,7 +78,7 @@ func TestPieChart(t *testing.T) { } return p.Bytes() }, - result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, } for _, tt := range tests { From 8740c55a1a90f6f895f6601a6afd8f3c4fcf0cbd Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 19 Jul 2022 20:12:31 +0800 Subject: [PATCH 22/87] feat: support padding for legend --- examples/line_chart/main.go | 8 +++++++- legend.go | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 45ff894..a941bca 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -89,7 +89,13 @@ func main() { "Video Ads", "Direct", "Search Engine", - }, charts.PositionCenter), + }, "50"), + func(opt *charts.ChartOption) { + opt.Legend.Padding = charts.Box{ + Top: 5, + Bottom: 10, + } + }, ) if err != nil { diff --git a/legend.go b/legend.go index 4e2bc82..8f21afb 100644 --- a/legend.go +++ b/legend.go @@ -59,6 +59,8 @@ type LegendOption struct { FontColor Color // The flag for show legend, set this to *false will hide legend Show *bool + // The padding of legend + Padding Box } // NewLegendOption returns a legend option @@ -111,9 +113,11 @@ func (l *legendPainter) Render() (Box, error) { if opt.Left == "" { opt.Left = PositionCenter } - p := l.p.Child(PainterPaddingOption(Box{ - Top: 5, - })) + padding := opt.Padding + if padding.IsZero() { + padding.Top = 5 + } + p := l.p.Child(PainterPaddingOption(padding)) p.SetTextStyle(Style{ FontSize: opt.FontSize, FontColor: opt.FontColor, @@ -242,6 +246,6 @@ func (l *legendPainter) Render() (Box, error) { return Box{ Right: width, - Bottom: height, + Bottom: height + padding.Bottom + padding.Top, }, nil } From 3d20bea84663801858ac28067bdbe2f536bb7408 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 22 Jul 2022 20:25:12 +0800 Subject: [PATCH 23/87] refactor: remove unused code --- title.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/title.go b/title.go index 5cdd161..74ab4f9 100644 --- a/title.go +++ b/title.go @@ -36,10 +36,6 @@ type TitleOption struct { Text string // Subtitle text, support \n for new line Subtext string - // // Title style - // Style Style - // // Subtitle style - // SubtextStyle Style // Distance between title component and the left side of the container. // It can be pixel value: 20, percentage value: 20%, // or position value: right, center. From cac6fd03d31477505f3c0e2fdeb62a546e0c8f3b Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 26 Jul 2022 20:44:50 +0800 Subject: [PATCH 24/87] fix: fix unit count of xasix --- axis.go | 7 +++++-- chart_option.go | 5 ++++- legend.go | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/axis.go b/axis.go index ebc6782..8e5bfda 100644 --- a/axis.go +++ b/axis.go @@ -156,8 +156,11 @@ func (a *axisPainter) Render() (Box, error) { // 增加30px来计算文本展示区域 textFillWidth := float64(textMaxWidth + 20) - textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) - unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) + // 根据文本宽度计算较为符合的展示项 + fitTextCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) + + unit := ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) + unit = chart.MaxInt(unit, opt.SplitNumber) // 偶数 if unit%2 == 0 && dataCount%(unit+1) == 0 { unit++ diff --git a/chart_option.go b/chart_option.go index 39de686..41fda46 100644 --- a/chart_option.go +++ b/chart_option.go @@ -108,9 +108,12 @@ func TitleOptionFunc(title TitleOption) OptionFunc { } // TitleTextOptionFunc set title text of chart -func TitleTextOptionFunc(text string) OptionFunc { +func TitleTextOptionFunc(text string, subtext ...string) OptionFunc { return func(opt *ChartOption) { opt.Title.Text = text + if len(subtext) != 0 { + opt.Title.Subtext = subtext[0] + } } } diff --git a/legend.go b/legend.go index 8f21afb..035642c 100644 --- a/legend.go +++ b/legend.go @@ -232,7 +232,7 @@ func (l *legendPainter) Render() (Box, error) { x0 += measureList[index].Width() if opt.Align == AlignRight { x0 += textOffset - x0 = drawIcon(0, x0) + x0 = drawIcon(y0, x0) } if opt.Orient == OrientVertical { y0 += offset From 1713bc283f6e742cf45cbed65cce010509945130 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 26 Jul 2022 20:45:04 +0800 Subject: [PATCH 25/87] docs: add doc --- start_zh.md | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 start_zh.md diff --git a/start_zh.md b/start_zh.md new file mode 100644 index 0000000..ee8359c --- /dev/null +++ b/start_zh.md @@ -0,0 +1,254 @@ +# go-charts + +`go-charts`主要分为了下几个模块: + +- `标题`:图表的标题,包括主副标题,位置为图表的顶部 +- `图例`:图表的图例列表,用于标识每个图例对应的颜色与名称信息,默认为图表的顶部,可自定义位置 +- `X轴`:图表的x轴,用于折线图、柱状图中,表示每个点对应的时间,位置图表的底部 +- `Y轴`:图表的y轴,用于折线图、柱状图中,最多可使用两组y轴(一左一右),默认位置图表的左侧 +- `内容`: 图表的内容,折线图、柱状图、饼图等,在图表的中间区域 + +## 标题 + +### 常用设置 + +标题一般仅需要设置主副标题即可,其它的属性均会设置默认值,常用的方式是使用`TitleTextOptionFunc`设置,其中副标题为可选值,方式如下: + +```go + charts.TitleTextOptionFunc("Text", "Subtext"), +``` + +### 个性化设置 + +```go +func(opt *charts.ChartOption) { + opt.Title = charts.TitleOption{ + // 主标题 + Text: "Text", + // 副标题 + Subtext: "Subtext", + // 标题左侧位置,可设置为"center","right",数值("20")或百份比("20%") + Left: charts.PositionRight, + // 标题顶部位置,只可调为数值 + Top: "20", + // 主标题文字大小 + FontSize: 14, + // 副标题文字大小 + SubtextFontSize: 12, + // 主标题字体颜色 + FontColor: charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, + // 副标题字体影响 + SubtextFontColor: charts.Color{ + R: 200, + G: 200, + B: 200, + A: 255, + }, + } +}, +``` + +### 部分属性个性化设置 + +```go +charts.TitleTextOptionFunc("Text", "Subtext"), +func(opt *charts.ChartOption) { + // 修改top的值 + opt.Title.Top = "20" +}, +``` + +## 图例 + +### 常用设置 + +图例组件与图表中的数据一一对应,常用仅设置其名称及左侧的值即可(可选),方式如下: + + +```go +charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", +}, "50"), +``` + +### 个性化设置 + +```go +func(opt *charts.ChartOption) { + opt.Legend = charts.LegendOption{ + // 图例名称 + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, + // 图例左侧位置,可设置为"center","right",数值("20")或百份比("20%") + // 如果示例有多行,只影响第一行,而且对于多行的示例,设置"center", "right"无效 + Left: "50", + // 图例顶部位置,只可调为数值 + Top: "10", + // 图例图标的位置,默认为左侧,只允许左或右 + Align: charts.AlignRight, + // 图例排列方式,默认为水平,只允许水平或垂直 + Orient: charts.OrientVertical, + // 图标类型,提供"rect"与"lineDot"两种类型 + Icon: charts.IconRect, + // 字体大小 + FontSize: 14, + // 字体颜色 + FontColor: charts.Color{ + R: 150, + G: 150, + B: 150, + A: 255, + }, + // 是否展示,如果不需要展示则设置 + // Show: charts.FalseFlag(), + // 图例区域的padding值 + Padding: charts.Box{ + Top: 10, + Left: 10, + }, + } +}, +``` + +### 部分属性个性化设置 + +```go +charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", +}, "50"), +func(opt *charts.ChartOption) { + opt.Legend.Top = "10" +}, +``` + +## X轴 + +### 常用设置 + +图表中X轴的展示,常用的设置方式是指定数组即可: + +```go +charts.XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", +}), +``` + +### 个性化设置 + +```go +func(opt *charts.ChartOption) { + opt.XAxis = charts.XAxisOption{ + // X轴内容 + Data: []string{ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + }, + // 如果数据点不居中,则设置为false + BoundaryGap: charts.FalseFlag(), + // 字体大小 + FontSize: 14, + // 是否展示,如果不需要展示则设置 + // Show: charts.FalseFlag(), + // 会根据文本内容以及此值选择适合的分块大小,一般不需要设置 + // SplitNumber: 3, + // 线条颜色 + StrokeColor: charts.Color{ + R: 200, + G: 200, + B: 200, + A: 255, + }, + // 文字颜色 + FontColor: charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, + } +}, +``` + +### 部分属性个性化设置 + +```go +charts.XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", +}), +func(opt *charts.ChartOption) { + opt.XAxis.FontColor = charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, +}, +``` + +## Y轴 + +图表中的y轴展示的相关数据会根据图表中的数据自动生成适合的值,如果需要自定义,则可自定义以下部分数据: + +```go +func(opt *charts.ChartOption) { + opt.YAxisOptions = []charts.YAxisOption{ + { + // 字体大小 + FontSize: 16, + // 字体颜色 + FontColor: charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, + // 内容,{value}会替换为对应的值 + Formatter: "{value} ml", + // Y轴颜色,如果设置此值,会覆盖font color + Color: charts.Color{ + R: 255, + G: 0, + B: 0, + A: 255, + }, + }, + } +}, +``` From e095223705464e4724ca3fb29512def0daecde55 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Jul 2022 20:27:49 +0800 Subject: [PATCH 26/87] fix: fix font setting for title, #15 --- .gitignore | 1 + chart_option.go | 3 +++ examples/chinese/main.go | 5 +++-- theme.go | 39 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2e33342..4a7b0d9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ *.png *.svg tmp +NotoSansSC.ttf diff --git a/chart_option.go b/chart_option.go index 41fda46..cb3bd3f 100644 --- a/chart_option.go +++ b/chart_option.go @@ -259,6 +259,9 @@ func (o *ChartOption) fillDefault() { if o.font == nil { o.font, _ = chart.GetDefaultFont() + } else { + // 如果指定了字体,则设置主题的字体 + t.SetFont(o.font) } if o.BackgroundColor.IsZero() { o.BackgroundColor = t.GetBackgroundColor() diff --git a/examples/chinese/main.go b/examples/chinese/main.go index bb7cc00..9068a08 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -25,7 +25,8 @@ func writeFile(buf []byte) error { func main() { // 字体文件需要自行下载 - buf, err := ioutil.ReadFile("../NotoSansSC.ttf") + // https://github.com/googlefonts/noto-cjk + buf, err := ioutil.ReadFile("./NotoSansSC.ttf") if err != nil { panic(err) } @@ -83,7 +84,7 @@ func main() { } p, err := charts.LineRender( values, - charts.TitleTextOptionFunc("Line"), + charts.TitleTextOptionFunc("测试"), charts.FontFamilyOptionFunc("noto"), charts.XAxisDataOptionFunc([]string{ "星期一", diff --git a/theme.go b/theme.go index 31c3bf8..8068687 100644 --- a/theme.go +++ b/theme.go @@ -36,12 +36,19 @@ const ThemeAnt = "ant" type ColorPalette interface { IsDark() bool GetAxisStrokeColor() Color + SetAxisStrokeColor(Color) GetAxisSplitLineColor() Color + SetAxisSplitLineColor(Color) GetSeriesColor(int) Color + SetSeriesColor([]Color) GetBackgroundColor() Color + SetBackgroundColor(Color) GetTextColor() Color + SetTextColor(Color) GetFontSize() float64 + SetFontSize(float64) GetFont() *truetype.Font + SetFont(*truetype.Font) } type themeColorPalette struct { @@ -64,7 +71,7 @@ type ThemeOption struct { SeriesColors []Color } -var palettes = map[string]ColorPalette{} +var palettes = map[string]*themeColorPalette{} const defaultFontSize = 12.0 @@ -241,7 +248,8 @@ func NewTheme(name string) ColorPalette { if !ok { p = palettes[ThemeLight] } - return p + clone := *p + return &clone } func (t *themeColorPalette) IsDark() bool { @@ -252,23 +260,42 @@ func (t *themeColorPalette) GetAxisStrokeColor() Color { return t.axisStrokeColor } +func (t *themeColorPalette) SetAxisStrokeColor(c Color) { + t.axisStrokeColor = c +} + func (t *themeColorPalette) GetAxisSplitLineColor() Color { return t.axisSplitLineColor } +func (t *themeColorPalette) SetAxisSplitLineColor(c Color) { + t.axisSplitLineColor = c +} + func (t *themeColorPalette) GetSeriesColor(index int) Color { colors := t.seriesColors return colors[index%len(colors)] } +func (t *themeColorPalette) SetSeriesColor(colors []Color) { + t.seriesColors = colors +} func (t *themeColorPalette) GetBackgroundColor() Color { return t.backgroundColor } +func (t *themeColorPalette) SetBackgroundColor(c Color) { + t.backgroundColor = c +} + func (t *themeColorPalette) GetTextColor() Color { return t.textColor } +func (t *themeColorPalette) SetTextColor(c Color) { + t.textColor = c +} + func (t *themeColorPalette) GetFontSize() float64 { if t.fontSize != 0 { return t.fontSize @@ -276,6 +303,10 @@ func (t *themeColorPalette) GetFontSize() float64 { return defaultFontSize } +func (t *themeColorPalette) SetFontSize(fontSize float64) { + t.fontSize = fontSize +} + func (t *themeColorPalette) GetFont() *truetype.Font { if t.font != nil { return t.font @@ -283,3 +314,7 @@ func (t *themeColorPalette) GetFont() *truetype.Font { f, _ := chart.GetDefaultFont() return f } + +func (t *themeColorPalette) SetFont(f *truetype.Font) { + t.font = f +} From 817fceff73798bba9450192a3dfa844319d08e8d Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Jul 2022 20:32:31 +0800 Subject: [PATCH 27/87] feat: support hide symbol of line chart --- chart_option.go | 2 ++ charts.go | 7 ++++--- examples/line_chart/main.go | 1 + line_chart.go | 6 +++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chart_option.go b/chart_option.go index cb3bd3f..71e9dfc 100644 --- a/chart_option.go +++ b/chart_option.go @@ -62,6 +62,8 @@ type ChartOption struct { RadarIndicators []RadarIndicator // The background color of chart BackgroundColor Color + // The flag for show symbol of line, set this to *false will hide symbol + SymbolShow *bool // The child charts Children []ChartOption } diff --git a/charts.go b/charts.go index 36bb17e..92a7e54 100644 --- a/charts.go +++ b/charts.go @@ -377,9 +377,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(lineSeriesList) != 0 { handler.Add(func() error { _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + SymbolShow: opt.SymbolShow, }).render(renderResult, lineSeriesList) return err }) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index a941bca..5edf65b 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -95,6 +95,7 @@ func main() { Top: 5, Bottom: 10, } + opt.SymbolShow = charts.FalseFlag() }, ) diff --git a/line_chart.go b/line_chart.go index 0770447..dee122f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -60,6 +60,8 @@ type LineChartOption struct { Title TitleOption // The legend option Legend LegendOption + // The flag for show symbol of line, set this to *false will hide symbol + SymbolShow *bool // background is filled backgroundIsFilled bool } @@ -123,7 +125,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } drawingStyle.StrokeWidth = 1 seriesPainter.SetDrawingStyle(drawingStyle) - seriesPainter.Dots(points) + if !isFalse(opt.SymbolShow) { + seriesPainter.Dots(points) + } markPointPainter.Add(markPointRenderOption{ FillColor: seriesColor, Font: opt.Font, From e530adccb66738e41d8eb9c7d042e38d3221f869 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 28 Jul 2022 20:49:00 +0800 Subject: [PATCH 28/87] feat: support stroke width of line chart --- chart_option.go | 2 ++ charts.go | 9 +++++---- examples/line_chart/main.go | 1 + line_chart.go | 8 +++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/chart_option.go b/chart_option.go index 71e9dfc..58001bd 100644 --- a/chart_option.go +++ b/chart_option.go @@ -64,6 +64,8 @@ type ChartOption struct { BackgroundColor Color // The flag for show symbol of line, set this to *false will hide symbol SymbolShow *bool + // The stroke width of line chart + LineStrokeWidth float64 // The child charts Children []ChartOption } diff --git a/charts.go b/charts.go index 92a7e54..d65f3c9 100644 --- a/charts.go +++ b/charts.go @@ -377,10 +377,11 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(lineSeriesList) != 0 { handler.Add(func() error { _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - SymbolShow: opt.SymbolShow, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + SymbolShow: opt.SymbolShow, + StrokeWidth: opt.LineStrokeWidth, }).render(renderResult, lineSeriesList) return err }) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 5edf65b..36eabee 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -96,6 +96,7 @@ func main() { Bottom: 10, } opt.SymbolShow = charts.FalseFlag() + opt.LineStrokeWidth = 1 }, ) diff --git a/line_chart.go b/line_chart.go index dee122f..3942d70 100644 --- a/line_chart.go +++ b/line_chart.go @@ -62,6 +62,8 @@ type LineChartOption struct { Legend LegendOption // The flag for show symbol of line, set this to *false will hide symbol SymbolShow *bool + // The stroke width of line + StrokeWidth float64 // background is filled backgroundIsFilled bool } @@ -95,12 +97,16 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( markPointPainter, markLinePainter, } + strokeWidth := opt.StrokeWidth + if strokeWidth == 0 { + strokeWidth = defaultStrokeWidth + } for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) drawingStyle := Style{ StrokeColor: seriesColor, - StrokeWidth: defaultStrokeWidth, + StrokeWidth: strokeWidth, } seriesPainter.SetDrawingStyle(drawingStyle) From 550b9874d23dc966e54248455a6e639c29affd26 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 29 Jul 2022 20:42:13 +0800 Subject: [PATCH 29/87] refactor: remove unused path --- axis.go | 5 ++++- axis_test.go | 4 ++-- bar_chart_test.go | 2 +- chart_option_test.go | 12 ++++++------ charts.go | 5 ++--- echarts_test.go | 2 +- horizontal_bar_chart_test.go | 2 +- line_chart_test.go | 4 ++-- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/axis.go b/axis.go index 8e5bfda..3c0484c 100644 --- a/axis.go +++ b/axis.go @@ -265,6 +265,7 @@ func (a *axisPainter) Render() (Box, error) { // 显示辅助线 if opt.SplitLineShow { style.StrokeColor = opt.SplitLineColor + style.StrokeWidth = 1 top.OverrideDrawingStyle(style) if isVertical { x0 := p.Width() @@ -273,7 +274,9 @@ func (a *axisPainter) Render() (Box, error) { x0 = 0 x1 = top.Width() - p.Width() } - for _, y := range autoDivide(height, tickCount) { + yValues := autoDivide(height, tickCount) + yValues = yValues[0 : len(yValues)-1] + for _, y := range yValues { top.LineStroke([]Point{ { X: x0, diff --git a/axis_test.go b/axis_test.go index 17fe8d6..d0cff41 100644 --- a/axis_test.go +++ b/axis_test.go @@ -113,7 +113,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 右侧 { @@ -135,7 +135,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 顶部 { diff --git a/bar_chart_test.go b/bar_chart_test.go index f1bd688..bee0583 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index 1238422..6f331b3 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { @@ -419,7 +419,7 @@ func TestRadarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) + assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) } func TestFunnelRender(t *testing.T) { @@ -447,5 +447,5 @@ func TestFunnelRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) + assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) } diff --git a/charts.go b/charts.go index d65f3c9..185e638 100644 --- a/charts.go +++ b/charts.go @@ -316,9 +316,8 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { TitleOption: opt.Title, LegendOption: opt.Legend, axisReversed: axisReversed, - } - if isChild { - renderOpt.backgroundIsFilled = true + // 前置已设置背景色 + backgroundIsFilled: true, } if len(pieSeriesList) != 0 || len(radarSeriesList) != 0 || diff --git a/echarts_test.go b/echarts_test.go index 8deda2d..5c2dbad 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index 5555df6..e078c4a 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/line_chart_test.go b/line_chart_test.go index 856cdf3..e169f90 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } From 93e03856cac44d574178f85f70fff4d4bc2ac1b3 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 10 Aug 2022 20:39:14 +0800 Subject: [PATCH 30/87] fix: fix NaN of radar chart, #17 --- radar_chart.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/radar_chart.go b/radar_chart.go index eab70d5..429850d 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -200,7 +200,11 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) continue } indicator := indicators[j] - percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min) + var percent float64 + offset := indicator.Max - indicator.Min + if offset > 0 { + percent = (item.Value - indicator.Min) / offset + } r := percent * radius p := getPolygonPoint(center, r, angles[j]) linePoints = append(linePoints, p) From dc1a89d3ff8937afc58a4f90a6c935d11aa859ab Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 25 Aug 2022 20:19:05 +0800 Subject: [PATCH 31/87] feat: support fill area of line chart --- .gitignore | 1 + chart_option.go | 2 + charts.go | 1 + examples/area_line_chart/main.go | 74 ++++++++++++++++++++++++++++++++ examples/charts/main.go | 31 +++++++++++++ line_chart.go | 22 +++++++++- 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 examples/area_line_chart/main.go diff --git a/.gitignore b/.gitignore index 4a7b0d9..57206ee 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ *.svg tmp NotoSansSC.ttf +.vscode \ No newline at end of file diff --git a/chart_option.go b/chart_option.go index 58001bd..93b81ba 100644 --- a/chart_option.go +++ b/chart_option.go @@ -66,6 +66,8 @@ type ChartOption struct { SymbolShow *bool // The stroke width of line chart LineStrokeWidth float64 + // Fill the area of line chart + FillArea bool // The child charts Children []ChartOption } diff --git a/charts.go b/charts.go index 185e638..849f0c7 100644 --- a/charts.go +++ b/charts.go @@ -381,6 +381,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { XAxis: opt.XAxis, SymbolShow: opt.SymbolShow, StrokeWidth: opt.LineStrokeWidth, + FillArea: opt.FillArea, }).render(renderResult, lineSeriesList) return err }) diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go new file mode 100644 index 0000000..7a84df0 --- /dev/null +++ b/examples/area_line_chart/main.go @@ -0,0 +1,74 @@ +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, "area-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, + }, + } + p, err := charts.LineRender( + values, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + charts.LegendLabelsOptionFunc([]string{ + "Email", + }, "50"), + func(opt *charts.ChartOption) { + opt.Legend.Padding = charts.Box{ + Top: 5, + Bottom: 10, + } + opt.FillArea = true + }, + ) + + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/examples/charts/main.go b/examples/charts/main.go index 7b14919..c3bb486 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "net/http" "strconv" @@ -261,6 +262,35 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, + { + Title: charts.TitleOption{ + Text: "Line Area", + }, + Legend: charts.NewLegendOption([]string{ + "Email", + }), + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }), + }, + FillArea: true, + }, // 柱状图 { Title: charts.TitleOption{ @@ -1935,5 +1965,6 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { func main() { http.HandleFunc("/", indexHandler) http.HandleFunc("/echarts", echartsHandler) + fmt.Println("http://127.0.0.1:3012/") http.ListenAndServe(":3012", nil) } diff --git a/line_chart.go b/line_chart.go index 3942d70..0b44cdf 100644 --- a/line_chart.go +++ b/line_chart.go @@ -64,6 +64,8 @@ type LineChartOption struct { SymbolShow *bool // The stroke width of line StrokeWidth float64 + // Fill the area of line + FillArea bool // background is filled backgroundIsFilled bool } @@ -109,7 +111,6 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( StrokeWidth: strokeWidth, } - seriesPainter.SetDrawingStyle(drawingStyle) yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { @@ -120,6 +121,25 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } points = append(points, p) } + // 如果需要填充区域 + if opt.FillArea { + areaPoints := make([]Point, len(points)) + copy(areaPoints, points) + bottomY := yRange.getRestHeight(yRange.min) + areaPoints = append(areaPoints, Point{ + X: areaPoints[len(areaPoints)-1].X, + Y: bottomY, + }, Point{ + X: areaPoints[0].X, + Y: bottomY, + }, areaPoints[0]) + seriesPainter.SetDrawingStyle(Style{ + FillColor: seriesColor.WithAlpha(200), + }) + seriesPainter.FillArea(areaPoints) + } + seriesPainter.SetDrawingStyle(drawingStyle) + // 画线 seriesPainter.LineStroke(points) From 128d5b277410e3c8c5a016dfcfade05f4bbd5cfb Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 28 Aug 2022 09:43:18 +0800 Subject: [PATCH 32/87] refactor: adjust max value of axis, #19 --- mark_line_test.go | 2 +- range.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mark_line_test.go b/mark_line_test.go index 84152ce..ef29e6f 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -67,7 +67,7 @@ func TestMarkLine(t *testing.T) { } return p.Bytes() }, - result: "\\n321", + result: "\\n321", }, } for _, tt := range tests { diff --git a/range.go b/range.go index 579a77f..ebd0b2d 100644 --- a/range.go +++ b/range.go @@ -60,7 +60,10 @@ func NewRange(opt AxisRangeOption) axisRange { r := math.Abs(max - min) // 最小单位计算 - unit := 2 + unit := 1 + if r > 5 { + unit = 2 + } if r > 10 { unit = 4 } @@ -85,6 +88,10 @@ func NewRange(opt AxisRangeOption) axisRange { } } max = min + float64(unit*divideCount) + expectMax := opt.Max * 2 + if max > expectMax { + max = float64(ceilFloatToInt(expectMax)) + } return axisRange{ divideCount: divideCount, min: min, From 4a1ff8055656382652b49cafc7ee22132884ebcc Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 1 Sep 2022 20:20:51 +0800 Subject: [PATCH 33/87] fix: fix min and max option of y axis --- .github/workflows/test.yml | 1 + charts.go | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 22e77a8..61449a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: go: + - '1.19' - '1.18' - '1.17' - '1.16' diff --git a/charts.go b/charts.go index 849f0c7..6d5dc56 100644 --- a/charts.go +++ b/charts.go @@ -174,12 +174,6 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e 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, @@ -188,6 +182,12 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e // 分隔数量 DivideCount: defaultAxisDivideCount, }) + if yAxisOption.Min != nil && *yAxisOption.Min <= min { + r.min = *yAxisOption.Min + } + if yAxisOption.Max != nil && *yAxisOption.Max >= max { + r.max = *yAxisOption.Max + } result.axisRanges[index] = r if yAxisOption.Theme == nil { From bb9af986be59b6c3be100e81ae54a4b713bf0a6c Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 2 Sep 2022 20:42:10 +0800 Subject: [PATCH 34/87] chore: update go modules --- go.mod | 4 ++-- go.sum | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 66145c7..de0bb9c 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.7.2 + github.com/stretchr/testify v1.8.0 github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect + golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5f953b0..e0b1547 100644 --- a/go.sum +++ b/go.sum @@ -8,17 +8,20 @@ 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +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-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= -golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 50605907c761ba72f14f9f666775b394202195c8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 15 Sep 2022 20:09:00 +0800 Subject: [PATCH 35/87] feat: support null value for line chart --- charts.go | 13 +++++++++++++ examples/line_chart/main.go | 3 ++- line_chart.go | 5 +++++ painter.go | 9 ++++++++- series.go | 4 ++++ 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/charts.go b/charts.go index 6d5dc56..f7e52e4 100644 --- a/charts.go +++ b/charts.go @@ -24,6 +24,7 @@ package charts import ( "errors" + "math" "sort" "github.com/wcharczuk/go-chart/v2" @@ -51,6 +52,18 @@ func SetDefaultHeight(height int) { } } +var nullValue = math.MaxFloat64 + +// SetNullValue sets the null value, default is MaxFloat64 +func SetNullValue(v float64) { + nullValue = v +} + +// GetNullValue gets the null value +func GetNullValue() float64 { + return nullValue +} + type Renderer interface { Render() (Box, error) } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 36eabee..97d5859 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -29,7 +29,8 @@ func main() { 120, 132, 101, - 134, + // 134, + charts.GetNullValue(), 90, 230, 210, diff --git a/line_chart.go b/line_chart.go index 0b44cdf..839aa6f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -23,6 +23,8 @@ package charts import ( + "math" + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -115,6 +117,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( points := make([]Point, 0) for i, item := range series.Data { h := yRange.getRestHeight(item.Value) + if item.Value == nullValue { + h = math.MaxInt + } p := Point{ X: xValues[i], Y: h, diff --git a/painter.go b/painter.go index 1a954e2..f172cb3 100644 --- a/painter.go +++ b/painter.go @@ -438,11 +438,18 @@ func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) { } func (p *Painter) LineStroke(points []Point) *Painter { + shouldMoveTo := false for index, point := range points { x := point.X y := point.Y - if index == 0 { + if y == math.MaxInt { + p.Stroke() + shouldMoveTo = true + continue + } + if shouldMoveTo || index == 0 { p.MoveTo(x, y) + shouldMoveTo = false } else { p.LineTo(x, y) } diff --git a/series.go b/series.go index ea71869..7bd6834 100644 --- a/series.go +++ b/series.go @@ -165,6 +165,10 @@ func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) { continue } for _, item := range series.Data { + // 如果为空值,忽略 + if item.Value == nullValue { + continue + } if item.Value > max { max = item.Value } From 825e65d93078aee8ae99916ff916daf7a425a56d Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 15 Sep 2022 20:15:05 +0800 Subject: [PATCH 36/87] refactor: use MaxInt32 instead of MaxInt --- line_chart.go | 2 +- painter.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/line_chart.go b/line_chart.go index 839aa6f..cdec280 100644 --- a/line_chart.go +++ b/line_chart.go @@ -118,7 +118,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( for i, item := range series.Data { h := yRange.getRestHeight(item.Value) if item.Value == nullValue { - h = math.MaxInt + h = int(math.MaxInt32) } p := Point{ X: xValues[i], diff --git a/painter.go b/painter.go index f172cb3..b7122b7 100644 --- a/painter.go +++ b/painter.go @@ -442,7 +442,7 @@ func (p *Painter) LineStroke(points []Point) *Painter { for index, point := range points { x := point.X y := point.Y - if y == math.MaxInt { + if y == int(math.MaxInt32) { p.Stroke() shouldMoveTo = true continue From de49ef8c68ea47881d103d6f650c17ac5a1ea14a Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 22 Sep 2022 20:10:45 +0800 Subject: [PATCH 37/87] feat: support label for line chart, #23 --- bar_chart.go | 17 +++------------ line_chart.go | 30 ++++++++++++++++++++++++++ series_label.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 series_label.go diff --git a/bar_chart.go b/bar_chart.go index 26f8da5..797f710 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -62,13 +62,6 @@ type BarChartOption struct { Legend LegendOption } -type barChartLabelRenderOption struct { - Text string - Style Style - X int - Y int -} - func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := b.p opt := b.opt @@ -100,11 +93,12 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) + labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ + labelPainter, markPointPainter, markLinePainter, } - labelRenderOptions := make([]barChartLabelRenderOption, 0) for index := range seriesList { series := seriesList[index] yRange := result.axisRanges[series.AxisIndex] @@ -168,8 +162,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } textBox := seriesPainter.MeasureText(text) - - labelRenderOptions = append(labelRenderOptions, barChartLabelRenderOption{ + labelPainter.Add(LabelValue{ Text: text, Style: labelStyle, X: x + (barWidth-textBox.Width())>>1, @@ -192,10 +185,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Range: yRange, }) } - for _, labelOpt := range labelRenderOptions { - seriesPainter.OverrideTextStyle(labelOpt.Style) - seriesPainter.Text(labelOpt.Text, labelOpt.X, labelOpt.Y) - } // 最大、最小的mark point err := doRender(rendererList...) if err != nil { diff --git a/line_chart.go b/line_chart.go index cdec280..bf39ae2 100644 --- a/line_chart.go +++ b/line_chart.go @@ -97,7 +97,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) + labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ + labelPainter, markPointPainter, markLinePainter, } @@ -105,6 +107,8 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( if strokeWidth == 0 { strokeWidth = defaultStrokeWidth } + seriesNames := seriesList.Names() + theme := opt.Theme for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) @@ -125,6 +129,32 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Y: h, } points = append(points, p) + + // 如果label不需要展示,则返回 + if !series.Label.Show { + continue + } + distance := series.Label.Distance + if distance == 0 { + distance = 5 + } + text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) + labelStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + labelStyle.FontColor = series.Label.Color + } + + textBox := seriesPainter.MeasureText(text) + labelPainter.Add(LabelValue{ + Text: text, + Style: labelStyle, + X: p.X - textBox.Width()>>1, + Y: p.Y - distance, + }) } // 如果需要填充区域 if opt.FillArea { diff --git a/series_label.go b/series_label.go new file mode 100644 index 0000000..c1850bb --- /dev/null +++ b/series_label.go @@ -0,0 +1,56 @@ +// 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" + +type LabelValue struct { + Text string + Style Style + X int + Y int +} + +type SeriesLabelPainter struct { + p *Painter + values []LabelValue +} + +func NewSeriesLabelPainter(p *Painter) *SeriesLabelPainter { + return &SeriesLabelPainter{ + p: p, + values: make([]LabelValue, 0), + } +} + +func (o *SeriesLabelPainter) Add(value LabelValue) { + o.values = append(o.values, value) +} + +func (o *SeriesLabelPainter) Render() (Box, error) { + for _, item := range o.values { + o.p.OverrideTextStyle(item.Style) + o.p.Text(item.Text, item.X, item.Y) + } + return chart.BoxZero, nil +} From 1f5b9d513ee4387b2fc5a0f8a2b3ccb3836ad42f Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 23 Sep 2022 20:50:42 +0800 Subject: [PATCH 38/87] refactor: adjust series label render --- bar_chart.go | 39 ++++++++++++--------------- bar_chart_test.go | 2 +- line_chart.go | 39 +++++++++++---------------- series_label.go | 69 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 797f710..8826ffb 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -93,9 +93,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) - labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ - labelPainter, markPointPainter, markLinePainter, } @@ -106,6 +104,18 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B divideValues := xRange.AutoDivide() points := make([]Point, len(series.Data)) + var labelPainter *SeriesLabelPainter + if series.Label.Show { + labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ + P: seriesPainter, + SeriesNames: seriesNames, + Label: series.Label, + Theme: opt.Theme, + Font: opt.Font, + }) + rendererList = append(rendererList, labelPainter) + } + for j, item := range series.Data { if j >= xRange.divideCount { continue @@ -144,29 +154,14 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Y: top, } // 如果label不需要展示,则返回 - if !series.Label.Show { + if labelPainter == nil { continue } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) - labelStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - - textBox := seriesPainter.MeasureText(text) labelPainter.Add(LabelValue{ - Text: text, - Style: labelStyle, - X: x + (barWidth-textBox.Width())>>1, - Y: barMaxHeight - h - distance, + Index: index, + Value: item.Value, + X: x + barWidth>>1, + Y: barMaxHeight - h, }) } diff --git a/bar_chart_test.go b/bar_chart_test.go index bee0583..e1522d6 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/line_chart.go b/line_chart.go index bf39ae2..26f94a4 100644 --- a/line_chart.go +++ b/line_chart.go @@ -97,9 +97,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) - labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ - labelPainter, markPointPainter, markLinePainter, } @@ -108,7 +106,6 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( strokeWidth = defaultStrokeWidth } seriesNames := seriesList.Names() - theme := opt.Theme for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) @@ -119,6 +116,17 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) + var labelPainter *SeriesLabelPainter + if series.Label.Show { + labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ + P: seriesPainter, + SeriesNames: seriesNames, + Label: series.Label, + Theme: opt.Theme, + Font: opt.Font, + }) + rendererList = append(rendererList, labelPainter) + } for i, item := range series.Data { h := yRange.getRestHeight(item.Value) if item.Value == nullValue { @@ -131,29 +139,14 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( points = append(points, p) // 如果label不需要展示,则返回 - if !series.Label.Show { + if labelPainter == nil { continue } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) - labelStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - - textBox := seriesPainter.MeasureText(text) labelPainter.Add(LabelValue{ - Text: text, - Style: labelStyle, - X: p.X - textBox.Width()>>1, - Y: p.Y - distance, + Index: index, + Value: item.Value, + X: p.X, + Y: p.Y, }) } // 如果需要填充区域 diff --git a/series_label.go b/series_label.go index c1850bb..57bd1bf 100644 --- a/series_label.go +++ b/series_label.go @@ -22,29 +22,80 @@ package charts -import "github.com/wcharczuk/go-chart/v2" +import ( + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" +) -type LabelValue struct { +type labelRenderValue struct { Text string Style Style X int Y int } -type SeriesLabelPainter struct { - p *Painter - values []LabelValue +type LabelValue struct { + Index int + Value float64 + X int + Y int } -func NewSeriesLabelPainter(p *Painter) *SeriesLabelPainter { +type SeriesLabelPainter struct { + p *Painter + seriesNames []string + label *SeriesLabel + theme ColorPalette + font *truetype.Font + values []labelRenderValue +} + +type SeriesLabelPainterParams struct { + P *Painter + SeriesNames []string + Label SeriesLabel + Theme ColorPalette + Font *truetype.Font +} + +func NewSeriesLabelPainter(params SeriesLabelPainterParams) *SeriesLabelPainter { return &SeriesLabelPainter{ - p: p, - values: make([]LabelValue, 0), + p: params.P, + seriesNames: params.SeriesNames, + label: ¶ms.Label, + theme: params.Theme, + font: params.Font, + values: make([]labelRenderValue, 0), } } func (o *SeriesLabelPainter) Add(value LabelValue) { - o.values = append(o.values, value) + label := o.label + distance := label.Distance + if distance == 0 { + distance = 5 + } + text := NewValueLabelFormatter(o.seriesNames, label.Formatter)(value.Index, value.Value, -1) + labelStyle := Style{ + FontColor: o.theme.GetTextColor(), + FontSize: labelFontSize, + Font: o.font, + } + if !label.Color.IsZero() { + labelStyle.FontColor = label.Color + } + o.p.OverrideDrawingStyle(labelStyle) + textBox := o.p.MeasureText(text) + renderValue := labelRenderValue{ + Text: text, + Style: labelStyle, + X: value.X - textBox.Width()>>1, + Y: value.Y - distance, + } + if textBox.Width()%2 != 0 { + renderValue.X++ + } + o.values = append(o.values, renderValue) } func (o *SeriesLabelPainter) Render() (Box, error) { From 0a80e7056f69f21d1561f4426877ab7fa376dd4f Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 28 Sep 2022 20:29:22 +0800 Subject: [PATCH 39/87] feat: support setting bar width for bar chart, #24 --- bar_chart.go | 10 ++++++++-- chart_option.go | 2 ++ charts.go | 7 ++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 8826ffb..2addd17 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -59,7 +59,8 @@ type BarChartOption struct { // The option of title Title TitleOption // The legend option - Legend LegendOption + Legend LegendOption + BarWidth int } func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -86,7 +87,12 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin - barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount + if opt.BarWidth > 0 && opt.BarWidth < barWidth { + barWidth = opt.BarWidth + // 重新计算margin + margin = (width - len(seriesList)*barWidth - barMargin*(seriesCount-1)) / 2 + } barMaxHeight := seriesPainter.Height() theme := opt.Theme seriesNames := seriesList.Names() diff --git a/chart_option.go b/chart_option.go index 93b81ba..447ef52 100644 --- a/chart_option.go +++ b/chart_option.go @@ -66,6 +66,8 @@ type ChartOption struct { SymbolShow *bool // The stroke width of line chart LineStrokeWidth float64 + // The bar with of bar chart + BarWidth int // Fill the area of line chart FillArea bool // The child charts diff --git a/charts.go b/charts.go index f7e52e4..f8c94a3 100644 --- a/charts.go +++ b/charts.go @@ -354,9 +354,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(barSeriesList) != 0 { handler.Add(func() error { _, err := NewBarChart(p, BarChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + BarWidth: opt.BarWidth, }).render(renderResult, barSeriesList) return err }) From 6652ece0fed83a33ea6dbe7bd0d99c8b14e945bc Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 29 Sep 2022 20:20:54 +0800 Subject: [PATCH 40/87] feat: support bar height for horizontal bar chart --- bar_chart.go | 2 +- chart_option.go | 2 ++ charts.go | 1 + horizontal_bar_chart.go | 9 +++++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 2addd17..d798c07 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -91,7 +91,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if opt.BarWidth > 0 && opt.BarWidth < barWidth { barWidth = opt.BarWidth // 重新计算margin - margin = (width - len(seriesList)*barWidth - barMargin*(seriesCount-1)) / 2 + margin = (width - seriesCount*barWidth - barMargin*(seriesCount-1)) / 2 } barMaxHeight := seriesPainter.Height() theme := opt.Theme diff --git a/chart_option.go b/chart_option.go index 447ef52..f3bf2cb 100644 --- a/chart_option.go +++ b/chart_option.go @@ -68,6 +68,8 @@ type ChartOption struct { LineStrokeWidth float64 // The bar with of bar chart BarWidth int + // The bar height of horizontal bar chart + BarHeight int // Fill the area of line chart FillArea bool // The child charts diff --git a/charts.go b/charts.go index f8c94a3..c7923f1 100644 --- a/charts.go +++ b/charts.go @@ -369,6 +369,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ Theme: opt.theme, Font: opt.font, + BarHeight: opt.BarHeight, YAxisOptions: opt.YAxisOptions, }).render(renderResult, horizontalBarSeriesList) return err diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 30a9b7d..8ffac44 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -48,7 +48,8 @@ type HorizontalBarChartOption struct { // The option of title Title TitleOption // The legend option - Legend LegendOption + Legend LegendOption + BarHeight int } // NewHorizontalBarChart returns a horizontal bar chart renderer @@ -82,7 +83,11 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri } seriesCount := len(seriesList) // 总的高度-两个margin-(总数-1)的barMargin - barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount + if opt.BarHeight > 0 && opt.BarHeight < barHeight { + barHeight = opt.BarHeight + margin = (height - seriesCount*barHeight - barMargin*(seriesCount-1)) / 2 + } theme := opt.Theme From 0a1061a8db90cf3829279afae2c72ca4968b7c8d Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 11 Oct 2022 20:17:22 +0800 Subject: [PATCH 41/87] docs: update document --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 87c42fa..c31cf77 100644 --- a/README_zh.md +++ b/README_zh.md @@ -569,7 +569,7 @@ BenchmarkMultiChartSVGRender-8 367 3356325 ns/op 默认使用的字符为`roboto`为英文字体库,因此如果需要显示中文字符需要增加中文字体库,`InstallFont`函数可添加对应的字体库,成功添加之后则指定`title.textStyle.fontFamily`即可。 在浏览器中使用`svg`时,如果指定的`fontFamily`不支持中文字符,展示的中文并不会乱码,但是会导致在计算字符宽度等错误。 -字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。 +字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败,字体尽量选择Bold类型,否则生成的图片会有点模糊。 示例见 [examples/chinese/main.go](examples/chinese/main.go) From 74a47a9858bd82972d25f39ae64bae7783721353 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 20 Oct 2022 20:27:42 +0800 Subject: [PATCH 42/87] refactor: enhance value format, #28 --- util.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/util.go b/util.go index a33c6d2..f8a451e 100644 --- a/util.go +++ b/util.go @@ -160,15 +160,25 @@ func NewFloatPoint(f float64) *float64 { v := f return &v } + +const K_VALUE = float64(1000) +const M_VALUE = K_VALUE * K_VALUE +const G_VALUE = M_VALUE * K_VALUE +const T_VALUE = G_VALUE * K_VALUE + func commafWithDigits(value float64) string { decimals := 2 - m := float64(1000 * 1000) - if value >= m { - return humanize.CommafWithDigits(value/m, decimals) + "M" + if value >= T_VALUE { + return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T" } - k := float64(1000) - if value >= k { - return humanize.CommafWithDigits(value/k, decimals) + "k" + if value >= G_VALUE { + return humanize.CommafWithDigits(value/G_VALUE, decimals) + "G" + } + if value >= M_VALUE { + return humanize.CommafWithDigits(value/M_VALUE, decimals) + "M" + } + if value >= K_VALUE { + return humanize.CommafWithDigits(value/K_VALUE, decimals) + "k" } return humanize.CommafWithDigits(value, decimals) } From a88e607bfc83b27502a54879ae749050683c9123 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 21 Oct 2022 20:37:09 +0800 Subject: [PATCH 43/87] refactor: support custom value formatter --- bar_chart.go | 1 + chart_option.go | 2 ++ charts.go | 8 ++++++-- examples/line_chart/main.go | 4 ++++ horizontal_bar_chart.go | 1 + mark_line_test.go | 1 + painter.go | 8 +++++++- range.go | 9 ++++++++- 8 files changed, 30 insertions(+), 4 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index d798c07..19c1664 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -69,6 +69,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B seriesPainter := result.seriesPainter xRange := NewRange(AxisRangeOption{ + Painter: b.p, DivideCount: len(opt.XAxis.Data), Size: seriesPainter.Width(), }) diff --git a/chart_option.go b/chart_option.go index f3bf2cb..d4605a1 100644 --- a/chart_option.go +++ b/chart_option.go @@ -74,6 +74,8 @@ type ChartOption struct { FillArea bool // The child charts Children []ChartOption + // The value formatter + ValueFormatter ValueFormatter } // OptionFunc option function diff --git a/charts.go b/charts.go index c7923f1..b66437c 100644 --- a/charts.go +++ b/charts.go @@ -188,8 +188,9 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e } max, min := opt.SeriesList.GetMaxMin(index) r := NewRange(AxisRangeOption{ - Min: min, - Max: max, + Painter: p, + Min: min, + Max: max, // 高度需要减去x轴的高度 Size: rangeHeight, // 分隔数量 @@ -287,6 +288,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { opt.Parent = p } p := opt.Parent + if opt.ValueFormatter != nil { + p.valueFormatter = opt.ValueFormatter + } if !opt.Box.IsZero() { p = p.Child(PainterBoxOption(opt.Box)) } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 97d5859..c1478a6 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -98,6 +99,9 @@ func main() { } opt.SymbolShow = charts.FalseFlag() opt.LineStrokeWidth = 1 + opt.ValueFormatter = func(f float64) string { + return fmt.Sprintf("%.0f", f) + } }, ) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 8ffac44..58c6e19 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -93,6 +93,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri max, min := seriesList.GetMaxMin(0) xRange := NewRange(AxisRangeOption{ + Painter: p, Min: min, Max: max, DivideCount: defaultAxisDivideCount, diff --git a/mark_line_test.go b/mark_line_test.go index ef29e6f..00d19ef 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -55,6 +55,7 @@ func TestMarkLine(t *testing.T) { StrokeColor: drawing.ColorBlack, Series: series, Range: NewRange(AxisRangeOption{ + Painter: p, Min: 0, Max: 5, Size: p.Height(), diff --git a/painter.go b/painter.go index b7122b7..efd5045 100644 --- a/painter.go +++ b/painter.go @@ -31,6 +31,8 @@ import ( "github.com/wcharczuk/go-chart/v2" ) +type ValueFormatter func(float64) string + type Painter struct { render chart.Renderer box Box @@ -39,7 +41,8 @@ type Painter struct { style Style theme ColorPalette // 类型 - outputType string + outputType string + valueFormatter ValueFormatter } type PainterOptions struct { @@ -188,6 +191,9 @@ func (p *Painter) setOptions(opts ...PainterOption) { func (p *Painter) Child(opt ...PainterOption) *Painter { child := &Painter{ + // 格式化 + valueFormatter: p.valueFormatter, + // render render: p.render, box: p.box.Clone(), font: p.font, diff --git a/range.go b/range.go index ebd0b2d..51d3332 100644 --- a/range.go +++ b/range.go @@ -29,6 +29,7 @@ import ( const defaultAxisDivideCount = 6 type axisRange struct { + p *Painter divideCount int min float64 max float64 @@ -37,6 +38,7 @@ type axisRange struct { } type AxisRangeOption struct { + Painter *Painter // The min value of axis Min float64 // The max value of axis @@ -93,6 +95,7 @@ func NewRange(opt AxisRangeOption) axisRange { max = float64(ceilFloatToInt(expectMax)) } return axisRange{ + p: opt.Painter, divideCount: divideCount, min: min, max: max, @@ -105,9 +108,13 @@ func NewRange(opt AxisRangeOption) axisRange { func (r axisRange) Values() []string { offset := (r.max - r.min) / float64(r.divideCount) values := make([]string, 0) + formatter := commafWithDigits + if r.p != nil && r.p.valueFormatter != nil { + formatter = r.p.valueFormatter + } for i := 0; i <= r.divideCount; i++ { v := r.min + float64(i)*offset - value := commafWithDigits(v) + value := formatter(v) values = append(values, value) } return values From bdcc871ab194dcaeeae1d934cc088bcffdff5fd2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 3 Nov 2022 21:31:53 +0800 Subject: [PATCH 44/87] fix: fix series render of horizontal bar, #31 --- axis.go | 15 ++++++++++----- charts.go | 10 +++++++++- examples/horizontal_bar_chart/main.go | 3 +++ yaxis.go | 3 +++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/axis.go b/axis.go index 3c0484c..578813c 100644 --- a/axis.go +++ b/axis.go @@ -75,6 +75,7 @@ type AxisOption struct { SplitLineShow bool // The color of split line SplitLineColor Color + Unit int } func (a *axisPainter) Render() (Box, error) { @@ -159,11 +160,15 @@ func (a *axisPainter) Render() (Box, error) { // 根据文本宽度计算较为符合的展示项 fitTextCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) - unit := ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) - unit = chart.MaxInt(unit, opt.SplitNumber) - // 偶数 - if unit%2 == 0 && dataCount%(unit+1) == 0 { - unit++ + unit := opt.Unit + if unit <= 0 { + + unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) + unit = chart.MaxInt(unit, opt.SplitNumber) + // 偶数 + if unit%2 == 0 && dataCount%(unit+1) == 0 { + unit++ + } } width := 0 diff --git a/charts.go b/charts.go index b66437c..d6745d3 100644 --- a/charts.go +++ b/charts.go @@ -186,6 +186,10 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if len(opt.YAxisOptions) > index { yAxisOption = opt.YAxisOptions[index] } + divideCount := yAxisOption.DivideCount + if divideCount <= 0 { + divideCount = defaultAxisDivideCount + } max, min := opt.SeriesList.GetMaxMin(index) r := NewRange(AxisRangeOption{ Painter: p, @@ -194,7 +198,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e // 高度需要减去x轴的高度 Size: rangeHeight, // 分隔数量 - DivideCount: defaultAxisDivideCount, + DivideCount: divideCount, }) if yAxisOption.Min != nil && *yAxisOption.Min <= min { r.min = *yAxisOption.Min @@ -346,6 +350,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { }, } } + if len(horizontalBarSeriesList) != 0 { + renderOpt.YAxisOptions[0].DivideCount = len(renderOpt.YAxisOptions[0].Data) + renderOpt.YAxisOptions[0].Unit = 1 + } renderResult, err := defaultRender(p, renderOpt) if err != nil { diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index 8b996b6..a0f5bda 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -26,6 +26,7 @@ func writeFile(buf []byte) error { func main() { values := [][]float64{ { + 8203, 18203, 23489, 29034, @@ -34,6 +35,7 @@ func main() { 630230, }, { + 9325, 19325, 23438, 31000, @@ -56,6 +58,7 @@ func main() { "2012", }), charts.YAxisDataOptionFunc([]string{ + "UN", "Brazil", "Indonesia", "USA", diff --git a/yaxis.go b/yaxis.go index eb9034c..bece2cc 100644 --- a/yaxis.go +++ b/yaxis.go @@ -47,6 +47,8 @@ type YAxisOption struct { Color Color // The flag for show axis, set this to *false will hide axis Show *bool + DivideCount int + Unit int isCategoryAxis bool } @@ -87,6 +89,7 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { SplitLineShow: true, SplitLineColor: theme.GetAxisSplitLineColor(), Show: opt.Show, + Unit: opt.Unit, } if !opt.Color.IsZero() { axisOpt.FontColor = opt.Color From 6f6d6c344730f48b5297bb9695a16f9ecc7874f1 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 7 Nov 2022 20:34:28 +0800 Subject: [PATCH 45/87] fix: fix label render of pie chart, #34 --- pie_chart.go | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index 0075ffc..b4714ac 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -101,8 +101,23 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B theme := opt.Theme currentValue := float64(0) - prevEndX := 0 - prevEndY := 0 + prevPoints := make([]Point, 0) + + isOverride := func(x, y int) bool { + for _, p := range prevPoints { + if math.Abs(float64(p.Y-y)) > labelFontSize { + continue + } + // label可能较多内容,不好计算横向占用空间 + // 因此x的位置需要中间位置两侧,否则认为override + if (p.X <= cx && x <= cx) || + (p.X > cx && x > cx) { + return true + } + } + return false + } + for index, v := range values { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, @@ -134,13 +149,17 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B endx := cx + int(labelRadius*math.Cos(angle)) endy := cy + int(labelRadius*math.Sin(angle)) // 计算是否有重叠,如果有则调整y坐标位置 - if index != 0 && - math.Abs(float64(endx-prevEndX)) < labelFontSize && - math.Abs(float64(endy-prevEndY)) < labelFontSize { + // 最多只尝试5次 + for i := 0; i < 5; i++ { + if !isOverride(endx, endy) { + break + } endy -= (labelFontSize << 1) } - prevEndX = endx - prevEndY = endy + prevPoints = append(prevPoints, Point{ + X: endx, + Y: endy, + }) seriesPainter.MoveTo(startx, starty) seriesPainter.LineTo(endx, endy) From 2ed86a81d018bcf9d0105bf217a9c424aa42bf5e Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 12 Nov 2022 10:48:24 +0800 Subject: [PATCH 46/87] fix: fix setting font family for table render --- chart_option.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chart_option.go b/chart_option.go index d4605a1..3c8ac4b 100644 --- a/chart_option.go +++ b/chart_option.go @@ -384,6 +384,9 @@ func TableOptionRender(opt TableChartOption) (*Painter, error) { if opt.Width <= 0 { opt.Width = defaultChartWidth } + if opt.FontFamily != "" { + opt.Font, _ = GetFont(opt.FontFamily) + } if opt.Font == nil { opt.Font, _ = chart.GetDefaultFont() } From de4250f60bfad7d22847b089bef62f2dce30091b Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 12 Nov 2022 20:01:36 +0800 Subject: [PATCH 47/87] feat: support get and set default font --- chart_option.go | 5 ++--- font.go | 19 ++++++++++++++++++- mark_line.go | 3 +-- painter.go | 2 +- painter_test.go | 2 +- theme.go | 3 +-- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/chart_option.go b/chart_option.go index 3c8ac4b..ee6851f 100644 --- a/chart_option.go +++ b/chart_option.go @@ -26,7 +26,6 @@ import ( "sort" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) type ChartOption struct { @@ -270,7 +269,7 @@ func (o *ChartOption) fillDefault() { o.font, _ = GetFont(o.FontFamily) if o.font == nil { - o.font, _ = chart.GetDefaultFont() + o.font, _ = GetDefaultFont() } else { // 如果指定了字体,则设置主题的字体 t.SetFont(o.font) @@ -388,7 +387,7 @@ func TableOptionRender(opt TableChartOption) (*Painter, error) { opt.Font, _ = GetFont(opt.FontFamily) } if opt.Font == nil { - opt.Font, _ = chart.GetDefaultFont() + opt.Font, _ = GetDefaultFont() } p, err := NewPainter(PainterOptions{ diff --git a/font.go b/font.go index c40b51e..dae5141 100644 --- a/font.go +++ b/font.go @@ -32,9 +32,13 @@ import ( var fonts = sync.Map{} var ErrFontNotExists = errors.New("font is not exists") +var defaultFontFamily = "defaultFontFamily" func init() { - _ = InstallFont("roboto", roboto.Roboto) + name := "roboto" + _ = InstallFont(name, roboto.Roboto) + font, _ := GetFont(name) + SetDefaultFont(font) } // InstallFont installs the font for charts @@ -47,6 +51,19 @@ func InstallFont(fontFamily string, data []byte) error { return nil } +// GetDefaultFont get default font +func GetDefaultFont() (*truetype.Font, error) { + return GetFont(defaultFontFamily) +} + +// SetDefaultFont set default font +func SetDefaultFont(font *truetype.Font) { + if font == nil { + return + } + fonts.Store(defaultFontFamily, font) +} + // GetFont get the font by font family func GetFont(fontFamily string) (*truetype.Font, error) { value, ok := fonts.Load(fontFamily) diff --git a/mark_line.go b/mark_line.go index af1062d..bc850bb 100644 --- a/mark_line.go +++ b/mark_line.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) // NewMarkLine returns a series mark line @@ -75,7 +74,7 @@ func (m *markLinePainter) Render() (Box, error) { } font := opt.Font if font == nil { - font, _ = chart.GetDefaultFont() + font, _ = GetDefaultFont() } summary := s.Summary() for _, markLine := range s.MarkLine.Data { diff --git a/painter.go b/painter.go index efd5045..97ad205 100644 --- a/painter.go +++ b/painter.go @@ -149,7 +149,7 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { } font := opts.Font if font == nil { - f, err := chart.GetDefaultFont() + f, err := GetDefaultFont() if err != nil { return nil, err } diff --git a/painter_test.go b/painter_test.go index 96e41ef..2392d5b 100644 --- a/painter_test.go +++ b/painter_test.go @@ -351,7 +351,7 @@ func TestPainterTextFit(t *testing.T) { Type: ChartOutputSVG, }) assert.Nil(err) - f, _ := chart.GetDefaultFont() + f, _ := GetDefaultFont() style := Style{ FontSize: 12, FontColor: chart.ColorBlack, diff --git a/theme.go b/theme.go index 8068687..17706ad 100644 --- a/theme.go +++ b/theme.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -311,7 +310,7 @@ func (t *themeColorPalette) GetFont() *truetype.Font { if t.font != nil { return t.font } - f, _ := chart.GetDefaultFont() + f, _ := GetDefaultFont() return f } From 7e1f003be85d09216e71a89337634bd38e4abed2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 12 Nov 2022 20:18:02 +0800 Subject: [PATCH 48/87] refactor: update demo --- examples/chinese/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 9068a08..d77216a 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -34,6 +34,8 @@ func main() { if err != nil { panic(err) } + font, _ := charts.GetFont("noto") + charts.SetDefaultFont(font) values := [][]float64{ { @@ -85,7 +87,6 @@ func main() { p, err := charts.LineRender( values, charts.TitleTextOptionFunc("测试"), - charts.FontFamilyOptionFunc("noto"), charts.XAxisDataOptionFunc([]string{ "星期一", "星期二", From a42d0727df41f5f788ae015c9472a5675bf27774 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 15 Nov 2022 20:09:29 +0800 Subject: [PATCH 49/87] feat: support text rotation --- painter.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/painter.go b/painter.go index 97ad205..6743b37 100644 --- a/painter.go +++ b/painter.go @@ -558,6 +558,12 @@ func (p *Painter) Text(body string, x, y int) *Painter { return p } +func (p *Painter) TextRotation(body string, x, y int, radians float64) { + p.render.SetTextRotation(radians) + p.render.Text(body, x, y) + p.render.ClearTextRotation() +} + func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box { style := p.style textWarp := style.TextWrap From 55eca7b0b9331b660ea1f1c03fab3d803769e815 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 16 Nov 2022 20:46:19 +0800 Subject: [PATCH 50/87] feat: support detect color dark or light --- chart_option_test.go | 2 +- echarts_test.go | 2 +- mark_point.go | 12 +++++------- theme.go | 13 +++++++++++++ util.go | 7 +++++++ util_test.go | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 9 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index 6f331b3..ff17750 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index 5c2dbad..2ce1715 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/mark_point.go b/mark_point.go index f6c93f3..fd8a88b 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" ) // NewMarkPoint returns a series mark point @@ -78,16 +77,15 @@ func (m *markPointPainter) Render() (Box, error) { symbolSize = 30 } textStyle := Style{ - FontColor: drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, FontSize: labelFontSize, StrokeWidth: 1, Font: opt.Font, } + if isLightColor(opt.FillColor) { + textStyle.FontColor = defaultLightFontColor + } else { + textStyle.FontColor = defaultDarkFontColor + } painter.OverrideDrawingStyle(Style{ FillColor: opt.FillColor, }).OverrideTextStyle(textStyle) diff --git a/theme.go b/theme.go index 17706ad..a6d624f 100644 --- a/theme.go +++ b/theme.go @@ -76,6 +76,19 @@ const defaultFontSize = 12.0 var defaultTheme ColorPalette +var defaultLightFontColor = drawing.Color{ + R: 70, + G: 70, + B: 70, + A: 255, +} +var defaultDarkFontColor = drawing.Color{ + R: 238, + G: 238, + B: 238, + A: 255, +} + func init() { echartSeriesColors := []Color{ parseColor("#5470c6"), diff --git a/util.go b/util.go index f8a451e..b333e6d 100644 --- a/util.go +++ b/util.go @@ -262,3 +262,10 @@ func getPolygonPoints(center Point, radius float64, sides int) []Point { } return points } + +func isLightColor(c Color) bool { + r := float64(c.R) * float64(c.R) * 0.299 + g := float64(c.G) * float64(c.G) * 0.587 + b := float64(c.B) * float64(c.B) * 0.114 + return math.Sqrt(r+g+b) > 127.5 +} diff --git a/util_test.go b/util_test.go index 7c2ab2f..62fd08d 100644 --- a/util_test.go +++ b/util_test.go @@ -189,3 +189,35 @@ func TestParseColor(t *testing.T) { A: 250, }, c) } + +func TestIsLightColor(t *testing.T) { + assert := assert.New(t) + + assert.True(isLightColor(drawing.Color{ + R: 255, + G: 255, + B: 255, + })) + assert.True(isLightColor(drawing.Color{ + R: 145, + G: 204, + B: 117, + })) + + assert.False(isLightColor(drawing.Color{ + R: 88, + G: 112, + B: 198, + })) + + assert.False(isLightColor(drawing.Color{ + R: 0, + G: 0, + B: 0, + })) + assert.False(isLightColor(drawing.Color{ + R: 16, + G: 12, + B: 42, + })) +} From 4fc250aefc0ec4e50099ca483b2a7a3f497b33a8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 22 Nov 2022 22:41:56 +0800 Subject: [PATCH 51/87] feat: support rotate series label --- bar_chart.go | 19 +++++++++++++++++- examples/charts/main.go | 4 ++++ painter.go | 9 ++++++++- series.go | 2 ++ series_label.go | 44 +++++++++++++++++++++++++++++++---------- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 19c1664..695b9fd 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,6 +23,8 @@ package charts import ( + "math" + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" ) @@ -164,11 +166,26 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if labelPainter == nil { continue } + y := barMaxHeight - h + radians := float64(0) + var fontColor Color + if series.Label.Position == PositionBottom { + y = barMaxHeight + radians = -math.Pi / 2 + if isLightColor(fillColor) { + fontColor = defaultLightFontColor + } else { + fontColor = defaultDarkFontColor + } + } labelPainter.Add(LabelValue{ Index: index, Value: item.Value, X: x + barWidth>>1, - Y: barMaxHeight - h, + Y: y, + // 旋转 + Radians: radians, + FontColor: fontColor, }) } diff --git a/examples/charts/main.go b/examples/charts/main.go index c3bb486..76aa42c 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -355,6 +355,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { Value: 180, }, }, + Label: charts.SeriesLabel{ + Show: true, + Position: charts.PositionBottom, + }, }, }, }, diff --git a/painter.go b/painter.go index 6743b37..a0f81ed 100644 --- a/painter.go +++ b/painter.go @@ -560,7 +560,14 @@ func (p *Painter) Text(body string, x, y int) *Painter { func (p *Painter) TextRotation(body string, x, y int, radians float64) { p.render.SetTextRotation(radians) - p.render.Text(body, x, y) + p.render.Text(body, x+p.box.Left, y+p.box.Top) + p.render.ClearTextRotation() +} + +func (p *Painter) SetTextRotation(radians float64) { + p.render.SetTextRotation(radians) +} +func (p *Painter) ClearTextRotation() { p.render.ClearTextRotation() } diff --git a/series.go b/series.go index 7bd6834..373c7dc 100644 --- a/series.go +++ b/series.go @@ -79,6 +79,8 @@ type SeriesLabel struct { Show bool // Distance to the host graphic element. Distance int + // The position of label + Position string } const ( diff --git a/series_label.go b/series_label.go index 57bd1bf..f2dd40f 100644 --- a/series_label.go +++ b/series_label.go @@ -32,6 +32,8 @@ type labelRenderValue struct { Style Style X int Y int + // 旋转 + Radians float64 } type LabelValue struct { @@ -39,6 +41,10 @@ type LabelValue struct { Value float64 X int Y int + // 旋转 + Radians float64 + // 字体颜色 + FontColor Color } type SeriesLabelPainter struct { @@ -81,19 +87,33 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { FontSize: labelFontSize, Font: o.font, } + if !value.FontColor.IsZero() { + label.Color = value.FontColor + } if !label.Color.IsZero() { labelStyle.FontColor = label.Color } - o.p.OverrideDrawingStyle(labelStyle) - textBox := o.p.MeasureText(text) - renderValue := labelRenderValue{ - Text: text, - Style: labelStyle, - X: value.X - textBox.Width()>>1, - Y: value.Y - distance, + p := o.p + p.OverrideDrawingStyle(labelStyle) + rotated := value.Radians != 0 + if rotated { + p.SetTextRotation(value.Radians) } - if textBox.Width()%2 != 0 { - renderValue.X++ + textBox := p.MeasureText(text) + renderValue := labelRenderValue{ + Text: text, + Style: labelStyle, + X: value.X - textBox.Width()>>1, + Y: value.Y - distance, + Radians: value.Radians, + } + if rotated { + renderValue.X = value.X + textBox.Width()>>1 - 1 + p.ClearTextRotation() + } else { + if textBox.Width()%2 != 0 { + renderValue.X++ + } } o.values = append(o.values, renderValue) } @@ -101,7 +121,11 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { func (o *SeriesLabelPainter) Render() (Box, error) { for _, item := range o.values { o.p.OverrideTextStyle(item.Style) - o.p.Text(item.Text, item.X, item.Y) + if item.Radians != 0 { + o.p.TextRotation(item.Text, item.X, item.Y, item.Radians) + } else { + o.p.Text(item.Text, item.X, item.Y) + } } return chart.BoxZero, nil } From 6db8e2c8dc5f5ead957474fddb4af20787b82b95 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 23 Nov 2022 23:01:52 +0800 Subject: [PATCH 52/87] feat: support series label for horizontal bar --- bar_chart.go | 1 + horizontal_bar_chart.go | 41 +++++++++++++++++++++++++++++++++++++++++ series.go | 2 ++ series_label.go | 16 ++++++++++++++-- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 695b9fd..8219472 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -186,6 +186,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B // 旋转 Radians: radians, FontColor: fontColor, + Offset: series.Label.Offset, }) } diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 58c6e19..5e433a6 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -99,11 +99,25 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri DivideCount: defaultAxisDivideCount, Size: seriesPainter.Width(), }) + seriesNames := seriesList.Names() + rendererList := []Renderer{} for index := range seriesList { series := seriesList[index] seriesColor := theme.GetSeriesColor(series.index) divideValues := yRange.AutoDivide() + + var labelPainter *SeriesLabelPainter + if series.Label.Show { + labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ + P: seriesPainter, + SeriesNames: seriesNames, + Label: series.Label, + Theme: opt.Theme, + Font: opt.Font, + }) + rendererList = append(rendererList, labelPainter) + } for j, item := range series.Data { if j >= yRange.divideCount { continue @@ -130,8 +144,35 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri Right: right, Bottom: y + barHeight, }) + // 如果label不需要展示,则返回 + if labelPainter == nil { + continue + } + x := right + var fontColor Color + if series.Label.Position == PositionLeft { + x = 0 + if isLightColor(fillColor) { + fontColor = defaultLightFontColor + } else { + fontColor = defaultDarkFontColor + } + } + labelPainter.Add(LabelValue{ + Orient: OrientHorizontal, + Index: index, + Value: item.Value, + X: x, + Y: y + barHeight>>1, + FontColor: fontColor, + Offset: series.Label.Offset, + }) } } + err := doRender(rendererList...) + if err != nil { + return BoxZero, err + } return p.box, nil } diff --git a/series.go b/series.go index 373c7dc..c36fa8b 100644 --- a/series.go +++ b/series.go @@ -81,6 +81,8 @@ type SeriesLabel struct { Distance int // The position of label Position string + // The offset of label's position + Offset Box } const ( diff --git a/series_label.go b/series_label.go index f2dd40f..f0fb2ec 100644 --- a/series_label.go +++ b/series_label.go @@ -45,6 +45,8 @@ type LabelValue struct { Radians float64 // 字体颜色 FontColor Color + Orient string + Offset Box } type SeriesLabelPainter struct { @@ -103,10 +105,18 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { renderValue := labelRenderValue{ Text: text, Style: labelStyle, - X: value.X - textBox.Width()>>1, - Y: value.Y - distance, + X: value.X, + Y: value.Y, Radians: value.Radians, } + if value.Orient != OrientHorizontal { + renderValue.X -= textBox.Width() >> 1 + renderValue.Y -= distance + } else { + renderValue.X += distance + renderValue.Y += textBox.Height() >> 1 + renderValue.Y -= 2 + } if rotated { renderValue.X = value.X + textBox.Width()>>1 - 1 p.ClearTextRotation() @@ -115,6 +125,8 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { renderValue.X++ } } + renderValue.X += value.Offset.Left + renderValue.Y += value.Offset.Top o.values = append(o.values, renderValue) } From 5f0aec60d3d3300316bd9d4d1ad58e357f7c4caf Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 24 Nov 2022 20:12:19 +0800 Subject: [PATCH 53/87] refactor: adjust label value of horizontal bar --- horizontal_bar_chart.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 5e433a6..1340103 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -148,25 +148,23 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri if labelPainter == nil { continue } - x := right - var fontColor Color + labelValue := LabelValue{ + Orient: OrientHorizontal, + Index: index, + Value: item.Value, + X: right, + Y: y + barHeight>>1, + Offset: series.Label.Offset, + } if series.Label.Position == PositionLeft { - x = 0 + labelValue.X = 0 if isLightColor(fillColor) { - fontColor = defaultLightFontColor + labelValue.FontColor = defaultLightFontColor } else { - fontColor = defaultDarkFontColor + labelValue.FontColor = defaultDarkFontColor } } - labelPainter.Add(LabelValue{ - Orient: OrientHorizontal, - Index: index, - Value: item.Value, - X: x, - Y: y + barHeight>>1, - FontColor: fontColor, - Offset: series.Label.Offset, - }) + labelPainter.Add(labelValue) } } err := doRender(rendererList...) From df6180e59aea0a7d3d45be72a9b49bb4a09df0c9 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 28 Nov 2022 19:55:14 +0800 Subject: [PATCH 54/87] fix: fix zero max value of nan, #37 --- range.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/range.go b/range.go index 51d3332..ec64c2d 100644 --- a/range.go +++ b/range.go @@ -121,6 +121,9 @@ func (r axisRange) Values() []string { } func (r *axisRange) getHeight(value float64) int { + if r.max <= r.min { + return 0 + } v := (value - r.min) / (r.max - r.min) return int(v * float64(r.size)) } From f9a534ea02fe56f1c4ec79c73839664ba8cb51a6 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 7 Dec 2022 19:57:35 +0800 Subject: [PATCH 55/87] fix: fix the color of series label, #37 --- bar_chart.go | 12 +++++++----- horizontal_bar_chart.go | 23 +++++++++++++---------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 8219472..d8a307e 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -168,14 +168,16 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } y := barMaxHeight - h radians := float64(0) - var fontColor Color + fontColor := series.Label.Color if series.Label.Position == PositionBottom { y = barMaxHeight radians = -math.Pi / 2 - if isLightColor(fillColor) { - fontColor = defaultLightFontColor - } else { - fontColor = defaultDarkFontColor + if fontColor.IsZero() { + if isLightColor(fillColor) { + fontColor = defaultLightFontColor + } else { + fontColor = defaultDarkFontColor + } } } labelPainter.Add(LabelValue{ diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 1340103..95d9a3d 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -149,19 +149,22 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri continue } labelValue := LabelValue{ - Orient: OrientHorizontal, - Index: index, - Value: item.Value, - X: right, - Y: y + barHeight>>1, - Offset: series.Label.Offset, + Orient: OrientHorizontal, + Index: index, + Value: item.Value, + X: right, + Y: y + barHeight>>1, + Offset: series.Label.Offset, + FontColor: series.Label.Color, } if series.Label.Position == PositionLeft { labelValue.X = 0 - if isLightColor(fillColor) { - labelValue.FontColor = defaultLightFontColor - } else { - labelValue.FontColor = defaultDarkFontColor + if labelValue.FontColor.IsZero() { + if isLightColor(fillColor) { + labelValue.FontColor = defaultLightFontColor + } else { + labelValue.FontColor = defaultDarkFontColor + } } } labelPainter.Add(labelValue) From ef04ac14abcfe6380464fdc4b3d923448286e198 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 9 Dec 2022 20:08:02 +0800 Subject: [PATCH 56/87] feat: support font size for series label, #38 --- bar_chart.go | 1 + horizontal_bar_chart.go | 1 + line_chart.go | 2 ++ series.go | 2 ++ series_label.go | 9 +++++++-- 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index d8a307e..efeb465 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -189,6 +189,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Radians: radians, FontColor: fontColor, Offset: series.Label.Offset, + FontSize: series.Label.FontSize, }) } diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 95d9a3d..2ab4c03 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -156,6 +156,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri Y: y + barHeight>>1, Offset: series.Label.Offset, FontColor: series.Label.Color, + FontSize: series.Label.FontSize, } if series.Label.Position == PositionLeft { labelValue.X = 0 diff --git a/line_chart.go b/line_chart.go index 26f94a4..9f350bd 100644 --- a/line_chart.go +++ b/line_chart.go @@ -147,6 +147,8 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Value: item.Value, X: p.X, Y: p.Y, + // 字体大小 + FontSize: series.Label.FontSize, }) } // 如果需要填充区域 diff --git a/series.go b/series.go index c36fa8b..13c637e 100644 --- a/series.go +++ b/series.go @@ -83,6 +83,8 @@ type SeriesLabel struct { Position string // The offset of label's position Offset Box + // The font size of label + FontSize float64 } const ( diff --git a/series_label.go b/series_label.go index f0fb2ec..10fd148 100644 --- a/series_label.go +++ b/series_label.go @@ -45,8 +45,10 @@ type LabelValue struct { Radians float64 // 字体颜色 FontColor Color - Orient string - Offset Box + // 字体大小 + FontSize float64 + Orient string + Offset Box } type SeriesLabelPainter struct { @@ -89,6 +91,9 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { FontSize: labelFontSize, Font: o.font, } + if value.FontSize != 0 { + labelStyle.FontSize = value.FontSize + } if !value.FontColor.IsZero() { label.Color = value.FontColor } From d5533447f565ed55b604a59d0578375c61d496cd Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Dec 2022 14:57:05 +0800 Subject: [PATCH 57/87] feat: support text rotation for series label, #38 --- axis.go | 26 ++++++++++++++++++++------ painter.go | 9 +++++++++ xaxis.go | 8 +++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/axis.go b/axis.go index 578813c..3f71451 100644 --- a/axis.go +++ b/axis.go @@ -75,7 +75,11 @@ type AxisOption struct { SplitLineShow bool // The color of split line 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) { @@ -153,7 +157,15 @@ func (a *axisPainter) Render() (Box, error) { } top.SetDrawingStyle(style).OverrideTextStyle(style) + isTextRotation := opt.TextRotation != 0 + + if isTextRotation { + top.SetTextRotation(opt.TextRotation) + } textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) + if isTextRotation { + top.ClearTextRotation() + } // 增加30px来计算文本展示区域 textFillWidth := float64(textMaxWidth + 20) @@ -261,11 +273,13 @@ func (a *axisPainter) Render() (Box, error) { Top: labelPaddingTop, Right: labelPaddingRight, })).MultiText(MultiTextOption{ - Align: textAlign, - TextList: data, - Orient: orient, - Unit: unit, - Position: labelPosition, + Align: textAlign, + TextList: data, + Orient: orient, + Unit: unit, + Position: labelPosition, + TextRotation: opt.TextRotation, + Offset: opt.LabelOffset, }) // 显示辅助线 if opt.SplitLineShow { diff --git a/painter.go b/painter.go index a0f81ed..71d205f 100644 --- a/painter.go +++ b/painter.go @@ -71,6 +71,9 @@ type MultiTextOption struct { Unit int Position string Align string + // The text rotation of label + TextRotation float64 + Offset Box } type GridOption struct { @@ -682,10 +685,13 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } + offset := opt.Offset for index, text := range opt.TextList { if opt.Unit != 0 && index%opt.Unit != showIndex { continue } + p.ClearTextRotation() + p.SetTextRotation(opt.TextRotation) box := p.MeasureText(text) start := values[index] if positionCenter { @@ -706,8 +712,11 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { x = start - box.Width()>>1 } + x += offset.Left + y += offset.Top p.Text(text, x, y) } + p.ClearTextRotation() return p } diff --git a/xaxis.go b/xaxis.go index 00636a5..95578ff 100644 --- a/xaxis.go +++ b/xaxis.go @@ -47,7 +47,11 @@ type XAxisOption struct { // The line color of axis StrokeColor Color // The color of label - FontColor Color + FontColor Color + // The text rotation of label + TextRotation float64 + // The offset of label + LabelOffset Box isValueAxis bool } @@ -81,6 +85,8 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { FontColor: opt.FontColor, Show: opt.Show, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + TextRotation: opt.TextRotation, + LabelOffset: opt.LabelOffset, } if opt.isValueAxis { axisOpt.SplitLineShow = true From 830d4bdd21201985bba34404086e7fbdcf8134fd Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Dec 2022 14:59:37 +0800 Subject: [PATCH 58/87] fix: fix test for text roration --- painter.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/painter.go b/painter.go index 71d205f..8f43940 100644 --- a/painter.go +++ b/painter.go @@ -685,13 +685,16 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } + isTextRotation := opt.TextRotation != 0 offset := opt.Offset for index, text := range opt.TextList { if opt.Unit != 0 && index%opt.Unit != showIndex { continue } - p.ClearTextRotation() - p.SetTextRotation(opt.TextRotation) + if isTextRotation { + p.ClearTextRotation() + p.SetTextRotation(opt.TextRotation) + } box := p.MeasureText(text) start := values[index] if positionCenter { @@ -716,7 +719,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { y += offset.Top p.Text(text, x, y) } - p.ClearTextRotation() + if isTextRotation { + p.ClearTextRotation() + } return p } From a767b3e1af4fc0a275b97a68483292ae53445a54 Mon Sep 17 00:00:00 2001 From: Thomas Knierim Date: Mon, 26 Dec 2022 15:06:53 +0700 Subject: [PATCH 59/87] added option for line chart bg fill opacity --- chart_option.go | 2 ++ charts.go | 1 + line_chart.go | 8 +++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/chart_option.go b/chart_option.go index ee6851f..5311d50 100644 --- a/chart_option.go +++ b/chart_option.go @@ -71,6 +71,8 @@ type ChartOption struct { BarHeight int // Fill the area of line chart FillArea bool + // background fill (alpha) opacity + Opacity uint8 // The child charts Children []ChartOption // The value formatter diff --git a/charts.go b/charts.go index d6745d3..8613050 100644 --- a/charts.go +++ b/charts.go @@ -409,6 +409,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { SymbolShow: opt.SymbolShow, StrokeWidth: opt.LineStrokeWidth, FillArea: opt.FillArea, + Opacity: opt.Opacity, }).render(renderResult, lineSeriesList) return err }) diff --git a/line_chart.go b/line_chart.go index 9f350bd..bdbd38e 100644 --- a/line_chart.go +++ b/line_chart.go @@ -70,6 +70,8 @@ type LineChartOption struct { FillArea bool // background is filled backgroundIsFilled bool + // background fill (alpha) opacity + Opacity uint8 } func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -156,6 +158,10 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( areaPoints := make([]Point, len(points)) copy(areaPoints, points) bottomY := yRange.getRestHeight(yRange.min) + var opacity uint8 = 200 + if opt.Opacity != 0 { + opacity = opt.Opacity + } areaPoints = append(areaPoints, Point{ X: areaPoints[len(areaPoints)-1].X, Y: bottomY, @@ -164,7 +170,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Y: bottomY, }, areaPoints[0]) seriesPainter.SetDrawingStyle(Style{ - FillColor: seriesColor.WithAlpha(200), + FillColor: seriesColor.WithAlpha(opacity), }) seriesPainter.FillArea(areaPoints) } From e10175594b517f9a12217478b440faecc8d3c455 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 5 Jan 2023 19:15:58 +0800 Subject: [PATCH 60/87] feat: support label format for funnel chart, #41 --- funnel_chart.go | 8 +++----- series.go | 8 ++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/funnel_chart.go b/funnel_chart.go index 719853a..300b539 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -23,9 +23,6 @@ package charts import ( - "fmt" - - "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" ) @@ -95,13 +92,14 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) y := 0 widthList := make([]int, len(seriesList)) textList := make([]string, len(seriesList)) + seriesNames := seriesList.Names() for index, item := range seriesList { value := item.Data[0].Value widthPercent := (value - min) / (max - min) w := int(widthPercent * float64(width)) widthList[index] = w - p := humanize.CommafWithDigits(value/max*100, 2) + "%" - textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) + percent := value / max + textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) } for index, w := range widthList { diff --git a/series.go b/series.go index 13c637e..f28bfa9 100644 --- a/series.go +++ b/series.go @@ -279,6 +279,14 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { 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 func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { From 8ba9e2e1b207e0ead3826ba28b3bbd15304651e8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 11 Jan 2023 20:41:16 +0800 Subject: [PATCH 61/87] fix: fix x axis label of horizontal bar chart, #42 --- charts.go | 11 ++++++++++- examples/horizontal_bar_chart/main.go | 28 +++++++++++++-------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/charts.go b/charts.go index 8613050..74db733 100644 --- a/charts.go +++ b/charts.go @@ -215,7 +215,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e yAxisOption.Data = r.Values() } else { yAxisOption.isCategoryAxis = true - opt.XAxis.Data = r.Values() + // 由于x轴为value部分,因此计算其label单独处理 + opt.XAxis.Data = NewRange(AxisRangeOption{ + Painter: p, + Min: min, + Max: max, + // 高度需要减去x轴的高度 + Size: rangeHeight, + // 分隔数量 + DivideCount: defaultAxisDivideCount, + }).Values() opt.XAxis.isValueAxis = true } reverseStringSlice(yAxisOption.Data) diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index a0f5bda..a1c50a7 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -26,22 +26,22 @@ func writeFile(buf []byte) error { func main() { values := [][]float64{ { - 8203, - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, + 10, + 30, + 50, + 70, + 90, + 110, + 130, }, { - 9325, - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, + 20, + 40, + 60, + 80, + 100, + 120, + 140, }, } p, err := charts.HorizontalBarRender( From d3f7a773afc152f3be5b6237baa23bd2f40177db Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 12 Jan 2023 20:20:36 +0800 Subject: [PATCH 62/87] fix: fix zero value of funnel chart, #43 --- examples/funnel_chart/main.go | 4 ++++ funnel_chart.go | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 8f21db6..24f8afe 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -30,6 +30,8 @@ func main() { 60, 40, 20, + 10, + 0, } p, err := charts.FunnelRender( values, @@ -40,6 +42,8 @@ func main() { "Visit", "Inquiry", "Order", + "Pay", + "Cancel", }), ) if err != nil { diff --git a/funnel_chart.go b/funnel_chart.go index 300b539..d4a8bdd 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -93,12 +93,21 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) widthList := make([]int, len(seriesList)) textList := make([]string, len(seriesList)) seriesNames := seriesList.Names() + offset := max - min for index, item := range seriesList { value := item.Data[0].Value - widthPercent := (value - min) / (max - min) + // 最大最小值一致则为100% + widthPercent := 100.0 + if offset != 0 { + widthPercent = (value - min) / offset + } w := int(widthPercent * float64(width)) widthList[index] = w - percent := value / max + // 如果最大值为0,则占比100% + percent := 1.0 + if max != 0 { + percent = value / max + } textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) } From 29a5ece5458638b95f3d85218f96be56abdadb0d Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 14 Feb 2023 20:35:54 +0800 Subject: [PATCH 63/87] chore: update go modules --- .github/workflows/test.yml | 1 + go.mod | 6 +++--- go.sum | 13 ++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61449a3..f591a3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: go: + - '1.20' - '1.19' - '1.18' - '1.17' diff --git a/go.mod b/go.mod index de0bb9c..e265627 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/vicanso/go-charts/v2 go 1.17 require ( - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e0b1547..ef2a000 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,24 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 20e8d4a078b7b17d373aea3a75e95d4119b5c12f Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Feb 2023 14:04:30 +0800 Subject: [PATCH 64/87] feat: support to set the first axis --- axis.go | 4 ++ examples/time_line_chart/main.go | 82 ++++++++++++++++++++++++++++++++ painter.go | 15 +++++- xaxis.go | 3 ++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 examples/time_line_chart/main.go diff --git a/axis.go b/axis.go index 3f71451..762a6a2 100644 --- a/axis.go +++ b/axis.go @@ -63,6 +63,8 @@ type AxisOption struct { StrokeWidth float64 // The length of the axis tick TickLength int + // The first axis + FirstAxis int // The margin value of label LabelMargin int // The font size of label @@ -255,6 +257,7 @@ func (a *axisPainter) Render() (Box, error) { Length: tickLength, Unit: unit, Orient: orient, + First: opt.FirstAxis, }) p.LineStroke([]Point{ { @@ -273,6 +276,7 @@ func (a *axisPainter) Render() (Box, error) { Top: labelPaddingTop, Right: labelPaddingRight, })).MultiText(MultiTextOption{ + First: opt.FirstAxis, Align: textAlign, TextList: data, Orient: orient, diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go new file mode 100644 index 0000000..10932cd --- /dev/null +++ b/examples/time_line_chart/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "time" + + "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, "time-line-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + xAxisValue := []string{} + values := []float64{} + now := time.Now() + firstAxis := 0 + for i := 0; i < 300; i++ { + // 设置首个axis为xx:00的时间点 + if firstAxis == 0 && now.Minute() == 0 { + firstAxis = i + } + xAxisValue = append(xAxisValue, now.Format("15:04")) + now = now.Add(time.Minute) + value, _ := rand.Int(rand.Reader, big.NewInt(100)) + values = append(values, float64(value.Int64())) + } + p, err := charts.LineRender( + [][]float64{ + values, + }, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()), + charts.LegendLabelsOptionFunc([]string{ + "Demo", + }, "50"), + func(opt *charts.ChartOption) { + opt.XAxis.FirstAxis = firstAxis + // 必须要比计算得来的最小值更大(每60分钟) + opt.XAxis.SplitNumber = 60 + opt.Legend.Padding = charts.Box{ + Top: 5, + Bottom: 10, + } + opt.SymbolShow = charts.FalseFlag() + opt.LineStrokeWidth = 1 + opt.ValueFormatter = func(f float64) string { + return fmt.Sprintf("%.0f", f) + } + }, + ) + + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/painter.go b/painter.go index 8f43940..18496fd 100644 --- a/painter.go +++ b/painter.go @@ -59,6 +59,8 @@ type PainterOptions struct { type PainterOption func(*Painter) type TicksOption struct { + // the first tick + First int Length int Orient string Count int @@ -74,6 +76,8 @@ type MultiTextOption struct { // The text rotation of label TextRotation float64 Offset Box + // The first text index + First int } type GridOption struct { @@ -616,6 +620,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { return p } count := opt.Count + first := opt.First width := p.Width() height := p.Height() unit := 1 @@ -630,7 +635,10 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { values = autoDivide(width, count) } for index, value := range values { - if index%unit != 0 { + if index < first { + continue + } + if (index-first)%unit != 0 { continue } if isVertical { @@ -688,7 +696,10 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { isTextRotation := opt.TextRotation != 0 offset := opt.Offset for index, text := range opt.TextList { - if opt.Unit != 0 && index%opt.Unit != showIndex { + if index < opt.First { + continue + } + if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { continue } if isTextRotation { diff --git a/xaxis.go b/xaxis.go index 95578ff..61698d7 100644 --- a/xaxis.go +++ b/xaxis.go @@ -50,6 +50,8 @@ type XAxisOption struct { FontColor Color // The text rotation of label TextRotation float64 + // The first axis + FirstAxis int // The offset of label LabelOffset Box isValueAxis bool @@ -87,6 +89,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { SplitLineColor: opt.Theme.GetAxisSplitLineColor(), TextRotation: opt.TextRotation, LabelOffset: opt.LabelOffset, + FirstAxis: opt.FirstAxis, } if opt.isValueAxis { axisOpt.SplitLineShow = true From e7a49c2c212d022df6cda4739b8c3e391fd3594a Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Thu, 4 May 2023 12:52:28 -0600 Subject: [PATCH 65/87] 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. --- painter.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/painter.go b/painter.go index 18496fd..450afdf 100644 --- a/painter.go +++ b/painter.go @@ -615,6 +615,17 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch return output } +func isTick(totalRange int, numTicks int, index int) bool { + step := float64(totalRange-1) / float64(numTicks-1) + for i := 0; i < numTicks; i++ { + value := float64(i) * step + if int(value + 0.5) == index { + return true + } + } + return false +} + func (p *Painter) Ticks(opt TicksOption) *Painter { if opt.Count <= 0 || opt.Length <= 0 { return p @@ -638,7 +649,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { if index < first { continue } - if (index-first)%unit != 0 { + if ! isTick(len(values), unit, index) { continue } if isVertical { @@ -674,15 +685,13 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true - showIndex := opt.Unit / 2 + tickLimit := true if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- - // 非居中 - showIndex = 0 } width := p.Width() height := p.Height() @@ -690,6 +699,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { isVertical := opt.Orient == OrientVertical if isVertical { values = autoDivide(height, count) + tickLimit = false } else { values = autoDivide(width, count) } @@ -699,7 +709,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { if index < opt.First { continue } - if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { + if opt.Unit != 0 && tickLimit && ! isTick(len(opt.TextList)-opt.First, opt.Unit, index-opt.First) { continue } if isTextRotation { @@ -724,7 +734,11 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { x = 0 } } else { - x = start - box.Width()>>1 + if index == len(opt.TextList) - 1 { + x = start - box.Width() + } else { + x = start - box.Width()>>1 + } } x += offset.Left y += offset.Top @@ -749,7 +763,6 @@ func (p *Painter) Grid(opt GridOption) *Painter { x1 := 0 y1 := 0 if isVertical { - x0 = v x1 = v y1 = height From 19173dfd37f737b4a5a681556b0a7fe661d749db Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Thu, 4 May 2023 17:59:11 -0600 Subject: [PATCH 66/87] 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. --- painter.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/painter.go b/painter.go index 450afdf..175b2f2 100644 --- a/painter.go +++ b/painter.go @@ -617,10 +617,12 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch func isTick(totalRange int, numTicks int, index int) bool { step := float64(totalRange-1) / float64(numTicks-1) - for i := 0; i < numTicks; i++ { - value := float64(i) * step - if int(value + 0.5) == index { + for i := int(float64(index) / step); i < numTicks; i++ { + value := int((float64(i) * step) + 0.5) + if value == index { return true + } else if value > index { + break } } return false From c810369730585ed5692c3e5e13c36a4c785568c1 Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Fri, 5 May 2023 09:44:09 -0600 Subject: [PATCH 67/87] 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. --- painter.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/painter.go b/painter.go index 175b2f2..d74b80d 100644 --- a/painter.go +++ b/painter.go @@ -615,7 +615,8 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch return output } -func isTick(totalRange int, numTicks int, index int) bool { +func isTick(totalRange int, unit int, index int) bool { + numTicks := (totalRange / unit) + 1 step := float64(totalRange-1) / float64(numTicks-1) for i := int(float64(index) / step); i < numTicks; i++ { value := int((float64(i) * step) + 0.5) @@ -737,7 +738,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } } else { if index == len(opt.TextList) - 1 { - x = start - box.Width() + x = start - box.Width() + 10 } else { x = start - box.Width()>>1 } From a158191faf90eb0baa08d2cbe8692afd5b09b58c Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Fri, 5 May 2023 09:55:55 -0600 Subject: [PATCH 68/87] 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. --- axis.go | 1 - xaxis.go | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/axis.go b/axis.go index 762a6a2..af104a8 100644 --- a/axis.go +++ b/axis.go @@ -176,7 +176,6 @@ func (a *axisPainter) Render() (Box, error) { unit := opt.Unit if unit <= 0 { - unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) unit = chart.MaxInt(unit, opt.SplitNumber) // 偶数 diff --git a/xaxis.go b/xaxis.go index 61698d7..5557015 100644 --- a/xaxis.go +++ b/xaxis.go @@ -40,7 +40,7 @@ type XAxisOption struct { 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 to avoid writing overlap. SplitNumber int // The position of axis, it can be 'top' or 'bottom' Position string @@ -55,6 +55,8 @@ type XAxisOption struct { // The offset of label LabelOffset Box isValueAxis bool + // This value overrides SplitNumber, specifying directly the frequency at which the axis is split into, higher numbers result in less ticks + Unit int } const defaultXAxisHeight = 30 @@ -90,6 +92,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { TextRotation: opt.TextRotation, LabelOffset: opt.LabelOffset, FirstAxis: opt.FirstAxis, + Unit: opt.Unit, } if opt.isValueAxis { axisOpt.SplitLineShow = true From 687baad0af8ff907e1e317f2671e3d76b91746c1 Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Fri, 5 May 2023 10:19:01 -0600 Subject: [PATCH 69/87] Unit test fixes Unit tests updated for new tick positions and in a couple cases additional one X axis sample. --- axis_test.go | 6 +++--- bar_chart_test.go | 2 +- chart_option_test.go | 6 +++--- echarts_test.go | 2 +- horizontal_bar_chart_test.go | 2 +- line_chart_test.go | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/axis_test.go b/axis_test.go index d0cff41..a04024d 100644 --- a/axis_test.go +++ b/axis_test.go @@ -53,7 +53,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 底部x轴文本居左 { @@ -72,7 +72,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 左侧y轴 { @@ -155,7 +155,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", + result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", }, } diff --git a/bar_chart_test.go b/bar_chart_test.go index e1522d6..aec6428 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400JanAprJulSepDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index ff17750..b7f4e93 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index 2ce1715..dd7562f 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index e078c4a..78f3e69 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/line_chart_test.go b/line_chart_test.go index e169f90..e8bc1d7 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } From 0ddb9e4ef1d089a08fdf712377e6d3d7d7396cc4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 12 May 2023 20:31:42 +0800 Subject: [PATCH 70/87] chore: update modules --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e265627..d8a492c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/wcharczuk/go-chart/v2 v2.1.0 ) diff --git a/go.sum b/go.sum index ef2a000..ac1d9f7 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= From 8bcb584abac4fe128bd83103a9b3a04a01db0346 Mon Sep 17 00:00:00 2001 From: Tree Xie Date: Wed, 27 Dec 2023 18:20:55 +0800 Subject: [PATCH 71/87] Revert "Improvements to how the X Axis is rendered" --- axis.go | 1 + axis_test.go | 6 +++--- bar_chart_test.go | 2 +- chart_option_test.go | 6 +++--- echarts_test.go | 2 +- horizontal_bar_chart_test.go | 2 +- line_chart_test.go | 4 ++-- painter.go | 30 +++++++----------------------- xaxis.go | 5 +---- 9 files changed, 20 insertions(+), 38 deletions(-) diff --git a/axis.go b/axis.go index af104a8..762a6a2 100644 --- a/axis.go +++ b/axis.go @@ -176,6 +176,7 @@ func (a *axisPainter) Render() (Box, error) { unit := opt.Unit if unit <= 0 { + unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) unit = chart.MaxInt(unit, opt.SplitNumber) // 偶数 diff --git a/axis_test.go b/axis_test.go index a04024d..d0cff41 100644 --- a/axis_test.go +++ b/axis_test.go @@ -53,7 +53,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 底部x轴文本居左 { @@ -72,7 +72,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 左侧y轴 { @@ -155,7 +155,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", + result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", }, } diff --git a/bar_chart_test.go b/bar_chart_test.go index aec6428..e1522d6 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400JanAprJulSepDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index b7f4e93..ff17750 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index dd7562f..2ce1715 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index 78f3e69..e078c4a 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/line_chart_test.go b/line_chart_test.go index e8bc1d7..e169f90 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } diff --git a/painter.go b/painter.go index d74b80d..18496fd 100644 --- a/painter.go +++ b/painter.go @@ -615,20 +615,6 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch return output } -func isTick(totalRange int, unit int, index int) bool { - numTicks := (totalRange / unit) + 1 - step := float64(totalRange-1) / float64(numTicks-1) - for i := int(float64(index) / step); i < numTicks; i++ { - value := int((float64(i) * step) + 0.5) - if value == index { - return true - } else if value > index { - break - } - } - return false -} - func (p *Painter) Ticks(opt TicksOption) *Painter { if opt.Count <= 0 || opt.Length <= 0 { return p @@ -652,7 +638,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { if index < first { continue } - if ! isTick(len(values), unit, index) { + if (index-first)%unit != 0 { continue } if isVertical { @@ -688,13 +674,15 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true - tickLimit := true + showIndex := opt.Unit / 2 if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- + // 非居中 + showIndex = 0 } width := p.Width() height := p.Height() @@ -702,7 +690,6 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { isVertical := opt.Orient == OrientVertical if isVertical { values = autoDivide(height, count) - tickLimit = false } else { values = autoDivide(width, count) } @@ -712,7 +699,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { if index < opt.First { continue } - if opt.Unit != 0 && tickLimit && ! isTick(len(opt.TextList)-opt.First, opt.Unit, index-opt.First) { + if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { continue } if isTextRotation { @@ -737,11 +724,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { x = 0 } } else { - if index == len(opt.TextList) - 1 { - x = start - box.Width() + 10 - } else { - x = start - box.Width()>>1 - } + x = start - box.Width()>>1 } x += offset.Left y += offset.Top @@ -766,6 +749,7 @@ func (p *Painter) Grid(opt GridOption) *Painter { x1 := 0 y1 := 0 if isVertical { + x0 = v x1 = v y1 = height diff --git a/xaxis.go b/xaxis.go index 5557015..61698d7 100644 --- a/xaxis.go +++ b/xaxis.go @@ -40,7 +40,7 @@ type XAxisOption struct { 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 to avoid writing overlap. + // Number of segments that the axis is split into. Note that this number serves only as a recommendation. SplitNumber int // The position of axis, it can be 'top' or 'bottom' Position string @@ -55,8 +55,6 @@ type XAxisOption struct { // The offset of label LabelOffset Box isValueAxis bool - // This value overrides SplitNumber, specifying directly the frequency at which the axis is split into, higher numbers result in less ticks - Unit int } const defaultXAxisHeight = 30 @@ -92,7 +90,6 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { TextRotation: opt.TextRotation, LabelOffset: opt.LabelOffset, FirstAxis: opt.FirstAxis, - Unit: opt.Unit, } if opt.isValueAxis { axisOpt.SplitLineShow = true From 98af9866a47cd7ac3665588501889601f7900ef4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Dec 2023 20:33:12 +0800 Subject: [PATCH 72/87] refactor: support label show for radar chart, #62 --- echarts.go | 5 +++++ radar_chart.go | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/echarts.go b/echarts.go index fbe9a36..5a0e5a0 100644 --- a/echarts.go +++ b/echarts.go @@ -344,6 +344,11 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { Data: NewSeriesDataFromValues(dataItem.Value.values), Max: item.Max, Min: item.Min, + Label: SeriesLabel{ + Color: parseColor(item.Label.Color), + Show: item.Label.Show, + Distance: item.Label.Distance, + }, }) } continue diff --git a/radar_chart.go b/radar_chart.go index 429850d..f3d63b9 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -25,6 +25,7 @@ package charts import ( "errors" + "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" @@ -230,9 +231,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) StrokeColor: color, FillColor: dotFillColor, }) - for _, point := range linePoints { + for index, point := range linePoints { seriesPainter.Circle(dotWith, point.X, point.Y) seriesPainter.FillStroke() + if series.Label.Show && index < len(series.Data) { + value := humanize.FtoaWithDigits(series.Data[index].Value, 2) + b := seriesPainter.MeasureText(value) + seriesPainter.Text(value, point.X-b.Width()/2, point.Y) + } + } } From c2f709a74299db5a1c0290f5d6c07606f0b9f124 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Dec 2023 20:34:05 +0800 Subject: [PATCH 73/87] chore: update modules --- .github/workflows/test.yml | 1 + go.mod | 6 +++--- go.sum | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f591a3a..8d6a027 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: go: + - '1.21' - '1.20' - '1.19' - '1.18' diff --git a/go.mod b/go.mod index d8a492c..b46ff02 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.2 - github.com/wcharczuk/go-chart/v2 v2.1.0 + github.com/stretchr/testify v1.8.4 + github.com/wcharczuk/go-chart/v2 v2.1.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect + golang.org/x/image v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ac1d9f7..e518b63 100644 --- a/go.sum +++ b/go.sum @@ -14,11 +14,48 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= +github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From e09ab2c3c7a5a338be739f503361bc17779733dd Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Dec 2023 20:37:18 +0800 Subject: [PATCH 74/87] Revert "chore: update modules" This reverts commit c2f709a74299db5a1c0290f5d6c07606f0b9f124. --- .github/workflows/test.yml | 1 - go.mod | 6 +++--- go.sum | 37 ------------------------------------- 3 files changed, 3 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d6a027..f591a3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,6 @@ jobs: strategy: matrix: go: - - '1.21' - '1.20' - '1.19' - '1.18' diff --git a/go.mod b/go.mod index b46ff02..d8a492c 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.4 - github.com/wcharczuk/go-chart/v2 v2.1.1 + github.com/stretchr/testify v1.8.2 + github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.14.0 // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e518b63..ac1d9f7 100644 --- a/go.sum +++ b/go.sum @@ -14,48 +14,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= -github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= -golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= -golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 310800a5f01eaa29226b7595cdcc0384d2d9f55b Mon Sep 17 00:00:00 2001 From: "xuejinwei.1112" Date: Tue, 2 Jan 2024 12:32:56 +0800 Subject: [PATCH 75/87] support dash line for line chart --- line_chart.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/line_chart.go b/line_chart.go index bdbd38e..363cd36 100644 --- a/line_chart.go +++ b/line_chart.go @@ -115,6 +115,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( StrokeColor: seriesColor, StrokeWidth: strokeWidth, } + if len(series.Style.StrokeDashArray) > 0 { + drawingStyle.StrokeDashArray = series.Style.StrokeDashArray + } yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) From f1a231ff4b0e660609709babc1a9336c89c0233a Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Feb 2024 12:36:26 +0800 Subject: [PATCH 76/87] feat: support split line show option for charts, #69 --- .github/workflows/test.yml | 2 ++ examples/area_line_chart/main.go | 3 +-- examples/bar_chart/main.go | 3 +-- examples/chinese/main.go | 2 +- examples/funnel_chart/main.go | 3 +-- examples/horizontal_bar_chart/main.go | 3 +-- examples/line_chart/main.go | 8 ++++++-- examples/painter/main.go | 3 +-- examples/pie_chart/main.go | 3 +-- examples/radar_chart/main.go | 3 +-- examples/table/main.go | 3 +-- examples/time_line_chart/main.go | 3 +-- yaxis.go | 5 +++++ 13 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f591a3a..5544970 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,8 @@ jobs: strategy: matrix: go: + - '1.22' + - '1.21' - '1.20' - '1.19' - '1.18' diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go index 7a84df0..ea8f1c2 100644 --- a/examples/area_line_chart/main.go +++ b/examples/area_line_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "area-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index c559a76..feea66e 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "bar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/chinese/main.go b/examples/chinese/main.go index d77216a..2d96b58 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -16,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "chinese-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 24f8afe..f29ccf9 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "funnel-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index a1c50a7..f2cabe8 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "horizontal-bar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index c1478a6..4e6448f 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" @@ -17,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } @@ -97,6 +96,11 @@ func main() { Top: 5, Bottom: 10, } + opt.YAxisOptions = []charts.YAxisOption{ + { + SplitLineShow: charts.FalseFlag(), + }, + } opt.SymbolShow = charts.FalseFlag() opt.LineStrokeWidth = 1 opt.ValueFormatter = func(f float64) string { diff --git a/examples/painter/main.go b/examples/painter/main.go index 3c31ce4..b7a5832 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -17,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "painter.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 3721ed1..38488d2 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "pie-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index 51f7409..e8095ae 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "radar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/table/main.go b/examples/table/main.go index 2701ec1..0210ecf 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" "strconv" @@ -19,7 +18,7 @@ func writeFile(buf []byte, filename string) error { } file := filepath.Join(tmpPath, filename) - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go index 10932cd..6cb3f3d 100644 --- a/examples/time_line_chart/main.go +++ b/examples/time_line_chart/main.go @@ -3,7 +3,6 @@ package main import ( "crypto/rand" "fmt" - "io/ioutil" "math/big" "os" "path/filepath" @@ -20,7 +19,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "time-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/yaxis.go b/yaxis.go index bece2cc..e58b7a6 100644 --- a/yaxis.go +++ b/yaxis.go @@ -50,6 +50,8 @@ type YAxisOption struct { DivideCount int Unit int isCategoryAxis bool + // The flag for show axis split line, set this to true will show axis split line + SplitLineShow *bool } // NewYAxisOptions returns a y axis option @@ -100,6 +102,9 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { axisOpt.StrokeWidth = 1 axisOpt.SplitLineShow = false } + if opt.SplitLineShow != nil { + axisOpt.SplitLineShow = *opt.SplitLineShow + } return axisOpt } From 06fe1006d5b2f1a79f7f68f5a986de4bd1c7b9d2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Feb 2024 12:39:39 +0800 Subject: [PATCH 77/87] chore: update test go version --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5544970..ce56fe7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,10 +20,6 @@ jobs: - '1.19' - '1.18' - '1.17' - - '1.16' - - '1.15' - - '1.14' - - '1.13' steps: - name: Go ${{ matrix.go }} test From 19a4d783fdfb2f6dedfbad90da9117d389443ffe Mon Sep 17 00:00:00 2001 From: Alexander Heidrich Date: Fri, 8 Mar 2024 20:24:13 +0100 Subject: [PATCH 78/87] fix: Label position of the pie chart --- pie_chart.go | 223 ++++++++++++++++++++++++----------- pie_chart_test.go | 294 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+), 70 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index b4714ac..bbf7814 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -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 / 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 // Bogenmaß + s.delta = chart.PercentToRadians(value / totalValue) + angle := s.start + s.delta/2 + s.lineStartX = cx + int(radius*math.Cos(angle)) + s.lineStartY = cy + int(radius*math.Sin(angle)) + s.lineBranchX = cx + int(labelRadius*math.Cos(angle)) + s.lineBranchY = cy + int(labelRadius*math.Sin(angle)) + s.offset = labelLineLength + if s.lineBranchX <= cx { + s.offset *= -1 + } + s.lineEndX = s.lineBranchX + s.offset + s.lineEndY = s.lineBranchY + s.series = series + s.color = color + s.showLabel = series.Label.Show + s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent) + return s +} + +func (s *sector) calculateY(prevY int) int { + for i := 0; i <= s.cy; i++ { + if s.quadrant <= 2 { + if (prevY - s.lineBranchY) > labelFontSize+5 { + break + } + s.lineBranchY -= 1 + } else { + if (s.lineBranchY - prevY) > labelFontSize+5 { + break + } + s.lineBranchY += 1 + } + } + s.lineEndY = s.lineBranchY + return s.lineBranchY +} + +func (s *sector) calculateTextXY(textBox Box) (x int, y int) { + textMargin := 3 + x = s.lineEndX + textMargin + y = s.lineEndY + textBox.Height()>>1 - 1 + if s.offset < 0 { + textWidth := textBox.Width() + x = s.lineEndX - textWidth - textMargin + } + return +} + func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { opt := p.opt values := make([]float64, len(seriesList)) @@ -101,98 +191,91 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B theme := opt.Theme currentValue := float64(0) - prevPoints := make([]Point, 0) - isOverride := func(x, y int) bool { - for _, p := range prevPoints { - if math.Abs(float64(p.Y-y)) > labelFontSize { - continue - } - // label可能较多内容,不好计算横向占用空间 - // 因此x的位置需要中间位置两侧,否则认为override - if (p.X <= cx && x <= cx) || - (p.X > cx && x > cx) { - return true + var quadrant1, quadrant2, quadrant3, quadrant4 []sector + for index, v := range values { + series := seriesList[index] + color := theme.GetSeriesColor(index) + if index == len(values)-1 { + if color == theme.GetSeriesColor(0) { + color = theme.GetSeriesColor(1) } } - 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{ StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(index), - FillColor: theme.GetSeriesColor(index), + StrokeColor: s.color, + FillColor: s.color, }) - seriesPainter.MoveTo(cx, cy) - start := chart.PercentToRadians(currentValue/total) - math.Pi/2 - currentValue += v - percent := (v / total) - delta := chart.PercentToRadians(percent) - seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). - LineTo(cx, cy). - Close(). - FillStroke() - - series := seriesList[index] - // 是否显示label - showLabel := series.Label.Show - if !showLabel { + seriesPainter.MoveTo(s.cx, s.cy) + seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke() + if !s.showLabel { continue } - - // label的角度为饼块中间 - angle := start + delta/2 - startx := cx + int(radius*math.Cos(angle)) - starty := cy + int(radius*math.Sin(angle)) - - endx := cx + int(labelRadius*math.Cos(angle)) - endy := cy + int(labelRadius*math.Sin(angle)) - // 计算是否有重叠,如果有则调整y坐标位置 - // 最多只尝试5次 - for i := 0; i < 5; i++ { - if !isOverride(endx, endy) { - break + if currentQuadrant != s.quadrant { + currentQuadrant = s.quadrant + if s.quadrant == 1 { + minY = cy * 2 + maxY = 0 + prevY = cy * 2 + } + if s.quadrant == 2 { + prevY = minY + } + if s.quadrant == 3 { + minY = cy * 2 + maxY = 0 + prevY = 0 + } + if s.quadrant == 4 { + prevY = maxY } - endy -= (labelFontSize << 1) } - prevPoints = append(prevPoints, Point{ - X: endx, - Y: endy, - }) - - seriesPainter.MoveTo(startx, starty) - seriesPainter.LineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 + prevY = s.calculateY(prevY) + if prevY > maxY { + maxY = prevY } - seriesPainter.MoveTo(endx, endy) - endx += offset - seriesPainter.LineTo(endx, endy) + if prevY < minY { + minY = prevY + } + seriesPainter.MoveTo(s.lineStartX, s.lineStartY) + seriesPainter.LineTo(s.lineBranchX, s.lineBranchY) + seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY) + seriesPainter.LineTo(s.lineEndX, s.lineEndY) seriesPainter.Stroke() - textStyle := Style{ FontColor: theme.GetTextColor(), FontSize: labelFontSize, Font: opt.Font, } - if !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color + if !s.series.Label.Color.IsZero() { + textStyle.FontColor = s.series.Label.Color } seriesPainter.OverrideTextStyle(textStyle) - text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) - textBox := seriesPainter.MeasureText(text) - textMargin := 3 - x := endx + textMargin - y := endy + textBox.Height()>>1 - 1 - if offset < 0 { - textWidth := textBox.Width() - x = endx - textWidth - textMargin - } - seriesPainter.Text(text, x, y) + x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label)) + seriesPainter.Text(s.label, x, y) } - return p.p.box, nil } diff --git a/pie_chart_test.go b/pie_chart_test.go index c373a7e..0b8f798 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -23,6 +23,7 @@ package charts import ( + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -98,3 +99,296 @@ func TestPieChart(t *testing.T) { assert.Equal(tt.result, string(data)) } } + +func TestPieChartWithLabelsValuesSortedDescending(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 84358845, + 68070697, + 58850717, + 48059777, + 36753736, + 19051562, + 17947406, + 11754004, + 10827529, + 10521556, + 10467366, + 10394055, + 9597085, + 9104772, + 6447710, + 5932654, + 5563970, + 5428792, + 5194336, + 3850894, + 2857279, + 2116792, + 1883008, + 1373101, + 920701, + 660809, + 542051, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + Formatter: "{b} ({c} ≅ {d})", + }, + Radius: "200", + }), + Title: TitleOption{ + Text: "European Union member states by population", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: []string{ + "Germany", + "France", + "Italy", + "Spain", + "Poland", + "Romania", + "Netherlands", + "Belgium", + "Czech Republic", + "Sweden", + "Portugal", + "Greece", + "Hungary", + "Austria", + "Bulgaria", + "Denmark", + "Finland", + "Slovakia", + "Ireland", + "Croatia", + "Lithuania", + "Slovenia", + "Latvia", + "Estonia", + "Cyprus", + "Luxembourg", + "Malta", + }, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Germany (84358845 ≅ 18.8%)Italy (58850717 ≅ 13.12%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1000, + Height: 800, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} + +func TestPieChartWithLabelsValuesUnsorted(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 9104772, + 11754004, + 6447710, + 3850894, + 920701, + 10827529, + 5932654, + 1373101, + 5563970, + 68070697, + 84358845, + 10394055, + 9597085, + 5194336, + 58850717, + 1883008, + 2857279, + 660809, + 542051, + 17947406, + 36753736, + 10467366, + 19051562, + 5428792, + 2116792, + 48059777, + 10521556, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + Formatter: "{b} ({c} ≅ {d})", + }, + Radius: "200", + }), + Title: TitleOption{ + Text: "European Union member states by population", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: []string{ + "Austria", + "Belgium", + "Bulgaria", + "Croatia", + "Cyprus", + "Czech Republic", + "Denmark", + "Estonia", + "Finland", + "France", + "Germany", + "Greece", + "Hungary", + "Ireland", + "Italy", + "Latvia", + "Lithuania", + "Luxembourg", + "Malta", + "Netherlands", + "Poland", + "Portugal", + "Romania", + "Slovakia", + "Slovenia", + "Spain", + "Sweden", + }, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Finland (5563970 ≅ 1.24%)Estonia (1373101 ≅ 0.3%)Denmark (5932654 ≅ 1.32%)Czech Republic (10827529 ≅ 2.41%)Cyprus (920701 ≅ 0.2%)Croatia (3850894 ≅ 0.85%)Bulgaria (6447710 ≅ 1.43%)Belgium (11754004 ≅ 2.62%)Austria (9104772 ≅ 2.02%)Germany (84358845 ≅ 18.8%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Poland (36753736 ≅ 8.19%)Netherlands (17947406 ≅ 4%)Malta (542051 ≅ 0.12%)Luxembourg (660809 ≅ 0.14%)Lithuania (2857279 ≅ 0.63%)Latvia (1883008 ≅ 0.41%)Italy (58850717 ≅ 13.12%)Ireland (5194336 ≅ 1.15%)Portugal (10467366 ≅ 2.33%)Romania (19051562 ≅ 4.24%)Slovakia (5428792 ≅ 1.21%)Slovenia (2116792 ≅ 0.47%)Spain (48059777 ≅ 10.71%)Sweden (10521556 ≅ 2.34%)", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1000, + Height: 800, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} + +func TestPieChartWith100Labels(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + var values []float64 + var labels []string + for i := 1; i <= 100; i++ { + values = append(values, float64(1)) + labels = append(labels, "Label "+strconv.Itoa(i)) + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + Radius: "200", + }), + Title: TitleOption{ + Text: "Test with 100 labels", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: labels, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nTest with 100 labelsLabel 25: 1%Label 24: 1%Label 23: 1%Label 22: 1%Label 21: 1%Label 20: 1%Label 19: 1%Label 18: 1%Label 17: 1%Label 16: 1%Label 15: 1%Label 14: 1%Label 13: 1%Label 12: 1%Label 11: 1%Label 10: 1%Label 9: 1%Label 8: 1%Label 7: 1%Label 6: 1%Label 5: 1%Label 4: 1%Label 3: 1%Label 2: 1%Label 1: 1%Label 26: 1%Label 27: 1%Label 28: 1%Label 29: 1%Label 30: 1%Label 31: 1%Label 32: 1%Label 33: 1%Label 34: 1%Label 35: 1%Label 36: 1%Label 37: 1%Label 38: 1%Label 39: 1%Label 40: 1%Label 41: 1%Label 42: 1%Label 43: 1%Label 44: 1%Label 45: 1%Label 46: 1%Label 47: 1%Label 48: 1%Label 49: 1%Label 50: 1%Label 75: 1%Label 74: 1%Label 73: 1%Label 72: 1%Label 71: 1%Label 70: 1%Label 69: 1%Label 68: 1%Label 67: 1%Label 66: 1%Label 65: 1%Label 64: 1%Label 63: 1%Label 62: 1%Label 61: 1%Label 60: 1%Label 59: 1%Label 58: 1%Label 57: 1%Label 56: 1%Label 55: 1%Label 54: 1%Label 53: 1%Label 52: 1%Label 51: 1%Label 76: 1%Label 77: 1%Label 78: 1%Label 79: 1%Label 80: 1%Label 81: 1%Label 82: 1%Label 83: 1%Label 84: 1%Label 85: 1%Label 86: 1%Label 87: 1%Label 88: 1%Label 89: 1%Label 90: 1%Label 91: 1%Label 92: 1%Label 93: 1%Label 94: 1%Label 95: 1%Label 96: 1%Label 97: 1%Label 98: 1%Label 99: 1%Label 100: 1%", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1000, + Height: 900, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 8c6c4e007c85b7c5fabf3340fff4952fc0810592 Mon Sep 17 00:00:00 2001 From: Alexander Heidrich Date: Fri, 22 Mar 2024 08:27:06 +0100 Subject: [PATCH 79/87] fix: Label position of the pie chart --- pie_chart.go | 4 +- pie_chart_test.go | 141 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index bbf7814..6cc48c4 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -94,7 +94,7 @@ func NewSector(cx int, cy int, radius float64, labelRadius float64, value float6 s.cy = cy s.rx = radius s.ry = radius - p := currentValue / totalValue + p := (currentValue + value/2) / totalValue if p < 0.25 { s.quadrant = 1 } else if p < 0.5 { @@ -104,7 +104,7 @@ func NewSector(cx int, cy int, radius float64, labelRadius float64, value float6 } else { s.quadrant = 2 } - s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2 // Bogenmaß + 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)) diff --git a/pie_chart_test.go b/pie_chart_test.go index 0b8f798..3795d32 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -194,7 +194,7 @@ func TestPieChartWithLabelsValuesSortedDescending(t *testing.T) { } return p.Bytes() }, - result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Germany (84358845 ≅ 18.8%)Italy (58850717 ≅ 13.12%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", + result: "\\nEuropean Union member states by populationGermany (84358845 ≅ 18.8%)France (68070697 ≅ 15.17%)Italy (58850717 ≅ 13.12%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", }, } for _, tt := range tests { @@ -392,3 +392,142 @@ func TestPieChartWith100Labels(t *testing.T) { assert.Equal(tt.result, string(data)) } } + +func TestPieChartFixLabelPos72586(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 397594, + 185596, + 149086, + 144258, + 120194, + 117514, + 99412, + 91135, + 87282, + 76790, + 72586, + 58818, + 58270, + 56306, + 55486, + 54792, + 53746, + 51460, + 41242, + 39476, + 37414, + 36644, + 33784, + 32788, + 32566, + 29608, + 29558, + 29384, + 28166, + 26998, + 26948, + 26054, + 25804, + 25730, + 24438, + 23782, + 22896, + 21404, + 428978, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + Formatter: "{b} ({c} ≅ {d})", + }, + Radius: "150", + }), + Title: TitleOption{ + Text: "Fix label K (72586)", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: []string{ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "AG", + "AH", + "AI", + "AJ", + "AK", + "AL", + "AM", + }, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nFix label K (72586)C (149086 ≅ 5.04%)B (185596 ≅ 6.28%)A (397594 ≅ 13.45%)D (144258 ≅ 4.88%)E (120194 ≅ 4.06%)F (117514 ≅ 3.97%)G (99412 ≅ 3.36%)H (91135 ≅ 3.08%)I (87282 ≅ 2.95%)J (76790 ≅ 2.59%)Z (29608 ≅ 1%)Y (32566 ≅ 1.1%)X (32788 ≅ 1.1%)W (33784 ≅ 1.14%)V (36644 ≅ 1.24%)U (37414 ≅ 1.26%)T (39476 ≅ 1.33%)S (41242 ≅ 1.39%)R (51460 ≅ 1.74%)Q (53746 ≅ 1.81%)P (54792 ≅ 1.85%)O (55486 ≅ 1.87%)N (56306 ≅ 1.9%)M (58270 ≅ 1.97%)L (58818 ≅ 1.99%)K (72586 ≅ 2.45%)AA (29558 ≅ 1%)AB (29384 ≅ 0.99%)AC (28166 ≅ 0.95%)AD (26998 ≅ 0.91%)AE (26948 ≅ 0.91%)AF (26054 ≅ 0.88%)AG (25804 ≅ 0.87%)AH (25730 ≅ 0.87%)AI (24438 ≅ 0.82%)AJ (23782 ≅ 0.8%)AK (22896 ≅ 0.77%)AL (21404 ≅ 0.72%)AM (428978 ≅ 14.52%)", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1150, + Height: 550, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 9b7634c2c230a0a1907c6e99be6bdea11f811a9e Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 16 May 2024 20:02:24 +0800 Subject: [PATCH 80/87] feat: support rounded rect for bar chart --- bar_chart.go | 27 ++++++++++++------ bar_chart_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++ painter.go | 42 ++++++++++++++++++++++++++++ painter_test.go | 23 ++++++++++++++++ series.go | 2 ++ 5 files changed, 156 insertions(+), 8 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index efeb465..508c63e 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -142,14 +142,25 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } top := barMaxHeight - h - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }) + if series.RoundRadius <= 0 { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: top, + Left: x, + Right: x + barWidth, + Bottom: barMaxHeight - 1, + }) + } else { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).RoundedRect(chart.Box{ + Top: top, + Left: x, + Right: x + barWidth, + Bottom: barMaxHeight - 1, + }, series.RoundRadius) + } // 用于生成marker point points[j] = Point{ // 居中的位置 diff --git a/bar_chart_test.go b/bar_chart_test.go index e1522d6..654c320 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -104,6 +104,76 @@ func TestBarChart(t *testing.T) { }, result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, + { + render: func(p *Painter) ([]byte, error) { + seriesList := NewSeriesListDataFromValues([][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }, + }) + for index := range seriesList { + seriesList[index].Label.Show = true + seriesList[index].RoundRadius = 5 + } + _, err := NewBarChart(p, BarChartOption{ + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, + SeriesList: seriesList, + XAxis: NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + YAxisOptions: NewYAxisOptions([]string{ + "Rainfall", + "Evaporation", + }), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + }, } for _, tt := range tests { diff --git a/painter.go b/painter.go index 18496fd..bc13418 100644 --- a/painter.go +++ b/painter.go @@ -803,6 +803,48 @@ func (p *Painter) Rect(box Box) *Painter { return p } +func (p *Painter) RoundedRect(box Box, radius int) *Painter { + r := (box.Right - box.Left) / 2 + if radius > r { + radius = r + } + rx := float64(radius) + ry := float64(radius) + p.MoveTo(box.Left+radius, box.Top) + p.LineTo(box.Right-radius, box.Top) + + cx := box.Right - radius + cy := box.Top + radius + // right top + p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2) + + p.LineTo(box.Right, box.Bottom-radius) + + // right bottom + cx = box.Right - radius + cy = box.Bottom - radius + p.ArcTo(cx, cy, rx, ry, 0.0, math.Pi/2) + + p.LineTo(box.Left+radius, box.Bottom) + + // left bottom + cx = box.Left + radius + cy = box.Bottom - radius + p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2) + + p.LineTo(box.Left, box.Top+radius) + + // left top + cx = box.Left + radius + cy = box.Top + radius + p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2) + + p.Close() + p.FillStroke() + p.Fill() + return p +} + func (p *Painter) LegendLineDot(box Box) *Painter { width := box.Width() height := box.Height() diff --git a/painter_test.go b/painter_test.go index 2392d5b..b159328 100644 --- a/painter_test.go +++ b/painter_test.go @@ -343,6 +343,29 @@ func TestPainter(t *testing.T) { } } +func TestRoundedRect(t *testing.T) { + assert := assert.New(t) + p, err := NewPainter(PainterOptions{ + Width: 400, + Height: 300, + Type: ChartOutputSVG, + }) + assert.Nil(err) + p.OverrideDrawingStyle(Style{ + FillColor: drawing.ColorWhite, + StrokeWidth: 1, + StrokeColor: drawing.ColorWhite, + }).RoundedRect(Box{ + Left: 10, + Right: 30, + Bottom: 150, + Top: 10, + }, 5) + buf, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\n", string(buf)) +} + func TestPainterTextFit(t *testing.T) { assert := assert.New(t) p, err := NewPainter(PainterOptions{ diff --git a/series.go b/series.go index f28bfa9..0ad135f 100644 --- a/series.go +++ b/series.go @@ -126,6 +126,8 @@ type Series struct { Name string // Radius for Pie chart, e.g.: 40%, default is "40%" Radius string + // Round for bar chart + RoundRadius int // Mark point for series MarkPoint SeriesMarkPoint // Make line for series From 96148357237a4698910b58e780f6a2fe095a4cfe Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 21 May 2024 20:26:40 +0800 Subject: [PATCH 81/87] feat: support rounded rect for horizontal bar chart --- examples/horizontal_bar_chart/main.go | 3 +++ horizontal_bar_chart.go | 28 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index f2cabe8..a7daa8c 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -65,6 +65,9 @@ func main() { "China", "World", }), + func(opt *charts.ChartOption) { + opt.SeriesList[0].RoundRadius = 5 + }, ) if err != nil { panic(err) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 2ab4c03..ca91242 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -136,14 +136,26 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri fillColor = item.Style.FillColor } right := w - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }) + if series.RoundRadius <= 0 { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }) + } else { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).RoundedRect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }, series.RoundRadius) + } + // 如果label不需要展示,则返回 if labelPainter == nil { continue From 32e6dd52d093ff3411bab31f2bed1357ddc29f48 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 5 Jun 2024 21:13:03 +0800 Subject: [PATCH 82/87] refactor: export `GetRenderer` function to get chart renderer --- painter.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/painter.go b/painter.go index bc13418..2bbbe2e 100644 --- a/painter.go +++ b/painter.go @@ -860,3 +860,7 @@ func (p *Painter) LegendLineDot(box Box) *Painter { p.FillStroke() return p } + +func (p *Painter) GetRenderer() chart.Renderer { + return p.render +} From e7dc4189d5b4bae6e8cf1b10851f6766cb3d4f39 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 7 Jun 2024 20:35:03 +0800 Subject: [PATCH 83/87] feat: support bar margin --- bar_chart.go | 7 +++++++ chart_option.go | 2 ++ charts.go | 10 ++++++---- horizontal_bar_chart.go | 5 +++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 508c63e..1bebc88 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,6 +23,7 @@ package charts import ( + "fmt" "math" "github.com/golang/freetype/truetype" @@ -63,6 +64,8 @@ type BarChartOption struct { // The legend option Legend LegendOption BarWidth int + // Margin of bar + BarMargin int } func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -88,6 +91,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B margin = 5 barMargin = 3 } + if opt.BarMargin > 0 { + barMargin = opt.BarMargin + } + fmt.Println(barMargin) seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount diff --git a/chart_option.go b/chart_option.go index 5311d50..d80a383 100644 --- a/chart_option.go +++ b/chart_option.go @@ -67,6 +67,8 @@ type ChartOption struct { LineStrokeWidth float64 // The bar with of bar chart BarWidth int + // The margin of each bar + BarMargin int // The bar height of horizontal bar chart BarHeight int // Fill the area of line chart diff --git a/charts.go b/charts.go index 74db733..91a0048 100644 --- a/charts.go +++ b/charts.go @@ -375,10 +375,11 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(barSeriesList) != 0 { handler.Add(func() error { _, err := NewBarChart(p, BarChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - BarWidth: opt.BarWidth, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + BarWidth: opt.BarWidth, + BarMargin: opt.BarMargin, }).render(renderResult, barSeriesList) return err }) @@ -391,6 +392,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { Theme: opt.theme, Font: opt.font, BarHeight: opt.BarHeight, + BarMargin: opt.BarMargin, YAxisOptions: opt.YAxisOptions, }).render(renderResult, horizontalBarSeriesList) return err diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index ca91242..2ea97bd 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -50,6 +50,8 @@ type HorizontalBarChartOption struct { // The legend option Legend LegendOption BarHeight int + // Margin of bar + BarMargin int } // NewHorizontalBarChart returns a horizontal bar chart renderer @@ -81,6 +83,9 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri margin = 5 barMargin = 3 } + if opt.BarMargin > 0 { + barMargin = opt.BarMargin + } seriesCount := len(seriesList) // 总的高度-两个margin-(总数-1)的barMargin barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount From 5842c71b1d23147fad578d8c11e0f587c464ef71 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 1 Aug 2024 21:44:52 +0800 Subject: [PATCH 84/87] refactor: remove unused code --- bar_chart.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 1bebc88..2d702f2 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "math" "github.com/golang/freetype/truetype" @@ -94,7 +93,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if opt.BarMargin > 0 { barMargin = opt.BarMargin } - fmt.Println(barMargin) seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount From d25a827706ac3d9c23b2676085c7d6fb845e7782 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 15 Aug 2024 20:37:07 +0800 Subject: [PATCH 85/87] fix: fix label position of pie, #86 --- pie_chart.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index 6cc48c4..9e036c6 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -234,23 +234,35 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B continue } if currentQuadrant != s.quadrant { - currentQuadrant = s.quadrant if s.quadrant == 1 { minY = cy * 2 maxY = 0 prevY = cy * 2 } if s.quadrant == 2 { - prevY = minY + if currentQuadrant != 3 { + prevY = s.lineEndY + } else { + prevY = minY + } } if s.quadrant == 3 { - minY = cy * 2 - maxY = 0 - prevY = 0 + if currentQuadrant != 4 { + prevY = s.lineEndY + } else { + minY = cy * 2 + maxY = 0 + prevY = 0 + } } if s.quadrant == 4 { - prevY = maxY + if currentQuadrant != 1 { + prevY = s.lineEndY + } else { + prevY = maxY + } } + currentQuadrant = s.quadrant } prevY = s.calculateY(prevY) if prevY > maxY { From 0eacc8e394512d513cd7fa53b8c2fe525b9b2f3c Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 13 May 2025 21:46:02 -0500 Subject: [PATCH 86/87] start migration to our packages --- README.md | 2 ++ alias.go | 4 ++-- axis.go | 2 +- axis_test.go | 2 +- bar_chart.go | 2 +- chart_option_test.go | 2 +- charts.go | 2 +- charts_test.go | 2 +- echarts.go | 2 +- echarts_test.go | 2 +- examples/painter/main.go | 2 +- examples/table/main.go | 2 +- font.go | 2 +- font_test.go | 2 +- go.mod | 12 +++++++----- go.sum | 18 ++++++++---------- grid_test.go | 2 +- horizontal_bar_chart.go | 2 +- line_chart.go | 2 +- mark_line_test.go | 2 +- mark_point_test.go | 2 +- painter.go | 2 +- painter_test.go | 4 ++-- pie_chart.go | 2 +- radar_chart.go | 4 ++-- series.go | 2 +- series_label.go | 2 +- table.go | 4 ++-- theme.go | 2 +- util.go | 4 ++-- util_test.go | 4 ++-- 31 files changed, 51 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 1e4ea8b..4cfb004 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 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) [![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) diff --git a/alias.go b/alias.go index a96f50b..edf0dec 100644 --- a/alias.go +++ b/alias.go @@ -23,8 +23,8 @@ package charts import ( - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type Box = chart.Box diff --git a/axis.go b/axis.go index 762a6a2..55fa219 100644 --- a/axis.go +++ b/axis.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type axisPainter struct { diff --git a/axis_test.go b/axis_test.go index d0cff41..85e18ca 100644 --- a/axis_test.go +++ b/axis_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestAxis(t *testing.T) { diff --git a/bar_chart.go b/bar_chart.go index 2d702f2..043e044 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -26,7 +26,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type barChart struct { diff --git a/chart_option_test.go b/chart_option_test.go index ff17750..c354b26 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestChartOption(t *testing.T) { diff --git a/charts.go b/charts.go index 91a0048..31df11c 100644 --- a/charts.go +++ b/charts.go @@ -27,7 +27,7 @@ import ( "math" "sort" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) const labelFontSize = 10 diff --git a/charts_test.go b/charts_test.go index da75ee5..bd581e9 100644 --- a/charts_test.go +++ b/charts_test.go @@ -26,7 +26,7 @@ import ( "errors" "testing" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) func BenchmarkMultiChartPNGRender(b *testing.B) { diff --git a/echarts.go b/echarts.go index 5a0e5a0..aaef1f1 100644 --- a/echarts.go +++ b/echarts.go @@ -29,7 +29,7 @@ import ( "regexp" "strconv" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) func convertToArray(data []byte) []byte { diff --git a/echarts_test.go b/echarts_test.go index 2ce1715..2077278 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -27,7 +27,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestConvertToArray(t *testing.T) { diff --git a/examples/painter/main.go b/examples/painter/main.go index b7a5832..193eb7c 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -5,7 +5,7 @@ import ( "path/filepath" charts "github.com/vicanso/go-charts/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func writeFile(buf []byte) error { diff --git a/examples/table/main.go b/examples/table/main.go index 0210ecf..036dc90 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/vicanso/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 { diff --git a/font.go b/font.go index dae5141..828654e 100644 --- a/font.go +++ b/font.go @@ -27,7 +27,7 @@ import ( "sync" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/roboto" + "git.smarteching.com/zeni/go-chart/v2/roboto" ) var fonts = sync.Map{} diff --git a/font_test.go b/font_test.go index 9dc731c..e0c56b2 100644 --- a/font_test.go +++ b/font_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/roboto" + "git.smarteching.com/zeni/go-chart/v2/roboto" ) func TestInstallFont(t *testing.T) { diff --git a/go.mod b/go.mod index d8a492c..b984e59 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,19 @@ -module github.com/vicanso/go-charts/v2 +module git.smarteching.com/zeni/go-charts/v2 -go 1.17 +go 1.24.1 require ( + git.smarteching.com/zeni/go-chart/v2 v2.1.4 github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.2 - github.com/wcharczuk/go-chart/v2 v2.1.0 + github.com/stretchr/testify v1.10.0 + github.com/vicanso/go-charts/v2 v2.6.10 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect + github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect + golang.org/x/image v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ac1d9f7..e7a75a8 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= +git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -7,20 +8,17 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vicanso/go-charts/v2 v2.6.10 h1:Nb2YBekEbUBPbvohnUO1oYMy31v75brUPk6n/fq+JXw= +github.com/vicanso/go-charts/v2 v2.6.10/go.mod h1:Ii2KDI3udTG1wPtiTnntzjlUBJVJTqNscMzh3oYHzUk= 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 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid_test.go b/grid_test.go index 3110a2b..fa9c3a6 100644 --- a/grid_test.go +++ b/grid_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestGrid(t *testing.T) { diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 2ea97bd..ed091c9 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type horizontalBarChart struct { diff --git a/line_chart.go b/line_chart.go index 363cd36..fb1d16a 100644 --- a/line_chart.go +++ b/line_chart.go @@ -26,7 +26,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type lineChart struct { diff --git a/mark_line_test.go b/mark_line_test.go index 00d19ef..0448cda 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestMarkLine(t *testing.T) { diff --git a/mark_point_test.go b/mark_point_test.go index ffa01a7..298345b 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestMarkPoint(t *testing.T) { diff --git a/painter.go b/painter.go index 2bbbe2e..bee646f 100644 --- a/painter.go +++ b/painter.go @@ -28,7 +28,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type ValueFormatter func(float64) string diff --git a/painter_test.go b/painter_test.go index b159328..07c4113 100644 --- a/painter_test.go +++ b/painter_test.go @@ -28,8 +28,8 @@ import ( "github.com/golang/freetype/truetype" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestPainterOption(t *testing.T) { diff --git a/pie_chart.go b/pie_chart.go index 9e036c6..5c04ed8 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -27,7 +27,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type pieChart struct { diff --git a/radar_chart.go b/radar_chart.go index f3d63b9..cf18135 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -27,8 +27,8 @@ import ( "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type radarChart struct { diff --git a/series.go b/series.go index 0ad135f..da50e64 100644 --- a/series.go +++ b/series.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type SeriesData struct { diff --git a/series_label.go b/series_label.go index 10fd148..af873fc 100644 --- a/series_label.go +++ b/series_label.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type labelRenderValue struct { diff --git a/table.go b/table.go index 86ef569..3e6f273 100644 --- a/table.go +++ b/table.go @@ -26,8 +26,8 @@ import ( "errors" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type tableChart struct { diff --git a/theme.go b/theme.go index a6d624f..85016a5 100644 --- a/theme.go +++ b/theme.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) const ThemeDark = "dark" diff --git a/util.go b/util.go index b333e6d..87ff31c 100644 --- a/util.go +++ b/util.go @@ -29,8 +29,8 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TrueFlag() *bool { diff --git a/util_test.go b/util_test.go index 62fd08d..5770776 100644 --- a/util_test.go +++ b/util_test.go @@ -26,8 +26,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestGetDefaultInt(t *testing.T) { From 958172a1f1206cc6b676c0c02da928ce7f21cf7f Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 13 May 2025 21:53:31 -0500 Subject: [PATCH 87/87] update URL in examples --- README.md | 16 ++++++++-------- README_zh.md | 16 ++++++++-------- examples/area_line_chart/main.go | 2 +- examples/bar_chart/main.go | 2 +- examples/charts/main.go | 2 +- examples/chinese/main.go | 2 +- examples/funnel_chart/main.go | 2 +- examples/horizontal_bar_chart/main.go | 2 +- examples/line_chart/main.go | 2 +- examples/painter/main.go | 2 +- examples/pie_chart/main.go | 2 +- examples/radar_chart/main.go | 2 +- examples/table/main.go | 2 +- examples/time_line_chart/main.go | 2 +- go.mod | 2 -- go.sum | 6 ------ 16 files changed, 28 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4cfb004..0650395 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ More examples can be found in the [./examples/](./examples/) directory. package main import ( - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -101,7 +101,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -176,7 +176,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -233,7 +233,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -288,7 +288,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -346,7 +346,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -386,7 +386,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -451,7 +451,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index c31cf77..3f35b97 100644 --- a/README_zh.md +++ b/README_zh.md @@ -32,7 +32,7 @@ package main import ( - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -98,7 +98,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -173,7 +173,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -230,7 +230,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -285,7 +285,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -343,7 +343,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -383,7 +383,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -447,7 +447,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go index ea8f1c2..57ca1e9 100644 --- a/examples/area_line_chart/main.go +++ b/examples/area_line_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index feea66e..91c9f81 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/charts/main.go b/examples/charts/main.go index 76aa42c..81bc4f2 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -6,7 +6,7 @@ import ( "net/http" "strconv" - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) var html = ` diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 2d96b58..601f54e 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index f29ccf9..653f834 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index a7daa8c..f5d8497 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 4e6448f..baee8a3 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/painter/main.go b/examples/painter/main.go index 193eb7c..1b842b3 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" "git.smarteching.com/zeni/go-chart/v2/drawing" ) diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 38488d2..5d70438 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index e8095ae..e7053af 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/table/main.go b/examples/table/main.go index 036dc90..de994eb 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" "git.smarteching.com/zeni/go-chart/v2/drawing" ) diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go index 6cb3f3d..c6c93bf 100644 --- a/examples/time_line_chart/main.go +++ b/examples/time_line_chart/main.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/go.mod b/go.mod index b984e59..76a47b6 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,11 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/stretchr/testify v1.10.0 - github.com/vicanso/go-charts/v2 v2.6.10 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect golang.org/x/image v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e7a75a8..3e1a48a 100644 --- a/go.sum +++ b/go.sum @@ -10,14 +10,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vicanso/go-charts/v2 v2.6.10 h1:Nb2YBekEbUBPbvohnUO1oYMy31v75brUPk6n/fq+JXw= -github.com/vicanso/go-charts/v2 v2.6.10/go.mod h1:Ii2KDI3udTG1wPtiTnntzjlUBJVJTqNscMzh3oYHzUk= -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.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=